dbo_postgres.php

Go to the documentation of this file.
00001 <?php
00002 /* SVN FILE: $Id: dbo__postgres_8php-source.html 580 2008-07-01 14:45:49Z gwoo $ */
00003 
00004 /**
00005  * PostgreSQL layer for DBO.
00006  *
00007  * Long description for file
00008  *
00009  * PHP versions 4 and 5
00010  *
00011  * CakePHP(tm) :  Rapid Development Framework <http://www.cakephp.org/>
00012  * Copyright 2005-2008, Cake Software Foundation, Inc.
00013  *                              1785 E. Sahara Avenue, Suite 490-204
00014  *                              Las Vegas, Nevada 89104
00015  *
00016  * Licensed under The MIT License
00017  * Redistributions of files must retain the above copyright notice.
00018  *
00019  * @filesource
00020  * @copyright       Copyright 2005-2008, Cake Software Foundation, Inc.
00021  * @link                http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
00022  * @package         cake
00023  * @subpackage      cake.cake.libs.model.datasources.dbo
00024  * @since           CakePHP(tm) v 0.9.1.114
00025  * @version         $Revision: 580 $
00026  * @modifiedby      $LastChangedBy: gwoo $
00027  * @lastmodified    $Date: 2008-07-01 09:45:49 -0500 (Tue, 01 Jul 2008) $
00028  * @license         http://www.opensource.org/licenses/mit-license.php The MIT License
00029  */
00030 
00031 /**
00032  * PostgreSQL layer for DBO.
00033  *
00034  * Long description for class
00035  *
00036  * @package     cake
00037  * @subpackage  cake.cake.libs.model.datasources.dbo
00038  */
00039 class DboPostgres extends DboSource {
00040 
00041 /**
00042  * Driver description
00043  *
00044  * @var string
00045  * @access public
00046  */
00047     var $description = "PostgreSQL DBO Driver";
00048 /**
00049  * Index of basic SQL commands
00050  *
00051  * @var array
00052  * @access protected
00053  */
00054     var $_commands = array(
00055         'begin'    => 'BEGIN',
00056         'commit'   => 'COMMIT',
00057         'rollback' => 'ROLLBACK'
00058     );
00059 /**
00060  * Base driver configuration settings.  Merged with user settings.
00061  *
00062  * @var array
00063  * @access protected
00064  */
00065     var $_baseConfig = array(
00066         'connect'   => 'pg_pconnect',
00067         'persistent' => true,
00068         'host' => 'localhost',
00069         'login' => 'root',
00070         'password' => '',
00071         'database' => 'cake',
00072         'schema' => 'public',
00073         'port' => 5432,
00074         'encoding' => ''
00075     );
00076 
00077     var $columns = array(
00078         'primary_key' => array('name' => 'serial NOT NULL'),
00079         'string' => array('name'  => 'varchar', 'limit' => '255'),
00080         'text' => array('name' => 'text'),
00081         'integer' => array('name' => 'integer', 'formatter' => 'intval'),
00082         'float' => array('name' => 'float', 'formatter' => 'floatval'),
00083         'datetime' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
00084         'timestamp' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
00085         'time' => array('name' => 'time', 'format' => 'H:i:s', 'formatter' => 'date'),
00086         'date' => array('name' => 'date', 'format' => 'Y-m-d', 'formatter' => 'date'),
00087         'binary' => array('name' => 'bytea'),
00088         'boolean' => array('name' => 'boolean'),
00089         'number' => array('name' => 'numeric'),
00090         'inet' => array('name'  => 'inet')
00091     );
00092 
00093     var $startQuote = '"';
00094 
00095     var $endQuote = '"';
00096 /**
00097  * Contains mappings of custom auto-increment sequences, if a table uses a sequence name
00098  * other than what is dictated by convention.
00099  *
00100  * @var array
00101  */
00102     var $_sequenceMap = array();
00103 /**
00104  * Connects to the database using options in the given configuration array.
00105  *
00106  * @return True if successfully connected.
00107  */
00108     function connect() {
00109         $config = $this->config;
00110         $conn  = "host='{$config['host']}' port='{$config['port']}' dbname='{$config['database']}' ";
00111         $conn .= "user='{$config['login']}' password='{$config['password']}'";
00112 
00113         if (!$config['persistent']) {
00114             $this->connection = pg_connect($conn, PGSQL_CONNECT_FORCE_NEW);
00115         } else {
00116             $this->connection = pg_pconnect($conn);
00117         }
00118         $this->connected = false;
00119 
00120         if ($this->connection) {
00121             $this->connected = true;
00122             $this->_execute("SET search_path TO " . $config['schema']);
00123         }
00124         if (!empty($config['encoding'])) {
00125             $this->setEncoding($config['encoding']);
00126         }
00127         return $this->connected;
00128     }
00129 /**
00130  * Disconnects from database.
00131  *
00132  * @return boolean True if the database could be disconnected, else false
00133  */
00134     function disconnect() {
00135         if ($this->hasResult()) {
00136             pg_free_result($this->_result);
00137         }
00138         if (is_resource($this->connection)) {
00139             $this->connected = !pg_close($this->connection);
00140         } else {
00141             $this->connected = false;
00142         }
00143         return !$this->connected;
00144     }
00145 /**
00146  * Executes given SQL statement.
00147  *
00148  * @param string $sql SQL statement
00149  * @return resource Result resource identifier
00150  */
00151     function _execute($sql) {
00152         return pg_query($this->connection, $sql);
00153     }
00154 /**
00155  * Returns an array of tables in the database. If there are no tables, an error is raised and the application exits.
00156  *
00157  * @return array Array of tablenames in the database
00158  */
00159     function listSources() {
00160         $cache = parent::listSources();
00161 
00162         if ($cache != null) {
00163             return $cache;
00164         }
00165 
00166         $schema = $this->config['schema'];
00167         $sql = "SELECT table_name as name FROM INFORMATION_SCHEMA.tables WHERE table_schema = '{$schema}';";
00168         $result = $this->fetchAll($sql, false);
00169 
00170         if (!$result) {
00171             return array();
00172         } else {
00173             $tables = array();
00174 
00175             foreach ($result as $item) {
00176                 $tables[] = $item[0]['name'];
00177             }
00178 
00179             parent::listSources($tables);
00180             return $tables;
00181         }
00182     }
00183 
00184 /**
00185  * Returns an array of the fields in given table name.
00186  *
00187  * @param string $tableName Name of database table to inspect
00188  * @return array Fields in table. Keys are name and type
00189  */
00190     function &describe(&$model) {
00191         $fields = parent::describe($model);
00192         $table = $this->fullTableName($model, false);
00193         $this->_sequenceMap[$table] = array();
00194 
00195         if ($fields === null) {
00196             $cols = $this->fetchAll("SELECT DISTINCT column_name AS name, data_type AS type, is_nullable AS null, column_default AS default, ordinal_position AS position, character_maximum_length AS char_length, character_octet_length AS oct_length FROM information_schema.columns WHERE table_name =" . $this->value($table) . " ORDER BY position", false);
00197 
00198             foreach ($cols as $column) {
00199                 $colKey = array_keys($column);
00200 
00201                 if (isset($column[$colKey[0]]) && !isset($column[0])) {
00202                     $column[0] = $column[$colKey[0]];
00203                 }
00204 
00205                 if (isset($column[0])) {
00206                     $c = $column[0];
00207 
00208                     if (!empty($c['char_length'])) {
00209                         $length = intval($c['char_length']);
00210                     } elseif (!empty($c['oct_length'])) {
00211                         $length = intval($c['oct_length']);
00212                     } else {
00213                         $length = $this->length($c['type']);
00214                     }
00215                     $fields[$c['name']] = array(
00216                         'type'    => $this->column($c['type']),
00217                         'null'    => ($c['null'] == 'NO' ? false : true),
00218                         'default' => preg_replace("/^'(.*)'$/", "$1", preg_replace('/::.*/', '', $c['default'])),
00219                         'length'  => $length
00220                     );
00221                     if ($c['name'] == $model->primaryKey) {
00222                         $fields[$c['name']]['key'] = 'primary';
00223                         if ($fields[$c['name']]['type'] !== 'string') {
00224                             $fields[$c['name']]['length'] = 11;
00225                         }
00226                     }
00227                     if ($fields[$c['name']]['default'] == 'NULL' || preg_match('/nextval\([\'"]?(\w+)/', $c['default'], $seq)) {
00228                         $fields[$c['name']]['default'] = null;
00229                         if (!empty($seq) && isset($seq[1])) {
00230                             $this->_sequenceMap[$table][$c['name']] = $seq[1];
00231                         }
00232                     }
00233                 }
00234             }
00235             $this->__cacheDescription($table, $fields);
00236         }
00237         if (isset($model->sequence)) {
00238             $this->_sequenceMap[$table][$model->primaryKey] = $model->sequence;
00239         }
00240         return $fields;
00241     }
00242 /**
00243  * Returns a quoted and escaped string of $data for use in an SQL statement.
00244  *
00245  * @param string $data String to be prepared for use in an SQL statement
00246  * @param string $column The column into which this data will be inserted
00247  * @return string Quoted and escaped
00248  * @todo Add logic that formats/escapes data based on column type
00249  */
00250     function value($data, $column = null) {
00251 
00252         $parent = parent::value($data, $column);
00253         if ($parent != null) {
00254             return $parent;
00255         }
00256 
00257         if ($data === null) {
00258             return 'NULL';
00259         }
00260         if (empty($column)) {
00261             $column = $this->introspectType($data);
00262         }
00263 
00264         switch($column) {
00265             case 'inet':
00266             case 'float':
00267             case 'integer':
00268                 if ($data === '') {
00269                     return 'DEFAULT';
00270                 }
00271             case 'binary':
00272                 $data = pg_escape_bytea($data);
00273             break;
00274             case 'boolean':
00275                 if ($data === true || $data === 't') {
00276                     return 'TRUE';
00277                 } elseif ($data === false || $data === 'f') {
00278                     return 'FALSE';
00279                 }
00280                 return (!empty($data) ? 'TRUE' : 'FALSE');
00281             break;
00282             default:
00283                 $data = pg_escape_string($data);
00284             break;
00285         }
00286         return "'" . $data . "'";
00287     }
00288 /**
00289  * Returns a formatted error message from previous database operation.
00290  *
00291  * @return string Error message
00292  */
00293     function lastError() {
00294         $error = pg_last_error($this->connection);
00295         if ($error) {
00296             return $error;
00297         }
00298         return null;
00299     }
00300 
00301 /**
00302  * Returns number of affected rows in previous database operation. If no previous operation exists, this returns false.
00303  *
00304  * @return integer Number of affected rows
00305  */
00306     function lastAffected() {
00307         if ($this->_result) {
00308             $return = pg_affected_rows($this->_result);
00309             return $return;
00310         }
00311         return false;
00312     }
00313 /**
00314  * Returns number of rows in previous resultset. If no previous resultset exists,
00315  * this returns false.
00316  *
00317  * @return integer Number of rows in resultset
00318  */
00319     function lastNumRows() {
00320         if ($this->_result) {
00321             $return = pg_num_rows($this->_result);
00322             return $return;
00323         }
00324         return false;
00325     }
00326 /**
00327  * Returns the ID generated from the previous INSERT operation.
00328  *
00329  * @param string $source Name of the database table
00330  * @param string $field Name of the ID database field. Defaults to "id"
00331  * @return integer
00332  */
00333     function lastInsertId($source, $field = 'id') {
00334         $seq = $this->getSequence($source, $field);
00335         $data = $this->fetchRow("SELECT currval('{$seq}') as max");
00336         return $data[0]['max'];
00337     }
00338 /**
00339  * Gets the associated sequence for the given table/field
00340  *
00341  * @param mixed $table Either a full table name (with prefix) as a string, or a model object
00342  * @param string $field Name of the ID database field. Defaults to "id"
00343  * @return string The associated sequence name from the sequence map, defaults to "{$table}_{$field}_seq"
00344  */
00345     function getSequence($table, $field = 'id') {
00346         if (is_object($table)) {
00347             $table = $this->fullTableName($table, false);
00348         }
00349         if (isset($this->_sequenceMap[$table]) && isset($this->_sequenceMap[$table][$field])) {
00350             return $this->_sequenceMap[$table][$field];
00351         } else {
00352             return "{$table}_{$field}_seq";
00353         }
00354     }
00355 /**
00356  * Deletes all the records in a table and drops all associated auto-increment sequences
00357  *
00358  * @param mixed $table A string or model class representing the table to be truncated
00359  * @param integer $reset If -1, sequences are dropped, if 0 (default), sequences are reset,
00360  *                      and if 1, sequences are not modified
00361  * @return boolean  SQL TRUNCATE TABLE statement, false if not applicable.
00362  * @access public
00363  */
00364     function truncate($table, $reset = 0) {
00365         if (parent::truncate($table)) {
00366             $table = $this->fullTableName($table, false);
00367             if (isset($this->_sequenceMap[$table]) && $reset !== 1) {
00368                 foreach ($this->_sequenceMap[$table] as $field => $sequence) {
00369                     if ($reset === 0) {
00370                         $this->execute("ALTER SEQUENCE \"{$sequence}\" RESTART WITH 1");
00371                     } elseif ($reset === -1) {
00372                         $this->execute("DROP SEQUENCE IF EXISTS \"{$sequence}\"");
00373                     }
00374                 }
00375             }
00376             return true;
00377         }
00378         return false;
00379     }
00380 /**
00381  * Prepares field names to be quoted by parent
00382  *
00383  * @param string $data
00384  * @return string SQL field
00385  */
00386     function name($data) {
00387         return parent::name(str_replace('"__"', '__', $data));
00388     }
00389 /**
00390  * Generates the fields list of an SQL query.
00391  *
00392  * @param Model $model
00393  * @param string $alias Alias tablename
00394  * @param mixed $fields
00395  * @return array
00396  */
00397     function fields(&$model, $alias = null, $fields = array(), $quote = true) {
00398         if (empty($alias)) {
00399             $alias = $model->alias;
00400         }
00401         $fields = parent::fields($model, $alias, $fields, false);
00402 
00403         if (!$quote) {
00404             return $fields;
00405         }
00406         $count = count($fields);
00407 
00408         if ($count >= 1 && $fields[0] != '*' && strpos($fields[0], 'COUNT(*)') === false) {
00409             for ($i = 0; $i < $count; $i++) {
00410                 if (!preg_match('/^.+\\(.*\\)/', $fields[$i]) && !preg_match('/\s+AS\s+/', $fields[$i])) {
00411                     $prepend = '';
00412                     if (strpos($fields[$i], 'DISTINCT') !== false) {
00413                         $prepend = 'DISTINCT ';
00414                         $fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i]));
00415                     }
00416 
00417                     if (strrpos($fields[$i], '.') === false) {
00418                         $fields[$i] = $prepend . $this->name($alias) . '.' . $this->name($fields[$i]) . ' AS ' . $this->name($alias . '__' . $fields[$i]);
00419                     } else {
00420                         $build = explode('.', $fields[$i]);
00421                         $fields[$i] = $prepend . $this->name($build[0]) . '.' . $this->name($build[1]) . ' AS ' . $this->name($build[0] . '__' . $build[1]);
00422                     }
00423                 }
00424             }
00425         }
00426         return $fields;
00427     }
00428 /**
00429  * Returns a limit statement in the correct format for the particular database.
00430  *
00431  * @param integer $limit Limit of results returned
00432  * @param integer $offset Offset from which to start results
00433  * @return string SQL limit/offset statement
00434  */
00435     function limit($limit, $offset = null) {
00436         if ($limit) {
00437             $rt = '';
00438             if (!strpos(strtolower($limit), 'limit') || strpos(strtolower($limit), 'limit') === 0) {
00439                 $rt = ' LIMIT';
00440             }
00441 
00442             $rt .= ' ' . $limit;
00443             if ($offset) {
00444                 $rt .= ' OFFSET ' . $offset;
00445             }
00446 
00447             return $rt;
00448         }
00449         return null;
00450     }
00451 /**
00452  * Converts database-layer column types to basic types
00453  *
00454  * @param string $real Real database-layer column type (i.e. "varchar(255)")
00455  * @return string Abstract column type (i.e. "string")
00456  */
00457     function column($real) {
00458         if (is_array($real)) {
00459             $col = $real['name'];
00460             if (isset($real['limit'])) {
00461                 $col .= '(' . $real['limit'] . ')';
00462             }
00463             return $col;
00464         }
00465 
00466         $col = str_replace(')', '', $real);
00467         $limit = null;
00468         if (strpos($col, '(') !== false) {
00469             list($col, $limit) = explode('(', $col);
00470         }
00471 
00472         if (in_array($col, array('date', 'time'))) {
00473             return $col;
00474         }
00475         if (strpos($col, 'timestamp') !== false) {
00476             return 'datetime';
00477         }
00478         if (strpos($col, 'time') === 0) {
00479             return 'time';
00480         }
00481         if ($col == 'inet') {
00482             return('inet');
00483         }
00484         if ($col == 'boolean') {
00485             return 'boolean';
00486         }
00487         if (strpos($col, 'int') !== false && $col != 'interval') {
00488             return 'integer';
00489         }
00490         if (strpos($col, 'char') !== false) {
00491             return 'string';
00492         }
00493         if (strpos($col, 'text') !== false) {
00494             return 'text';
00495         }
00496         if (strpos($col, 'bytea') !== false) {
00497             return 'binary';
00498         }
00499         if (in_array($col, array('float', 'float4', 'float8', 'double', 'double precision', 'decimal', 'real', 'numeric'))) {
00500             return 'float';
00501         }
00502         return 'text';
00503     }
00504 /**
00505  * Gets the length of a database-native column description, or null if no length
00506  *
00507  * @param string $real Real database-layer column type (i.e. "varchar(255)")
00508  * @return int An integer representing the length of the column
00509  */
00510     function length($real) {
00511         $col = str_replace(array(')', 'unsigned'), '', $real);
00512         $limit = null;
00513 
00514         if (strpos($col, '(') !== false) {
00515             list($col, $limit) = explode('(', $col);
00516         }
00517 
00518         if ($limit != null) {
00519             return intval($limit);
00520         }
00521         return null;
00522     }
00523 /**
00524  * Enter description here...
00525  *
00526  * @param unknown_type $results
00527  */
00528     function resultSet(&$results) {
00529         $this->results =& $results;
00530         $this->map = array();
00531         $num_fields = pg_num_fields($results);
00532         $index = 0;
00533         $j = 0;
00534 
00535         while ($j < $num_fields) {
00536             $columnName = pg_field_name($results, $j);
00537 
00538             if (strpos($columnName, '__')) {
00539                 $parts = explode('__', $columnName);
00540                 $this->map[$index++] = array($parts[0], $parts[1]);
00541             } else {
00542                 $this->map[$index++] = array(0, $columnName);
00543             }
00544             $j++;
00545         }
00546     }
00547 /**
00548  * Fetches the next row from the current result set
00549  *
00550  * @return unknown
00551  */
00552     function fetchResult() {
00553         if ($row = pg_fetch_row($this->results)) {
00554             $resultRow = array();
00555             $i = 0;
00556 
00557             foreach ($row as $index => $field) {
00558                 list($table, $column) = $this->map[$index];
00559                 $resultRow[$table][$column] = $row[$index];
00560                 $i++;
00561             }
00562             return $resultRow;
00563         } else {
00564             return false;
00565         }
00566     }
00567 /**
00568  * Translates between PHP boolean values and PostgreSQL boolean values
00569  *
00570  * @param mixed $data Value to be translated
00571  * @param boolean $quote    True to quote value, false otherwise
00572  * @return mixed Converted boolean value
00573  */
00574     function boolean($data, $quote = true) {
00575         switch (true) {
00576             case ($data === true || $data === false):
00577                 return $data;
00578             case ($data === 't' || $data === 'f'):
00579                 return ($data === 't');
00580             default:
00581                 return (bool)$data;
00582             break;
00583         }
00584     }
00585 /**
00586  * Sets the database encoding
00587  *
00588  * @param mixed $enc Database encoding
00589  * @return boolean True on success, false on failure
00590  */
00591     function setEncoding($enc) {
00592         return pg_set_client_encoding($this->connection, $enc) == 0;
00593     }
00594 /**
00595  * Gets the database encoding
00596  *
00597  * @return string The database encoding
00598  */
00599     function getEncoding() {
00600         return pg_client_encoding($this->connection);
00601     }
00602 /**
00603  * Generate a Postgres-native column schema string
00604  *
00605  * @param array $column An array structured like the following: array('name'=>'value', 'type'=>'value'[, options]),
00606  *                      where options can be 'default', 'length', or 'key'.
00607  * @return string
00608  */
00609     function buildColumn($column) {
00610         $out = preg_replace('/integer\([0-9]+\)/', 'integer', parent::buildColumn($column));
00611         $out = str_replace('integer serial', 'serial', $out);
00612 
00613         if (strpos($out, 'DEFAULT DEFAULT')) {
00614             if (isset($column['null']) && $column['null']) {
00615                 $out = str_replace('DEFAULT DEFAULT', 'DEFAULT NULL', $out);
00616             } elseif (in_array($column['type'], array('integer', 'float'))) {
00617                 $out = str_replace('DEFAULT DEFAULT', 'DEFAULT 0', $out);
00618             } elseif ($column['type'] == 'boolean') {
00619                 $out = str_replace('DEFAULT DEFAULT', 'DEFAULT FALSE', $out);
00620             }
00621         }
00622         return $out;
00623     }
00624 /**
00625  * Format indexes for create table
00626  *
00627  * @param array $indexes
00628  * @param string $table
00629  * @return string
00630  */
00631     function buildIndex($indexes, $table = null) {
00632         $join = array();
00633 
00634         foreach ($indexes as $name => $value) {
00635             if ($name == 'PRIMARY') {
00636                 $out = 'PRIMARY KEY  (' . $this->name($value['column']) . ')';
00637             } else {
00638                 $out = 'CREATE ';
00639                 if (!empty($value['unique'])) {
00640                     $out .= 'UNIQUE ';
00641                 }
00642                 if (is_array($value['column'])) {
00643                     $value['column'] = join(', ', array_map(array(&$this, 'name'), $value['column']));
00644                 } else {
00645                     $value['column'] = $this->name($value['column']);
00646                 }
00647                 $out .= "INDEX {$name} ON {$table}({$value['column']});";
00648             }
00649             $join[] = $out;
00650         }
00651         return $join;
00652     }
00653 /**
00654  * Overrides DboSource::renderStatement to handle schema generation with Postgres-style indexes
00655  *
00656  * @param string $type
00657  * @param array $data
00658  * @return string
00659  */
00660     function renderStatement($type, $data) {
00661         switch (strtolower($type)) {
00662             case 'schema':
00663                 extract($data);
00664 
00665                 foreach ($indexes as $i => $index) {
00666                     if (preg_match('/PRIMARY KEY/', $index)) {
00667                         unset($indexes[$i]);
00668                         $columns[] = $index;
00669                         break;
00670                     }
00671                 }
00672 
00673                 foreach (array('columns', 'indexes') as $var) {
00674                     if (is_array(${$var})) {
00675                         ${$var} = "\t" . join(",\n\t", array_filter(${$var}));
00676                     }
00677                 }
00678                 return "CREATE TABLE {$table} (\n{$columns});\n{$indexes}";
00679             break;
00680             default:
00681                 return parent::renderStatement($type, $data);
00682             break;
00683         }
00684     }
00685 }
00686 
00687 ?>