'datname', 'dbcomment' => 'description', 'encoding' => 'encoding', 'owner' => 'owner'); var $tbFields = array('tbname' => 'tablename', 'tbowner' => 'tableowner', 'tbcomment' => 'tablecomment'); var $vwFields = array('vwname' => 'viewname', 'vwowner' => 'viewowner', 'vwdef' => 'definition'); var $uFields = array('uname' => 'usename', 'usuper' => 'usesuper', 'ucreatedb' => 'usecreatedb', 'uexpires' => 'valuntil'); var $grpFields = array('groname' => 'groname', 'grolist' => 'grolist'); var $sqFields = array('seqname' => 'relname', 'seqowner' => 'usename', 'lastvalue' => 'last_value', 'incrementby' => 'increment_by', 'maxvalue' => 'max_value', 'minvalue'=> 'min_value', 'cachevalue' => 'cache_value', 'logcount' => 'log_cnt', 'iscycled' => 'is_cycled', 'iscalled' => 'is_called' ); var $ixFields = array('idxname' => 'relname', 'idxdef' => 'pg_get_indexdef', 'uniquekey' => 'indisunique', 'primarykey' => 'indisprimary'); var $rlFields = array('rulename' => 'rulename', 'ruledef' => 'definition'); var $tgFields = array('tgname' => 'tgname', 'tgdef' => 'tgdef'); var $cnFields = array('conname' => 'conname', 'consrc' => 'consrc', 'contype' => 'contype'); var $typFields = array('typname' => 'typname', 'typowner' => 'typowner', 'typin' => 'typin', 'typout' => 'typout', 'typlen' => 'typlen', 'typdef' => 'typdef', 'typelem' => 'typelem', 'typdelim' => 'typdelim', 'typbyval' => 'typbyval', 'typalign' => 'typalign', 'typstorage' => 'typstorage'); var $fnFields = array('fnname' => 'proname', 'fnreturns' => 'return_type', 'fnarguments' => 'arguments','fnoid' => 'oid', 'fndef' => 'source', 'fnlang' => 'language', 'setof' => 'proretset' ); var $langFields = array('lanname' => 'lanname'); // Array of allowed type alignments var $typAligns = array('char', 'int2', 'int4', 'double'); // The default type alignment var $typAlignDef = 'int4'; // Array of allowed type storage attributes var $typStorages = array('plain', 'external', 'extended', 'main'); // The default type storage var $typStorageDef = 'plain'; // Extra "magic" types var $extraTypes = array('SERIAL'); // Array of allowed index types var $typIndexes = array('BTREE', 'RTREE', 'GIST', 'HASH'); // Default index type var $typIndexDef = 'BTREE'; // Array of allowed trigger events var $triggerEvents= array('INSERT', 'UPDATE', 'DELETE', 'INSERT OR UPDATE', 'INSERT OR DELETE', 'DELETE OR UPDATE', 'INSERT OR DELETE OR UPDATE'); // When to execute the trigger var $triggerExecTimes = array('BEFORE', 'AFTER'); // Foreign key actions var $fkactions = array('NO ACTION', 'RESTRICT', 'CASCADE', 'SET NULL', 'SET DEFAULT'); // Function properties var $funcprops = array(array('', 'ISCACHABLE')); var $defaultprops = array(''); // Last oid assigned to a system object var $_lastSystemOID = 18539; var $_maxNameLen = 31; // Name of id column var $id = 'oid'; // Map of database encoding names to HTTP encoding names. If a // database encoding does not appear in this list, then its HTTP // encoding name is the same as its database encoding name. var $codemap = array( 'ALT' => 'CP866', 'EUC_CN' => 'GB2312', 'EUC_JP' => 'EUC-JP', 'EUC_KR' => 'EUC-KR', 'EUC_TW' => 'EUC-TW', 'ISO_8859_5' => 'ISO-8859-5', 'ISO_8859_6' => 'ISO-8859-6', 'ISO_8859_7' => 'ISO-8859-7', 'ISO_8859_8' => 'ISO-8859-8', 'JOHAB' => 'CP1361', 'KOI8' => 'KOI8-R', 'LATIN1' => 'ISO-8859-1', 'LATIN2' => 'ISO-8859-2', 'LATIN3' => 'ISO-8859-3', 'LATIN4' => 'ISO-8859-4', // The following encoding map is a known error in PostgreSQL < 7.2 // See the constructor for Postgres72. 'LATIN5' => 'ISO-8859-5', 'LATIN6' => 'ISO-8859-10', 'LATIN7' => 'ISO-8859-13', 'LATIN8' => 'ISO-8859-14', 'LATIN9' => 'ISO-8859-15', 'LATIN10' => 'ISO-8859-16', 'SQL_ASCII' => 'US-ASCII', 'TCVN' => 'CP1258', 'UNICODE' => 'UTF-8', 'WIN' => 'CP1251', 'WIN874' => 'CP874', 'WIN1256' => 'CP1256' ); // List of all legal privileges that can be applied to different types // of objects. var $privlist = array( 'table' => array('SELECT', 'INSERT', 'UPDATE', 'RULE', 'ALL'), 'view' => array('SELECT', 'RULE', 'ALL'), 'sequence' => array('SELECT', 'UPDATE', 'ALL') ); // List of characters in acl lists and the privileges they // refer to. var $privmap = array( 'r' => 'SELECT', 'w' => 'UPDATE', 'a' => 'INSERT', 'R' => 'RULE' ); // Rule action types var $rule_events = array('SELECT', 'INSERT', 'UPDATE', 'DELETE'); // Select operators // Operators of type 'i' are 'infix', eg. a = '1'. Type 'p' means postfix unary, eg. a IS TRUE. // 'x' is a bracketed subquery form. eg. IN (1,2,3) var $selectOps = array('=' => 'i', '!=' => 'i', '<' => 'i', '>' => 'i', '<=' => 'i', '>=' => 'i', 'LIKE' => 'i', 'NOT LIKE' => 'i', '~' => 'i', '!~' => 'i', '~*' => 'i', '!~*' => 'i', 'IS NULL' => 'p', 'IS NOT NULL' => 'p', 'IN' => 'x', 'NOT IN' => 'x'); /** * Constructor * @param $conn The database connection */ function Postgres($conn) { $this->BaseDB($conn); } /** * Cleans (escapes) a string * @param $str The string to clean, by reference * @return The cleaned string */ function clean(&$str) { if ($str === null) return null; if (function_exists('pg_escape_string')) $str = pg_escape_string($str); else $str = addslashes($str); return $str; } /** * Cleans (escapes) an object name (eg. table, field) * @param $str The string to clean, by reference * @return The cleaned string */ function fieldClean(&$str) { if ($str === null) return null; $str = str_replace('"', '""', $str); return $str; } /** * Cleans (escapes) an array * @param $arr The array to clean, by reference * @return The cleaned array */ function arrayClean(&$arr) { foreach ($arr as $k => $v) { if ($v === null) continue; if (function_exists('pg_escape_string')) $arr[$k] = pg_escape_string($v); else $arr[$k] = addslashes($v); } return $arr; } /** * Cleans (escapes) an array of field names * @param $arr The array to clean, by reference * @return The cleaned array */ function fieldArrayClean(&$arr) { foreach ($arr as $k => $v) { if ($v === null) continue; $arr[$k] = str_replace('"', '""', $v); } return $arr; } // Database functions /** * Return all database available on the server * @return A list of databases, sorted alphabetically */ function &getDatabases() { global $conf; if (isset($conf['owned_only']) && $conf['owned_only'] && !$this->isSuperUser($_SESSION['webdbUsername'])) { $username = $_SESSION['webdbUsername']; $this->clean($username); $clause = " AND pu.usename='{$username}'"; } else $clause = ''; if (!$conf['show_system']) $where = "AND pdb.datname NOT IN ('template1')"; else $where = ''; $sql = "SELECT pdb.datname, pu.usename AS owner, pg_encoding_to_char(encoding) AS encoding, (SELECT description FROM pg_description pd WHERE pdb.oid=pd.objoid) AS description FROM pg_database pdb, pg_user pu WHERE pdb.datdba = pu.usesysid {$where} {$clause} ORDER BY pdb.datname"; return $this->selectSet($sql); } /** * Return all information about a particular database * @param $database The name of the database to retrieve * @return The database info */ function &getDatabase($database) { $this->clean($database); $sql = "SELECT * FROM pg_database WHERE datname='{$database}'"; return $this->selectSet($sql); } /** * Returns the current database encoding * @return The encoding. eg. SQL_ASCII, UTF-8, etc. */ function getDatabaseEncoding() { $sql = "SELECT getdatabaseencoding() AS encoding"; return $this->selectField($sql, 'encoding'); } /** * Sets the client encoding * @param $encoding The encoding to for the client * @return 0 success */ function setClientEncoding($encoding) { return -99; } // Schema functions /** * Sets the current working schema. This is a do nothing method for * < 7.3 and is just here for polymorphism's sake. * @param $schema The the name of the schema to work in * @return 0 success */ function setSchema($schema) { return 0; } // Table functions /** * Returns the SQL for changing the current user * @param $user The user to change to * @return The SQL */ function getChangeUserSQL($user) { $this->fieldClean($user); return "\\connect - \"{$user}\""; } /** * Sets up the data object for a dump. eg. Starts the appropriate * transaction, sets variables, etc. * @return 0 success */ function beginDump() { // Begin serializable transaction (to dump consistent data) $status = $this->beginTransaction(); if ($status != 0) return -1; // Set serializable $sql = "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -1; } // Set datestyle to ISO $sql = "SET DATESTYLE = ISO"; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -1; } } /** * Ends the data object for a dump. * @return 0 success */ function endDump() { return $this->endTransaction(); } /** * Returns the SQL definition for the table. * @pre MUST be run within a transaction * @param $table The table to define * @param $clean True to issue drop command, false otherwise * @return A string containing the formatted SQL code * @return null On error */ function &getTableDefPrefix($table, $clean = false) { // Fetch table $t = &$this->getTable($table); if (!is_object($t) || $t->recordCount() != 1) { $this->rollbackTransaction(); return null; } $this->fieldClean($t->f['tablename']); // Fetch attributes $atts = &$this->getTableAttributes($table); if (!is_object($atts)) { $this->rollbackTransaction(); return null; } // Fetch constraints $cons = &$this->getConstraints($table); if (!is_object($cons)) { $this->rollbackTransaction(); return null; } // Output a reconnect command to create the table as the correct user $sql = $this->getChangeUserSQL($t->f['tableowner']) . "\n\n"; // Set schema search path if we support schemas if ($this->hasSchemas()) { $sql .= "SET search_path = \"{$this->_schema}\", pg_catalog;\n\n"; } // Begin CREATE TABLE definition $sql .= "-- Definition\n\n"; // DROP TABLE must be fully qualified in case a table with the same name exists // in pg_catalog. if (!$clean) $sql .= "-- "; $sql .= "DROP TABLE "; if ($this->hasSchemas()) { $sql .= "\"{$this->_schema}\"."; } $sql .= "\"{$t->f['tablename']}\";\n"; $sql .= "CREATE TABLE \"{$t->f['tablename']}\" (\n"; // Output all table columns $num = $atts->recordCount() + $cons->recordCount(); $i = 1; while (!$atts->EOF) { $this->fieldClean($atts->f['attname']); $sql .= " \"{$atts->f['attname']}\""; // Dump SERIAL and BIGSERIAL columns correctly if ($this->phpBool($atts->f['attisserial']) && ($atts->f['type'] == 'integer' || $atts->f['type'] == 'bigint')) { if ($atts->f['type'] == 'integer') $sql .= " SERIAL"; else $sql .= " BIGSERIAL"; } else { $sql .= " " . $this->formatType($atts->f['type'], $atts->f['atttypmod']); // Add NOT NULL if necessary if ($this->phpBool($atts->f['attnotnull'])) $sql .= " NOT NULL"; // Add default if necessary if ($atts->f['adsrc'] !== null) $sql .= " DEFAULT {$atts->f['adsrc']}"; } // Output comma or not if ($i < $num) $sql .= ",\n"; else $sql .= "\n"; $atts->moveNext(); $i++; } // Output all table constraints while (!$cons->EOF) { $this->fieldClean($cons->f['conname']); $sql .= " CONSTRAINT \"{$cons->f['conname']}\" "; // Nasty hack to support pre-7.4 PostgreSQL if ($cons->f['consrc'] !== null) $sql .= $cons->f['consrc']; else { switch ($cons->f['contype']) { case 'p': $keys = &$this->getKeys($table, explode(' ', $cons->f['indkey'])); $sql .= "PRIMARY KEY (" . join(',', $keys) . ")"; break; case 'u': $keys = &$this->getKeys($table, explode(' ', $cons->f['indkey'])); $sql .= "UNIQUE (" . join(',', $keys) . ")"; break; default: // Unrecognised constraint $this->rollbackTransaction(); return null; } } // Output comma or not if ($i < $num) $sql .= ",\n"; else $sql .= "\n"; $cons->moveNext(); $i++; } $sql .= ")"; // @@@@ DUMP CLUSTERING INFORMATION // Inherits /* * XXX: This is currently commented out as handling inheritance isn't this simple. * You also need to make sure you don't dump inherited columns and defaults, as well * as inherited NOT NULL and CHECK constraints. So for the time being, we just do * not claim to support inheritance. $parents = &$this->getTableParents($table); if ($parents->recordCount() > 0) { $sql .= " INHERITS ("; while (!$parents->EOF) { $this->fieldClean($parents->f['relname']); // Qualify the parent table if it's in another schema if ($this->hasSchemas() && $parents->f['schemaname'] != $this->_schema) { $this->fieldClean($parents->f['schemaname']); $sql .= "\"{$parents->f['schemaname']}\"."; } $sql .= "\"{$parents->f['relname']}\""; $parents->moveNext(); if (!$parents->EOF) $sql .= ', '; } $sql .= ")"; } */ // Handle WITHOUT OIDS if ($this->hasWithoutOIDs()) { if ($this->hasObjectID($table)) $sql .= " WITH OIDS"; else $sql .= " WITHOUT OIDS"; } $sql .= ";\n"; // Column storage and statistics $atts->moveFirst(); $first = true; while (!$atts->EOF) { $this->fieldClean($atts->f['attname']); // Statistics first if ($atts->f['attstattarget'] >= 0) { if ($first) { $sql .= "\n"; $first = false; } $sql .= "ALTER TABLE ONLY \"{$t->f['tablename']}\" ALTER COLUMN \"{$atts->f['attname']}\" SET STATISTICS {$atts->f['attstattarget']};\n"; } // Then storage if ($atts->f['attstorage'] != $atts->f['typstorage']) { switch ($atts->f['attstorage']) { case 'p': $storage = 'PLAIN'; break; case 'e': $storage = 'EXTERNAL'; break; case 'm': $storage = 'MAIN'; break; case 'x': $storage = 'EXTENDED'; break; default: // Unknown storage type $this->rollbackTransaction(); return null; } $sql .= "ALTER TABLE ONLY \"{$t->f['tablename']}\" ALTER COLUMN \"{$atts->f['attname']}\" SET STORAGE {$storage};\n"; } $atts->moveNext(); } // Comment if ($t->f['tablecomment'] !== null) { $this->clean($t->f['tablecomment']); $sql .= "\n-- Comment\n\n"; $sql .= "COMMENT ON TABLE \"{$t->f['tablename']}\" IS '{$t->f['tablecomment']}';\n"; } // Privileges $privs = &$this->getPrivileges($table, 'table'); if (!is_array($privs)) { $this->rollbackTransaction(); return null; } if (sizeof($privs) > 0) { $sql .= "\n-- Privileges\n\n"; /* * Always start with REVOKE ALL FROM PUBLIC, so that we don't have to * wire-in knowledge about the default public privileges for different * kinds of objects. */ $sql .= "REVOKE ALL ON TABLE \"{$t->f['tablename']}\" FROM PUBLIC;\n"; foreach ($privs as $v) { // Get non-GRANT OPTION privs $nongrant = array_diff($v[2], $v[4]); // Skip empty or owner ACEs if (sizeof($v[2]) == 0 || ($v[0] == 'user' && $v[1] == $t->f['tableowner'])) continue; // Change user if necessary if ($this->hasGrantOption() && $v[3] != $t->f['tableowner']) { $grantor = $v[3]; $this->clean($grantor); $sql .= "SET SESSION AUTHORIZATION '{$grantor}';\n"; } // Output privileges with no GRANT OPTION $sql .= "GRANT " . join(', ', $nongrant) . " ON TABLE \"{$t->f['tablename']}\" TO "; switch ($v[0]) { case 'public': $sql .= "PUBLIC;\n"; break; case 'user': $this->fieldClean($v[1]); $sql .= "\"{$v[1]}\";\n"; break; case 'group': $this->fieldClean($v[1]); $sql .= "GROUP \"{$v[1]}\";\n"; break; default: // Unknown privilege type - fail $this->rollbackTransaction(); return null; } // Reset user if necessary if ($this->hasGrantOption() && $v[3] != $t->f['tableowner']) { $sql .= "RESET SESSION AUTHORIZATION;\n"; } // Output privileges with GRANT OPTION // Skip empty or owner ACEs if (!$this->hasGrantOption() || sizeof($v[4]) == 0) continue; // Change user if necessary if ($this->hasGrantOption() && $v[3] != $t->f['tableowner']) { $grantor = $v[3]; $this->clean($grantor); $sql .= "SET SESSION AUTHORIZATION '{$grantor}';\n"; } $sql .= "GRANT " . join(', ', $v[4]) . " ON \"{$t->f['tablename']}\" TO "; switch ($v[0]) { case 'public': $sql .= "PUBLIC"; break; case 'user': $this->fieldClean($v[1]); $sql .= "\"{$v[1]}\""; break; case 'group': $this->fieldClean($v[1]); $sql .= "GROUP \"{$v[1]}\""; break; default: // Unknown privilege type - fail return null; } $sql .= " WITH GRANT OPTION;\n"; // Reset user if necessary if ($this->hasGrantOption() && $v[3] != $t->f['tableowner']) { $sql .= "RESET SESSION AUTHORIZATION;\n"; } } } // Add a newline to separate data that follows (if any) $sql .= "\n"; return $sql; } /** * Returns extra table definition information that is most usefully * dumped after the table contents for speed and efficiency reasons * @param $table The table to define * @return A string containing the formatted SQL code * @return null On error */ function &getTableDefSuffix($table) { $sql = ''; // Indexes if ($this->hasIndicies()) { $indexes = &$this->getIndexes($table); if (!is_object($indexes)) { $this->rollbackTransaction(); return null; } if ($indexes->recordCount() > 0) { $sql .= "\n-- Indexes\n\n"; while (!$indexes->EOF) { $sql .= $indexes->f['pg_get_indexdef'] . ";\n"; $indexes->moveNext(); } } } // Triggers if ($this->hasTriggers()) { $triggers = &$this->getTriggers($table); if (!is_object($triggers)) { $this->rollbackTransaction(); return null; } if ($triggers->recordCount() > 0) { $sql .= "\n-- Triggers\n\n"; while (!$triggers->EOF) { // Nasty hack to support pre-7.4 PostgreSQL if ($triggers->f['tgdef'] !== null) $sql .= $triggers->f['tgdef']; else $sql .= $this->getTriggerDef($triggers->f); $sql .= ";\n"; $triggers->moveNext(); } } } // Rules if ($this->hasRules()) { $rules = &$this->getRules($table); if (!is_object($rules)) { $this->rollbackTransaction(); return null; } if ($rules->recordCount() > 0) { $sql .= "\n-- Rules\n\n"; while (!$rules->EOF) { $sql .= $rules->f['definition'] . "\n"; $rules->moveNext(); } } } return $sql; } /** * Checks to see whether or not a table has a unique id column * @param $table The table name * @return True if it has a unique id, false otherwise * @return -99 error */ function hasObjectID($table) { // 7.0 and 7.1 always had an oid column return true; } /** * Given an array of attnums and a relation, returns an array mapping * atttribute number to attribute name. * @param $table The table to get attributes for * @param $atts An array of attribute numbers * @return An array mapping attnum to attname * @return -1 $atts must be an array * @return -2 wrong number of attributes found */ function getAttributeNames($table, $atts) { $this->clean($table); $this->arrayClean($atts); if (!is_array($atts)) return -1; if (sizeof($atts) == 0) return array(); $sql = "SELECT attnum, attname FROM pg_attribute WHERE attrelid=(SELECT oid FROM pg_class WHERE relname='{$table}') AND attnum IN ('" . join("','", $atts) . "')"; $rs = $this->selectSet($sql); if ($rs->recordCount() != sizeof($atts)) { return -2; } else { $temp = array(); while (!$rs->EOF) { $temp[$rs->f['attnum']] = $rs->f['attname']; $rs->moveNext(); } return $temp; } } /** * Get the fields for uniquely identifying a row in a table * @param $table The table for which to retrieve the identifier * @return An array mapping attribute number to attribute name, empty for no identifiers * @return -1 error */ function getRowIdentifier($table) { $oldtable = $table; $this->clean($table); $status = $this->beginTransaction(); if ($status != 0) return -1; // Get the first primary or unique index (sorting primary keys first) that // is NOT a partial index. $sql = "SELECT indrelid, indkey FROM pg_index WHERE indisunique AND indrelid=(SELECT oid FROM pg_class WHERE relname='{$table}') AND indpred='' AND indproc=0 ORDER BY indisprimary DESC LIMIT 1"; $rs = $this->selectSet($sql); // If none, check for an OID column. Even though OIDs can be duplicated, the edit and delete row // functions check that they're only modiying a single row. Otherwise, return empty array. if ($rs->recordCount() == 0) { // Check for OID column $temp = array(); if ($this->hasObjectID($table)) { $temp = array('oid'); } $this->endTransaction(); return $temp; } // Otherwise find the names of the keys else { $attnames = $this->getAttributeNames($oldtable, explode(' ', $rs->f['indkey'])); if (!is_array($attnames)) { $this->rollbackTransaction(); return -1; } else { $this->endTransaction(); return $attnames; } } } // Inheritance functions /** * Finds the names and schemas of parent tables (in order) * @param $table The table to find the parents for * @return A recordset */ function &getTableParents($table) { $this->clean($table); $sql = " SELECT NULL AS schemaname, relname FROM pg_class pc, pg_inherits pi WHERE pc.oid=pi.inhparent AND pi.inhrelid = (SELECT oid from pg_class WHERE relname='{$table}') ORDER BY pi.inhseqno "; return $this->selectSet($sql); } /** * Finds the names and schemas of child tables * @param $table The table to find the children for * @return A recordset */ function &getTableChildren($table) { $this->clean($table); $sql = " SELECT NULL AS schemaname, relname FROM pg_class pc, pg_inherits pi WHERE pc.oid=pi.inhrelid AND pi.inhparent = (SELECT oid from pg_class WHERE relname='{$table}') "; return $this->selectSet($sql); } // Formatting functions /** * Outputs the HTML code for a particular field * @param $name The name to give the field * @param $value The value of the field. Note this could be 'numeric(7,2)' sort of thing... * @param $type The database type of the field */ function printField($name, $value, $type) { global $lang; switch ($type) { case 'bool': case 'boolean': if ($value !== null && $value == '') $value = null; elseif ($value == 'true') $value = 't'; elseif ($value == 'false') $value = 'f'; // If value is null, 't' or 'f'... if ($value === null || $value == 't' || $value == 'f') { echo "\n"; } else { echo "\n"; } break; case 'text': case 'bytea': // addCSlashes converts all weird ASCII characters to octal representation, // EXCEPT the 'special' ones like \r \n \t, etc. if ($type == 'bytea') $value = addCSlashes($value, "\0..\37\177..\377"); echo "\n"; break; default: // echo "\n"; echo "\n"; break; } } /** * Formats a value or expression for sql purposes * @param $type The type of the field * @param $mode VALUE or EXPRESSION * @param $value The actual value entered in the field. Can be NULL * @return The suitably quoted and escaped value. */ function formatValue($type, $format, $value) { switch ($type) { case 'bool': case 'boolean': if ($value == 't') return 'TRUE'; elseif ($value == 'f') return 'FALSE'; elseif ($value == '') return 'NULL'; else return $value; break; default: // Checking variable fields is difficult as there might be a size // attribute... if (strpos($type, 'time') === 0) { // Assume it's one of the time types... if ($value == '') return "''"; elseif (strcasecmp($value, 'CURRENT_TIMESTAMP') == 0 || strcasecmp($value, 'CURRENT_TIME') == 0 || strcasecmp($value, 'CURRENT_DATE') == 0 || strcasecmp($value, 'LOCALTIME') == 0 || strcasecmp($value, 'LOCALTIMESTAMP') == 0) { return $value; } elseif ($format == 'EXPRESSION') return $value; else { $this->clean($value); return "'{$value}'"; } } else { if ($format == 'VALUE') { $this->clean($value); return "'{$value}'"; } return $value; } } } /** * Creates a database * @param $database The name of the database to create * @param $encoding Encoding of the database * @return 0 success */ function createDatabase($database, $encoding) { $this->fieldClean($database); $this->clean($encoding); if ( $encoding == '' ) { $sql = "CREATE DATABASE \"{$database}\""; } else { $sql = "CREATE DATABASE \"{$database}\" WITH ENCODING='{$encoding}'"; } return $this->execute($sql); } /** * Drops a database * @param $database The name of the database to drop * @return 0 success */ function dropDatabase($database) { $this->fieldClean($database); $sql = "DROP DATABASE \"{$database}\""; return $this->execute($sql); } // Table functions /** * Returns table information * @param $table The name of the table * @return A recordset */ function &getTable($table) { $this->clean($table); $sql = "SELECT pc.relname AS tablename, pg_get_userbyid(pc.relowner) AS tableowner, (SELECT description FROM pg_description pd WHERE pc.oid=pd.objoid) AS tablecomment FROM pg_class pc WHERE pc.relname='{$table}'"; return $this->selectSet($sql); } /** * Return all tables in current database * @param $all True to fetch all tables, false for just in current schema * @return All tables, sorted alphabetically */ function &getTables($all = false) { global $conf; if (!$conf['show_system'] || $all) $where = "WHERE tablename NOT LIKE 'pg\\\\_%' "; else $where = ''; $sql = "SELECT NULL AS schemaname, tablename, tableowner FROM pg_tables {$where}ORDER BY tablename"; return $this->selectSet($sql); } /** * Retrieve the attribute definition of a table * @param $table The name of the table * @param $field (optional) The name of a field to return * @return All attributes in order */ function &getTableAttributes($table, $field = '') { $this->clean($table); $this->clean($field); if ($field == '') { $sql = "SELECT a.attname, t.typname as type, a.attlen, a.atttypmod, a.attnotnull, a.atthasdef, -1 AS attstattarget, a.attstorage, (SELECT adsrc FROM pg_attrdef adef WHERE a.attrelid=adef.adrelid AND a.attnum=adef.adnum) AS adsrc, a.attstorage AS typstorage, false AS attisserial FROM pg_attribute a, pg_class c, pg_type t WHERE c.relname = '{$table}' AND a.attnum > 0 AND a.attrelid = c.oid AND a.atttypid = t.oid ORDER BY a.attnum"; } else { $sql = "SELECT a.attname, t.typname as type, a.attlen, a.atttypmod, a.attnotnull, a.atthasdef, -1 AS attstattarget, a.attstorage, (SELECT adsrc FROM pg_attrdef adef WHERE a.attrelid=adef.adrelid AND a.attnum=adef.adnum) AS adsrc, a.attstorage AS typstorage FROM pg_attribute a , pg_class c, pg_type t WHERE c.relname = '{$table}' AND a.attname='{$field}' AND a.attrelid = c.oid AND a.atttypid = t.oid ORDER BY a.attnum"; } return $this->selectSet($sql); } /** * Formats a type correctly for display. Postgres 7.0 had no 'format_type' * built-in function, and hence we need to do it manually. * @param $typname The name of the type * @param $typmod The contents of the typmod field */ function formatType($typname, $typmod) { // This is a specific constant in the 7.0 source $varhdrsz = 4; // Show lengths on bpchar and varchar if ($typname == 'bpchar') { $len = $typmod - $varhdrsz; $temp = 'character'; if ($len > 1) $temp .= "({$len})"; } elseif ($typname == 'varchar') { $temp = 'character varying'; if ($typmod != -1) $temp .= "(" . ($typmod - $varhdrsz) . ")"; } elseif ($typname == 'numeric') { $temp = 'numeric'; if ($typmod != -1) { $tmp_typmod = $typmod - $varhdrsz; $precision = ($tmp_typmod >> 16) & 0xffff; $scale = $tmp_typmod & 0xffff; $temp .= "({$precision}, {$scale})"; } } else $temp = $typname; return $temp; } /** * Drops a column from a table * @param $table The table from which to drop a column * @param $column The column to be dropped * @param $cascade True to cascade drop, false to restrict * @return 0 success * @return -99 not implemented */ function dropColumn($table, $column, $cascade) { return -99; } /** * Alters a column in a table * @param $table The table in which the column resides * @param $column The column to alter * @param $name The new name for the column * @param $notnull (boolean) True if not null, false otherwise * @param $default The new default for the column * @param $olddefault THe old default for the column * @return 0 success * @return -1 set not null error * @return -2 set default error * @return -3 rename column error */ function alterColumn($table, $column, $name, $notnull, $default, $olddefault) { $this->beginTransaction(); // @@ NEED TO HANDLE "NESTED" TRANSACTION HERE $status = $this->setColumnNull($table, $column, !$notnull); if ($status != 0) { $this->rollbackTransaction(); return -1; } // Set default, if it has changed if ($default != $olddefault) { if ($default == '') $status = $this->dropColumnDefault($table, $column); else $status = $this->setColumnDefault($table, $column, $default); if ($status != 0) { $this->rollbackTransaction(); return -2; } } // Rename the column, if it has been changed if ($column != $name) { $status = $this->renameColumn($table, $column, $name); if ($status != 0) { $this->rollbackTransaction(); return -3; } } return $this->endTransaction(); } /** * Creates a new table in the database * @param $name The name of the table * @param $fields The number of fields * @param $field An array of field names * @param $type An array of field types * @param $length An array of field lengths * @param $notnull An array of not null * @param $default An array of default values * @param $withoutoids True if WITHOUT OIDS, false otherwise * @return 0 success * @return -1 no fields supplied */ function createTable($name, $fields, $field, $type, $length, $notnull, $default, $withoutoids) { $this->fieldClean($name); $found = false; $sql = "CREATE TABLE \"{$name}\" ("; for ($i = 0; $i < $fields; $i++) { $this->fieldClean($field[$i]); $this->clean($type[$i]); $this->clean($length[$i]); // Skip blank columns - for user convenience if ($field[$i] == '' || $type[$i] == '') continue; switch ($type[$i]) { // Have to account for weird placing of length for with/without // time zone types case 'timestamp with time zone': case 'timestamp without time zone': $qual = substr($type[$i], 9); $sql .= "\"{$field[$i]}\" timestamp"; if ($length[$i] != '') $sql .= "({$length[$i]})"; $sql .= $qual; break; case 'time with time zone': case 'time without time zone': $qual = substr($type[$i], 4); $sql .= "\"{$field[$i]}\" time"; if ($length[$i] != '') $sql .= "({$length[$i]})"; $sql .= $qual; break; default: $sql .= "\"{$field[$i]}\" {$type[$i]}"; if ($length[$i] != '') $sql .= "({$length[$i]})"; } if (isset($notnull[$i])) $sql .= " NOT NULL"; if ($default[$i] != '') $sql .= " DEFAULT {$default[$i]}"; if ($i != $fields - 1) $sql .= ", "; $found = true; } if (!$found) return -1; $sql .= ")"; // WITHOUT OIDS if ($this->hasWithoutOIDs() && $withoutoids) $sql .= ' WITHOUT OIDS'; return $this->execute($sql); } /** * Alters a table * @param $table The name of the table * @param $name The new name for the table * @param $owner The new owner for the table * @param $comment The comment on the table * @return 0 success * @return -1 transaction error * @return -2 owner error * @return -3 rename error * @return -4 comment error * @return -5 get existing owner error */ function alterTable($table, $name, $owner, $comment) { $this->fieldClean($table); $this->fieldClean($name); $this->fieldClean($owner); $this->clean($comment); $status = $this->beginTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } // Comment $sql = "COMMENT ON TABLE \"{$table}\" IS "; if ($comment == '') $sql .= 'NULL'; else $sql .= "'{$comment}'"; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -4; } // Owner if ($this->hasAlterTableOwner() && $owner != '') { // Fetch existing owner $data = &$this->getTable($table); if ($data->recordCount() != 1) { $this->rollbackTransaction(); return -5; } // If owner has been changed, then do the alteration. We are // careful to avoid this generally as changing owner is a // superuser only function. if ($data->f[$this->tbFields['tbowner']] != $owner) { $sql = "ALTER TABLE \"{$table}\" OWNER TO \"{$owner}\""; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -2; } } } // Rename (only if name has changed) if ($name != $table) { $sql = "ALTER TABLE \"{$table}\" RENAME TO \"{$name}\""; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -3; } } return $this->endTransaction(); } /** * Removes a table from the database * @param $table The table to drop * @param $cascade True to cascade drop, false to restrict * @return 0 success */ function dropTable($table, $cascade) { $this->fieldClean($table); $sql = "DROP TABLE \"{$table}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } /** * Empties a table in the database * @param $table The table to be emptied * @return 0 success */ function emptyTable($table) { $this->fieldClean($table); $sql = "DELETE FROM \"{$table}\""; return $this->execute($sql); } /** * Renames a table * @param $table The table to be renamed * @param $newName The new name for the table * @return 0 success */ function renameTable($table, $newName) { $this->fieldClean($table); $this->fieldClean($newName); $sql = "ALTER TABLE \"{$table}\" RENAME TO \"{$newName}\""; return $this->execute($sql); } /** * Finds the number of rows that would be returned by a * query. * @param $query The SQL query * @param $count The count query * @return The count of rows * @return -1 error */ function browseQueryCount($query, $count) { // Count the number of rows $rs = $this->selectSet($query); if (!is_object($rs)) { return -1; } return $rs->recordCount(); } /** * Returns a recordset of all columns in a query. Supports paging. * @param $type Either 'QUERY' if it is an SQL query, or 'TABLE' if it is a table identifier, * or 'SELECT" if it's a select query * @param $table The base table of the query. NULL for no table. * @param $query The query that is being executed. NULL for no query. * @param $sortkey The column number to sort by, or '' or null for no sorting * @param $sortdir The direction in which to sort the specified column ('asc' or 'desc') * @param $page The page of the relation to retrieve * @param $page_size The number of rows per page * @param &$max_pages (return-by-ref) The max number of pages in the relation * @return A recordset on success * @return -1 transaction error * @return -2 counting error * @return -3 page or page_size invalid * @return -4 unknown type */ function &browseQuery($type, $table, $query, $sortkey, $sortdir, $page, $page_size, &$max_pages) { // Check that we're not going to divide by zero if (!is_numeric($page_size) || $page_size != (int)$page_size || $page_size <= 0) return -3; // If $type is TABLE, then generate the query switch ($type) { case 'TABLE': if (ereg('^[0-9]+$', $sortkey) && $sortkey > 0) $orderby = array($sortkey => $sortdir); else $orderby = array(); $query = $this->getSelectSQL($table, array(), array(), array(), $orderby); break; case 'QUERY': case 'SELECT': // Trim query $query = trim($query); // Trim off trailing semi-colon if there is one if (substr($query, strlen($query) - 1, 1) == ';') $query = substr($query, 0, strlen($query) - 1); break; default: return -4; } // Generate count query $count = "SELECT COUNT(*) AS total FROM ($query) AS sub"; // Open a transaction $status = $this->beginTransaction(); if ($status != 0) return -1; // Count the number of rows $total = $this->browseQueryCount($query, $count); if ($total < 0) { $this->rollbackTransaction(); return -2; } // Calculate max pages $max_pages = ceil($total / $page_size); // Check that page is less than or equal to max pages if (!is_numeric($page) || $page != (int)$page || $page > $max_pages || $page < 1) { $this->rollbackTransaction(); return -3; } // Set fetch mode to NUM so that duplicate field names are properly returned // for non-table queries. Since the SELECT feature only allows selecting one // table, duplicate fields shouldn't appear. if ($type == 'QUERY') $this->conn->setFetchMode(ADODB_FETCH_NUM); // Figure out ORDER BY. Sort key is always the column number (based from one) // of the column to order by. Only need to do this for non-TABLE queries if ($type != 'TABLE' && ereg('^[0-9]+$', $sortkey) && $sortkey > 0) { $orderby = " ORDER BY {$sortkey}"; // Add sort order if ($sortdir == 'desc') $orderby .= ' DESC'; else $orderby .= ' ASC'; } else $orderby = ''; // Actually retrieve the rows, with offset and limit if ($this->hasFullSubqueries()) $rs = $this->selectSet("SELECT * FROM ({$query}) AS sub {$orderby} LIMIT {$page_size} OFFSET " . ($page - 1) * $page_size); else $rs = $this->selectSet("{$query} LIMIT {$page_size} OFFSET " . ($page - 1) * $page_size); $status = $this->endTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } return $rs; } /** * Returns a recordset of all columns in a table * @param $table The name of a table * @param $key The associative array holding the key to retrieve * @return A recordset */ function &browseRow($table, $key) { $this->fieldClean($table); $sql = "SELECT * FROM \"{$table}\""; if (is_array($key) && sizeof($key) > 0) { $sql .= " WHERE true"; foreach ($key as $k => $v) { $this->fieldClean($k); $this->clean($v); $sql .= " AND \"{$k}\"='{$v}'"; } } return $this->selectSet($sql); } // Sequence functions /** * Returns all sequences in the current database * @return A recordset */ function &getSequences() { $sql = "SELECT c.relname, u.usename FROM pg_class c, pg_user u WHERE c.relowner=u.usesysid AND c.relkind = 'S' ORDER BY relname"; return $this->selectSet( $sql ); } /** * Returns properties of a single sequence * @param $sequence Sequence name * @return A recordset */ function &getSequence($sequence) { $this->fieldClean($sequence); $sql = "SELECT sequence_name AS relname, * FROM \"{$sequence}\""; return $this->selectSet( $sql ); } /** * Drops a given sequence * @param $sequence Sequence name * @param $cascade True to cascade drop, false to restrict * @return 0 success */ function dropSequence($sequence, $cascade) { $this->fieldClean($sequence); $sql = "DROP SEQUENCE \"{$sequence}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } /** * Resets a given sequence to min value of sequence * @param $sequence Sequence name * @return 0 success * @return -1 sequence not found */ function &resetSequence($sequence) { // Get the minimum value of the sequence $seq = &$this->getSequence($sequence); if ($seq->recordCount() != 1) return -1; $minvalue = $seq->f[$this->sqFields['minvalue']]; /* This double-cleaning is deliberate */ $this->fieldClean($sequence); $this->clean($sequence); $sql = "SELECT SETVAL('\"{$sequence}\"', {$minvalue})"; return $this->execute($sql); } /** * Creates a new sequence * @param $sequence Sequence name * @param $increment The increment * @param $minvalue The min value * @param $maxvalue The max value * @param $startvalue The starting value * @return 0 success */ function createSequence($sequence, $increment, $minvalue, $maxvalue, $startvalue) { $this->fieldClean($sequence); $this->clean($increment); $this->clean($minvalue); $this->clean($maxvalue); $this->clean($startvalue); $sql = "CREATE SEQUENCE \"{$sequence}\""; if ($increment != '') $sql .= " INCREMENT {$increment}"; if ($minvalue != '') $sql .= " MINVALUE {$minvalue}"; if ($maxvalue != '') $sql .= " MAXVALUE {$maxvalue}"; if ($startvalue != '') $sql .= " START {$startvalue}"; return $this->execute($sql); } // Constraint functions /** * Adds a check constraint to a table * @param $table The table to which to add the check * @param $definition The definition of the check * @param $name (optional) The name to give the check, otherwise default name is assigned * @return 0 success */ function addCheckConstraint($table, $definition, $name = '') { $this->fieldClean($table); $this->fieldClean($name); // @@ How the heck do you clean a definition??? $sql = "ALTER TABLE \"{$table}\" ADD "; if ($name != '') $sql .= "CONSTRAINT \"{$name}\" "; $sql .= "CHECK ({$definition})"; return $this->execute($sql); } /** * Drops a check constraint from a table * @param $table The table from which to drop the check * @param $name The name of the check to be dropped * @return 0 success * @return -2 transaction error * @return -3 lock error * @return -4 check drop error */ function dropCheckConstraint($table, $name) { $this->clean($table); $this->clean($name); // Begin transaction $status = $this->beginTransaction(); if ($status != 0) return -2; // Properly lock the table $sql = "LOCK TABLE \"{$table}\" IN ACCESS EXCLUSIVE MODE"; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -3; } // Delete the check constraint $sql = "DELETE FROM pg_relcheck WHERE rcrelid=(SELECT oid FROM pg_class WHERE relname='{$table}') AND rcname='{$name}'"; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -4; } // Update the pg_class catalog to reflect the new number of checks $sql = "UPDATE pg_class SET relchecks=(SELECT COUNT(*) FROM pg_relcheck WHERE rcrelid=(SELECT oid FROM pg_class WHERE relname='{$table}')) WHERE relname='{$table}'"; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -4; } // Otherwise, close the transaction return $this->endTransaction(); } // Constraint functions /** * Removes a constraint from a relation * @param $constraint The constraint to drop * @param $relation The relation from which to drop * @param $type The type of constraint (c, f, u or p) * @param $cascade True to cascade drop, false to restrict * @return 0 success * @return -99 dropping foreign keys not supported */ function dropConstraint($constraint, $relation, $type, $cascade) { $this->fieldClean($constraint); $this->fieldClean($relation); switch ($type) { case 'c': // CHECK constraint return $this->dropCheckConstraint($relation, $constraint); break; case 'p': case 'u': // PRIMARY KEY or UNIQUE constraint return $this->dropIndex($constraint, $cascade); break; case 'f': // FOREIGN KEY constraint return -99; } } /** * Adds a unique constraint to a table * @param $table The table to which to add the unique * @param $fields (array) An array of fields over which to add the unique * @param $name (optional) The name to give the unique, otherwise default name is assigned * @return 0 success * @return -1 invalid fields */ function addUniqueKey($table, $fields, $name = '') { if (!is_array($fields) || sizeof($fields) == 0) return -1; $this->fieldClean($table); $this->fieldArrayClean($fields); $this->fieldClean($name); if ($name != '') $sql = "CREATE UNIQUE INDEX \"{$name}\" ON \"{$table}\"(\"" . join('","', $fields) . "\")"; else return -99; // Not supported return $this->execute($sql); } /** * Adds a foreign key constraint to a table * @param $targschema The schema that houses the target table to which to add the foreign key * @param $targtable The table to which to add the foreign key * @param $target The table that contains the target columns * @param $sfields (array) An array of source fields over which to add the foreign key * @param $tfields (array) An array of target fields over which to add the foreign key * @param $upd_action The action for updates (eg. RESTRICT) * @param $del_action The action for deletes (eg. RESTRICT) * @param $name (optional) The name to give the key, otherwise default name is assigned * @return 0 success * @return -1 no fields given */ function addForeignKey($table, $targschema, $targtable, $sfields, $tfields, $upd_action, $del_action, $name = '') { if (!is_array($sfields) || sizeof($sfields) == 0 || !is_array($tfields) || sizeof($tfields) == 0) return -1; $this->fieldClean($table); $this->fieldClean($targschema); $this->fieldClean($targtable); $this->fieldArrayClean($sfields); $this->fieldArrayClean($tfields); $this->fieldClean($name); $sql = "ALTER TABLE \"{$table}\" ADD "; if ($name != '') $sql .= "CONSTRAINT \"{$name}\" "; $sql .= "FOREIGN KEY (\"" . join('","', $sfields) . "\") "; $sql .= "REFERENCES "; // Target table needs to be fully qualified if ($this->hasSchemas()) { $sql .= "\"{$targschema}\"."; } $sql .= "\"{$targtable}\"(\"" . join('","', $tfields) . "\") "; if ($upd_action != 'NO ACTION') $sql .= " ON UPDATE {$upd_action}"; if ($del_action != 'NO ACTION') $sql .= " ON DELETE {$del_action}"; return $this->execute($sql); } /** * Adds a primary key constraint to a table * @param $table The table to which to add the primery key * @param $fields (array) An array of fields over which to add the primary key * @param $name (optional) The name to give the key, otherwise default name is assigned * @return 0 success */ function addPrimaryKey($table, $fields, $name = '') { // This function can be faked with a unique index and a catalog twiddle, however // how do we ensure that it's only used on NOT NULL fields? return -99; // Not supported. } /** * Changes the owner of a table * @param $table The table whose owner is to change * @param $owner The new owner (username) of the table * @return 0 success */ function setOwnerOfTable($table, $owner) { $this->fieldClean($table); $this->fieldClean($owner); $sql = "ALTER TABLE \"{$table}\" OWNER TO \"{$owner}\""; return $this->execute($sql); } /** * Finds the foreign keys that refer to the specified table * @param $table The table to find referrers for * @return A recordset */ function &getReferrers($table) { // In PostgreSQL < 7.3, there is no way to discover foreign keys return -99; } // Column Functions /** * Add a new column to a table * @param $table The table to add to * @param $column The name of the new column * @param $type The type of the column * @param $length The optional size of the column (ie. 30 for varchar(30)) * @return 0 success */ function addColumn($table, $column, $type, $length) { $this->fieldClean($table); $this->fieldClean($column); $this->clean($type); $this->clean($length); if ($length == '') $sql = "ALTER TABLE \"{$table}\" ADD COLUMN \"{$column}\" {$type}"; else { switch ($type) { // Have to account for weird placing of length for with/without // time zone types case 'timestamp with time zone': case 'timestamp without time zone': $qual = substr($type, 9); $sql = "ALTER TABLE \"{$table}\" ADD COLUMN \"{$column}\" timestamp({$length}){$qual}"; break; case 'time with time zone': case 'time without time zone': $qual = substr($type, 4); $sql = "ALTER TABLE \"{$table}\" ADD COLUMN \"{$column}\" time({$length}){$qual}"; break; default: $sql = "ALTER TABLE \"{$table}\" ADD COLUMN \"{$column}\" {$type}({$length})"; } } return $this->execute($sql); } /** * Sets default value of a column * @param $table The table from which to drop * @param $column The column name to set * @param $default The new default value * @return 0 success */ function setColumnDefault($table, $column, $default) { $this->fieldClean($table); $this->fieldClean($column); $sql = "ALTER TABLE \"{$table}\" ALTER COLUMN \"{$column}\" SET DEFAULT {$default}"; return $this->execute($sql); } /** * Drops default value of a column * @param $table The table from which to drop * @param $column The column name to drop default * @return 0 success */ function dropColumnDefault($table, $column) { $this->fieldClean($table); $this->fieldClean($column); $sql = "ALTER TABLE \"{$table}\" ALTER COLUMN \"{$column}\" DROP DEFAULT"; return $this->execute($sql); } /** * Sets whether or not a column can contain NULLs * @param $table The table that contains the column * @param $column The column to alter * @param $state True to set null, false to set not null * @return 0 success * @return -1 attempt to set not null, but column contains nulls * @return -2 transaction error * @return -3 lock error * @return -4 update error */ function setColumnNull($table, $column, $state) { $this->fieldClean($table); $this->fieldClean($column); // Begin transaction $status = $this->beginTransaction(); if ($status != 0) return -2; // Properly lock the table $sql = "LOCK TABLE \"{$table}\" IN ACCESS EXCLUSIVE MODE"; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -3; } // Check for existing nulls if (!$state) { $sql = "SELECT COUNT(*) AS total FROM \"{$table}\" WHERE \"{$column}\" IS NULL"; $result = $this->selectField($sql, 'total'); if ($result > 0) { $this->rollbackTransaction(); return -1; } } // Otherwise update the table. Note the reverse-sensed $state variable $sql = "UPDATE pg_attribute SET attnotnull = " . (($state) ? 'false' : 'true') . " WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = '{$table}') AND attname = '{$column}'"; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -4; } // Otherwise, close the transaction return $this->endTransaction(); } /** * Renames a column in a table * @param $table The table containing the column to be renamed * @param $column The column to be renamed * @param $newName The new name for the column * @return 0 success */ function renameColumn($table, $column, $newName) { $this->fieldClean($table); $this->fieldClean($column); $this->fieldClean($newName); $sql = "ALTER TABLE \"{$table}\" RENAME COLUMN \"{$column}\" TO \"{$newName}\""; return $this->execute($sql); } /** * Grabs a list of indexes for a table * @param $table The name of a table whose indexes to retrieve * @return A recordset */ function &getIndexes($table = '') { $this->clean($table); $sql = "SELECT c2.relname, i.indisprimary, i.indisunique, pg_get_indexdef(i.indexrelid) FROM pg_class c, pg_class c2, pg_index i WHERE c.relname = '{$table}' AND c.oid = i.indrelid AND i.indexrelid = c2.oid AND NOT i.indisprimary AND NOT i.indisunique ORDER BY c2.relname"; return $this->selectSet($sql); } /** * Creates an index * @param $name The index name * @param $table The table on which to add the index * @param $columns An array of columns that form the index * @param $type The index type * @param $unique True if unique, false otherwise * @param $where Index predicate ('' for none) * @return 0 success */ function createIndex($name, $table, $columns, $type, $unique, $where) { $this->fieldClean($name); $this->fieldClean($table); $this->arrayClean($columns); $sql = "CREATE"; if ($unique) $sql .= " UNIQUE"; $sql .= " INDEX \"{$name}\" ON \"{$table}\" USING {$type} "; $sql .= "(\"" . implode('","', $columns) . "\")"; if ($this->hasPartialIndexes() && trim($where) != '') { $sql .= " WHERE ({$where})"; } return $this->execute($sql); } /** * Removes an index from the database * @param $index The index to drop * @param $cascade True to cascade drop, false to restrict * @return 0 success */ function dropIndex($index, $cascade) { $this->fieldClean($index); $sql = "DROP INDEX \"{$index}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } // Rule functions /** * Removes a rule from a relation * @param $rule The rule to drop * @param $relation The relation from which to drop (unused) * @param $cascade True to cascade drop, false to restrict * @return 0 success */ function dropRule($rule, $relation, $cascade) { $this->fieldClean($rule); $sql = "DROP RULE \"{$rule}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } /** * Creates a rule * @param $name The name of the new rule * @param $event SELECT, INSERT, UPDATE or DELETE * @param $table Table on which to create the rule * @param $where When to execute the rule, '' indicates always * @param $instead True if an INSTEAD rule, false otherwise * @param $type NOTHING for a do nothing rule, SOMETHING to use given action * @param $action The action to take * @param $replace (optional) True to replace existing rule, false otherwise * @return 0 success * @return -1 invalid event */ function createRule($name, $event, $table, $where, $instead, $type, $action, $replace = false) { $this->fieldClean($name); $this->fieldClean($table); if (!in_array($event, $this->rule_events)) return -1; $sql = "CREATE"; if ($replace) $sql .= " OR REPLACE"; $sql .= " RULE \"{$name}\" AS ON {$event} TO \"{$table}\""; // Can't escape WHERE clause if ($where != '') $sql .= " WHERE {$where}"; $sql .= " DO"; if ($instead) $sql .= " INSTEAD"; if ($type == 'NOTHING') $sql .= " NOTHING"; else $sql .= " ({$action})"; return $this->execute($sql); } /** * Edits a rule * @param $name The name of the new rule * @param $event SELECT, INSERT, UPDATE or DELETE * @param $table Table on which to create the rule * @param $where When to execute the rule, '' indicates always * @param $instead True if an INSTEAD rule, false otherwise * @param $type NOTHING for a do nothing rule, SOMETHING to use given action * @param $action The action to take * @return 0 success * @return -1 invalid event * @return -2 transaction error * @return -3 drop existing rule error * @return -4 create new rule error */ function setRule($name, $event, $table, $where, $instead, $type, $action) { $status = $this->beginTransaction(); if ($status != 0) return -2; $status = $this->dropRule($name, $table); if ($status != 0) { $this->rollbackTransaction(); return -3; } $status = $this->createRule($name, $event, $table, $where, $instead, $type, $action); if ($status != 0) { $this->rollbackTransaction(); return -4; } $status = $this->endTransaction(); return ($status == 0) ? 0 : -2; } // View functions /** * Returns a list of all views in the database * @return All views */ function &getViews() { global $conf; if (!$conf['show_system']) $where = "WHERE viewname NOT LIKE 'pg\\\\_%'"; else $where = ''; $sql = "SELECT viewname, viewowner FROM pg_views {$where} ORDER BY viewname"; return $this->selectSet($sql); } /** * Returns all details for a particular view * @param $view The name of the view to retrieve * @return View info */ function &getView($view) { $this->clean($view); $sql = "SELECT viewname, viewowner, definition FROM pg_views WHERE viewname='$view'"; return $this->selectSet($sql); } /** * Creates a new view. * @param $viewname The name of the view to create * @param $definition The definition for the new view * @param $replace True to replace the view, false otherwise * @return 0 success */ function createView($viewname, $definition, $replace) { $this->fieldClean($viewname); // Note: $definition not cleaned $sql = "CREATE "; if ($replace) $sql .= "OR REPLACE "; $sql .= "VIEW \"{$viewname}\" AS {$definition}"; return $this->execute($sql); } /** * Drops a view. * @param $viewname The name of the view to drop * @param $cascade True to cascade drop, false to restrict * @return 0 success */ function dropView($viewname, $cascade) { $this->fieldClean($viewname); $sql = "DROP VIEW \"{$viewname}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } /** * Updates a view. Postgres 7.1 and below don't have CREATE OR REPLACE view, * so we do it with a drop and a recreate. * @param $viewname The name fo the view to update * @param $definition The new definition for the view * @return 0 success * @return -1 transaction error * @return -2 drop view error * @return -3 create view error */ function setView($viewname, $definition) { $status = $this->beginTransaction(); if ($status != 0) return -1; $status = $this->dropView($viewname, false); if ($status != 0) { $this->rollbackTransaction(); return -2; } $status = $this->createView($viewname, $definition, false); if ($status != 0) { $this->rollbackTransaction(); return -3; } $status = $this->endTransaction(); return ($status == 0) ? 0 : -1; } // Find object functions /** * Searches all system catalogs to find objects that match a certain name. * @param $term The search term * @return A recordset */ function findObject($term) { global $conf; // Escape search term for ~* match $special = array('.', '*', '^', '$', ':', '?', '+', ',', '=', '!', '[', ']', '(', ')', '{', '}', '<', '>', '-', '\\'); foreach ($special as $v) { $term = str_replace($v, "\\{$v}", $term); } $this->clean($term); // Build SQL, excluding system relations as necessary // Relations $sql = " SELECT CASE WHEN relkind='r' THEN 'TABLE'::VARCHAR WHEN relkind='v' THEN 'VIEW'::VARCHAR WHEN relkind='S' THEN 'SEQUENCE'::VARCHAR END AS type, pc.oid, NULL::VARCHAR AS schemaname, NULL::VARCHAR AS relname, pc.relname AS name FROM pg_class pc WHERE relkind IN ('r', 'v', 'S') AND relname ~* '.*{$term}.*'"; if (!$conf['show_system']) $sql .= " AND pc.relname NOT LIKE 'pg\\\\_%'"; // Columns $sql .= " UNION ALL SELECT 'COLUMN', NULL, NULL, pc.relname, pa.attname FROM pg_class pc, pg_attribute pa WHERE pc.oid=pa.attrelid AND pa.attname ~* '.*{$term}.*' AND pa.attnum > 0 AND pc.relkind IN ('r', 'v')"; if (!$conf['show_system']) $sql .= " AND pc.relname NOT LIKE 'pg\\\\_%'"; // Functions $sql .= " UNION ALL SELECT 'FUNCTION', pp.oid, NULL, NULL, pp.proname FROM pg_proc pp WHERE proname ~* '.*{$term}.*'"; if (!$conf['show_system']) $sql .= " AND pp.oid > '{$this->_lastSystemOID}'::oid"; // Indexes $sql .= " UNION ALL SELECT 'INDEX', NULL, NULL, pc.relname, pc2.relname FROM pg_class pc, pg_index pi, pg_class pc2 WHERE pc.oid=pi.indrelid AND pi.indexrelid=pc2.oid AND pc2.relname ~* '.*{$term}.*' AND NOT pi.indisprimary AND NOT pi.indisunique"; if (!$conf['show_system']) $sql .= " AND pc2.relname NOT LIKE 'pg\\\\_%'"; // Check Constraints $sql .= " UNION ALL SELECT 'CONSTRAINT', NULL, NULL, pc.relname, pr.rcname FROM pg_class pc, pg_relcheck pr WHERE pc.oid=pr.rcrelid AND pr.rcname ~* '.*{$term}.*'"; if (!$conf['show_system']) $sql .= " AND pc.relname NOT LIKE 'pg\\\\_%'"; // Unique and Primary Key Constraints $sql .= " UNION ALL SELECT 'CONSTRAINT', NULL, NULL, pc.relname, pc2.relname FROM pg_class pc, pg_index pi, pg_class pc2 WHERE pc.oid=pi.indrelid AND pi.indexrelid=pc2.oid AND pc2.relname ~* '.*{$term}.*' AND (pi.indisprimary OR pi.indisunique)"; if (!$conf['show_system']) $sql .= " AND pc2.relname NOT LIKE 'pg\\\\_%'"; // Triggers $sql .= " UNION ALL SELECT 'TRIGGER', NULL, NULL, pc.relname, pt.tgname FROM pg_class pc, pg_trigger pt WHERE pc.oid=pt.tgrelid AND pt.tgname ~* '.*{$term}.*'"; if (!$conf['show_system']) $sql .= " AND pc.relname NOT LIKE 'pg\\\\_%'"; // Rules $sql .= " UNION ALL SELECT 'RULE', NULL, NULL, tablename, rulename FROM pg_rules WHERE rulename ~* '.*{$term}.*'"; if (!$conf['show_system']) $sql .= " AND tablename NOT LIKE 'pg\\\\_%'"; // Advanced Objects if ($conf['show_advanced']) { // Types $sql .= " UNION ALL SELECT 'TYPE', pt.oid, NULL, NULL, pt.typname FROM pg_type pt WHERE typname ~* '.*{$term}.*' AND (pt.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_class c WHERE c.oid = pt.typrelid))"; if (!$conf['show_system']) $sql .= " AND pt.oid > '{$this->_lastSystemOID}'::oid"; // Operators $sql .= " UNION ALL SELECT 'OPERATOR', po.oid, NULL, NULL, po.oprname FROM pg_operator po WHERE oprname ~* '.*{$term}.*'"; if (!$conf['show_system']) $sql .= " AND po.oid > '{$this->_lastSystemOID}'::oid"; // Languages $sql .= " UNION ALL SELECT 'LANGUAGE', pl.oid, NULL, NULL, pl.lanname FROM pg_language pl WHERE lanname ~* '.*{$term}.*'"; if (!$conf['show_system']) $sql .= " AND pl.lanispl"; // Aggregates $sql .= " UNION ALL SELECT DISTINCT ON (a.aggname) 'AGGREGATE', a.oid, NULL, NULL, a.aggname FROM pg_aggregate a WHERE aggname ~* '.*{$term}.*'"; if (!$conf['show_system']) $sql .= " AND a.oid > '{$this->_lastSystemOID}'::oid"; // Op Classes $sql .= " UNION ALL SELECT DISTINCT ON (po.opcname) 'OPCLASS', po.oid, NULL, NULL, po.opcname FROM pg_opclass po WHERE po.opcname ILIKE '%{$term}%'"; if (!$conf['show_system']) $sql .= " AND po.oid > '{$this->_lastSystemOID}'::oid"; } $sql .= " ORDER BY type, schemaname, relname, name"; return $this->selectSet($sql); } // Operator functions /** * Returns a list of all operators in the database * @return All operators */ function &getOperators() { global $conf; if (!$conf['show_system']) $where = "WHERE po.oid > '{$this->_lastSystemOID}'::oid"; else $where = ''; $sql = " SELECT po.oid, po.oprname, (SELECT typname FROM pg_type pt WHERE pt.oid=po.oprleft) AS oprleftname, (SELECT typname FROM pg_type pt WHERE pt.oid=po.oprright) AS oprrightname, (SELECT typname FROM pg_type pt WHERE pt.oid=po.oprresult) AS resultname FROM pg_operator po {$where} ORDER BY po.oprname, oprleftname, oprrightname "; return $this->selectSet($sql); } /** * Returns all details for a particular operator * @param $operator_oid The oid of the operator * @return Function info */ function getOperator($operator_oid) { $this->clean($operator_oid); $sql = " SELECT po.oid, po.oprname, (SELECT typname FROM pg_type pt WHERE pt.oid=po.oprleft) AS oprleftname, (SELECT typname FROM pg_type pt WHERE pt.oid=po.oprright) AS oprrightname, (SELECT typname FROM pg_type pt WHERE pt.oid=po.oprresult) AS resultname, po.oprcanhash, (SELECT oprname FROM pg_operator po2 WHERE po2.oid=po.oprcom) AS oprcom, (SELECT oprname FROM pg_operator po2 WHERE po2.oid=po.oprnegate) AS oprnegate, (SELECT oprname FROM pg_operator po2 WHERE po2.oid=po.oprlsortop) AS oprlsortop, (SELECT oprname FROM pg_operator po2 WHERE po2.oid=po.oprltcmpop) AS oprltcmpop, (SELECT oprname FROM pg_operator po2 WHERE po2.oid=po.oprgtcmpop) AS oprgtcmpop, po.oprcode::regproc AS oprcode, --(SELECT proname FROM pg_proc pp WHERE pp.oid=po.oprcode) AS oprcode, (SELECT proname FROM pg_proc pp WHERE pp.oid=po.oprrest) AS oprrest, (SELECT proname FROM pg_proc pp WHERE pp.oid=po.oprjoin) AS oprjoin FROM pg_operator po WHERE po.oid='{$operator_oid}' "; return $this->selectSet($sql); } /** * Drops an operator * @param $operator_oid The OID of the operator to drop * @param $cascade True to cascade drop, false to restrict * @return 0 success */ function dropOperator($operator_oid, $cascade) { // Function comes in with $object as operator OID $opr = &$this->getOperator($operator_oid); $this->fieldClean($opr->f['oprname']); $sql = "DROP OPERATOR {$opr->f['oprname']} ("; // Quoting or formatting here??? if ($opr->f['oprleftname'] !== null) $sql .= $opr->f['oprleftname'] . ', '; else $sql .= "NONE, "; if ($opr->f['oprrightname'] !== null) $sql .= $opr->f['oprrightname'] . ')'; else $sql .= "NONE)"; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } // User functions /** * Changes a user's password * @param $username The username * @param $password The new password * @return 0 success */ function changePassword($username, $password) { $this->fieldClean($username); $this->clean($password); $sql = "ALTER USER \"{$username}\" WITH PASSWORD '{$password}'"; return $this->execute($sql); } /** * Returns all users in the database cluster * @return All users */ function &getUsers() { $sql = "SELECT usename, usesuper, usecreatedb, valuntil"; if ($this->hasUserSessionDefaults()) $sql .= ", useconfig"; $sql .= " FROM pg_user ORDER BY usename"; return $this->selectSet($sql); } /** * Returns information about a single user * @param $username The username of the user to retrieve * @return The user's data */ function &getUser($username) { $this->clean($username); $sql = "SELECT usename, usesuper, usecreatedb, valuntil"; if ($this->hasUserSessionDefaults()) $sql .= ", useconfig"; $sql .= " FROM pg_user WHERE usename='{$username}'"; return $this->selectSet($sql); } /** * Determines whether or not a user is a super user * @param $username The username of the user * @return True if is a super user, false otherwise */ function isSuperUser($username) { $this->clean($username); $sql = "SELECT usesuper FROM pg_user WHERE usename='{$username}'"; $usesuper = $this->selectField($sql, 'usesuper'); if ($usesuper == -1) return false; else return $usesuper == 't'; } /** * Creates a new user * @param $username The username of the user to create * @param $password A password for the user * @param $createdb boolean Whether or not the user can create databases * @param $createuser boolean Whether or not the user can create other users * @param $expiry string Format 'YYYY-MM-DD HH:MM:SS'. '' means never expire * @param $group (array) The groups to create the user in * @return 0 success */ function createUser($username, $password, $createdb, $createuser, $expiry, $groups) { $this->fieldClean($username); $this->clean($password); $this->clean($expiry); $this->fieldArrayClean($groups); $sql = "CREATE USER \"{$username}\""; if ($password != '') $sql .= " WITH PASSWORD '{$password}'"; $sql .= ($createdb) ? ' CREATEDB' : ' NOCREATEDB'; $sql .= ($createuser) ? ' CREATEUSER' : ' NOCREATEUSER'; if (is_array($groups) && sizeof($groups) > 0) $sql .= " IN GROUP \"" . join('", "', $groups) . "\""; if ($expiry != '') $sql .= " VALID UNTIL '{$expiry}'"; else $sql .= " VALID UNTIL 'infinity'"; return $this->execute($sql); } /** * Adjusts a user's info * @param $username The username of the user to modify * @param $password A new password for the user * @param $createdb boolean Whether or not the user can create databases * @param $createuser boolean Whether or not the user can create other users * @param $expiry string Format 'YYYY-MM-DD HH:MM:SS'. '' means never expire. * @return 0 success */ function setUser($username, $password, $createdb, $createuser, $expiry) { $this->fieldClean($username); $this->clean($password); $this->clean($expiry); $sql = "ALTER USER \"{$username}\""; if ($password != '') $sql .= " WITH PASSWORD '{$password}'"; $sql .= ($createdb) ? ' CREATEDB' : ' NOCREATEDB'; $sql .= ($createuser) ? ' CREATEUSER' : ' NOCREATEUSER'; if ($expiry != '') $sql .= " VALID UNTIL '{$expiry}'"; else $sql .= " VALID UNTIL 'infinity'"; return $this->execute($sql); } /** * Removes a user * @param $username The username of the user to drop * @return 0 success */ function dropUser($username) { $this->fieldClean($username); $sql = "DROP USER \"{$username}\""; return $this->execute($sql); } // Group functions /** * Returns all groups in the database cluser * @return All groups */ function &getGroups() { $sql = "SELECT groname FROM pg_group ORDER BY groname"; return $this->selectSet($sql); } /** * Returns users in a specific group * @param $groname The name of the group * @return All users in the group */ function &getGroup($groname) { $this->clean($groname); $sql = "SELECT grolist FROM pg_group WHERE groname = '{$groname}'"; $grodata = $this->selectSet($sql); if ($grodata->f['grolist'] !== null && $grodata->f['grolist'] != '{}') { $members = $grodata->f['grolist']; $members = ereg_replace("\{|\}","",$members); $this->clean($members); $sql = "SELECT usename FROM pg_user WHERE usesysid IN ({$members}) ORDER BY usename"; } else $sql = "SELECT usename FROM pg_user WHERE false"; return $this->selectSet($sql); } /** * Creates a new group * @param $groname The name of the group * @param $users An array of users to add to the group * @return 0 success */ function createGroup($groname, $users) { $this->fieldClean($groname); $sql = "CREATE GROUP \"{$groname}\""; if (is_array($users) && sizeof($users) > 0) { $this->fieldArrayClean($users); $sql .= ' WITH USER "' . join('", "', $users) . '"'; } return $this->execute($sql); } /** * Removes a group * @param $groname The name of the group to drop * @return 0 success */ function dropGroup($groname) { $this->fieldClean($groname); $sql = "DROP GROUP \"{$groname}\""; return $this->execute($sql); } /** * Adds a group member * @param $groname The name of the group * @param $user The name of the user to add to the group * @return 0 success */ function addGroupMember($groname, $user) { $this->fieldClean($groname); $this->fieldClean($user); $sql = "ALTER GROUP \"{$groname}\" ADD USER \"{$user}\""; return $this->execute($sql); } /** * Removes a group member * @param $groname The name of the group * @param $user The name of the user to remove from the group * @return 0 success */ function dropGroupMember($groname, $user) { $this->fieldClean($groname); $this->fieldClean($user); $sql = "ALTER GROUP \"{$groname}\" DROP USER \"{$user}\""; return $this->execute($sql); } // Type functions /** * Returns a list of all types in the database * @param $all If true, will find all available functions, if false just those in search path * @return A recordet */ function &getTypes($all = false, $tabletypes = false) { global $conf; if ($all || $conf['show_system']) { $where = ''; } else { $where = "AND pt.oid > '{$this->_lastSystemOID}'::oid"; } // Never show system table types $where2 = "AND c.oid > '{$this->_lastSystemOID}'::oid"; if ($tabletypes) $tqry = "'c', 'r', 'v'"; else $tqry = "'c'"; $sql = "SELECT pt.typname AS basename, pt.typname, pu.usename AS typowner FROM pg_type pt, pg_user pu WHERE pt.typowner = pu.usesysid AND (pt.typrelid = 0 OR (SELECT c.relkind IN ({$tqry}) FROM pg_class c WHERE c.oid = pt.typrelid {$where2})) AND typname !~ '^_' {$where} ORDER BY typname "; return $this->selectSet($sql); } /** * Returns all details for a particular type * @param $typname The name of the view to retrieve * @return Type info */ function &getType($typname) { $this->clean($typname); $sql = "SELECT *, typinput AS typin, typoutput AS typout FROM pg_type WHERE typname='{$typname}'"; return $this->selectSet($sql); } /** * Creates a new type * @param ... * @return 0 success */ function createType($typname, $typin, $typout, $typlen, $typdef, $typelem, $typdelim, $typbyval, $typalign, $typstorage) { $this->fieldClean($typname); $this->fieldClean($typin); $this->fieldClean($typout); $sql = " CREATE TYPE \"{$typname}\" ( INPUT = \"{$typin}\", OUTPUT = \"{$typout}\", INTERNALLENGTH = {$typlen}"; if ($typdef != '') $sql .= ", DEFAULT = {$typdef}"; if ($typelem != '') $sql .= ", ELEMENT = {$typelem}"; if ($typdelim != '') $sql .= ", DELIMITER = {$typdelim}"; if ($typbyval) $sql .= ", PASSEDBYVALUE, "; if ($typalign != '') $sql .= ", ALIGNMENT = {$typalign}"; if ($typstorage != '') $sql .= ", STORAGE = {$typstorage}"; $sql .= ")"; return $this->execute($sql); } /** * Drops a type. * @param $typname The name of the type to drop * @param $cascade True to cascade drop, false to restrict * @return 0 success */ function dropType($typname, $cascade) { $this->fieldClean($typname); $sql = "DROP TYPE \"{$typname}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } // Trigger functions /** * A helper function for getTriggers that translates * an array of attribute numbers to an array of field names. * @param $trigger An array containing fields from the trigger table * @return The trigger definition string */ function &getTriggerDef($trigger) { // Constants to figure out tgtype if (!defined('TRIGGER_TYPE_ROW')) define ('TRIGGER_TYPE_ROW', (1 << 0)); if (!defined('TRIGGER_TYPE_BEFORE')) define ('TRIGGER_TYPE_BEFORE', (1 << 1)); if (!defined('TRIGGER_TYPE_INSERT')) define ('TRIGGER_TYPE_INSERT', (1 << 2)); if (!defined('TRIGGER_TYPE_DELETE')) define ('TRIGGER_TYPE_DELETE', (1 << 3)); if (!defined('TRIGGER_TYPE_UPDATE')) define ('TRIGGER_TYPE_UPDATE', (1 << 4)); $trigger['tgisconstraint'] = $this->phpBool($trigger['tgisconstraint']); $trigger['tgdeferrable'] = $this->phpBool($trigger['tgdeferrable']); $trigger['tginitdeferred'] = $this->phpBool($trigger['tginitdeferred']); // Constraint trigger or normal trigger if ($trigger['tgisconstraint']) $tgdef = 'CREATE CONSTRAINT TRIGGER '; else $tgdef = 'CREATE TRIGGER '; $tgdef .= "\"{$trigger['tgname']}\" "; // Trigger type $findx = 0; if (($trigger['tgtype'] & TRIGGER_TYPE_BEFORE) == TRIGGER_TYPE_BEFORE) $tgdef .= 'BEFORE'; else $tgdef .= 'AFTER'; if (($trigger['tgtype'] & TRIGGER_TYPE_INSERT) == TRIGGER_TYPE_INSERT) { $tgdef .= ' INSERT'; $findx++; } if (($trigger['tgtype'] & TRIGGER_TYPE_DELETE) == TRIGGER_TYPE_DELETE) { if ($findx > 0) $tgdef .= ' OR DELETE'; else { $tgdef .= ' DELETE'; $findx++; } } if (($trigger['tgtype'] & TRIGGER_TYPE_UPDATE) == TRIGGER_TYPE_UPDATE) { if ($findx > 0) $tgdef .= ' OR UPDATE'; else $tgdef .= ' UPDATE'; } // Table name $tgdef .= " ON \"{$trigger['relname']}\" "; // Deferrability if ($trigger['tgisconstraint']) { if ($trigger['tgconstrrelid'] != 0) { // Assume constrelname is not null $tgdef .= " FROM \"{$trigger['tgconstrrelname']}\" "; } if (!$trigger['tgdeferrable']) $tgdef .= 'NOT '; $tgdef .= 'DEFERRABLE INITIALLY '; if ($trigger['tginitdeferred']) $tgdef .= 'DEFERRED '; else $tgdef .= 'IMMEDIATE '; } // Row or statement if ($trigger['tgtype'] & TRIGGER_TYPE_ROW == TRIGGER_TYPE_ROW) $tgdef .= 'FOR EACH ROW '; else $tgdef .= 'FOR EACH STATEMENT '; // Execute procedure $tgdef .= "EXECUTE PROCEDURE \"{$trigger['tgfname']}\"("; // Parameters // Escape null characters $v = addCSlashes($trigger['tgargs'], "\0"); // Split on escaped null characters $params = explode('\\000', $v); for ($findx = 0; $findx < $trigger['tgnargs']; $findx++) { $param = "'" . str_replace('\'', '\\\'', $params[$findx]) . "'"; $tgdef .= $param; if ($findx < ($trigger['tgnargs'] - 1)) $tgdef .= ', '; } // Finish it off $tgdef .= ')'; return $tgdef; } /** * Grabs a list of triggers on a table * @param $table The name of a table whose triggers to retrieve * @return A recordset */ function &getTriggers($table = '') { $this->clean($table); // We include constraint triggers $sql = "SELECT t.tgname, t.tgisconstraint, t.tgdeferrable, t.tginitdeferred, t.tgtype, t.tgargs, t.tgnargs, t.tgconstrrelid, (SELECT relname FROM pg_class c2 WHERE c2.oid=t.tgconstrrelid) AS tgconstrrelname, (SELECT proname FROM pg_proc p WHERE t.tgfoid=p.oid) AS tgfname, c.relname, NULL AS tgdef FROM pg_trigger t, pg_class c WHERE t.tgrelid=c.oid AND c.relname='{$table}'"; return $this->selectSet($sql); } /** * Creates a trigger * @param $tgname The name of the trigger to create * @param $table The name of the table * @param $tgproc The function to execute * @param $tgtime BEFORE or AFTER * @param $tgevent Event * @param $tgargs The function arguments * @return 0 success */ function createTrigger($tgname, $table, $tgproc, $tgtime, $tgevent, $tgargs) { $this->fieldClean($tgname); $this->fieldClean($table); $this->fieldClean($tgproc); /* No Statement Level Triggers in PostgreSQL (by now) */ $sql = "CREATE TRIGGER \"{$tgname}\" {$tgtime} {$tgevent} ON \"{$table}\" FOR EACH ROW EXECUTE PROCEDURE \"{$tgproc}\"({$tgargs})"; return $this->execute($sql); } /** * Drops a trigger * @param $tgname The name of the trigger to drop * @param $table The table from which to drop the trigger * @param $cascade True to cascade drop, false to restrict * @return 0 success */ function dropTrigger($tgname, $table, $cascade) { $this->fieldClean($tgname); $this->fieldClean($table); $sql = "DROP TRIGGER \"{$tgname}\" ON \"{$table}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } // Privilege functions /** * Internal function used for parsing ACLs * @param $acl The ACL to parse (of type aclitem[]) * @return Privileges array */ function _parseACL($acl) { // Take off the first and last characters (the braces) $acl = substr($acl, 1, strlen($acl) - 2); // Pick out individual ACE's by carefully parsing. This is necessary in order // to cope with usernames and stuff that contain commas $aces = array(); $i = $j = 0; $in_quotes = false; while ($i < strlen($acl)) { // If current char is a double quote and it's not escaped, then // enter quoted bit $char = substr($acl, $i, 1); if ($char == '"' && ($i == 0 || substr($acl, $i - 1, 1) != '\\')) $in_quotes = !$in_quotes; elseif ($char == ',' && !$in_quotes) { // Add text so far to the array $aces[] = substr($acl, $j, $i - $j); $j = $i + 1; } $i++; } // Add final text to the array $aces[] = substr($acl, $j); // Create the array to be returned $temp = array(); // For each ACE, generate an entry in $temp foreach ($aces as $v) { // If the ACE begins with a double quote, strip them off both ends // and unescape backslashes and double quotes $unquote = false; if (strpos($v, '"') === 0) { $v = substr($v, 1, strlen($v) - 2); $v = str_replace('\\"', '"', $v); $v = str_replace('\\\\', '\\', $v); } // Figure out type of ACE (public, user or group) if (strpos($v, '=') === 0) $atype = 'public'; elseif (strpos($v, 'group ') === 0) { $atype = 'group'; // Tear off 'group' prefix $v = substr($v, 6); } else $atype = 'user'; // Break on unquoted equals sign... $i = 0; $in_quotes = false; $entity = null; $chars = null; while ($i < strlen($v)) { // If current char is a double quote and it's not escaped, then // enter quoted bit $char = substr($v, $i, 1); $next_char = substr($v, $i + 1, 1); if ($char == '"' && ($i == 0 || $next_char != '"')) { $in_quotes = !$in_quotes; } // Skip over escaped double quotes elseif ($char == '"' && $next_char == '"') { $i++; } elseif ($char == '=' && !$in_quotes) { // Split on current equals sign $entity = substr($v, 0, $i); $chars = substr($v, $i + 1); break; } $i++; } // Check for quoting on entity name, and unescape if necessary if (strpos($entity, '"') === 0) { $entity = substr($entity, 1, strlen($entity) - 2); $entity = str_replace('""', '"', $entity); } // New row to be added to $temp // (type, grantee, privileges, grantor, grant option? $row = array($atype, $entity, array(), '', array()); // Loop over chars and add privs to $row for ($i = 0; $i < strlen($chars); $i++) { // Append to row's privs list the string representing // the privilege $char = substr($chars, $i, 1); if ($char == '*') $row[4][] = $this->privmap[substr($chars, $i - 1, 1)]; elseif ($char == '/') { $grantor = substr($chars, $i + 1); // Check for quoting if (strpos($grantor, '"') === 0) { $grantor = substr($grantor, 1, strlen($grantor) - 2); $grantor = str_replace('""', '"', $grantor); } $row[3] = $grantor; break; } else { if (!isset($this->privmap[$char])) return -3; else $row[2][] = $this->privmap[$char]; } } // Append row to temp $temp[] = $row; } return $temp; } /** * Grabs an array of users and their privileges for an object, * given its type. * @param $object The name of the object whose privileges are to be retrieved * @param $type The type of the object (eg. relation, view or sequence) * @return Privileges array * @return -1 invalid type * @return -2 object not found * @return -3 unknown privilege type */ function getPrivileges($object, $type) { $this->clean($object); switch ($type) { case 'table': case 'view': case 'sequence': $sql = "SELECT relacl AS acl FROM pg_class WHERE relname='{$object}'"; break; default: return -1; } // Fetch the ACL for object $acl = $this->selectField($sql, 'acl'); if ($acl == -1) return -2; elseif ($acl == '' || $acl == null) return array(); else return $this->_parseACL($acl); } /** * Grants a privilege to a user, group or public * @param $mode 'GRANT' or 'REVOKE'; * @param $type The type of object * @param $object The name of the object * @param $public True to grant to public, false otherwise * @param $usernames The array of usernames to grant privs to. * @param $groupnames The array of group names to grant privs to. * @param $privileges The array of privileges to grant (eg. ('SELECT', 'ALL PRIVILEGES', etc.) ) * @param $grantoption True if has grant option, false otherwise * @param $cascade True for cascade revoke, false otherwise * @return 0 success * @return -1 invalid type * @return -2 invalid entity * @return -3 invalid privileges * @return -4 not granting to anything * @return -4 invalid mode */ function setPrivileges($mode, $type, $object, $public, $usernames, $groupnames, $privileges, $grantoption, $cascade) { $this->fieldArrayClean($usernames); $this->fieldArrayClean($groupnames); // Input checking if (!is_array($privileges) || sizeof($privileges) == 0) return -3; if (!is_array($usernames) || !is_array($groupnames) || (!$public && sizeof($usernames) == 0 && sizeof($groupnames) == 0)) return -4; if ($mode != 'GRANT' && $mode != 'REVOKE') return -5; $sql = $mode; // Grant option if ($this->hasGrantOption() && $mode == 'REVOKE' && $grantoption) { $sql .= ' GRANT OPTION FOR'; } if (in_array('ALL PRIVILEGES', $privileges)) $sql .= " ALL PRIVILEGES ON"; else $sql .= " " . join(', ', $privileges) . " ON"; switch ($type) { case 'table': case 'view': case 'sequence': $this->fieldClean($object); $sql .= " \"{$object}\""; break; case 'database': $this->fieldClean($object); $sql .= " DATABASE \"{$object}\""; break; case 'function': // Function comes in with $object as function OID $fn = &$this->getFunction($object); $this->fieldClean($fn->f[$this->fnFields['fnname']]); $sql .= " FUNCTION \"{$fn->f[$this->fnFields['fnname']]}\"({$fn->f[$this->fnFields['fnarguments']]})"; break; case 'language': $this->fieldClean($object); $sql .= " LANGUAGE \"{$object}\""; break; case 'schema': $this->fieldClean($object); $sql .= " SCHEMA \"{$object}\""; break; default: return -1; } // Dump PUBLIC $first = true; $sql .= ($mode == 'GRANT') ? ' TO ' : ' FROM '; if ($public) { $sql .= 'PUBLIC'; $first = false; } // Dump users foreach ($usernames as $v) { if ($first) { $sql .= "\"{$v}\""; $first = false; } else { $sql .= ", \"{$v}\""; } } // Dump groups foreach ($groupnames as $v) { if ($first) { $sql .= "GROUP \"{$v}\""; $first = false; } else { $sql .= ", GROUP \"{$v}\""; } } // Grant option if ($this->hasGrantOption() && $mode == 'GRANT' && $grantoption) { $sql .= ' WITH GRANT OPTION'; } // Cascade revoke if ($this->hasGrantOption() && $mode == 'REVOKE' && $cascade) { $sql .= ' CASCADE'; } return $this->execute($sql); } // Administration functions /** * Vacuums a database * @param $table (optional) The table to vacuum */ function vacuumDB($table = '') { if ($table != '') { $this->fieldClean($table); $sql = "VACUUM \"{$table}\""; } else $sql = "VACUUM"; return $this->execute($sql); } /** * Analyze a database * @param $table (optional) The table to analyze */ function analyzeDB($table = '') { if ($table != '') { $this->fieldClean($table); $sql = "VACUUM ANALYZE \"{$table}\""; } else $sql = "VACUUM ANALYZE"; return $this->execute($sql); } // Constraint functions /** * A helper function for getConstraints that translates * an array of attribute numbers to an array of field names. * @param $table The name of the table * @param $columsn An array of column ids * @return An array of column names */ function &getKeys($table, $colnums) { $this->clean($table); $this->arrayClean($colnums); $sql = "SELECT attnum, attname FROM pg_attribute WHERE attnum IN ('" . join("','", $colnums) . "') AND attrelid = (SELECT oid FROM pg_class WHERE relname='{$table}')"; $rs = $this->selectSet($sql); $temp = array(); while (!$rs->EOF) { $temp[$rs->f['attnum']] = $rs->f['attname']; $rs->moveNext(); } $atts = array(); foreach ($colnums as $v) { $atts[] = '"' . $temp[$v] . '"'; } return $atts; } /** * Returns a list of all constraints on a table * @param $table The table to find rules for * @return A recordset */ function &getConstraints($table) { $this->clean($table); $status = $this->beginTransaction(); if ($status != 0) return -1; $sql = " SELECT rcname AS conname, 'CHECK (' || rcsrc || ')' AS consrc, 'c' AS contype, NULL::int2vector AS indkey FROM pg_relcheck WHERE rcrelid = (SELECT oid FROM pg_class WHERE relname='{$table}') UNION ALL SELECT pc.relname, NULL, CASE WHEN indisprimary THEN 'p' ELSE 'u' END, indkey FROM pg_class pc, pg_index pi WHERE pc.oid=pi.indexrelid AND (pi.indisunique OR pi.indisprimary) AND pi.indrelid = (SELECT oid FROM pg_class WHERE relname='{$table}') ORDER BY 1 "; return $this->selectSet($sql); } // Function functions /** * Returns a list of all functions in the database * @param $all If true, will find all available functions, if false just userland ones * @return All functions */ function &getFunctions($all = false) { global $conf; if ($all || $conf['show_system']) $where = ''; else $where = "AND pc.oid > '{$this->_lastSystemOID}'::oid"; $sql = "SELECT pc.oid, proname, proretset, pt.typname AS return_type, oidvectortypes(pc.proargtypes) AS arguments FROM pg_proc pc, pg_user pu, pg_type pt WHERE pc.proowner = pu.usesysid AND pc.prorettype = pt.oid {$where} UNION SELECT pc.oid, proname, proretset, 'OPAQUE' AS result, oidvectortypes(pc.proargtypes) AS arguments FROM pg_proc pc, pg_user pu, pg_type pt WHERE pc.proowner = pu.usesysid AND pc.prorettype = 0 {$where} ORDER BY proname, return_type "; return $this->selectSet($sql); } /** * Returns a list of all functions that can be used in triggers */ function &getTriggerFunctions() { return $this->getFunctions(true); } /** * Returns all details for a particular function * @param $function_oid The OID of the function to retrieve * @return Function info */ function getFunction($function_oid) { $this->clean($function_oid); $sql = "SELECT pc.oid, proname, lanname as language, pt.typname as return_type, prosrc as source, probin as binary, proretset, proiscachable, oidvectortypes(pc.proargtypes) AS arguments FROM pg_proc pc, pg_language pl, pg_type pt WHERE pc.oid = '$function_oid'::oid AND pc.prolang = pl.oid AND pc.prorettype = pt.oid "; return $this->selectSet($sql); } /** * Returns an array containing a function's properties * @param $f The array of data for the function * @return An array containing the properties */ function getFunctionProperties($f) { $temp = array(); // Cachable $f['proiscachable'] = $this->phpBool($f['proiscachable']); if ($f['proiscachable']) $temp[] = 'ISCACHABLE'; else $temp[] = ''; return $temp; } /** * Updates a function. Postgres 7.1 doesn't have CREATE OR REPLACE function, * so we do it with a drop and a recreate. * @param $function_oid The OID of the function * @param $funcname The name of the function to create * @param $args The array of argument types * @param $returns The return type * @param $definition The definition for the new function * @param $language The language the function is written for * @param $flags An array of optional flags * @param $setof True if returns a set, false otherwise * @return 0 success * @return -1 transaction error * @return -2 drop function error * @return -3 create function error */ function setFunction($function_oid, $funcname, $args, $returns, $definition, $language, $flags, $setof) { $status = $this->beginTransaction(); if ($status != 0) return -1; $status = $this->dropFunction($function_oid, false); if ($status != 0) { $this->rollbackTransaction(); return -2; } $status = $this->createFunction($funcname, $args, $returns, $definition, $language, $flags, $setof, false); if ($status != 0) { $this->rollbackTransaction(); return -3; } $status = $this->endTransaction(); return ($status == 0) ? 0 : -1; } /** * Creates a new function. * @param $funcname The name of the function to create * @param $args A comma separated string of types * @param $returns The return type * @param $definition The definition for the new function * @param $language The language the function is written for * @param $flags An array of optional flags * @param $setof True if it returns a set, false otherwise * @param $replace (optional) True if OR REPLACE, false for normal * @return 0 success */ function createFunction($funcname, $args, $returns, $definition, $language, $flags, $setof, $replace = false) { $this->fieldClean($funcname); $this->clean($args); $this->clean($definition); $this->clean($language); $this->arrayClean($flags); $sql = "CREATE"; if ($replace) $sql .= " OR REPLACE"; $sql .= " FUNCTION \"{$funcname}\" ("; if ($args != '') $sql .= $args; // For some reason, the returns field cannot have quotes... $sql .= ") RETURNS "; if ($setof) $sql .= "SETOF "; $sql .= "{$returns} AS '\n"; $sql .= $definition; $sql .= "\n'"; $sql .= " LANGUAGE '{$language}'"; // Add flags $first = true; foreach ($flags as $v) { // Skip default flags if ($v == '') continue; elseif ($first) { $sql .= " WITH ({$v}"; $first = false; } else { $sql .= ", {$v}"; } } // Close off WITH clause if necessary if (!$first) $sql .= ")"; return $this->execute($sql); } /** * Drops a function. * @param $function_oid The OID of the function to drop * @param $cascade True to cascade drop, false to restrict * @return 0 success */ function dropFunction($function_oid, $cascade) { // Function comes in with $object as function OID $fn = &$this->getFunction($function_oid); $this->fieldClean($fn->f[$this->fnFields['fnname']]); $sql = "DROP FUNCTION \"{$fn->f[$this->fnFields['fnname']]}\"({$fn->f[$this->fnFields['fnarguments']]})"; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } // Rule functions /** * Returns a list of all rules on a table * @param $table The table to find rules for * @return A recordset */ function &getRules($table) { $this->clean($table); $sql = "SELECT * FROM pg_rules WHERE tablename='{$table}' ORDER BY rulename "; return $this->selectSet($sql); } // Language functions /** * Gets all languages * @param $all True to get all languages, regardless of show_system * @return A recordset */ function &getLanguages($all = false) { global $conf; if ($conf['show_system'] || $all) $where = ''; else $where = 'WHERE lanispl'; $sql = " SELECT lanname, lanpltrusted, lanplcallfoid::regproc AS lanplcallf FROM pg_language {$where} ORDER BY lanname "; return $this->selectSet($sql); } // Aggregate functions /** * Gets all aggregates * @return A recordset */ function &getAggregates() { global $conf; if ($conf['show_system']) $where = ''; else $where = "WHERE a.oid > '{$this->_lastSystemOID}'::oid"; $sql = " SELECT a.aggname AS proname, CASE a.aggbasetype WHEN 0 THEN NULL ELSE (SELECT typname FROM pg_type t WHERE t.oid=a.aggbasetype) END AS proargtypes FROM pg_aggregate a {$where} ORDER BY 1, 2; "; return $this->selectSet($sql); } // Operator Class functions /** * Gets all opclasses * @return A recordset */ function &getOpClasses() { global $conf; if ($conf['show_system']) $where = ''; else $where = "AND po.oid > '{$this->_lastSystemOID}'::oid"; $sql = " SELECT DISTINCT pa.amname, po.opcname, (SELECT typname FROM pg_type t WHERE t.oid=opcdeftype) AS opcintype, TRUE AS opcdefault FROM pg_opclass po, pg_am pa, pg_amop pam WHERE pam.amopid=pa.oid AND pam.amopclaid=po.oid {$where} ORDER BY 1,2 "; return $this->selectSet($sql); } // Type conversion routines /** * Change the value of a parameter to 't' or 'f' depending on whether it evaluates to true or false * @param $parameter the parameter */ function dbBool(&$parameter) { if ($parameter) $parameter = 't'; else $parameter = 'f'; return $parameter; } /** * Change a parameter from 't' or 'f' to a boolean, (others evaluate to false) * @param $parameter the parameter */ function phpBool($parameter) { $parameter = ($parameter == 't'); return $parameter; } // Capabilities function hasTables() { return true; } function hasViews() { return true; } function hasSequences() { return true; } function hasFunctions() { return true; } function hasTriggers() { return true; } function hasOperators() { return true; } function hasTypes() { return true; } function hasAggregates() { return true; } function hasIndicies() { return true; } function hasRules() { return true; } function hasLanguages() { return true; } function hasDropColumn() { return false; } function hasSRFs() { return true; } function hasOpClasses() { return true; } function hasUserSessionDefaults() { return false; } function hasUserRename() { return false; } } ?>