'BIG5', 'EUC_CN' => 'GB2312', 'EUC_JP' => 'EUC-JP', 'EUC_KR' => 'EUC-KR', 'EUC_TW' => 'EUC-TW', 'GB18030' => 'GB18030', 'GBK' => 'GB2312', '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', 'LATIN5' => 'ISO-8859-9', 'LATIN6' => 'ISO-8859-10', 'LATIN7' => 'ISO-8859-13', 'LATIN8' => 'ISO-8859-14', 'LATIN9' => 'ISO-8859-15', 'LATIN10' => 'ISO-8859-16', 'SJIS' => 'SHIFT_JIS', 'SQL_ASCII' => 'US-ASCII', 'UHC' => 'WIN949', 'UTF8' => 'UTF-8', 'WIN866' => 'CP866', 'WIN874' => 'CP874', 'WIN1250' => 'CP1250', 'WIN1251' => 'CP1251', 'WIN1252' => 'CP1252', 'WIN1256' => 'CP1256', 'WIN1258' => 'CP1258' ); var $defaultprops = array('', '', ''); // Extra "magic" types. BIGSERIAL was added in PostgreSQL 7.2. var $extraTypes = array('SERIAL', 'BIGSERIAL'); // Foreign key stuff. First element MUST be the default. var $fkactions = array('NO ACTION', 'RESTRICT', 'CASCADE', 'SET NULL', 'SET DEFAULT'); var $fkdeferrable = array('NOT DEFERRABLE', 'DEFERRABLE'); var $fkinitial = array('INITIALLY IMMEDIATE', 'INITIALLY DEFERRED'); var $fkmatches = array('MATCH SIMPLE', 'MATCH FULL'); // Function properties var $funcprops = array( array('', 'VOLATILE', 'IMMUTABLE', 'STABLE'), array('', 'CALLED ON NULL INPUT', 'RETURNS NULL ON NULL INPUT'), array('', 'SECURITY INVOKER', 'SECURITY DEFINER')); // Default help URL var $help_base; // Help sub pages var $help_page; // Name of id column var $id = 'oid'; // Supported join operations for use with view wizard var $joinOps = array('INNER JOIN' => 'INNER JOIN', 'LEFT JOIN' => 'LEFT JOIN', 'RIGHT JOIN' => 'RIGHT JOIN', 'FULL JOIN' => 'FULL JOIN'); // Map of internal language name to syntax highlighting name var $langmap = array( 'sql' => 'SQL', 'plpgsql' => 'SQL', 'php' => 'PHP', 'phpu' => 'PHP', 'plphp' => 'PHP', 'plphpu' => 'PHP', 'perl' => 'Perl', 'perlu' => 'Perl', 'plperl' => 'Perl', 'plperlu' => 'Perl', 'java' => 'Java', 'javau' => 'Java', 'pljava' => 'Java', 'pljavau' => 'Java', 'plj' => 'Java', 'plju' => 'Java', 'python' => 'Python', 'pythonu' => 'Python', 'plpython' => 'Python', 'plpythonu' => 'Python', 'ruby' => 'Ruby', 'rubyu' => 'Ruby', 'plruby' => 'Ruby', 'plrubyu' => 'Ruby' ); // Predefined size types var $predefined_size_types = array('abstime','aclitem','bigserial','boolean','bytea','cid','cidr','circle','date','float4','float8','gtsvector','inet','int2','int4','int8','macaddr','money','oid','path','polygon','refcursor','regclass','regoper','regoperator','regproc','regprocedure','regtype','reltime','serial','smgr','text','tid','tinterval','tsquery','tsvector','varbit','void','xid'); // List of all legal privileges that can be applied to different types // of objects. var $privlist = array( 'table' => array('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'REFERENCES', 'TRIGGER', 'ALL PRIVILEGES'), 'view' => array('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'REFERENCES', 'TRIGGER', 'ALL PRIVILEGES'), 'sequence' => array('SELECT', 'UPDATE', 'ALL PRIVILEGES'), 'database' => array('CREATE', 'TEMPORARY', 'CONNECT', 'ALL PRIVILEGES'), 'function' => array('EXECUTE', 'ALL PRIVILEGES'), 'language' => array('USAGE', 'ALL PRIVILEGES'), 'schema' => array('CREATE', 'USAGE', 'ALL PRIVILEGES'), 'tablespace' => array('CREATE', 'ALL PRIVILEGES'), 'column' => array('SELECT', 'INSERT', 'UPDATE', 'REFERENCES','ALL PRIVILEGES') ); // List of characters in acl lists and the privileges they // refer to. var $privmap = array( 'r' => 'SELECT', 'w' => 'UPDATE', 'a' => 'INSERT', 'd' => 'DELETE', 'D' => 'TRUNCATE', 'R' => 'RULE', 'x' => 'REFERENCES', 't' => 'TRIGGER', 'X' => 'EXECUTE', 'U' => 'USAGE', 'C' => 'CREATE', 'T' => 'TEMPORARY', 'c' => 'CONNECT' ); // Rule action types var $rule_events = array('SELECT', 'INSERT', 'UPDATE', 'DELETE'); // Select operators var $selectOps = array('=' => 'i', '!=' => 'i', '<' => 'i', '>' => 'i', '<=' => 'i', '>=' => 'i', '<<' => 'i', '>>' => 'i', '<<=' => 'i', '>>=' => 'i', 'LIKE' => 'i', 'NOT LIKE' => 'i', 'ILIKE' => 'i', 'NOT ILIKE' => 'i', 'SIMILAR TO' => 'i', 'NOT SIMILAR TO' => 'i', '~' => 'i', '!~' => 'i', '~*' => 'i', '!~*' => 'i', 'IS NULL' => 'p', 'IS NOT NULL' => 'p', 'IN' => 'x', 'NOT IN' => 'x', '@@' => 'i', '@@@' => 'i', '@>' => 'i', '<@' => 'i', '@@ to_tsquery' => 't', '@@@ to_tsquery' => 't', '@> to_tsquery' => 't', '<@ to_tsquery' => 't', '@@ plainto_tsquery' => 't', '@@@ plainto_tsquery' => 't', '@> plainto_tsquery' => 't', '<@ plainto_tsquery' => 't'); // 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'); // How often to execute the trigger var $triggerFrequency = array('ROW','STATEMENT'); // Array of allowed type alignments var $typAligns = array('char', 'int2', 'int4', 'double'); // The default type alignment var $typAlignDef = 'int4'; // Default index type var $typIndexDef = 'BTREE'; // Array of allowed index types var $typIndexes = array('BTREE', 'RTREE', 'GIST', 'GIN', 'HASH'); // Array of allowed type storage attributes var $typStorages = array('plain', 'external', 'extended', 'main'); // The default type storage var $typStorageDef = 'plain'; /** * Constructor * @param $conn The database connection */ function Postgres($conn) { $this->ADODB_base($conn); } // Formatting functions /** * Cleans (escapes) a string * @param $str The string to clean, by reference * @return The cleaned string */ function clean(&$str) { if ($str === null) return null; $str = str_replace("\r\n","\n",$str); 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 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; } /** * 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; } /** * Escapes bytea data for display on the screen * @param $data The bytea data * @return Data formatted for on-screen display */ function escapeBytea($data) { if (function_exists('pg_escape_bytea')) return stripslashes(pg_escape_bytea($data)); else { $translations = array('\\a' => '\\007', '\\b' => '\\010', '\\t' => '\\011', '\\n' => '\\012', '\\v' => '\\013', '\\f' => '\\014', '\\r' => '\\015'); return strtr(addCSlashes($data, "\0..\37\177..\377"), $translations); } } /** * 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 * @param $extras An array of attributes name as key and attributes' values as value */ function printField($name, $value, $type, $extras = array()) { global $lang; // Determine actions string $extra_str = ''; foreach ($extras as $k => $v) { $extra_str .= " {$k}=\"" . htmlspecialchars($v) . "\""; } switch (substr($type,0,9)) { 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 'bytea': case 'bytea[]': $value = $this->escapeBytea($value); case 'text': case 'text[]': case 'xml': case 'xml[]': $n = substr_count($value, "\n"); $n = $n < 5 ? 5 : $n; $n = $n > 20 ? 20 : $n; echo "\n"; break; case 'character': case 'character[]': $n = substr_count($value, "\n"); $n = $n < 5 ? 5 : $n; $n = $n > 20 ? 20 : $n; echo "\n"; break; default: echo "\n"; break; } } /** * Formats a value or expression for sql purposes * @param $type The type of the field * @param $format 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; } } } /** * 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; // If the first character is an underscore, it's an array type $is_array = false; if (substr($typname, 0, 1) == '_') { $is_array = true; $typname = substr($typname, 1); } // 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; // Add array qualifier if it's an array if ($is_array) $temp .= '[]'; return $temp; } // Help functions /** * Fetch a URL (or array of URLs) for a given help page. */ function getHelp($help) { $this->getHelpPages(); if (isset($this->help_page[$help])) { if (is_array($this->help_page[$help])) { $urls = array(); foreach ($this->help_page[$help] as $link) { $urls[] = $this->help_base . $link; } return $urls; } else return $this->help_base . $this->help_page[$help]; } else return null; } function getHelpPages() { include_once('./help/PostgresDoc90.php'); return $this->help_page; } // Database functions /** * 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); } /** * Return all database available on the server * @param $currentdatabase database name that should be on top of the resultset * * @return A list of databases, sorted alphabetically */ function getDatabases($currentdatabase = NULL) { global $conf, $misc; $server_info = $misc->getServerInfo(); if (isset($conf['owned_only']) && $conf['owned_only'] && !$this->isSuperUser($server_info['username'])) { $username = $server_info['username']; $this->clean($username); $clause = " AND pr.rolname='{$username}'"; } else $clause = ''; if ($currentdatabase != NULL) { $this->clean($currentdatabase); $orderby = "ORDER BY pdb.datname = '{$currentdatabase}' DESC, pdb.datname"; } else $orderby = "ORDER BY pdb.datname"; if (!$conf['show_system']) $where = ' AND NOT pdb.datistemplate'; else $where = ' AND pdb.datallowconn'; $sql = " SELECT pdb.datname AS datname, pr.rolname AS datowner, pg_encoding_to_char(encoding) AS datencoding, (SELECT description FROM pg_catalog.pg_shdescription pd WHERE pdb.oid=pd.objoid) AS datcomment, (SELECT spcname FROM pg_catalog.pg_tablespace pt WHERE pt.oid=pdb.dattablespace) AS tablespace, CASE WHEN pg_catalog.has_database_privilege(current_user, pdb.oid, 'CONNECT') THEN pg_catalog.pg_database_size(pdb.oid) ELSE -1 -- set this magic value, which we will convert to no access later END as dbsize, pdb.datcollate, pdb.datctype FROM pg_catalog.pg_database pdb LEFT JOIN pg_catalog.pg_roles pr ON (pdb.datdba = pr.oid) WHERE true {$where} {$clause} {$orderby}"; return $this->selectSet($sql); } /** * Return the database comment of a db from the shared description table * @param string $database the name of the database to get the comment for * @return recordset of the db comment info */ function getDatabaseComment($database) { $this->clean($database); $sql = "SELECT description FROM pg_catalog.pg_database JOIN pg_catalog.pg_shdescription ON (oid=objoid) WHERE pg_database.datname = '{$database}' "; return $this->selectSet($sql); } /** * Return the database owner of a db * @param string $database the name of the database to get the owner for * @return recordset of the db owner info */ function getDatabaseOwner($database) { $this->clean($database); $sql = "SELECT usename FROM pg_user, pg_database WHERE pg_user.usesysid = pg_database.datdba AND pg_database.datname = '{$database}' "; return $this->selectSet($sql); } /** * Returns the current database encoding * @return The encoding. eg. SQL_ASCII, UTF-8, etc. */ function getDatabaseEncoding() { // Try to avoid a query if at all possible (5) if (function_exists('pg_parameter_status')) { $encoding = pg_parameter_status($this->conn->_connectionID, 'server_encoding'); if ($encoding !== false) return $encoding; } $sql = "SELECT getdatabaseencoding() AS encoding"; return $this->selectField($sql, 'encoding'); } /** * Returns the current default_with_oids setting * @return default_with_oids setting */ function getDefaultWithOid() { // Try to avoid a query if at all possible (5) if (function_exists('pg_parameter_status')) { $default = pg_parameter_status($this->conn->_connectionID, 'default_with_oids'); if ($default !== false) return $default; } $sql = "SHOW default_with_oids"; return $this->selectField($sql, 'default_with_oids'); } /** * Creates a database * @param $database The name of the database to create * @param $encoding Encoding of the database * @param $tablespace (optional) The tablespace name * @return 0 success * @return -1 tablespace error * @return -2 comment error */ function createDatabase($database, $encoding, $tablespace = '', $comment = '', $template = 'template1', $lc_collate = '', $lc_ctype = '') { $this->fieldClean($database); $this->clean($encoding); $this->fieldClean($tablespace); $this->fieldClean($template); $this->clean($lc_collate); $this->clean($lc_ctype); $sql = "CREATE DATABASE \"{$database}\" WITH TEMPLATE=\"{$template}\""; if ($encoding != '') $sql .= " ENCODING='{$encoding}'"; if ($lc_collate != '') $sql .= " LC_COLLATE='{$lc_collate}'"; if ($lc_ctype != '') $sql .= " LC_CTYPE='{$lc_ctype}'"; if ($tablespace != '' && $this->hasTablespaces()) $sql .= " TABLESPACE \"{$tablespace}\""; $status = $this->execute($sql); if ($status != 0) return -1; if ($comment != '' && $this->hasSharedComments()) { $status = $this->setComment('DATABASE',$database,'',$comment); if ($status != 0) return -2; } return 0; } /** * Renames a database, note that this operation cannot be * performed on a database that is currently being connected to * @param string $oldName name of database to rename * @param string $newName new name of database * @return int 0 on success */ function alterDatabaseRename($oldName, $newName) { $this->fieldClean($oldName); $this->fieldClean($newName); if ($oldName != $newName) { $sql = "ALTER DATABASE \"{$oldName}\" RENAME TO \"{$newName}\""; return $this->execute($sql); } else //just return success, we're not going to do anything return 0; } /** * 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); } /** * Changes ownership of a database * This can only be done by a superuser or the owner of the database * @param string $dbName database to change ownership of * @param string $newOwner user that will own the database * @return int 0 on success */ function alterDatabaseOwner($dbName, $newOwner) { $this->fieldClean($dbName); $this->fieldClean($newOwner); $sql = "ALTER DATABASE \"{$dbName}\" OWNER TO \"{$newOwner}\""; return $this->execute($sql); } /** * Alters a database * the multiple return vals are for postgres 8+ which support more functionality in alter database * @param $dbName The name of the database * @param $newName new name for the database * @param $newOwner The new owner for the database * @return 0 success * @return -1 transaction error * @return -2 owner error * @return -3 rename error * @return -4 comment error */ function alterDatabase($dbName, $newName, $newOwner = '', $comment = '') { $status = $this->beginTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } if ($dbName != $newName) { $status = $this->alterDatabaseRename($dbName, $newName); if ($status != 0) { $this->rollbackTransaction(); return -3; } $dbName = $newName; } if ($newOwner != '') { $status = $this->alterDatabaseOwner($newName, $newOwner); if ($status != 0) { $this->rollbackTransaction(); return -2; } } $this->fieldClean($dbName); $status = $this->setComment('DATABASE', $dbName, '', $comment); if ($status != 0) { $this->rollbackTransaction(); return -4; } return $this->endTransaction(); } /** * Returns prepared transactions information * @param $database (optional) Find only prepared transactions executed in a specific database * @return A recordset */ function getPreparedXacts($database = null) { if ($database === null) $sql = "SELECT * FROM pg_prepared_xacts"; else { $this->clean($database); $sql = "SELECT transaction, gid, prepared, owner FROM pg_prepared_xacts WHERE database='{$database}' ORDER BY owner"; } return $this->selectSet($sql); } /** * Searches all system catalogs to find objects that match a certain name. * @param $term The search term * @param $filter The object type to restrict to ('' means no restriction) * @return A recordset */ function findObject($term, $filter) { global $conf; /*about escaping: * SET standard_conforming_string is not available before 8.2 * So we must use PostgreSQL specific notation :/ * E'' notation is not available before 8.1 * $$ is available since 8.0 * Nothing specific from 7.4 **/ // Escape search term for ILIKE match $this->clean($term); $this->clean($filter); $term = str_replace('_', '\_', $term); $term = str_replace('%', '\%', $term); // Exclude system relations if necessary if (!$conf['show_system']) { // XXX: The mention of information_schema here is in the wrong place, but // it's the quickest fix to exclude the info schema from 7.4 $where = " AND pn.nspname NOT LIKE \$_PATERN_\$pg\_%\$_PATERN_\$ AND pn.nspname != 'information_schema'"; $lan_where = "AND pl.lanispl"; } else { $where = ''; $lan_where = ''; } // Apply outer filter $sql = ''; if ($filter != '') { $sql = "SELECT * FROM ("; } $term = "\$_PATERN_\$%{$term}%\$_PATERN_\$"; $sql .= " SELECT 'SCHEMA' AS type, oid, NULL AS schemaname, NULL AS relname, nspname AS name FROM pg_catalog.pg_namespace pn WHERE nspname ILIKE {$term} {$where} UNION ALL SELECT CASE WHEN relkind='r' THEN 'TABLE' WHEN relkind='v' THEN 'VIEW' WHEN relkind='S' THEN 'SEQUENCE' END, pc.oid, pn.nspname, NULL, pc.relname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn WHERE pc.relnamespace=pn.oid AND relkind IN ('r', 'v', 'S') AND relname ILIKE {$term} {$where} UNION ALL SELECT CASE WHEN pc.relkind='r' THEN 'COLUMNTABLE' ELSE 'COLUMNVIEW' END, NULL, pn.nspname, pc.relname, pa.attname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn, pg_catalog.pg_attribute pa WHERE pc.relnamespace=pn.oid AND pc.oid=pa.attrelid AND pa.attname ILIKE {$term} AND pa.attnum > 0 AND NOT pa.attisdropped AND pc.relkind IN ('r', 'v') {$where} UNION ALL SELECT 'FUNCTION', pp.oid, pn.nspname, NULL, pp.proname || '(' || pg_catalog.oidvectortypes(pp.proargtypes) || ')' FROM pg_catalog.pg_proc pp, pg_catalog.pg_namespace pn WHERE pp.pronamespace=pn.oid AND NOT pp.proisagg AND pp.proname ILIKE {$term} {$where} UNION ALL SELECT 'INDEX', NULL, pn.nspname, pc.relname, pc2.relname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn, pg_catalog.pg_index pi, pg_catalog.pg_class pc2 WHERE pc.relnamespace=pn.oid AND pc.oid=pi.indrelid AND pi.indexrelid=pc2.oid AND NOT EXISTS ( SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c ON (d.refclassid = c.tableoid AND d.refobjid = c.oid) WHERE d.classid = pc2.tableoid AND d.objid = pc2.oid AND d.deptype = 'i' AND c.contype IN ('u', 'p') ) AND pc2.relname ILIKE {$term} {$where} UNION ALL SELECT 'CONSTRAINTTABLE', NULL, pn.nspname, pc.relname, pc2.conname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn, pg_catalog.pg_constraint pc2 WHERE pc.relnamespace=pn.oid AND pc.oid=pc2.conrelid AND pc2.conrelid != 0 AND CASE WHEN pc2.contype IN ('f', 'c') THEN TRUE ELSE NOT EXISTS ( SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c ON (d.refclassid = c.tableoid AND d.refobjid = c.oid) WHERE d.classid = pc2.tableoid AND d.objid = pc2.oid AND d.deptype = 'i' AND c.contype IN ('u', 'p') ) END AND pc2.conname ILIKE {$term} {$where} UNION ALL SELECT 'CONSTRAINTDOMAIN', pt.oid, pn.nspname, pt.typname, pc.conname FROM pg_catalog.pg_type pt, pg_catalog.pg_namespace pn, pg_catalog.pg_constraint pc WHERE pt.typnamespace=pn.oid AND pt.oid=pc.contypid AND pc.contypid != 0 AND pc.conname ILIKE {$term} {$where} UNION ALL SELECT 'TRIGGER', NULL, pn.nspname, pc.relname, pt.tgname FROM pg_catalog.pg_class pc, pg_catalog.pg_namespace pn, pg_catalog.pg_trigger pt WHERE pc.relnamespace=pn.oid AND pc.oid=pt.tgrelid AND ( pt.tgconstraint = 0 OR NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c ON (d.refclassid = c.tableoid AND d.refobjid = c.oid) WHERE d.classid = pt.tableoid AND d.objid = pt.oid AND d.deptype = 'i' AND c.contype = 'f')) AND pt.tgname ILIKE {$term} {$where} UNION ALL SELECT 'RULETABLE', NULL, pn.nspname AS schemaname, c.relname AS tablename, r.rulename FROM pg_catalog.pg_rewrite r JOIN pg_catalog.pg_class c ON c.oid = r.ev_class LEFT JOIN pg_catalog.pg_namespace pn ON pn.oid = c.relnamespace WHERE c.relkind='r' AND r.rulename != '_RETURN' AND r.rulename ILIKE {$term} {$where} UNION ALL SELECT 'RULEVIEW', NULL, pn.nspname AS schemaname, c.relname AS tablename, r.rulename FROM pg_catalog.pg_rewrite r JOIN pg_catalog.pg_class c ON c.oid = r.ev_class LEFT JOIN pg_catalog.pg_namespace pn ON pn.oid = c.relnamespace WHERE c.relkind='v' AND r.rulename != '_RETURN' AND r.rulename ILIKE {$term} {$where} "; // Add advanced objects if show_advanced is set if ($conf['show_advanced']) { $sql .= " UNION ALL SELECT CASE WHEN pt.typtype='d' THEN 'DOMAIN' ELSE 'TYPE' END, pt.oid, pn.nspname, NULL, pt.typname FROM pg_catalog.pg_type pt, pg_catalog.pg_namespace pn WHERE pt.typnamespace=pn.oid AND typname ILIKE {$term} AND (pt.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = pt.typrelid)) {$where} UNION ALL SELECT 'OPERATOR', po.oid, pn.nspname, NULL, po.oprname FROM pg_catalog.pg_operator po, pg_catalog.pg_namespace pn WHERE po.oprnamespace=pn.oid AND oprname ILIKE {$term} {$where} UNION ALL SELECT 'CONVERSION', pc.oid, pn.nspname, NULL, pc.conname FROM pg_catalog.pg_conversion pc, pg_catalog.pg_namespace pn WHERE pc.connamespace=pn.oid AND conname ILIKE {$term} {$where} UNION ALL SELECT 'LANGUAGE', pl.oid, NULL, NULL, pl.lanname FROM pg_catalog.pg_language pl WHERE lanname ILIKE {$term} {$lan_where} UNION ALL SELECT DISTINCT ON (p.proname) 'AGGREGATE', p.oid, pn.nspname, NULL, p.proname FROM pg_catalog.pg_proc p LEFT JOIN pg_catalog.pg_namespace pn ON p.pronamespace=pn.oid WHERE p.proisagg AND p.proname ILIKE {$term} {$where} UNION ALL SELECT DISTINCT ON (po.opcname) 'OPCLASS', po.oid, pn.nspname, NULL, po.opcname FROM pg_catalog.pg_opclass po, pg_catalog.pg_namespace pn WHERE po.opcnamespace=pn.oid AND po.opcname ILIKE {$term} {$where} "; } // Otherwise just add domains else { $sql .= " UNION ALL SELECT 'DOMAIN', pt.oid, pn.nspname, NULL, pt.typname FROM pg_catalog.pg_type pt, pg_catalog.pg_namespace pn WHERE pt.typnamespace=pn.oid AND pt.typtype='d' AND typname ILIKE {$term} AND (pt.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = pt.typrelid)) {$where} "; } if ($filter != '') { // We use like to make RULE, CONSTRAINT and COLUMN searches work $sql .= ") AS sub WHERE type LIKE '{$filter}%' "; } $sql .= "ORDER BY type, schemaname, relname, name"; return $this->selectSet($sql); } /** * Returns all available variable information. * @return A recordset */ function getVariables() { $sql = "SHOW ALL"; return $this->selectSet($sql); } // Schema functons /** * Return all schemas in the current database. * @return All schemas, sorted alphabetically */ function getSchemas() { global $conf, $slony; if (!$conf['show_system']) { $where = "WHERE nspname NOT LIKE 'pg@_%' ESCAPE '@' AND nspname != 'information_schema'"; if (isset($slony) && $slony->isEnabled()) { $temp = $slony->slony_schema; $this->clean($temp); $where .= " AND nspname != '{$temp}'"; } } else $where = "WHERE nspname !~ '^pg_t(emp_[0-9]+|oast)$'"; $sql = " SELECT pn.nspname, pu.rolname AS nspowner, pg_catalog.obj_description(pn.oid, 'pg_namespace') AS nspcomment FROM pg_catalog.pg_namespace pn LEFT JOIN pg_catalog.pg_roles pu ON (pn.nspowner = pu.oid) {$where} ORDER BY nspname"; return $this->selectSet($sql); } /** * Return all information relating to a schema * @param $schema The name of the schema * @return Schema information */ function getSchemaByName($schema) { $this->clean($schema); $sql = " SELECT nspname, nspowner, r.rolname AS ownername, nspacl, pg_catalog.obj_description(pn.oid, 'pg_namespace') as nspcomment FROM pg_catalog.pg_namespace pn LEFT JOIN pg_roles as r ON pn.nspowner = r.oid WHERE nspname='{$schema}'"; return $this->selectSet($sql); } /** * Sets the current working schema. Will also set Class variable. * @param $schema The the name of the schema to work in * @return 0 success */ function setSchema($schema) { // Get the current schema search path, including 'pg_catalog'. $search_path = $this->getSearchPath(); // Prepend $schema to search path array_unshift($search_path, $schema); $status = $this->setSearchPath($search_path); if ($status == 0) { $this->_schema = $schema; return 0; } else return $status; } /** * Sets the current schema search path * @param $paths An array of schemas in required search order * @return 0 success * @return -1 Array not passed * @return -2 Array must contain at least one item */ function setSearchPath($paths) { if (!is_array($paths)) return -1; elseif (sizeof($paths) == 0) return -2; elseif (sizeof($paths) == 1 && $paths[0] == '') { // Need to handle empty paths in some cases $paths[0] = 'pg_catalog'; } // Loop over all the paths to check that none are empty $temp = array(); foreach ($paths as $schema) { if ($schema != '') $temp[] = $schema; } $this->fieldArrayClean($temp); $sql = 'SET SEARCH_PATH TO "' . implode('","', $temp) . '"'; return $this->execute($sql); } /** * Creates a new schema. * @param $schemaname The name of the schema to create * @param $authorization (optional) The username to create the schema for. * @param $comment (optional) If omitted, defaults to nothing * @return 0 success */ function createSchema($schemaname, $authorization = '', $comment = '') { $this->fieldClean($schemaname); $this->fieldClean($authorization); $sql = "CREATE SCHEMA \"{$schemaname}\""; if ($authorization != '') $sql .= " AUTHORIZATION \"{$authorization}\""; if ($comment != '') { $status = $this->beginTransaction(); if ($status != 0) return -1; } // Create the new schema $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -1; } // Set the comment if ($comment != '') { $status = $this->setComment('SCHEMA', $schemaname, '', $comment); if ($status != 0) { $this->rollbackTransaction(); return -1; } return $this->endTransaction(); } return 0; } /** * Updates a schema. * @param $schemaname The name of the schema to drop * @param $comment The new comment for this schema * @param $owner The new owner for this schema * @return 0 success */ function updateSchema($schemaname, $comment, $name, $owner) { $this->fieldClean($schemaname); $this->fieldClean($name); $this->fieldClean($owner); $status = $this->beginTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } $status = $this->setComment('SCHEMA', $schemaname, '', $comment); if ($status != 0) { $this->rollbackTransaction(); return -1; } $schema_rs = $this->getSchemaByName($schemaname); /* Only if the owner change */ if ($schema_rs->fields['ownername'] != $owner) { $sql = "ALTER SCHEMA \"{$schemaname}\" OWNER TO \"{$owner}\""; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -1; } } // Only if the name has changed if ($name != $schemaname) { $sql = "ALTER SCHEMA \"{$schemaname}\" RENAME TO \"{$name}\""; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -1; } } return $this->endTransaction(); } /** * Drops a schema. * @param $schemaname The name of the schema to drop * @param $cascade True to cascade drop, false to restrict * @return 0 success */ function dropSchema($schemaname, $cascade) { $this->fieldClean($schemaname); $sql = "DROP SCHEMA \"{$schemaname}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } /** * Return the current schema search path * @return Array of schema names */ function getSearchPath() { $sql = 'SELECT current_schemas(false) AS search_path'; return $this->phpArray($this->selectField($sql, 'search_path')); } // Table functions /** * 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 null error **/ function hasObjectID($table) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); $sql = "SELECT relhasoids FROM pg_catalog.pg_class WHERE relname='{$table}' AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}')"; $rs = $this->selectSet($sql); if ($rs->recordCount() != 1) return null; else { $rs->fields['relhasoids'] = $this->phpBool($rs->fields['relhasoids']); return $rs->fields['relhasoids']; } } /** * Returns table information * @param $table The name of the table * @return A recordset */ function getTable($table) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); $sql = " SELECT c.relname, n.nspname, u.usename AS relowner, pg_catalog.obj_description(c.oid, 'pg_class') AS relcomment, (SELECT spcname FROM pg_catalog.pg_tablespace pt WHERE pt.oid=c.reltablespace) AS tablespace FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_user u ON u.usesysid = c.relowner LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'r' AND n.nspname = '{$c_schema}' AND n.oid = c.relnamespace AND c.relname = '{$table}'"; return $this->selectSet($sql); } /** * Return all tables in current database (and schema) * @param $all True to fetch all tables, false for just in current schema * @return All tables, sorted alphabetically */ function getTables($all = false) { $c_schema = $this->_schema; $this->clean($c_schema); if ($all) { // Exclude pg_catalog and information_schema tables $sql = "SELECT schemaname AS nspname, tablename AS relname, tableowner AS relowner FROM pg_catalog.pg_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') ORDER BY schemaname, tablename"; } else { $sql = "SELECT c.relname, pg_catalog.pg_get_userbyid(c.relowner) AS relowner, pg_catalog.obj_description(c.oid, 'pg_class') AS relcomment, reltuples::bigint, (SELECT spcname FROM pg_catalog.pg_tablespace pt WHERE pt.oid=c.reltablespace) AS tablespace FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'r' AND nspname='{$c_schema}' ORDER BY c.relname"; } 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 = '') { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); $this->clean($field); if ($field == '') { // This query is made much more complex by the addition of the 'attisserial' field. // The subquery to get that field checks to see if there is an internally dependent // sequence on the field. $sql = " SELECT a.attname, a.attnum, pg_catalog.format_type(a.atttypid, a.atttypmod) as type, a.atttypmod, a.attnotnull, a.atthasdef, pg_catalog.pg_get_expr(adef.adbin, adef.adrelid, true) as adsrc, a.attstattarget, a.attstorage, t.typstorage, ( SELECT 1 FROM pg_catalog.pg_depend pd, pg_catalog.pg_class pc WHERE pd.objid=pc.oid AND pd.classid=pc.tableoid AND pd.refclassid=pc.tableoid AND pd.refobjid=a.attrelid AND pd.refobjsubid=a.attnum AND pd.deptype='i' AND pc.relkind='S' ) IS NOT NULL AS attisserial, pg_catalog.col_description(a.attrelid, a.attnum) AS comment FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_attrdef adef ON a.attrelid=adef.adrelid AND a.attnum=adef.adnum LEFT JOIN pg_catalog.pg_type t ON a.atttypid=t.oid WHERE a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname='{$table}' AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = '{$c_schema}')) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum"; } else { $sql = " SELECT a.attname, a.attnum, pg_catalog.format_type(a.atttypid, a.atttypmod) as type, pg_catalog.format_type(a.atttypid, NULL) as base_type, a.atttypmod, a.attnotnull, a.atthasdef, pg_catalog.pg_get_expr(adef.adbin, adef.adrelid, true) as adsrc, a.attstattarget, a.attstorage, t.typstorage, pg_catalog.col_description(a.attrelid, a.attnum) AS comment FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_attrdef adef ON a.attrelid=adef.adrelid AND a.attnum=adef.adnum LEFT JOIN pg_catalog.pg_type t ON a.atttypid=t.oid WHERE a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname='{$table}' AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = '{$c_schema}')) AND a.attname = '{$field}'"; } return $this->selectSet($sql); } /** * 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) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); $sql = " SELECT pn.nspname, relname FROM pg_catalog.pg_class pc, pg_catalog.pg_inherits pi, pg_catalog.pg_namespace pn WHERE pc.oid=pi.inhparent AND pc.relnamespace=pn.oid AND pi.inhrelid = (SELECT oid from pg_catalog.pg_class WHERE relname='{$table}' AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = '{$c_schema}')) 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) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); $sql = " SELECT pn.nspname, relname FROM pg_catalog.pg_class pc, pg_catalog.pg_inherits pi, pg_catalog.pg_namespace pn WHERE pc.oid=pi.inhrelid AND pc.relnamespace=pn.oid AND pi.inhparent = (SELECT oid from pg_catalog.pg_class WHERE relname='{$table}' AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = '{$c_schema}')) "; return $this->selectSet($sql); } /** * 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->fields['relname']); $this->fieldClean($t->fields['nspname']); // 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->fields['relowner']) . "\n\n"; // Set schema search path $sql .= "SET search_path = \"{$t->fields['nspname']}\", 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 "; $sql .= "\"{$t->fields['nspname']}\".\"{$t->fields['relname']}\";\n"; $sql .= "CREATE TABLE \"{$t->fields['nspname']}\".\"{$t->fields['relname']}\" (\n"; // Output all table columns $col_comments_sql = ''; // Accumulate comments on columns $num = $atts->recordCount() + $cons->recordCount(); $i = 1; while (!$atts->EOF) { $this->fieldClean($atts->fields['attname']); $sql .= " \"{$atts->fields['attname']}\""; // Dump SERIAL and BIGSERIAL columns correctly if ($this->phpBool($atts->fields['attisserial']) && ($atts->fields['type'] == 'integer' || $atts->fields['type'] == 'bigint')) { if ($atts->fields['type'] == 'integer') $sql .= " SERIAL"; else $sql .= " BIGSERIAL"; } else { $sql .= " " . $this->formatType($atts->fields['type'], $atts->fields['atttypmod']); // Add NOT NULL if necessary if ($this->phpBool($atts->fields['attnotnull'])) $sql .= " NOT NULL"; // Add default if necessary if ($atts->fields['adsrc'] !== null) $sql .= " DEFAULT {$atts->fields['adsrc']}"; } // Output comma or not if ($i < $num) $sql .= ",\n"; else $sql .= "\n"; // Does this column have a comment? if ($atts->fields['comment'] !== null) { $this->clean($atts->fields['comment']); $col_comments_sql .= "COMMENT ON COLUMN \"{$t->fields['relname']}\".\"{$atts->fields['attname']}\" IS '{$atts->fields['comment']}';\n"; } $atts->moveNext(); $i++; } // Output all table constraints while (!$cons->EOF) { $this->fieldClean($cons->fields['conname']); $sql .= " CONSTRAINT \"{$cons->fields['conname']}\" "; // Nasty hack to support pre-7.4 PostgreSQL if ($cons->fields['consrc'] !== null) $sql .= $cons->fields['consrc']; else { switch ($cons->fields['contype']) { case 'p': $keys = $this->getAttributeNames($table, explode(' ', $cons->fields['indkey'])); $sql .= "PRIMARY KEY (" . join(',', $keys) . ")"; break; case 'u': $keys = $this->getAttributeNames($table, explode(' ', $cons->fields['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->fields['relname']); // Qualify the parent table if it's in another schema if ($parents->fields['schemaname'] != $this->_schema) { $this->fieldClean($parents->fields['schemaname']); $sql .= "\"{$parents->fields['schemaname']}\"."; } $sql .= "\"{$parents->fields['relname']}\""; $parents->moveNext(); if (!$parents->EOF) $sql .= ', '; } $sql .= ")"; } */ // Handle WITHOUT OIDS 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->fields['attname']); // Statistics first if ($atts->fields['attstattarget'] >= 0) { if ($first) { $sql .= "\n"; $first = false; } $sql .= "ALTER TABLE ONLY \"{$t->fields['nspname']}\".\"{$t->fields['relname']}\" ALTER COLUMN \"{$atts->fields['attname']}\" SET STATISTICS {$atts->fields['attstattarget']};\n"; } // Then storage if ($atts->fields['attstorage'] != $atts->fields['typstorage']) { switch ($atts->fields['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->fields['nspname']}\".\"{$t->fields['relname']}\" ALTER COLUMN \"{$atts->fields['attname']}\" SET STORAGE {$storage};\n"; } $atts->moveNext(); } // Comment if ($t->fields['relcomment'] !== null) { $this->clean($t->fields['relcomment']); $sql .= "\n-- Comment\n\n"; $sql .= "COMMENT ON TABLE \"{$t->fields['nspname']}\".\"{$t->fields['relname']}\" IS '{$t->fields['relcomment']}';\n"; } // Add comments on columns, if any if ($col_comments_sql != '') $sql .= $col_comments_sql; // 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->fields['nspname']}\".\"{$t->fields['relname']}\" 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->fields['relowner'])) continue; // Change user if necessary if ($this->hasGrantOption() && $v[3] != $t->fields['relowner']) { $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->fields['relname']}\" 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->fields['relowner']) { $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->fields['relowner']) { $grantor = $v[3]; $this->clean($grantor); $sql .= "SET SESSION AUTHORIZATION '{$grantor}';\n"; } $sql .= "GRANT " . join(', ', $v[4]) . " ON \"{$t->fields['relname']}\" 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->fields['relowner']) { $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 $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->fields['inddef'] . ";\n"; $indexes->moveNext(); } } // Triggers $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->fields['tgdef'] !== null) $sql .= $triggers->fields['tgdef']; else $sql .= $this->getTriggerDef($triggers->fields); $sql .= ";\n"; $triggers->moveNext(); } } // Rules $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->fields['definition'] . "\n"; $rules->moveNext(); } } return $sql; } /** * 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 $array An array of '' or '[]' for each type if it's an array or not * @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 * @param $colcomment An array of comments * @param $comment Table comment * @param $tablespace The tablespace name ('' means none/default) * @param $uniquekey An Array indicating the fields that are unique (those indexes that are set) * @param $primarykey An Array indicating the field used for the primarykey (those indexes that are set) * @return 0 success * @return -1 no fields supplied */ function createTable($name, $fields, $field, $type, $array, $length, $notnull, $default, $withoutoids, $colcomment, $tblcomment, $tablespace, $uniquekey, $primarykey) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($name); $status = $this->beginTransaction(); if ($status != 0) return -1; $found = false; $first = true; $comment_sql = ''; //Accumulate comments for the columns $sql = "CREATE TABLE \"{$f_schema}\".\"{$name}\" ("; for ($i = 0; $i < $fields; $i++) { $this->fieldClean($field[$i]); $this->clean($type[$i]); $this->clean($length[$i]); $this->clean($colcomment[$i]); // Skip blank columns - for user convenience if ($field[$i] == '' || $type[$i] == '') continue; // If not the first column, add a comma if (!$first) $sql .= ", "; else $first = false; 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]})"; } // Add array qualifier if necessary if ($array[$i] == '[]') $sql .= '[]'; // Add other qualifiers if (!isset($primarykey[$i])) { if (isset($uniquekey[$i])) $sql .= " UNIQUE"; if (isset($notnull[$i])) $sql .= " NOT NULL"; } if ($default[$i] != '') $sql .= " DEFAULT {$default[$i]}"; if ($colcomment[$i] != '') $comment_sql .= "COMMENT ON COLUMN \"{$name}\".\"{$field[$i]}\" IS '{$colcomment[$i]}';\n"; $found = true; } if (!$found) return -1; // PRIMARY KEY $primarykeycolumns = array(); for ($i = 0; $i < $fields; $i++) { if (isset($primarykey[$i])) { $primarykeycolumns[] = "\"{$field[$i]}\""; } } if (count($primarykeycolumns) > 0) { $sql .= ", PRIMARY KEY (" . implode(", ", $primarykeycolumns) . ")"; } $sql .= ")"; // WITHOUT OIDS if ($withoutoids) $sql .= ' WITHOUT OIDS'; else $sql .= ' WITH OIDS'; // Tablespace if ($this->hasTablespaces() && $tablespace != '') { $this->fieldClean($tablespace); $sql .= " TABLESPACE \"{$tablespace}\""; } $status = $this->execute($sql); if ($status) { $this->rollbackTransaction(); return -1; } if ($tblcomment != '') { $status = $this->setComment('TABLE', '', $name, $tblcomment, true); if ($status) { $this->rollbackTransaction(); return -1; } } if ($comment_sql != '') { $status = $this->execute($comment_sql); if ($status) { $this->rollbackTransaction(); return -1; } } return $this->endTransaction(); } /** * Creates a new table in the database copying attribs and other properties from another table * @param $name The name of the table * @param $like an array giving the schema ans the name of the table from which attribs are copying from: * array( * 'table' => table name, * 'schema' => the schema name, * ) * @param $defaults if true, copy the defaults values as well * @param $constraints if true, copy the constraints as well (CHECK on table & attr) * @param $tablespace The tablespace name ('' means none/default) */ function createTableLike($name, $like, $defaults = false, $constraints = false, $idx = false, $tablespace = '') { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($name); $this->fieldClean($like['schema']); $this->fieldClean($like['table']); $like = "\"{$like['schema']}\".\"{$like['table']}\""; $status = $this->beginTransaction(); if ($status != 0) return -1; $sql = "CREATE TABLE \"{$f_schema}\".\"{$name}\" (LIKE {$like}"; if ($defaults) $sql .= " INCLUDING DEFAULTS"; if ($this->hasCreateTableLikeWithConstraints() && $constraints) $sql .= " INCLUDING CONSTRAINTS"; if ($this->hasCreateTableLikeWithIndexes() && $idx) $sql .= " INCLUDING INDEXES"; $sql .= ")"; if ($this->hasTablespaces() && $tablespace != '') { $this->fieldClean($tablespace); $sql .= " TABLESPACE \"{$tablespace}\""; } $status = $this->execute($sql); if ($status) { $this->rollbackTransaction(); return -1; } return $this->endTransaction(); } /** * Alter a table's name * /!\ this function is called from _alterTable which take care of escaping fields * @param $tblrs The table RecordSet returned by getTable() * @param $name The new table's name * @return 0 success */ function alterTableName($tblrs, $name = null) { /* vars cleaned in _alterTable */ // Rename (only if name has changed) if (!empty($name) && ($name != $tblrs->fields['relname'])) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $sql = "ALTER TABLE \"{$f_schema}\".\"{$tblrs->fields['relname']}\" RENAME TO \"{$name}\""; $status = $this->execute($sql); if ($status == 0) $tblrs->fields['relname'] = $name; else return $status; } return 0; } /** * Alter a table's owner * /!\ this function is called from _alterTable which take care of escaping fields * @param $tblrs The table RecordSet returned by getTable() * @param $name The new table's owner * @return 0 success */ function alterTableOwner($tblrs, $owner = null) { /* vars cleaned in _alterTable */ if (!empty($owner) && ($tblrs->fields['relowner'] != $owner)) { $f_schema = $this->_schema; $this->fieldClean($f_schema); // If owner has been changed, then do the alteration. We are // careful to avoid this generally as changing owner is a // superuser only function. $sql = "ALTER TABLE \"{$f_schema}\".\"{$tblrs->fields['relname']}\" OWNER TO \"{$owner}\""; return $this->execute($sql); } return 0; } /** * Alter a table's tablespace * /!\ this function is called from _alterTable which take care of escaping fields * @param $tblrs The table RecordSet returned by getTable() * @param $name The new table's tablespace * @return 0 success */ function alterTableTablespace($tblrs, $tablespace = null) { /* vars cleaned in _alterTable */ if (!empty($tablespace) && ($tblrs->fields['tablespace'] != $tablespace)) { $f_schema = $this->_schema; $this->fieldClean($f_schema); // If tablespace has been changed, then do the alteration. We // don't want to do this unnecessarily. $sql = "ALTER TABLE \"{$f_schema}\".\"{$tblrs->fields['relname']}\" SET TABLESPACE \"{$tablespace}\""; return $this->execute($sql); } return 0; } /** * Alter a table's schema * /!\ this function is called from _alterTable which take care of escaping fields * @param $tblrs The table RecordSet returned by getTable() * @param $name The new table's schema * @return 0 success */ function alterTableSchema($tblrs, $schema = null) { /* vars cleaned in _alterTable */ if (!empty($schema) && ($tblrs->fields['nspname'] != $schema)) { $f_schema = $this->_schema; $this->fieldClean($f_schema); // If tablespace has been changed, then do the alteration. We // don't want to do this unnecessarily. $sql = "ALTER TABLE \"{$f_schema}\".\"{$tblrs->fields['relname']}\" SET SCHEMA \"{$schema}\""; return $this->execute($sql); } return 0; } /** * Protected method which alter a table * SHOULDN'T BE CALLED OUTSIDE OF A TRANSACTION * @param $tblrs The table recordSet returned by getTable() * @param $name The new name for the table * @param $owner The new owner for the table * @param $schema The new schema for the table * @param $comment The comment on the table * @param $tablespace The new tablespace for the table ('' means leave as is) * @return 0 success * @return -3 rename error * @return -4 comment error * @return -5 owner error * @return -6 tablespace error * @return -7 schema error */ protected function _alterTable($tblrs, $name, $owner, $schema, $comment, $tablespace) { $this->fieldArrayClean($tblrs->fields); // Comment $status = $this->setComment('TABLE', '', $tblrs->fields['relname'], $comment); if ($status != 0) return -4; // Owner $this->fieldClean($owner); $status = $this->alterTableOwner($tblrs, $owner); if ($status != 0) return -5; // Tablespace $this->fieldClean($tablespace); $status = $this->alterTableTablespace($tblrs, $tablespace); if ($status != 0) return -6; // Rename $this->fieldClean($name); $status = $this->alterTableName($tblrs, $name); if ($status != 0) return -3; // Schema $this->fieldClean($schema); $status = $this->alterTableSchema($tblrs, $schema); if ($status != 0) return -7; return 0; } /** * Alter table properties * @param $table The name of the table * @param $name The new name for the table * @param $owner The new owner for the table * @param $schema The new schema for the table * @param $comment The comment on the table * @param $tablespace The new tablespace for the table ('' means leave as is) * @return 0 success * @return -1 transaction error * @return -2 get existing table error * @return $this->_alterTable error code */ function alterTable($table, $name, $owner, $schema, $comment, $tablespace) { $data = $this->getTable($table); if ($data->recordCount() != 1) return -2; $status = $this->beginTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } $status = $this->_alterTable($data, $name, $owner, $schema, $comment, $tablespace); if ($status != 0) { $this->rollbackTransaction(); return $status; } return $this->endTransaction(); } /** * Returns the SQL for changing the current user * @param $user The user to change to * @return The SQL */ function getChangeUserSQL($user) { $this->clean($user); return "SET SESSION AUTHORIZATION '{$user}';"; } /** * Given an array of attnums and a relation, returns an array mapping * attribute 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) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); $this->arrayClean($atts); if (!is_array($atts)) return -1; if (sizeof($atts) == 0) return array(); $sql = "SELECT attnum, attname FROM pg_catalog.pg_attribute WHERE attrelid=(SELECT oid FROM pg_catalog.pg_class WHERE relname='{$table}' AND relnamespace=(SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}')) AND attnum IN ('" . join("','", $atts) . "')"; $rs = $this->selectSet($sql); if ($rs->recordCount() != sizeof($atts)) { return -2; } else { $temp = array(); while (!$rs->EOF) { $temp[$rs->fields['attnum']] = $rs->fields['attname']; $rs->moveNext(); } return $temp; } } /** * Empties a table in the database * @param $table The table to be emptied * @return 0 success */ function emptyTable($table) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $sql = "DELETE FROM \"{$f_schema}\".\"{$table}\""; return $this->execute($sql); } /** * 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) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $sql = "DROP TABLE \"{$f_schema}\".\"{$table}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } /** * 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 $array True if array type, false otherwise * @param $notnull True if NOT NULL, false otherwise * @param $default The default for the column. '' for none. * @param $length The optional size of the column (ie. 30 for varchar(30)) * @return 0 success */ function addColumn($table, $column, $type, $array, $length, $notnull, $default, $comment) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $this->fieldClean($column); $this->clean($type); $this->clean($length); if ($length == '') $sql = "ALTER TABLE \"{$f_schema}\".\"{$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 \"{$f_schema}\".\"{$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 \"{$f_schema}\".\"{$table}\" ADD COLUMN \"{$column}\" time({$length}){$qual}"; break; default: $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ADD COLUMN \"{$column}\" {$type}({$length})"; } } // Add array qualifier, if requested if ($array) $sql .= '[]'; // If we have advanced column adding, add the extra qualifiers if ($this->hasCreateFieldWithConstraints()) { // NOT NULL clause if ($notnull) $sql .= ' NOT NULL'; // DEFAULT clause if ($default != '') $sql .= ' DEFAULT ' . $default; } $status = $this->beginTransaction(); if ($status != 0) return -1; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -1; } $status = $this->setComment('COLUMN', $column, $table, $comment); if ($status != 0) { $this->rollbackTransaction(); return -1; } return $this->endTransaction(); } /** * 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 $oldnotnull (boolean) True if column is already not null, false otherwise * @param $default The new default for the column * @param $olddefault The old default for the column * @param $type The new type for the column * @param $array True if array type, false otherwise * @param $length The optional size of the column (ie. 30 for varchar(30)) * @param $oldtype The old type for the column * @param $comment Comment for the column * @return 0 success * @return -1 batch alteration failed * @return -4 rename column error * @return -5 comment error * @return -6 transaction error */ function alterColumn($table, $column, $name, $notnull, $oldnotnull, $default, $olddefault, $type, $length, $array, $oldtype, $comment) { // Begin transaction $status = $this->beginTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -6; } // Rename the column, if it has been changed if ($column != $name) { $status = $this->renameColumn($table, $column, $name); if ($status != 0) { $this->rollbackTransaction(); return -4; } } $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($name); $this->fieldClean($table); $this->fieldClean($column); $toAlter = array(); // Create the command for changing nullability if ($notnull != $oldnotnull) { $toAlter[] = "ALTER COLUMN \"{$name}\" ". (($notnull) ? 'SET' : 'DROP') . " NOT NULL"; } // Add default, if it has changed if ($default != $olddefault) { if ($default == '') { $toAlter[] = "ALTER COLUMN \"{$name}\" DROP DEFAULT"; } else { $toAlter[] = "ALTER COLUMN \"{$name}\" SET DEFAULT {$default}"; } } // Add type, if it has changed if ($length == '') $ftype = $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); $ftype = "timestamp({$length}){$qual}"; break; case 'time with time zone': case 'time without time zone': $qual = substr($type, 4); $ftype = "time({$length}){$qual}"; break; default: $ftype = "{$type}({$length})"; } } // Add array qualifier, if requested if ($array) $ftype .= '[]'; if ($ftype != $oldtype) { $toAlter[] = "ALTER COLUMN \"{$name}\" TYPE {$ftype}"; } // Attempt to process the batch alteration, if anything has been changed if (!empty($toAlter)) { // Initialise an empty SQL string $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" " . implode(',', $toAlter); $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -1; } } // Update the comment on the column $status = $this->setComment('COLUMN', $name, $table, $comment); if ($status != 0) { $this->rollbackTransaction(); return -5; } 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) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $this->fieldClean($column); $this->fieldClean($newName); $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" RENAME COLUMN \"{$column}\" TO \"{$newName}\""; 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) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $this->fieldClean($column); $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ALTER COLUMN \"{$column}\" SET DEFAULT {$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 */ function setColumnNull($table, $column, $state) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $this->fieldClean($column); $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ALTER COLUMN \"{$column}\" " . (($state) ? 'DROP' : 'SET') . " NOT NULL"; return $this->execute($sql); } /** * 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 */ function dropColumn($table, $column, $cascade) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $this->fieldClean($column); $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" DROP COLUMN \"{$column}\""; if ($cascade) $sql .= " CASCADE"; 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) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $this->fieldClean($column); $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ALTER COLUMN \"{$column}\" DROP DEFAULT"; return $this->execute($sql); } /** * 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; } // Set extra_float_digits to 2 $sql = "SET extra_float_digits TO 2"; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -1; } return 0; } /** * Ends the data object for a dump. * @return 0 success */ function endDump() { return $this->endTransaction(); } /** * Returns a recordset of all columns in a relation. Used for data export. * @@ Note: Really needs to use a cursor * @param $relation The name of a relation * @return A recordset on success * @return -1 Failed to set datestyle */ function dumpRelation($relation, $oids) { $this->fieldClean($relation); // Actually retrieve the rows if ($oids) $oid_str = $this->id . ', '; else $oid_str = ''; return $this->selectSet("SELECT {$oid_str}* FROM \"{$relation}\""); } /** * Returns all available autovacuum per table information. * @param $table if given, return autovacuum info for the given table or return all informations for all table * * @return A recordset */ function getTableAutovacuum($table='') { $sql = ''; if ($table !== '') { $this->clean($table); $c_schema = $this->_schema; $this->clean($c_schema); $sql = "SELECT c.oid, nspname, relname, pg_catalog.array_to_string(reloptions, E',') AS reloptions FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'r'::\"char\" AND n.nspname NOT IN ('pg_catalog','information_schema') AND c.reloptions IS NOT NULL AND c.relname = '{$table}' AND n.nspname = '{$c_schema}' ORDER BY nspname, relname"; } else { $sql = "SELECT c.oid, nspname, relname, pg_catalog.array_to_string(reloptions, E',') AS reloptions FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'r'::\"char\" AND n.nspname NOT IN ('pg_catalog','information_schema') AND c.reloptions IS NOT NULL ORDER BY nspname, relname"; } /* tmp var to parse the results */ $_autovacs = $this->selectSet($sql); /* result aray to return as RS */ $autovacs = array(); while (!$_autovacs->EOF) { $_ = array( 'nspname' => $_autovacs->fields['nspname'], 'relname' => $_autovacs->fields['relname'] ); foreach (explode(',', $_autovacs->fields['reloptions']) AS $var) { list($o, $v) = explode('=', $var); $_[$o] = $v; } $autovacs[] = $_; $_autovacs->moveNext(); } include_once('./classes/ArrayRecordSet.php'); return new ArrayRecordSet($autovacs); } // Row functions /** * 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; $c_schema = $this->_schema; $this->clean($c_schema); $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_catalog.pg_index WHERE indisunique AND indrelid=( SELECT oid FROM pg_catalog.pg_class WHERE relname='{$table}' AND relnamespace=( SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}' ) ) AND indpred IS NULL AND indexprs IS NULL 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->fields['indkey'])); if (!is_array($attnames)) { $this->rollbackTransaction(); return -1; } else { $this->endTransaction(); return $attnames; } } } /** * Adds a new row to a table * @param $table The table in which to insert * @param $fields Array of given field in values * @param $values Array of new values for the row * @param $nulls An array mapping column => something if it is to be null * @param $format An array of the data type (VALUE or EXPRESSION) * @param $types An array of field types * @return 0 success * @return -1 invalid parameters */ function insertRow($table, $fields, $values, $nulls, $format, $types) { if (!is_array($fields) || !is_array($values) || !is_array($nulls) || !is_array($format) || !is_array($types) || (count($fields) != count($values)) ) { return -1; } else { // Build clause if (count($values) > 0) { // Escape all field names $fields = array_map(array('Postgres','fieldClean'), $fields); $f_schema = $this->_schema; $this->fieldClean($table); $this->fieldClean($f_schema); $sql = ''; foreach($values as $i => $value) { // Handle NULL values if (isset($nulls[$i])) $sql .= ',NULL'; else $sql .= ',' . $this->formatValue($types[$i], $format[$i], $value); } $sql = "INSERT INTO \"{$f_schema}\".\"{$table}\" (\"". implode('","', $fields) ."\") VALUES (". substr($sql, 1) .")"; return $this->execute($sql); } } return -1; } /** * Updates a row in a table * @param $table The table in which to update * @param $vars An array mapping new values for the row * @param $nulls An array mapping column => something if it is to be null * @param $format An array of the data type (VALUE or EXPRESSION) * @param $types An array of field types * @param $keyarr An array mapping column => value to update * @return 0 success * @return -1 invalid parameters */ function editRow($table, $vars, $nulls, $format, $types, $keyarr) { if (!is_array($vars) || !is_array($nulls) || !is_array($format) || !is_array($types)) return -1; else { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); // Build clause if (sizeof($vars) > 0) { foreach($vars as $key => $value) { $this->fieldClean($key); // Handle NULL values if (isset($nulls[$key])) $tmp = 'NULL'; else $tmp = $this->formatValue($types[$key], $format[$key], $value); if (isset($sql)) $sql .= ", \"{$key}\"={$tmp}"; else $sql = "UPDATE \"{$f_schema}\".\"{$table}\" SET \"{$key}\"={$tmp}"; } $first = true; foreach ($keyarr as $k => $v) { $this->fieldClean($k); $this->clean($v); if ($first) { $sql .= " WHERE \"{$k}\"='{$v}'"; $first = false; } else $sql .= " AND \"{$k}\"='{$v}'"; } } // Begin transaction. We do this so that we can ensure only one row is // edited $status = $this->beginTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } $status = $this->execute($sql); if ($status != 0) { // update failed $this->rollbackTransaction(); return -1; } elseif ($this->conn->Affected_Rows() != 1) { // more than one row could be updated $this->rollbackTransaction(); return -2; } // End transaction return $this->endTransaction(); } } /** * Delete a row from a table * @param $table The table from which to delete * @param $key An array mapping column => value to delete * @return 0 success */ function deleteRow($table, $key, $schema=false) { if (!is_array($key)) return -1; else { // Begin transaction. We do this so that we can ensure only one row is // deleted $status = $this->beginTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } if ($schema === false) $schema = $this->_schema; $status = $this->delete($table, $key, $schema); if ($status != 0 || $this->conn->Affected_Rows() != 1) { $this->rollbackTransaction(); return -2; } // End transaction return $this->endTransaction(); } } // Sequence functions /** * Returns properties of a single sequence * @param $sequence Sequence name * @return A recordset */ function getSequence($sequence) { $c_schema = $this->_schema; $this->clean($c_schema); $c_sequence = $sequence; $this->fieldClean($sequence); $this->clean($c_sequence); $sql = " SELECT c.relname AS seqname, s.*, pg_catalog.obj_description(s.tableoid, 'pg_class') AS seqcomment, u.usename AS seqowner, n.nspname FROM \"{$sequence}\" AS s, pg_catalog.pg_class c, pg_catalog.pg_user u, pg_catalog.pg_namespace n WHERE c.relowner=u.usesysid AND c.relnamespace=n.oid AND c.relname = '{$c_sequence}' AND c.relkind = 'S' AND n.nspname='{$c_schema}' AND n.oid = c.relnamespace"; return $this->selectSet( $sql ); } /** * Returns all sequences in the current database * @return A recordset */ function getSequences($all = false) { if ($all) { // Exclude pg_catalog and information_schema tables $sql = "SELECT n.nspname, c.relname AS seqname, u.usename AS seqowner FROM pg_catalog.pg_class c, pg_catalog.pg_user u, pg_catalog.pg_namespace n WHERE c.relowner=u.usesysid AND c.relnamespace=n.oid AND c.relkind = 'S' AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') ORDER BY nspname, seqname"; } else { $c_schema = $this->_schema; $this->clean($c_schema); $sql = "SELECT c.relname AS seqname, u.usename AS seqowner, pg_catalog.obj_description(c.oid, 'pg_class') AS seqcomment, (SELECT spcname FROM pg_catalog.pg_tablespace pt WHERE pt.oid=c.reltablespace) AS tablespace FROM pg_catalog.pg_class c, pg_catalog.pg_user u, pg_catalog.pg_namespace n WHERE c.relowner=u.usesysid AND c.relnamespace=n.oid AND c.relkind = 'S' AND n.nspname='{$c_schema}' ORDER BY seqname"; } return $this->selectSet( $sql ); } /** * Execute nextval on a given sequence * @param $sequence Sequence name * @return 0 success * @return -1 sequence not found */ function nextvalSequence($sequence) { /* This double-cleaning is deliberate */ $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->clean($f_schema); $this->fieldClean($sequence); $this->clean($sequence); $sql = "SELECT pg_catalog.NEXTVAL('\"{$f_schema}\".\"{$sequence}\"')"; return $this->execute($sql); } /** * Execute setval on a given sequence * @param $sequence Sequence name * @param $nextvalue The next value * @return 0 success * @return -1 sequence not found */ function setvalSequence($sequence, $nextvalue) { /* This double-cleaning is deliberate */ $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->clean($f_schema); $this->fieldClean($sequence); $this->clean($sequence); $this->clean($nextvalue); $sql = "SELECT pg_catalog.SETVAL('\"{$f_schema}\".\"{$sequence}\"', '{$nextvalue}')"; return $this->execute($sql); } /** * Restart a given sequence to its start value * @param $sequence Sequence name * @return 0 success * @return -1 sequence not found */ function restartSequence($sequence) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($sequence); $sql = "ALTER SEQUENCE \"{$f_schema}\".\"{$sequence}\" RESTART;"; 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->fields['min_value']; $f_schema = $this->_schema; $this->fieldClean($f_schema); /* This double-cleaning is deliberate */ $this->fieldClean($sequence); $this->clean($sequence); $sql = "SELECT pg_catalog.SETVAL('\"{$f_schema}\".\"{$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 * @param $cachevalue The cache value * @param $cycledvalue True if cycled, false otherwise * @return 0 success */ function createSequence($sequence, $increment, $minvalue, $maxvalue, $startvalue, $cachevalue, $cycledvalue) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($sequence); $this->clean($increment); $this->clean($minvalue); $this->clean($maxvalue); $this->clean($startvalue); $this->clean($cachevalue); $sql = "CREATE SEQUENCE \"{$f_schema}\".\"{$sequence}\""; if ($increment != '') $sql .= " INCREMENT {$increment}"; if ($minvalue != '') $sql .= " MINVALUE {$minvalue}"; if ($maxvalue != '') $sql .= " MAXVALUE {$maxvalue}"; if ($startvalue != '') $sql .= " START {$startvalue}"; if ($cachevalue != '') $sql .= " CACHE {$cachevalue}"; if ($cycledvalue) $sql .= " CYCLE"; return $this->execute($sql); } /** * Rename a sequence * @param $seqrs The sequence RecordSet returned by getSequence() * @param $name The new name for the sequence * @return 0 success */ function alterSequenceName($seqrs, $name) { /* vars are cleaned in _alterSequence */ if (!empty($name) && ($seqrs->fields['seqname'] != $name)) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $sql = "ALTER SEQUENCE \"{$f_schema}\".\"{$seqrs->fields['seqname']}\" RENAME TO \"{$name}\""; $status = $this->execute($sql); if ($status == 0) $seqrs->fields['seqname'] = $name; else return $status; } return 0; } /** * Alter a sequence's owner * @param $seqrs The sequence RecordSet returned by getSequence() * @param $name The new owner for the sequence * @return 0 success */ function alterSequenceOwner($seqrs, $owner) { // If owner has been changed, then do the alteration. We are // careful to avoid this generally as changing owner is a // superuser only function. /* vars are cleaned in _alterSequence */ if (!empty($owner) && ($seqrs->fields['seqowner'] != $owner)) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $sql = "ALTER SEQUENCE \"{$f_schema}\".\"{$seqrs->fields['seqname']}\" OWNER TO \"{$owner}\""; return $this->execute($sql); } return 0; } /** * Alter a sequence's schema * @param $seqrs The sequence RecordSet returned by getSequence() * @param $name The new schema for the sequence * @return 0 success */ function alterSequenceSchema($seqrs, $schema) { /* vars are cleaned in _alterSequence */ if (!empty($schema) && ($seqrs->fields['nspname'] != $schema)) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $sql = "ALTER SEQUENCE \"{$f_schema}\".\"{$seqrs->fields['seqname']}\" SET SCHEMA {$schema}"; return $this->execute($sql); } return 0; } /** * Alter a sequence's properties * @param $seqrs The sequence RecordSet returned by getSequence() * @param $increment The sequence incremental value * @param $minvalue The sequence minimum value * @param $maxvalue The sequence maximum value * @param $restartvalue The sequence current value * @param $cachevalue The sequence cache value * @param $cycledvalue Sequence can cycle ? * @param $startvalue The sequence start value when issueing a restart * @return 0 success */ function alterSequenceProps($seqrs, $increment, $minvalue, $maxvalue, $restartvalue, $cachevalue, $cycledvalue, $startvalue) { $sql = ''; /* vars are cleaned in _alterSequence */ if (!empty($increment) && ($increment != $seqrs->fields['increment_by'])) $sql .= " INCREMENT {$increment}"; if (!empty($minvalue) && ($minvalue != $seqrs->fields['min_value'])) $sql .= " MINVALUE {$minvalue}"; if (!empty($maxvalue) && ($maxvalue != $seqrs->fields['max_value'])) $sql .= " MAXVALUE {$maxvalue}"; if (!empty($restartvalue) && ($restartvalue != $seqrs->fields['last_value'])) $sql .= " RESTART {$restartvalue}"; if (!empty($cachevalue) && ($cachevalue != $seqrs->fields['cache_value'])) $sql .= " CACHE {$cachevalue}"; if (!empty($startvalue) && ($startvalue != $seqrs->fields['start_value'])) $sql .= " START {$startvalue}"; // toggle cycle yes/no if (!is_null($cycledvalue)) $sql .= (!$cycledvalue ? ' NO ' : '') . " CYCLE"; if ($sql != '') { $f_schema = $this->_schema; $this->fieldClean($f_schema); $sql = "ALTER SEQUENCE \"{$f_schema}\".\"{$seqrs->fields['seqname']}\" {$sql}"; return $this->execute($sql); } return 0; } /** * Protected method which alter a sequence * SHOULDN'T BE CALLED OUTSIDE OF A TRANSACTION * @param $seqrs The sequence recordSet returned by getSequence() * @param $name The new name for the sequence * @param $comment The comment on the sequence * @param $owner The new owner for the sequence * @param $schema The new schema for the sequence * @param $increment The increment * @param $minvalue The min value * @param $maxvalue The max value * @param $restartvalue The starting value * @param $cachevalue The cache value * @param $cycledvalue True if cycled, false otherwise * @param $startvalue The sequence start value when issueing a restart * @return 0 success * @return -3 rename error * @return -4 comment error * @return -5 owner error * @return -6 get sequence props error * @return -7 schema error */ protected function _alterSequence($seqrs, $name, $comment, $owner, $schema, $increment, $minvalue, $maxvalue, $restartvalue, $cachevalue, $cycledvalue, $startvalue) { $this->fieldArrayClean($seqrs->fields); // Comment $status = $this->setComment('SEQUENCE', $seqrs->fields['seqname'], '', $comment); if ($status != 0) return -4; // Owner $this->fieldClean($owner); $status = $this->alterSequenceOwner($seqrs, $owner); if ($status != 0) return -5; // Props $this->clean($increment); $this->clean($minvalue); $this->clean($maxvalue); $this->clean($restartvalue); $this->clean($cachevalue); $this->clean($cycledvalue); $this->clean($startvalue); $status = $this->alterSequenceProps($seqrs, $increment, $minvalue, $maxvalue, $restartvalue, $cachevalue, $cycledvalue, $startvalue); if ($status != 0) return -6; // Rename $this->fieldClean($name); $status = $this->alterSequenceName($seqrs, $name); if ($status != 0) return -3; // Schema $this->clean($schema); $status = $this->alterSequenceSchema($seqrs, $schema); if ($status != 0) return -7; return 0; } /** * Alters a sequence * @param $sequence The name of the sequence * @param $name The new name for the sequence * @param $comment The comment on the sequence * @param $owner The new owner for the sequence * @param $schema The new schema for the sequence * @param $increment The increment * @param $minvalue The min value * @param $maxvalue The max value * @param $restartvalue The starting value * @param $cachevalue The cache value * @param $cycledvalue True if cycled, false otherwise * @param $startvalue The sequence start value when issueing a restart * @return 0 success * @return -1 transaction error * @return -2 get existing sequence error * @return $this->_alterSequence error code */ function alterSequence($sequence, $name, $comment, $owner=null, $schema=null, $increment=null, $minvalue=null, $maxvalue=null, $restartvalue=null, $cachevalue=null, $cycledvalue=null, $startvalue=null) { $this->fieldClean($sequence); $data = $this->getSequence($sequence); if ($data->recordCount() != 1) return -2; $status = $this->beginTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } $status = $this->_alterSequence($data, $name, $comment, $owner, $schema, $increment, $minvalue, $maxvalue, $restartvalue, $cachevalue, $cycledvalue, $startvalue); if ($status != 0) { $this->rollbackTransaction(); return $status; } return $this->endTransaction(); } /** * Drops a given sequence * @param $sequence Sequence name * @param $cascade True to cascade drop, false to restrict * @return 0 success */ function dropSequence($sequence, $cascade) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($sequence); $sql = "DROP SEQUENCE \"{$f_schema}\".\"{$sequence}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } // View functions /** * Returns all details for a particular view * @param $view The name of the view to retrieve * @return View info */ function getView($view) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($view); $sql = " SELECT c.relname, n.nspname, pg_catalog.pg_get_userbyid(c.relowner) AS relowner, pg_catalog.pg_get_viewdef(c.oid, true) AS vwdefinition, pg_catalog.obj_description(c.oid, 'pg_class') AS relcomment FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) WHERE (c.relname = '{$view}') AND n.nspname='{$c_schema}'"; return $this->selectSet($sql); } /** * Returns a list of all views in the database * @return All views */ function getViews() { $c_schema = $this->_schema; $this->clean($c_schema); $sql = " SELECT c.relname, pg_catalog.pg_get_userbyid(c.relowner) AS relowner, pg_catalog.obj_description(c.oid, 'pg_class') AS relcomment FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) WHERE (n.nspname='{$c_schema}') AND (c.relkind = 'v'::\"char\") ORDER BY relname"; return $this->selectSet($sql); } /** * Updates a view. * @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,$comment) { return $this->createView($viewname, $definition, true, $comment); } /** * 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, $comment) { $status = $this->beginTransaction(); if ($status != 0) return -1; $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($viewname); // Note: $definition not cleaned $sql = "CREATE "; if ($replace) $sql .= "OR REPLACE "; $sql .= "VIEW \"{$f_schema}\".\"{$viewname}\" AS {$definition}"; $status = $this->execute($sql); if ($status) { $this->rollbackTransaction(); return -1; } if ($comment != '') { $status = $this->setComment('VIEW', $viewname, '', $comment); if ($status) { $this->rollbackTransaction(); return -1; } } return $this->endTransaction(); } /** * Rename a view * @param $vwrs The view recordSet returned by getView() * @param $name The new view's name * @return 0 success */ function alterViewName($vwrs, $name) { // Rename (only if name has changed) /* $vwrs and $name are cleaned in _alterView */ if (!empty($name) && ($name != $vwrs->fields['relname'])) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $sql = "ALTER VIEW \"{$f_schema}\".\"{$vwrs->fields['relname']}\" RENAME TO \"{$name}\""; $status = $this->execute($sql); if ($status == 0) $vwrs->fields['relname'] = $name; else return $status; } return 0; } /** * Alter a view's owner * @param $vwrs The view recordSet returned by getView() * @param $name The new view's owner * @return 0 success */ function alterViewOwner($vwrs, $owner = null) { /* $vwrs and $owner are cleaned in _alterView */ if ((!empty($owner)) && ($vwrs->fields['relowner'] != $owner)) { $f_schema = $this->_schema; $this->fieldClean($f_schema); // If owner has been changed, then do the alteration. We are // careful to avoid this generally as changing owner is a // superuser only function. $sql = "ALTER TABLE \"{$f_schema}\".\"{$vwrs->fields['relname']}\" OWNER TO \"{$owner}\""; return $this->execute($sql); } return 0; } /** * Alter a view's schema * @param $vwrs The view recordSet returned by getView() * @param $name The new view's schema * @return 0 success */ function alterViewSchema($vwrs, $schema) { /* $vwrs and $schema are cleaned in _alterView */ if (!empty($schema) && ($vwrs->fields['nspname'] != $schema)) { $f_schema = $this->_schema; $this->fieldClean($f_schema); // If tablespace has been changed, then do the alteration. We // don't want to do this unnecessarily. $sql = "ALTER TABLE \"{$f_schema}\".\"{$vwrs->fields['relname']}\" SET SCHEMA \"{$schema}\""; return $this->execute($sql); } return 0; } /** * Protected method which alter a view * SHOULDN'T BE CALLED OUTSIDE OF A TRANSACTION * @param $vwrs The view recordSet returned by getView() * @param $name The new name for the view * @param $owner The new owner for the view * @param $comment The comment on the view * @return 0 success * @return -3 rename error * @return -4 comment error * @return -5 owner error * @return -6 schema error */ protected function _alterView($vwrs, $name, $owner, $schema, $comment) { $this->fieldArrayClean($vwrs->fields); // Comment if ($this->setComment('VIEW', $vwrs->fields['relname'], '', $comment) != 0) return -4; // Owner $this->fieldClean($owner); $status = $this->alterViewOwner($vwrs, $owner); if ($status != 0) return -5; // Rename $this->fieldClean($name); $status = $this->alterViewName($vwrs, $name); if ($status != 0) return -3; // Schema $this->fieldClean($schema); $status = $this->alterViewSchema($vwrs, $schema); if ($status != 0) return -6; return 0; } /** * Alter view properties * @param $view The name of the view * @param $name The new name for the view * @param $owner The new owner for the view * @param $schema The new schema for the view * @param $comment The comment on the view * @return 0 success * @return -1 transaction error * @return -2 get existing view error * @return $this->_alterView error code */ function alterView($view, $name, $owner, $schema, $comment) { $data = $this->getView($view); if ($data->recordCount() != 1) return -2; $status = $this->beginTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } $status = $this->_alterView($data, $name, $owner, $schema, $comment); if ($status != 0) { $this->rollbackTransaction(); return $status; } return $this->endTransaction(); } /** * 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) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($viewname); $sql = "DROP VIEW \"{$f_schema}\".\"{$viewname}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } // Index functions /** * Grabs a list of indexes for a table * @param $table The name of a table whose indexes to retrieve * @param $unique Only get unique/pk indexes * @return A recordset */ function getIndexes($table = '', $unique = false) { $this->clean($table); $sql = " SELECT c2.relname AS indname, i.indisprimary, i.indisunique, i.indisclustered, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true) AS inddef FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i WHERE c.relname = '{$table}' AND pg_catalog.pg_table_is_visible(c.oid) AND c.oid = i.indrelid AND i.indexrelid = c2.oid "; if ($unique) $sql .= " AND i.indisunique "; $sql .= " ORDER BY c2.relname"; return $this->selectSet($sql); } /** * test if a table has been clustered on an index * @param $table The table to test * * @return true if the table has been already clustered */ function alreadyClustered($table) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); $sql = "SELECT i.indisclustered FROM pg_catalog.pg_class c, pg_catalog.pg_index i WHERE c.relname = '{$table}' AND c.oid = i.indrelid AND i.indisclustered AND c.relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}') "; $v = $this->selectSet($sql); if ($v->recordCount() == 0) return false; return true; } /** * 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 * or a string expression for a functional index * @param $type The index type * @param $unique True if unique, false otherwise * @param $where Index predicate ('' for none) * @param $tablespace The tablespaces ('' means none/default) * @return 0 success */ function createIndex($name, $table, $columns, $type, $unique, $where, $tablespace, $concurrently) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($name); $this->fieldClean($table); $sql = "CREATE"; if ($unique) $sql .= " UNIQUE"; $sql .= " INDEX"; if ($concurrently) $sql .= " CONCURRENTLY"; $sql .= " \"{$name}\" ON \"{$f_schema}\".\"{$table}\" USING {$type} "; if (is_array($columns)) { $this->arrayClean($columns); $sql .= "(\"" . implode('","', $columns) . "\")"; } else { $sql .= "(" . $columns .")"; } // Tablespace if ($this->hasTablespaces() && $tablespace != '') { $this->fieldClean($tablespace); $sql .= " TABLESPACE \"{$tablespace}\""; } // Predicate if (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) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($index); $sql = "DROP INDEX \"{$f_schema}\".\"{$index}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } /** * Rebuild indexes * @param $type 'DATABASE' or 'TABLE' or 'INDEX' * @param $name The name of the specific database, table, or index to be reindexed * @param $force If true, recreates indexes forcedly in PostgreSQL 7.0-7.1, forces rebuild of system indexes in 7.2-7.3, ignored in >=7.4 */ function reindex($type, $name, $force = false) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($name); switch($type) { case 'DATABASE': $sql = "REINDEX {$type} \"{$name}\""; if ($force) $sql .= ' FORCE'; break; case 'TABLE': case 'INDEX': $sql = "REINDEX {$type} \"{$f_schema}\".\"{$name}\""; if ($force) $sql .= ' FORCE'; break; default: return -1; } return $this->execute($sql); } /** * Clusters an index * @param $index The name of the index * @param $table The table the index is on * @return 0 success */ function clusterIndex($table='', $index='') { $sql = 'CLUSTER'; // We don't bother with a transaction here, as there's no point rolling // back an expensive cluster if a cheap analyze fails for whatever reason if (!empty($table)) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $sql .= " \"{$f_schema}\".\"{$table}\""; if (!empty($index)) { $this->fieldClean($index); $sql .= " USING \"{$index}\""; } } return $this->execute($sql); } // Constraint functions /** * Returns a list of all constraints on a table * @param $table The table to find rules for * @return A recordset */ function getConstraints($table) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); // This SQL is greatly complicated by the need to retrieve // index clustering information for primary and unique constraints $sql = "SELECT pc.conname, pg_catalog.pg_get_constraintdef(pc.oid, true) AS consrc, pc.contype, CASE WHEN pc.contype='u' OR pc.contype='p' THEN ( SELECT indisclustered FROM pg_catalog.pg_depend pd, pg_catalog.pg_class pl, pg_catalog.pg_index pi WHERE pd.refclassid=pc.tableoid AND pd.refobjid=pc.oid AND pd.objid=pl.oid AND pl.oid=pi.indexrelid ) ELSE NULL END AS indisclustered FROM pg_catalog.pg_constraint pc WHERE pc.conrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname='{$table}' AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}')) ORDER BY 1 "; return $this->selectSet($sql); } /** * Returns a list of all constraints on a table, * including constraint name, definition, related col and referenced namespace, * table and col if needed * @param $table the table where we are looking for fk * @return a recordset */ function getConstraintsWithFields($table) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); // get the max number of col used in a constraint for the table $sql = "SELECT DISTINCT max(SUBSTRING(array_dims(c.conkey) FROM \$patern\$^\\[.*:(.*)\\]$\$patern\$)) as nb FROM pg_catalog.pg_constraint AS c JOIN pg_catalog.pg_class AS r ON (c.conrelid=r.oid) JOIN pg_catalog.pg_namespace AS ns ON (r.relnamespace=ns.oid) WHERE r.relname = '{$table}' AND ns.nspname='{$c_schema}'"; $rs = $this->selectSet($sql); if ($rs->EOF) $max_col = 0; else $max_col = $rs->fields['nb']; $sql = ' SELECT c.oid AS conid, c.contype, c.conname, pg_catalog.pg_get_constraintdef(c.oid, true) AS consrc, ns1.nspname as p_schema, r1.relname as p_table, ns2.nspname as f_schema, r2.relname as f_table, f1.attname as p_field, f1.attnum AS p_attnum, f2.attname as f_field, f2.attnum AS f_attnum, pg_catalog.obj_description(c.oid, \'pg_constraint\') AS constcomment, c.conrelid, c.confrelid FROM pg_catalog.pg_constraint AS c JOIN pg_catalog.pg_class AS r1 ON (c.conrelid=r1.oid) JOIN pg_catalog.pg_attribute AS f1 ON (f1.attrelid=r1.oid AND (f1.attnum=c.conkey[1]'; for ($i = 2; $i <= $rs->fields['nb']; $i++) { $sql.= " OR f1.attnum=c.conkey[$i]"; } $sql.= ')) JOIN pg_catalog.pg_namespace AS ns1 ON r1.relnamespace=ns1.oid LEFT JOIN ( pg_catalog.pg_class AS r2 JOIN pg_catalog.pg_namespace AS ns2 ON (r2.relnamespace=ns2.oid) ) ON (c.confrelid=r2.oid) LEFT JOIN pg_catalog.pg_attribute AS f2 ON (f2.attrelid=r2.oid AND ((c.confkey[1]=f2.attnum AND c.conkey[1]=f1.attnum)'; for ($i = 2; $i <= $rs->fields['nb']; $i++) $sql.= " OR (c.confkey[$i]=f2.attnum AND c.conkey[$i]=f1.attnum)"; $sql .= sprintf(")) WHERE r1.relname = '%s' AND ns1.nspname='%s' ORDER BY 1", $table, $c_schema); return $this->selectSet($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 * @param $tablespace (optional) The tablespace for the schema, '' indicates default. * @return 0 success * @return -1 no fields given */ function addPrimaryKey($table, $fields, $name = '', $tablespace = '') { if (!is_array($fields) || sizeof($fields) == 0) return -1; $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $this->fieldArrayClean($fields); $this->fieldClean($name); $this->fieldClean($tablespace); $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ADD "; if ($name != '') $sql .= "CONSTRAINT \"{$name}\" "; $sql .= "PRIMARY KEY (\"" . join('","', $fields) . "\")"; if ($tablespace != '' && $this->hasTablespaces()) $sql .= " USING INDEX TABLESPACE \"{$tablespace}\""; return $this->execute($sql); } /** * Adds a unique constraint to a table * @param $table The table to which to add the unique key * @param $fields (array) An array of fields over which to add the unique key * @param $name (optional) The name to give the key, otherwise default name is assigned * @param $tablespace (optional) The tablespace for the schema, '' indicates default. * @return 0 success * @return -1 no fields given */ function addUniqueKey($table, $fields, $name = '', $tablespace = '') { if (!is_array($fields) || sizeof($fields) == 0) return -1; $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $this->fieldArrayClean($fields); $this->fieldClean($name); $this->fieldClean($tablespace); $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ADD "; if ($name != '') $sql .= "CONSTRAINT \"{$name}\" "; $sql .= "UNIQUE (\"" . join('","', $fields) . "\")"; if ($tablespace != '' && $this->hasTablespaces()) $sql .= " USING INDEX TABLESPACE \"{$tablespace}\""; return $this->execute($sql); } /** * 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 = '') { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $this->fieldClean($name); // @@ How the heck do you clean a definition??? $sql = "ALTER TABLE \"{$f_schema}\".\"{$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) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $c_schema = $this->_schema; $this->clean($c_schema); $c_table = $table; $this->fieldClean($table); $this->clean($c_table); $this->clean($name); // Begin transaction $status = $this->beginTransaction(); if ($status != 0) return -2; // Properly lock the table $sql = "LOCK TABLE \"{$f_schema}\".\"{$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_catalog.pg_class WHERE relname='{$c_table}' AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = '{$c_schema}')) 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_catalog.pg_class WHERE relname='{$c_table}' AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = '{$c_schema}'))) WHERE relname='{$c_table}'"; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -4; } // Otherwise, close the transaction return $this->endTransaction(); } /** * 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 $match The match type (eg. MATCH FULL) * @param $deferrable The deferrability (eg. NOT DEFERRABLE) * @param $intially The initial deferrability (eg. INITIALLY IMMEDIATE) * @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, $match, $deferrable, $initially, $name = '') { if (!is_array($sfields) || sizeof($sfields) == 0 || !is_array($tfields) || sizeof($tfields) == 0) return -1; $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $this->fieldClean($targschema); $this->fieldClean($targtable); $this->fieldArrayClean($sfields); $this->fieldArrayClean($tfields); $this->fieldClean($name); $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ADD "; if ($name != '') $sql .= "CONSTRAINT \"{$name}\" "; $sql .= "FOREIGN KEY (\"" . join('","', $sfields) . "\") "; // Target table needs to be fully qualified $sql .= "REFERENCES \"{$targschema}\".\"{$targtable}\"(\"" . join('","', $tfields) . "\") "; if ($match != $this->fkmatches[0]) $sql .= " {$match}"; if ($upd_action != $this->fkactions[0]) $sql .= " ON UPDATE {$upd_action}"; if ($del_action != $this->fkactions[0]) $sql .= " ON DELETE {$del_action}"; if ($deferrable != $this->fkdeferrable[0]) $sql .= " {$deferrable}"; if ($initially != $this->fkinitial[0]) $sql .= " {$initially}"; return $this->execute($sql); } /** * 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 */ function dropConstraint($constraint, $relation, $type, $cascade) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($constraint); $this->fieldClean($relation); $sql = "ALTER TABLE \"{$f_schema}\".\"{$relation}\" DROP CONSTRAINT \"{$constraint}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } /** * A function for getting all columns linked by foreign keys given a group of tables * @param $tables multi dimensional assoc array that holds schema and table name * @return A recordset of linked tables and columns * @return -1 $tables isn't an array */ function getLinkingKeys($tables) { if (!is_array($tables)) return -1; $this->clean($tables[0]['tablename']); $this->clean($tables[0]['schemaname']); $tables_list = "'{$tables[0]['tablename']}'"; $schema_list = "'{$tables[0]['schemaname']}'"; $schema_tables_list = "'{$tables[0]['schemaname']}.{$tables[0]['tablename']}'"; for ($i = 1; $i < sizeof($tables); $i++) { $this->clean($tables[$i]['tablename']); $this->clean($tables[$i]['schemaname']); $tables_list .= ", '{$tables[$i]['tablename']}'"; $schema_list .= ", '{$tables[$i]['schemaname']}'"; $schema_tables_list .= ", '{$tables[$i]['schemaname']}.{$tables[$i]['tablename']}'"; } $maxDimension = 1; $sql = " SELECT DISTINCT array_dims(pc.conkey) AS arr_dim, pgc1.relname AS p_table FROM pg_catalog.pg_constraint AS pc, pg_catalog.pg_class AS pgc1 WHERE pc.contype = 'f' AND (pc.conrelid = pgc1.relfilenode OR pc.confrelid = pgc1.relfilenode) AND pgc1.relname IN ($tables_list) "; //parse our output to find the highest dimension of foreign keys since pc.conkey is stored in an array $rs = $this->selectSet($sql); while (!$rs->EOF) { $arrData = explode(':', $rs->fields['arr_dim']); $tmpDimension = intval(substr($arrData[1], 0, strlen($arrData[1] - 1))); $maxDimension = $tmpDimension > $maxDimension ? $tmpDimension : $maxDimension; $rs->MoveNext(); } //we know the highest index for foreign keys that conkey goes up to, expand for us in an IN query $cons_str = '( (pfield.attnum = conkey[1] AND cfield.attnum = confkey[1]) '; for ($i = 2; $i <= $maxDimension; $i++) { $cons_str .= "OR (pfield.attnum = conkey[{$i}] AND cfield.attnum = confkey[{$i}]) "; } $cons_str .= ') '; $sql = " SELECT pgc1.relname AS p_table, pgc2.relname AS f_table, pfield.attname AS p_field, cfield.attname AS f_field, pgns1.nspname AS p_schema, pgns2.nspname AS f_schema FROM pg_catalog.pg_constraint AS pc, pg_catalog.pg_class AS pgc1, pg_catalog.pg_class AS pgc2, pg_catalog.pg_attribute AS pfield, pg_catalog.pg_attribute AS cfield, (SELECT oid AS ns_id, nspname FROM pg_catalog.pg_namespace WHERE nspname IN ($schema_list) ) AS pgns1, (SELECT oid AS ns_id, nspname FROM pg_catalog.pg_namespace WHERE nspname IN ($schema_list) ) AS pgns2 WHERE pc.contype = 'f' AND pgc1.relnamespace = pgns1.ns_id AND pgc2.relnamespace = pgns2.ns_id AND pc.conrelid = pgc1.relfilenode AND pc.confrelid = pgc2.relfilenode AND pfield.attrelid = pc.conrelid AND cfield.attrelid = pc.confrelid AND $cons_str AND pgns1.nspname || '.' || pgc1.relname IN ($schema_tables_list) AND pgns2.nspname || '.' || pgc2.relname IN ($schema_tables_list) "; return $this->selectSet($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) { $this->clean($table); $status = $this->beginTransaction(); if ($status != 0) return -1; $c_schema = $this->_schema; $this->clean($c_schema); $sql = " SELECT pn.nspname, pl.relname, pc.conname, pg_catalog.pg_get_constraintdef(pc.oid) AS consrc FROM pg_catalog.pg_constraint pc, pg_catalog.pg_namespace pn, pg_catalog.pg_class pl WHERE pc.connamespace = pn.oid AND pc.conrelid = pl.oid AND pc.contype = 'f' AND confrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname='{$table}' AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}')) ORDER BY 1,2,3 "; return $this->selectSet($sql); } // Domain functions /** * Gets all information for a single domain * @param $domain The name of the domain to fetch * @return A recordset */ function getDomain($domain) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($domain); $sql = " SELECT t.typname AS domname, pg_catalog.format_type(t.typbasetype, t.typtypmod) AS domtype, t.typnotnull AS domnotnull, t.typdefault AS domdef, pg_catalog.pg_get_userbyid(t.typowner) AS domowner, pg_catalog.obj_description(t.oid, 'pg_type') AS domcomment FROM pg_catalog.pg_type t WHERE t.typtype = 'd' AND t.typname = '{$domain}' AND t.typnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = '{$c_schema}')"; return $this->selectSet($sql); } /** * Return all domains in current schema. Excludes domain constraints. * @return All tables, sorted alphabetically */ function getDomains() { $c_schema = $this->_schema; $this->clean($c_schema); $sql = " SELECT t.typname AS domname, pg_catalog.format_type(t.typbasetype, t.typtypmod) AS domtype, t.typnotnull AS domnotnull, t.typdefault AS domdef, pg_catalog.pg_get_userbyid(t.typowner) AS domowner, pg_catalog.obj_description(t.oid, 'pg_type') AS domcomment FROM pg_catalog.pg_type t WHERE t.typtype = 'd' AND t.typnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}') ORDER BY t.typname"; return $this->selectSet($sql); } /** * Get domain constraints * @param $domain The name of the domain whose constraints to fetch * @return A recordset */ function getDomainConstraints($domain) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($domain); $sql = " SELECT conname, contype, pg_catalog.pg_get_constraintdef(oid, true) AS consrc FROM pg_catalog.pg_constraint WHERE contypid = ( SELECT oid FROM pg_catalog.pg_type WHERE typname='{$domain}' AND typnamespace = ( SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = '{$c_schema}') ) ORDER BY conname"; return $this->selectSet($sql); } /** * Creates a domain * @param $domain The name of the domain to create * @param $type The base type for the domain * @param $length Optional type length * @param $array True for array type, false otherwise * @param $notnull True for NOT NULL, false otherwise * @param $default Default value for domain * @param $check A CHECK constraint if there is one * @return 0 success */ function createDomain($domain, $type, $length, $array, $notnull, $default, $check) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($domain); $sql = "CREATE DOMAIN \"{$f_schema}\".\"{$domain}\" AS "; if ($length == '') $sql .= $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 .= "timestamp({$length}){$qual}"; break; case 'time with time zone': case 'time without time zone': $qual = substr($type, 4); $sql .= "time({$length}){$qual}"; break; default: $sql .= "{$type}({$length})"; } } // Add array qualifier, if requested if ($array) $sql .= '[]'; if ($notnull) $sql .= ' NOT NULL'; if ($default != '') $sql .= " DEFAULT {$default}"; if ($this->hasDomainConstraints() && $check != '') $sql .= " CHECK ({$check})"; return $this->execute($sql); } /** * Alters a domain * @param $domain The domain to alter * @param $domdefault The domain default * @param $domnotnull True for NOT NULL, false otherwise * @param $domowner The domain owner * @return 0 success * @return -1 transaction error * @return -2 default error * @return -3 not null error * @return -4 owner error */ function alterDomain($domain, $domdefault, $domnotnull, $domowner) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($domain); $this->fieldClean($domowner); $status = $this->beginTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } // Default if ($domdefault == '') $sql = "ALTER DOMAIN \"{$f_schema}\".\"{$domain}\" DROP DEFAULT"; else $sql = "ALTER DOMAIN \"{$f_schema}\".\"{$domain}\" SET DEFAULT {$domdefault}"; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -2; } // NOT NULL if ($domnotnull) $sql = "ALTER DOMAIN \"{$f_schema}\".\"{$domain}\" SET NOT NULL"; else $sql = "ALTER DOMAIN \"{$f_schema}\".\"{$domain}\" DROP NOT NULL"; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -3; } // Owner $sql = "ALTER DOMAIN \"{$f_schema}\".\"{$domain}\" OWNER TO \"{$domowner}\""; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -4; } return $this->endTransaction(); } /** * Drops a domain. * @param $domain The name of the domain to drop * @param $cascade True to cascade drop, false to restrict * @return 0 success */ function dropDomain($domain, $cascade) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($domain); $sql = "DROP DOMAIN \"{$f_schema}\".\"{$domain}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } /** * Adds a check constraint to a domain * @param $domain The domain 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 addDomainCheckConstraint($domain, $definition, $name = '') { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($domain); $this->fieldClean($name); $sql = "ALTER DOMAIN \"{$f_schema}\".\"{$domain}\" ADD "; if ($name != '') $sql .= "CONSTRAINT \"{$name}\" "; $sql .= "CHECK ({$definition})"; return $this->execute($sql); } /** * Drops a domain constraint * @param $domain The domain from which to remove the constraint * @param $constraint The constraint to remove * @param $cascade True to cascade, false otherwise * @return 0 success */ function dropDomainConstraint($domain, $constraint, $cascade) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($domain); $this->fieldClean($constraint); $sql = "ALTER DOMAIN \"{$f_schema}\".\"{$domain}\" DROP CONSTRAINT \"{$constraint}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } // Function functions /** * Returns all details for a particular function * @param $func The name of the function to retrieve * @return Function info */ function getFunction($function_oid) { $this->clean($function_oid); $sql = " SELECT pc.oid AS prooid, proname, pg_catalog.pg_get_userbyid(proowner) AS proowner, nspname as proschema, lanname as prolanguage, procost, prorows, pg_catalog.format_type(prorettype, NULL) as proresult, prosrc, probin, proretset, proisstrict, provolatile, prosecdef, pg_catalog.oidvectortypes(pc.proargtypes) AS proarguments, proargnames AS proargnames, pg_catalog.obj_description(pc.oid, 'pg_proc') AS procomment, proconfig FROM pg_catalog.pg_proc pc, pg_catalog.pg_language pl, pg_catalog.pg_namespace pn WHERE pc.oid = '{$function_oid}'::oid AND pc.prolang = pl.oid AND pc.pronamespace = pn.oid "; return $this->selectSet($sql); } /** * Returns a list of all functions in the database * @param $all If true, will find all available functions, if false just those in search path * @param $type If not null, will find all functions with return value = type * * @return All functions */ function getFunctions($all = false, $type = null) { if ($all) { $where = 'pg_catalog.pg_function_is_visible(p.oid)'; $distinct = 'DISTINCT ON (p.proname)'; if ($type) { $where .= " AND p.prorettype = (select oid from pg_catalog.pg_type p where p.typname = 'trigger') "; } } else { $c_schema = $this->_schema; $this->clean($c_schema); $where = "n.nspname = '{$c_schema}'"; $distinct = ''; } $sql = " SELECT {$distinct} p.oid AS prooid, p.proname, p.proretset, pg_catalog.format_type(p.prorettype, NULL) AS proresult, pg_catalog.oidvectortypes(p.proargtypes) AS proarguments, pl.lanname AS prolanguage, pg_catalog.obj_description(p.oid, 'pg_proc') AS procomment, p.proname || ' (' || pg_catalog.oidvectortypes(p.proargtypes) || ')' AS proproto, CASE WHEN p.proretset THEN 'setof ' ELSE '' END || pg_catalog.format_type(p.prorettype, NULL) AS proreturns, u.usename AS proowner FROM pg_catalog.pg_proc p INNER JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace INNER JOIN pg_catalog.pg_language pl ON pl.oid = p.prolang LEFT JOIN pg_catalog.pg_user u ON u.usesysid = p.proowner WHERE NOT p.proisagg AND {$where} ORDER BY p.proname, proresult "; 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(); // Volatility if ($f['provolatile'] == 'v') $temp[] = 'VOLATILE'; elseif ($f['provolatile'] == 'i') $temp[] = 'IMMUTABLE'; elseif ($f['provolatile'] == 's') $temp[] = 'STABLE'; else return -1; // Null handling $f['proisstrict'] = $this->phpBool($f['proisstrict']); if ($f['proisstrict']) $temp[] = 'RETURNS NULL ON NULL INPUT'; else $temp[] = 'CALLED ON NULL INPUT'; // Security $f['prosecdef'] = $this->phpBool($f['prosecdef']); if ($f['prosecdef']) $temp[] = 'SECURITY DEFINER'; else $temp[] = 'SECURITY INVOKER'; return $temp; } /** * Updates (replaces) a function. * @param $function_oid The OID of the function * @param $funcname The name of the function to create * @param $newname The new name for the function * @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 * @param $comment The comment on the function * @return 0 success * @return -1 transaction error * @return -3 create function error * @return -4 comment error * @return -5 rename function error * @return -6 alter owner error * @return -7 alter schema error */ function setFunction($function_oid, $funcname, $newname, $args, $returns, $definition, $language, $flags, $setof, $funcown, $newown, $funcschema, $newschema, $cost, $rows, $comment) { // Begin a transaction $status = $this->beginTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } // Replace the existing function $status = $this->createFunction($funcname, $args, $returns, $definition, $language, $flags, $setof, $cost, $rows, $comment, true); if ($status != 0) { $this->rollbackTransaction(); return $status; } $f_schema = $this->_schema; $this->fieldClean($f_schema); // Rename the function, if necessary $this->fieldClean($newname); /* $funcname is escaped in createFunction */ if ($funcname != $newname) { $sql = "ALTER FUNCTION \"{$f_schema}\".\"{$funcname}\"({$args}) RENAME TO \"{$newname}\""; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -5; } $funcname = $newname; } // Alter the owner, if necessary if ($this->hasFunctionAlterOwner()) { $this->fieldClean($newown); if ($funcown != $newown) { $sql = "ALTER FUNCTION \"{$f_schema}\".\"{$funcname}\"({$args}) OWNER TO \"{$newown}\""; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -6; } } } // Alter the schema, if necessary if ($this->hasFunctionAlterSchema()) { $this->fieldClean($newschema); /* $funcschema is escaped in createFunction */ if ($funcschema != $newschema) { $sql = "ALTER FUNCTION \"{$f_schema}\".\"{$funcname}\"({$args}) SET SCHEMA \"{$newschema}\""; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -7; } } } return $this->endTransaction(); } /** * 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 $rows number of rows planner should estimate will be returned * @param $cost cost the planner should use in the function execution step * @param $comment Comment for the function * @param $replace (optional) True if OR REPLACE, false for normal * @return 0 success * @return -3 create function failed * @return -4 set comment failed */ function createFunction($funcname, $args, $returns, $definition, $language, $flags, $setof, $cost, $rows, $comment, $replace = false) { // Begin a transaction $status = $this->beginTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } $this->fieldClean($funcname); $this->clean($args); $this->fieldClean($language); $this->arrayClean($flags); $this->clean($cost); $this->clean($rows); $f_schema = $this->_schema; $this->fieldClean($f_schema); $sql = "CREATE"; if ($replace) $sql .= " OR REPLACE"; $sql .= " FUNCTION \"{$f_schema}\".\"{$funcname}\" ("; if ($args != '') $sql .= $args; // For some reason, the returns field cannot have quotes... $sql .= ") RETURNS "; if ($setof) $sql .= "SETOF "; $sql .= "{$returns} AS "; if (is_array($definition)) { $this->arrayClean($definition); $sql .= "'" . $definition[0] . "'"; if ($definition[1]) { $sql .= ",'" . $definition[1] . "'"; } } else { $this->clean($definition); $sql .= "'" . $definition . "'"; } $sql .= " LANGUAGE \"{$language}\""; // Add costs if (!empty($cost)) $sql .= " COST {$cost}"; if ($rows <> 0 ){ $sql .= " ROWS {$rows}"; } // Add flags foreach ($flags as $v) { // Skip default flags if ($v == '') continue; else $sql .= "\n{$v}"; } $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -3; } /* set the comment */ $status = $this->setComment('FUNCTION', "\"{$funcname}\"({$args})", null, $comment); if ($status != 0) { $this->rollbackTransaction(); return -4; } return $this->endTransaction(); } /** * 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); $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($fn->fields['proname']); $sql = "DROP FUNCTION \"{$f_schema}\".\"{$fn->fields['proname']}\"({$fn->fields['proarguments']})"; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } // Type functions /** * 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 typtype, typbyval, typname, typinput AS typin, typoutput AS typout, typlen, typalign FROM pg_type WHERE typname='{$typname}'"; return $this->selectSet($sql); } /** * Returns a list of all types in the database * @param $all If true, will find all available types, if false just those in search path * @param $tabletypes If true, will include table types * @param $domains If true, will include domains * @return A recordet */ function getTypes($all = false, $tabletypes = false, $domains = false) { if ($all) $where = '1 = 1'; else { $c_schema = $this->_schema; $this->clean($c_schema); $where = "n.nspname = '{$c_schema}'"; } // Never show system table types $where2 = "AND c.relnamespace NOT IN (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname LIKE 'pg@_%' ESCAPE '@')"; // Create type filter $tqry = "'c'"; if ($tabletypes) $tqry .= ", 'r', 'v'"; // Create domain filter if (!$domains) $where .= " AND t.typtype != 'd'"; $sql = "SELECT t.typname AS basename, pg_catalog.format_type(t.oid, NULL) AS typname, pu.usename AS typowner, t.typtype, pg_catalog.obj_description(t.oid, 'pg_type') AS typcomment FROM (pg_catalog.pg_type t LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace) LEFT JOIN pg_catalog.pg_user pu ON t.typowner = pu.usesysid WHERE (t.typrelid = 0 OR (SELECT c.relkind IN ({$tqry}) FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid {$where2})) AND t.typname !~ '^_' AND {$where} ORDER BY 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) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($typname); $this->fieldClean($typin); $this->fieldClean($typout); $sql = " CREATE TYPE \"{$f_schema}\".\"{$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) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($typname); $sql = "DROP TYPE \"{$f_schema}\".\"{$typname}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } /** * Creates a new enum type in the database * @param $name The name of the type * @param $values An array of values * @param $typcomment Type comment * @return 0 success * @return -1 transaction error * @return -2 no values supplied */ function createEnumType($name, $values, $typcomment) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($name); if (empty($values)) return -2; $status = $this->beginTransaction(); if ($status != 0) return -1; $values = array_unique($values); $nbval = count($values); for ($i = 0; $i < $nbval; $i++) $this->clean($values[$i]); $sql = "CREATE TYPE \"{$f_schema}\".\"{$name}\" AS ENUM ('"; $sql.= implode("','", $values); $sql .= "')"; $status = $this->execute($sql); if ($status) { $this->rollbackTransaction(); return -1; } if ($typcomment != '') { $status = $this->setComment('TYPE', $name, '', $typcomment, true); if ($status) { $this->rollbackTransaction(); return -1; } } return $this->endTransaction(); } /** * Get defined values for a given enum * @return A recordset */ function getEnumValues($name) { $this->clean($name); $sql = "SELECT enumlabel AS enumval FROM pg_catalog.pg_type t JOIN pg_catalog.pg_enum e ON (t.oid=e.enumtypid) WHERE t.typname = '{$name}' ORDER BY e.oid"; return $this->selectSet($sql); } /** * Creates a new composite type in the database * @param $name The name of the type * @param $fields The number of fields * @param $field An array of field names * @param $type An array of field types * @param $array An array of '' or '[]' for each type if it's an array or not * @param $length An array of field lengths * @param $colcomment An array of comments * @param $typcomment Type comment * @return 0 success * @return -1 no fields supplied */ function createCompositeType($name, $fields, $field, $type, $array, $length, $colcomment, $typcomment) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($name); $status = $this->beginTransaction(); if ($status != 0) return -1; $found = false; $first = true; $comment_sql = ''; // Accumulate comments for the columns $sql = "CREATE TYPE \"{$f_schema}\".\"{$name}\" AS ("; for ($i = 0; $i < $fields; $i++) { $this->fieldClean($field[$i]); $this->clean($type[$i]); $this->clean($length[$i]); $this->clean($colcomment[$i]); // Skip blank columns - for user convenience if ($field[$i] == '' || $type[$i] == '') continue; // If not the first column, add a comma if (!$first) $sql .= ", "; else $first = false; 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]})"; } // Add array qualifier if necessary if ($array[$i] == '[]') $sql .= '[]'; if ($colcomment[$i] != '') $comment_sql .= "COMMENT ON COLUMN \"{$f_schema}\".\"{$name}\".\"{$field[$i]}\" IS '{$colcomment[$i]}';\n"; $found = true; } if (!$found) return -1; $sql .= ")"; $status = $this->execute($sql); if ($status) { $this->rollbackTransaction(); return -1; } if ($typcomment != '') { $status = $this->setComment('TYPE', $name, '', $typcomment, true); if ($status) { $this->rollbackTransaction(); return -1; } } if ($comment_sql != '') { $status = $this->execute($comment_sql); if ($status) { $this->rollbackTransaction(); return -1; } } return $this->endTransaction(); } /** * Returns a list of all casts in the database * @return All casts */ function getCasts() { global $conf; if ($conf['show_system']) $where = ''; else $where = ' AND n1.nspname NOT LIKE $$pg\_%$$ AND n2.nspname NOT LIKE $$pg\_%$$ AND n3.nspname NOT LIKE $$pg\_%$$ '; $sql = " SELECT c.castsource::pg_catalog.regtype AS castsource, c.casttarget::pg_catalog.regtype AS casttarget, CASE WHEN c.castfunc=0 THEN NULL ELSE c.castfunc::pg_catalog.regprocedure END AS castfunc, c.castcontext, obj_description(c.oid, 'pg_cast') as castcomment FROM (pg_catalog.pg_cast c LEFT JOIN pg_catalog.pg_proc p ON c.castfunc=p.oid JOIN pg_catalog.pg_namespace n3 ON p.pronamespace=n3.oid), pg_catalog.pg_type t1, pg_catalog.pg_type t2, pg_catalog.pg_namespace n1, pg_catalog.pg_namespace n2 WHERE c.castsource=t1.oid AND c.casttarget=t2.oid AND t1.typnamespace=n1.oid AND t2.typnamespace=n2.oid {$where} ORDER BY 1, 2 "; return $this->selectSet($sql); } /** * Returns a list of all conversions in the database * @return All conversions */ function getConversions() { $c_schema = $this->_schema; $this->clean($c_schema); $sql = " SELECT c.conname, pg_catalog.pg_encoding_to_char(c.conforencoding) AS conforencoding, pg_catalog.pg_encoding_to_char(c.contoencoding) AS contoencoding, c.condefault, pg_catalog.obj_description(c.oid, 'pg_conversion') AS concomment FROM pg_catalog.pg_conversion c, pg_catalog.pg_namespace n WHERE n.oid = c.connamespace AND n.nspname='{$c_schema}' ORDER BY 1; "; return $this->selectSet($sql); } // Rule functions /** * Returns a list of all rules on a table OR view * @param $table The table to find rules for * @return A recordset */ function getRules($table) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); $sql = " SELECT * FROM pg_catalog.pg_rules WHERE schemaname='{$c_schema}' AND tablename='{$table}' ORDER BY rulename "; return $this->selectSet($sql); } /** * Edits a rule on a table OR view * @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 */ function setRule($name, $event, $table, $where, $instead, $type, $action) { return $this->createRule($name, $event, $table, $where, $instead, $type, $action, true); } /** * 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) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $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 \"{$f_schema}\".\"{$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); } /** * Removes a rule from a table OR view * @param $rule The rule to drop * @param $relation The relation from which to drop * @param $cascade True to cascade drop, false to restrict * @return 0 success */ function dropRule($rule, $relation, $cascade) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($rule); $this->fieldClean($relation); $sql = "DROP RULE \"{$rule}\" ON \"{$f_schema}\".\"{$relation}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } // Trigger functions /** * Grabs a single trigger * @param $table The name of a table whose triggers to retrieve * @param $trigger The name of the trigger to retrieve * @return A recordset */ function getTrigger($table, $trigger) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); $this->clean($trigger); $sql = " SELECT * FROM pg_catalog.pg_trigger t, pg_catalog.pg_class c WHERE t.tgrelid=c.oid AND c.relname='{$table}' AND t.tgname='{$trigger}' AND c.relnamespace=( SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}')"; return $this->selectSet($sql); } /** * 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 = '') { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); $sql = "SELECT t.tgname, pg_catalog.pg_get_triggerdef(t.oid) AS tgdef, CASE WHEN t.tgenabled = 'D' THEN FALSE ELSE TRUE END AS tgenabled, p.oid AS prooid, p.proname || ' (' || pg_catalog.oidvectortypes(p.proargtypes) || ')' AS proproto, ns.nspname AS pronamespace FROM pg_catalog.pg_trigger t, pg_catalog.pg_proc p, pg_catalog.pg_namespace ns WHERE t.tgrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname='{$table}' AND relnamespace=(SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}')) AND ( tgconstraint = 0 OR NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c ON (d.refclassid = c.tableoid AND d.refobjid = c.oid) WHERE d.classid = t.tableoid AND d.objid = t.oid AND d.deptype = 'i' AND c.contype = 'f')) AND p.oid=t.tgfoid AND p.pronamespace = ns.oid"; return $this->selectSet($sql); } /** * 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) { $this->fieldArrayClean($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'; } $f_schema = $this->_schema; $this->fieldClean($f_schema); // Table name $tgdef .= " ON \"{$f_schema}\".\"{$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; } /** * Returns a list of all functions that can be used in triggers */ function getTriggerFunctions() { return $this->getFunctions(true, 'trigger'); } /** * 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, $tgfrequency, $tgargs) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($tgname); $this->fieldClean($table); $this->fieldClean($tgproc); /* No Statement Level Triggers in PostgreSQL (by now) */ $sql = "CREATE TRIGGER \"{$tgname}\" {$tgtime} {$tgevent} ON \"{$f_schema}\".\"{$table}\" FOR EACH {$tgfrequency} EXECUTE PROCEDURE \"{$tgproc}\"({$tgargs})"; return $this->execute($sql); } /** * Alters a trigger * @param $table The name of the table containing the trigger * @param $trigger The name of the trigger to alter * @param $name The new name for the trigger * @return 0 success */ function alterTrigger($table, $trigger, $name) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $this->fieldClean($trigger); $this->fieldClean($name); $sql = "ALTER TRIGGER \"{$trigger}\" ON \"{$f_schema}\".\"{$table}\" RENAME TO \"{$name}\""; 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) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($tgname); $this->fieldClean($table); $sql = "DROP TRIGGER \"{$tgname}\" ON \"{$f_schema}\".\"{$table}\""; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } /** * Enables a trigger * @param $tgname The name of the trigger to enable * @param $table The table in which to enable the trigger * @return 0 success */ function enableTrigger($tgname, $table) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($tgname); $this->fieldClean($table); $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" ENABLE TRIGGER \"{$tgname}\""; return $this->execute($sql); } /** * Disables a trigger * @param $tgname The name of the trigger to disable * @param $table The table in which to disable the trigger * @return 0 success */ function disableTrigger($tgname, $table) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($tgname); $this->fieldClean($table); $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" DISABLE TRIGGER \"{$tgname}\""; return $this->execute($sql); } // Operator functions /** * Returns a list of all operators in the database * @return All operators */ function getOperators() { $c_schema = $this->_schema; $this->clean($c_schema); // We stick with the subselects here, as you cannot ORDER BY a regtype $sql = " SELECT po.oid, po.oprname, (SELECT pg_catalog.format_type(oid, NULL) FROM pg_catalog.pg_type pt WHERE pt.oid=po.oprleft) AS oprleftname, (SELECT pg_catalog.format_type(oid, NULL) FROM pg_catalog.pg_type pt WHERE pt.oid=po.oprright) AS oprrightname, po.oprresult::pg_catalog.regtype AS resultname, pg_catalog.obj_description(po.oid, 'pg_operator') AS oprcomment FROM pg_catalog.pg_operator po WHERE po.oprnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}') 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, oprleft::pg_catalog.regtype AS oprleftname, oprright::pg_catalog.regtype AS oprrightname, oprresult::pg_catalog.regtype AS resultname, po.oprcanhash, oprcanmerge, oprcom::pg_catalog.regoperator AS oprcom, oprnegate::pg_catalog.regoperator AS oprnegate, po.oprcode::pg_catalog.regproc AS oprcode, po.oprrest::pg_catalog.regproc AS oprrest, po.oprjoin::pg_catalog.regproc AS oprjoin FROM pg_catalog.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); $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($opr->fields['oprname']); $sql = "DROP OPERATOR \"{$f_schema}\".{$opr->fields['oprname']} ("; // Quoting or formatting here??? if ($opr->fields['oprleftname'] !== null) $sql .= $opr->fields['oprleftname'] . ', '; else $sql .= "NONE, "; if ($opr->fields['oprrightname'] !== null) $sql .= $opr->fields['oprrightname'] . ')'; else $sql .= "NONE)"; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } // Operator Class functions /** * Gets all opclasses * * @return A recordset */ function getOpClasses() { $c_schema = $this->_schema; $this->clean($c_schema); $sql = " SELECT pa.amname, po.opcname, po.opcintype::pg_catalog.regtype AS opcintype, po.opcdefault, pg_catalog.obj_description(po.oid, 'pg_opclass') AS opccomment FROM pg_catalog.pg_opclass po, pg_catalog.pg_am pa, pg_catalog.pg_namespace pn WHERE po.opcmethod=pa.oid AND po.opcnamespace=pn.oid AND pn.nspname='{$c_schema}' ORDER BY 1,2 "; return $this->selectSet($sql); } // FTS functions /** * Creates a new FTS configuration. * @param string $cfgname The name of the FTS configuration to create * @param string $parser The parser to be used in new FTS configuration * @param string $locale Locale of the FTS configuration * @param string $template The existing FTS configuration to be used as template for the new one * @param string $withmap Should we copy whole map of existing FTS configuration to the new one * @param string $makeDefault Should this configuration be the default for locale given * @param string $comment If omitted, defaults to nothing * * @return 0 success */ function createFtsConfiguration($cfgname, $parser = '', $template = '', $comment = '') { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($cfgname); $sql = "CREATE TEXT SEARCH CONFIGURATION \"{$f_schema}\".\"{$cfgname}\" ("; if ($parser != '') { $this->fieldClean($parser['schema']); $this->fieldClean($parser['parser']); $parser = "\"{$parser['schema']}\".\"{$parser['parser']}\""; $sql .= " PARSER = {$parser}"; } if ($template != '') { $this->fieldClean($template['schema']); $this->fieldClean($template['name']); $sql .= " COPY = \"{$template['schema']}\".\"{$template['name']}\""; } $sql .= ")"; if ($comment != '') { $status = $this->beginTransaction(); if ($status != 0) return -1; } // Create the FTS configuration $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -1; } // Set the comment if ($comment != '') { $status = $this->setComment('TEXT SEARCH CONFIGURATION', $cfgname, '', $comment); if ($status != 0) { $this->rollbackTransaction(); return -1; } return $this->endTransaction(); } return 0; } /** * Returns available FTS configurations * @param $all if false, returns schema qualified FTS confs * * @return A recordset */ function getFtsConfigurations($all = true) { $c_schema = $this->_schema; $this->clean($c_schema); $sql = " SELECT n.nspname as schema, c.cfgname as name, pg_catalog.obj_description(c.oid, 'pg_ts_config') as comment FROM pg_catalog.pg_ts_config c JOIN pg_catalog.pg_namespace n ON n.oid = c.cfgnamespace WHERE pg_catalog.pg_ts_config_is_visible(c.oid)"; if (!$all) $sql.= " AND n.nspname='{$c_schema}'\n"; $sql.= "ORDER BY name"; return $this->selectSet($sql); } /** * Return all information related to a FTS configuration * @param $ftscfg The name of the FTS configuration * * @return FTS configuration information */ function getFtsConfigurationByName($ftscfg) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($ftscfg); $sql = " SELECT n.nspname as schema, c.cfgname as name, p.prsname as parser, c.cfgparser as parser_id, pg_catalog.obj_description(c.oid, 'pg_ts_config') as comment FROM pg_catalog.pg_ts_config c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.cfgnamespace LEFT JOIN pg_catalog.pg_ts_parser p ON p.oid = c.cfgparser WHERE pg_catalog.pg_ts_config_is_visible(c.oid) AND c.cfgname = '{$ftscfg}' AND n.nspname='{$c_schema}'"; return $this->selectSet($sql); } /** * Returns the map of FTS configuration given * (list of mappings (tokens) and their processing dictionaries) * @param string $ftscfg Name of the FTS configuration * * @return RecordSet */ function getFtsConfigurationMap($ftscfg) { $c_schema = $this->_schema; $this->clean($c_schema); $this->fieldClean($ftscfg); $oidSet = $this->selectSet("SELECT c.oid FROM pg_catalog.pg_ts_config AS c LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.cfgnamespace) WHERE c.cfgname = '{$ftscfg}' AND n.nspname='{$c_schema}'"); $oid = $oidSet->fields['oid']; $sql = " SELECT (SELECT t.alias FROM pg_catalog.ts_token_type(c.cfgparser) AS t WHERE t.tokid = m.maptokentype) AS name, (SELECT t.description FROM pg_catalog.ts_token_type(c.cfgparser) AS t WHERE t.tokid = m.maptokentype) AS description, c.cfgname AS cfgname, n.nspname ||'.'|| d.dictname as dictionaries FROM pg_catalog.pg_ts_config AS c, pg_catalog.pg_ts_config_map AS m, pg_catalog.pg_ts_dict d, pg_catalog.pg_namespace n WHERE c.oid = {$oid} AND m.mapcfg = c.oid AND m.mapdict = d.oid AND d.dictnamespace = n.oid ORDER BY name "; return $this->selectSet($sql); } /** * Returns FTS parsers available * @param $all if false, return only Parsers from the current schema * * @return RecordSet */ function getFtsParsers($all = true) { $c_schema = $this->_schema; $this->clean($c_schema); $sql = " SELECT n.nspname as schema, p.prsname as name, pg_catalog.obj_description(p.oid, 'pg_ts_parser') as comment FROM pg_catalog.pg_ts_parser p LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = p.prsnamespace) WHERE pg_catalog.pg_ts_parser_is_visible(p.oid)"; if (!$all) $sql.= " AND n.nspname='{$c_schema}'\n"; $sql.= "ORDER BY name"; return $this->selectSet($sql); } /** * Returns FTS dictionaries available * @param $all if false, return only Dics from the current schema * * @returns RecordSet */ function getFtsDictionaries($all = true) { $c_schema = $this->_schema; $this->clean($c_schema); $sql = " SELECT n.nspname as schema, d.dictname as name, pg_catalog.obj_description(d.oid, 'pg_ts_dict') as comment FROM pg_catalog.pg_ts_dict d LEFT JOIN pg_catalog.pg_namespace n ON n.oid = d.dictnamespace WHERE pg_catalog.pg_ts_dict_is_visible(d.oid)"; if (!$all) $sql.= " AND n.nspname='{$c_schema}'\n"; $sql.= "ORDER BY name;"; return $this->selectSet($sql); } /** * Returns all FTS dictionary templates available */ function getFtsDictionaryTemplates() { $sql = " SELECT n.nspname as schema, t.tmplname as name, ( SELECT COALESCE(np.nspname, '(null)')::pg_catalog.text || '.' || p.proname FROM pg_catalog.pg_proc p LEFT JOIN pg_catalog.pg_namespace np ON np.oid = p.pronamespace WHERE t.tmplinit = p.oid ) AS init, ( SELECT COALESCE(np.nspname, '(null)')::pg_catalog.text || '.' || p.proname FROM pg_catalog.pg_proc p LEFT JOIN pg_catalog.pg_namespace np ON np.oid = p.pronamespace WHERE t.tmpllexize = p.oid ) AS lexize, pg_catalog.obj_description(t.oid, 'pg_ts_template') as comment FROM pg_catalog.pg_ts_template t LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.tmplnamespace WHERE pg_catalog.pg_ts_template_is_visible(t.oid) ORDER BY name;"; return $this->selectSet($sql); } /** * Drops FTS coniguration * @param $ftscfg The configuration's name * @param $cascade Cascade to dependenced objects * * @return 0 on success */ function dropFtsConfiguration($ftscfg, $cascade) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($ftscfg); $sql = "DROP TEXT SEARCH CONFIGURATION \"{$f_schema}\".\"{$ftscfg}\""; if ($cascade) $sql .= ' CASCADE'; return $this->execute($sql); } /** * Drops FTS dictionary * @param $ftsdict The dico's name * @param $cascade Cascade to dependenced objects * * @todo Support of dictionary templates dropping * @return 0 on success */ function dropFtsDictionary($ftsdict, $cascade) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($ftsdict); $sql = "DROP TEXT SEARCH DICTIONARY"; $sql .= " \"{$f_schema}\".\"{$ftsdict}\""; if ($cascade) $sql .= ' CASCADE'; return $this->execute($sql); } /** * Alters FTS configuration * @param $cfgname The conf's name * @param $comment A comment on for the conf * @param $name The new conf name * * @return 0 on success */ function updateFtsConfiguration($cfgname, $comment, $name) { $status = $this->beginTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } $this->fieldClean($cfgname); $status = $this->setComment('TEXT SEARCH CONFIGURATION', $cfgname, '', $comment); if ($status != 0) { $this->rollbackTransaction(); return -1; } // Only if the name has changed if ($name != $cfgname) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($name); $sql = "ALTER TEXT SEARCH CONFIGURATION \"{$f_schema}\".\"{$cfgname}\" RENAME TO \"{$name}\""; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -1; } } return $this->endTransaction(); } /** * Creates a new FTS dictionary or FTS dictionary template. * @param string $dictname The name of the FTS dictionary to create * @param boolean $isTemplate Flag whether we create usual dictionary or dictionary template * @param string $template The existing FTS dictionary to be used as template for the new one * @param string $lexize The name of the function, which does transformation of input word * @param string $init The name of the function, which initializes dictionary * @param string $option Usually, it stores various options required for the dictionary * @param string $comment If omitted, defaults to nothing * * @return 0 success */ function createFtsDictionary($dictname, $isTemplate = false, $template = '', $lexize = '', $init = '', $option = '', $comment = '') { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($dictname); $this->fieldClean($template); $this->fieldClean($lexize); $this->fieldClean($init); $this->fieldClean($option); $sql = "CREATE TEXT SEARCH"; if ($isTemplate) { $sql .= " TEMPLATE \"{$f_schema}\".\"{$dictname}\" ("; if ($lexize != '') $sql .= " LEXIZE = {$lexize}"; if ($init != '') $sql .= ", INIT = {$init}"; $sql .= ")"; $whatToComment = 'TEXT SEARCH TEMPLATE'; } else { $sql .= " DICTIONARY \"{$f_schema}\".\"{$dictname}\" ("; if ($template != '') { $this->fieldClean($template['schema']); $this->fieldClean($template['name']); $template = "\"{$template['schema']}\".\"{$template['name']}\""; $sql .= " TEMPLATE = {$template}"; } if ($option != '') $sql .= ", {$option}"; $sql .= ")"; $whatToComment = 'TEXT SEARCH DICTIONARY'; } /* if comment, begin a transaction to * run both commands */ if ($comment != '') { $status = $this->beginTransaction(); if ($status != 0) return -1; } // Create the FTS dictionary $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -1; } // Set the comment if ($comment != '') { $status = $this->setComment($whatToComment, $dictname, '', $comment); if ($status != 0) { $this->rollbackTransaction(); return -1; } } return $this->endTransaction(); } /** * Alters FTS dictionary or dictionary template * @param $dictname The dico's name * @param $comment The comment * @param $name The new dico's name * * @return 0 on success */ function updateFtsDictionary($dictname, $comment, $name) { $status = $this->beginTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } $this->fieldClean($dictname); $status = $this->setComment('TEXT SEARCH DICTIONARY', $dictname, '', $comment); if ($status != 0) { $this->rollbackTransaction(); return -1; } // Only if the name has changed if ($name != $dictname) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($name); $sql = "ALTER TEXT SEARCH DICTIONARY \"{$f_schema}\".\"{$dictname}\" RENAME TO \"{$name}\""; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -1; } } return $this->endTransaction(); } /** * Return all information relating to a FTS dictionary * @param $ftsdict The name of the FTS dictionary * * @return RecordSet of FTS dictionary information */ function getFtsDictionaryByName($ftsdict) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($ftsdict); $sql = "SELECT n.nspname as schema, d.dictname as name, ( SELECT COALESCE(nt.nspname, '(null)')::pg_catalog.text || '.' || t.tmplname FROM pg_catalog.pg_ts_template t LEFT JOIN pg_catalog.pg_namespace nt ON nt.oid = t.tmplnamespace WHERE d.dicttemplate = t.oid ) AS template, d.dictinitoption as init, pg_catalog.obj_description(d.oid, 'pg_ts_dict') as comment FROM pg_catalog.pg_ts_dict d LEFT JOIN pg_catalog.pg_namespace n ON n.oid = d.dictnamespace WHERE d.dictname = '{$ftsdict}' AND pg_catalog.pg_ts_dict_is_visible(d.oid) AND n.nspname='{$c_schema}' ORDER BY name"; return $this->selectSet($sql); } /** * Creates/updates/deletes FTS mapping. * @param string $cfgname The name of the FTS configuration to alter * @param array $mapping Array of tokens' names * @param string $action What to do with the mapping: add, alter or drop * @param string $dictname Dictionary that will process tokens given or null in case of drop action * * @return 0 success */ function changeFtsMapping($ftscfg, $mapping, $action, $dictname = null) { if (count($mapping) > 0) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($ftscfg); $this->fieldClean($dictname); $this->arrayClean($mapping); switch ($action) { case 'alter': $whatToDo = "ALTER"; break; case 'drop': $whatToDo = "DROP"; break; default: $whatToDo = "ADD"; break; } $sql = "ALTER TEXT SEARCH CONFIGURATION \"{$f_schema}\".\"{$ftscfg}\" {$whatToDo} MAPPING FOR "; $sql .= implode(",", $mapping); if ($action != 'drop' && !empty($dictname)) { $sql .= " WITH {$dictname}"; } return $this->execute($sql); } else { return -1; } } /** * Return all information related to a given FTS configuration's mapping * @param $ftscfg The name of the FTS configuration * @param $mapping The name of the mapping * * @return FTS configuration information */ function getFtsMappingByName($ftscfg, $mapping) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($ftscfg); $this->clean($mapping); $oidSet = $this->selectSet("SELECT c.oid, cfgparser FROM pg_catalog.pg_ts_config AS c LEFT JOIN pg_catalog.pg_namespace AS n ON n.oid = c.cfgnamespace WHERE c.cfgname = '{$ftscfg}' AND n.nspname='{$c_schema}'"); $oid = $oidSet->fields['oid']; $cfgparser = $oidSet->fields['cfgparser']; $tokenIdSet = $this->selectSet("SELECT tokid FROM pg_catalog.ts_token_type({$cfgparser}) WHERE alias = '{$mapping}'"); $tokid = $tokenIdSet->fields['tokid']; $sql = "SELECT (SELECT t.alias FROM pg_catalog.ts_token_type(c.cfgparser) AS t WHERE t.tokid = m.maptokentype) AS name, d.dictname as dictionaries FROM pg_catalog.pg_ts_config AS c, pg_catalog.pg_ts_config_map AS m, pg_catalog.pg_ts_dict d WHERE c.oid = {$oid} AND m.mapcfg = c.oid AND m.maptokentype = {$tokid} AND m.mapdict = d.oid LIMIT 1;"; return $this->selectSet($sql); } /** * Return list of FTS mappings possible for given parser * (specified by given configuration since configuration can only have 1 parser) * @param $ftscfg The config's name that use the parser * * @return 0 on success */ function getFtsMappings($ftscfg) { $cfg = $this->getFtsConfigurationByName($ftscfg); $sql = "SELECT alias AS name, description FROM pg_catalog.ts_token_type({$cfg->fields['parser_id']}) ORDER BY name"; 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::pg_catalog.regproc AS lanplcallf FROM pg_catalog.pg_language {$where} ORDER BY lanname "; return $this->selectSet($sql); } // Aggregate functions /** * Creates a new aggregate in the database * @param $name The name of the aggregate * @param $basetype The input data type of the aggregate * @param $sfunc The name of the state transition function for the aggregate * @param $stype The data type for the aggregate's state value * @param $ffunc The name of the final function for the aggregate * @param $initcond The initial setting for the state value * @param $sortop The sort operator for the aggregate * @param $comment Aggregate comment * @return 0 success * @return -1 error */ function createAggregate($name, $basetype, $sfunc, $stype, $ffunc, $initcond, $sortop, $comment) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($name); $this->fieldClean($basetype); $this->fieldClean($sfunc); $this->fieldClean($stype); $this->fieldClean($ffunc); $this->fieldClean($initcond); $this->fieldClean($sortop); $this->beginTransaction(); $sql = "CREATE AGGREGATE \"{$f_schema}\".\"{$name}\" (BASETYPE = \"{$basetype}\", SFUNC = \"{$sfunc}\", STYPE = \"{$stype}\""; if(trim($ffunc) != '') $sql .= ", FINALFUNC = \"{$ffunc}\""; if(trim($initcond) != '') $sql .= ", INITCOND = \"{$initcond}\""; if(trim($sortop) != '') $sql .= ", SORTOP = \"{$sortop}\""; $sql .= ")"; $status = $this->execute($sql); if ($status) { $this->rollbackTransaction(); return -1; } if (trim($comment) != '') { $status = $this->setComment('AGGREGATE', $name, '', $comment, $basetype); if ($status) { $this->rollbackTransaction(); return -1; } } return $this->endTransaction(); } /** * Renames an aggregate function * @param $aggrname The actual name of the aggregate * @param $aggrtype The actual input data type of the aggregate * @param $newaggrname The new name of the aggregate * @return 0 success */ function renameAggregate($aggrschema, $aggrname, $aggrtype, $newaggrname) { /* this function is called from alterAggregate where params are cleaned */ $sql = "ALTER AGGREGATE \"{$aggrschema}\"" . '.' . "\"{$aggrname}\" (\"{$aggrtype}\") RENAME TO \"{$newaggrname}\""; return $this->execute($sql); } /** * Removes an aggregate function from the database * @param $aggrname The name of the aggregate * @param $aggrtype The input data type of the aggregate * @param $cascade True to cascade drop, false to restrict * @return 0 success */ function dropAggregate($aggrname, $aggrtype, $cascade) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($aggrname); $this->fieldClean($aggrtype); $sql = "DROP AGGREGATE \"{$f_schema}\".\"{$aggrname}\" (\"{$aggrtype}\")"; if ($cascade) $sql .= " CASCADE"; return $this->execute($sql); } /** * Gets all information for an aggregate * @param $name The name of the aggregate * @param $basetype The input data type of the aggregate * @return A recordset */ function getAggregate($name, $basetype) { $c_schema = $this->_schema; $this->clean($c_schema); $this->fieldclean($name); $this->fieldclean($basetype); $sql = " SELECT p.proname, CASE p.proargtypes[0] WHEN 'pg_catalog.\"any\"'::pg_catalog.regtype THEN NULL ELSE pg_catalog.format_type(p.proargtypes[0], NULL) END AS proargtypes, a.aggtransfn, format_type(a.aggtranstype, NULL) AS aggstype, a.aggfinalfn, a.agginitval, a.aggsortop, u.usename, pg_catalog.obj_description(p.oid, 'pg_proc') AS aggrcomment FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n, pg_catalog.pg_user u, pg_catalog.pg_aggregate a WHERE n.oid = p.pronamespace AND p.proowner=u.usesysid AND p.oid=a.aggfnoid AND p.proisagg AND n.nspname='{$c_schema}' AND p.proname='" . $name . "' AND CASE p.proargtypes[0] WHEN 'pg_catalog.\"any\"'::pg_catalog.regtype THEN '' ELSE pg_catalog.format_type(p.proargtypes[0], NULL) END ='" . $basetype . "'"; return $this->selectSet($sql); } /** * Gets all aggregates * @return A recordset */ function getAggregates() { $c_schema = $this->_schema; $this->clean($c_schema); $sql = "SELECT p.proname, CASE p.proargtypes[0] WHEN 'pg_catalog.\"any\"'::pg_catalog.regtype THEN NULL ELSE pg_catalog.format_type(p.proargtypes[0], NULL) END AS proargtypes, a.aggtransfn, u.usename, pg_catalog.obj_description(p.oid, 'pg_proc') AS aggrcomment FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n, pg_catalog.pg_user u, pg_catalog.pg_aggregate a WHERE n.oid = p.pronamespace AND p.proowner=u.usesysid AND p.oid=a.aggfnoid AND p.proisagg AND n.nspname='{$c_schema}' ORDER BY 1, 2"; return $this->selectSet($sql); } /** * Changes the owner of an aggregate function * @param $aggrname The name of the aggregate * @param $aggrtype The input data type of the aggregate * @param $newaggrowner The new owner of the aggregate * @return 0 success */ function changeAggregateOwner($aggrname, $aggrtype, $newaggrowner) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($aggrname); $this->fieldClean($newaggrowner); $sql = "ALTER AGGREGATE \"{$f_schema}\".\"{$aggrname}\" (\"{$aggrtype}\") OWNER TO \"{$newaggrowner}\""; return $this->execute($sql); } /** * Changes the schema of an aggregate function * @param $aggrname The name of the aggregate * @param $aggrtype The input data type of the aggregate * @param $newaggrschema The new schema for the aggregate * @return 0 success */ function changeAggregateSchema($aggrname, $aggrtype, $newaggrschema) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($aggrname); $this->fieldClean($newaggrschema); $sql = "ALTER AGGREGATE \"{$f_schema}\".\"{$aggrname}\" (\"{$aggrtype}\") SET SCHEMA \"{$newaggrschema}\""; return $this->execute($sql); } /** * Alters an aggregate * @param $aggrname The actual name of the aggregate * @param $aggrtype The actual input data type of the aggregate * @param $aggrowner The actual owner of the aggregate * @param $aggrschema The actual schema the aggregate belongs to * @param $aggrcomment The actual comment for the aggregate * @param $newaggrname The new name of the aggregate * @param $newaggrowner The new owner of the aggregate * @param $newaggrschema The new schema where the aggregate will belong to * @param $newaggrcomment The new comment for the aggregate * @return 0 success * @return -1 change owner error * @return -2 change comment error * @return -3 change schema error * @return -4 change name error */ function alterAggregate($aggrname, $aggrtype, $aggrowner, $aggrschema, $aggrcomment, $newaggrname, $newaggrowner, $newaggrschema, $newaggrcomment) { // Clean fields $this->fieldClean($aggrname); $this->fieldClean($aggrtype); $this->fieldClean($aggrowner); $this->fieldClean($aggrschema); $this->fieldClean($newaggrname); $this->fieldClean($newaggrowner); $this->fieldClean($newaggrschema); $this->beginTransaction(); // Change the owner, if it has changed if($aggrowner != $newaggrowner) { $status = $this->changeAggregateOwner($aggrname, $aggrtype, $newaggrowner); if($status != 0) { $this->rollbackTransaction(); return -1; } } // Set the comment, if it has changed if($aggrcomment != $newaggrcomment) { $status = $this->setComment('AGGREGATE', $aggrname, '', $newaggrcomment, $aggrtype); if ($status) { $this->rollbackTransaction(); return -2; } } // Change the schema, if it has changed if($aggrschema != $newaggrschema) { $status = $this->changeAggregateSchema($aggrname, $aggrtype, $newaggrschema); if($status != 0) { $this->rollbackTransaction(); return -3; } } // Rename the aggregate, if it has changed if($aggrname != $newaggrname) { $status = $this->renameAggregate($newaggrschema, $aggrname, $aggrtype, $newaggrname); if($status != 0) { $this->rollbackTransaction(); return -4; } } return $this->endTransaction(); } // Role, User/Group functions /** * Returns all roles in the database cluster * @param $rolename (optional) The role name to exclude from the select * @return All roles */ function getRoles($rolename = '') { $sql = ' SELECT rolname, rolsuper, rolcreatedb, rolcreaterole, rolinherit, rolcanlogin, rolconnlimit, rolvaliduntil, rolconfig FROM pg_catalog.pg_roles'; if($rolename) $sql .= " WHERE rolname!='{$rolename}'"; $sql .= ' ORDER BY rolname'; return $this->selectSet($sql); } /** * Returns information about a single role * @param $rolename The name of the role to retrieve * @return The role's data */ function getRole($rolename) { $this->clean($rolename); $sql = " SELECT rolname, rolsuper, rolcreatedb, rolcreaterole, rolinherit, rolcanlogin, rolconnlimit, rolvaliduntil, rolconfig FROM pg_catalog.pg_roles WHERE rolname='{$rolename}'"; return $this->selectSet($sql); } /** * Grants membership in a role * @param $role The name of the target role * @param $rolename The name of the role that will belong to the target role * @param $admin (optional) Flag to grant the admin option * @return 0 success */ function grantRole($role, $rolename, $admin=0) { $this->fieldClean($role); $this->fieldClean($rolename); $sql = "GRANT \"{$role}\" TO \"{$rolename}\""; if($admin == 1) $sql .= ' WITH ADMIN OPTION'; return $this->execute($sql); } /** * Revokes membership in a role * @param $role The name of the target role * @param $rolename The name of the role that will not belong to the target role * @param $admin (optional) Flag to revoke only the admin option * @param $type (optional) Type of revoke: RESTRICT | CASCADE * @return 0 success */ function revokeRole($role, $rolename, $admin = 0, $type = 'RESTRICT') { $this->fieldClean($role); $this->fieldClean($rolename); $sql = "REVOKE "; if($admin == 1) $sql .= 'ADMIN OPTION FOR '; $sql .= "\"{$role}\" FROM \"{$rolename}\" {$type}"; return $this->execute($sql); } /** * Returns all users in the database cluster * @return All users */ function getUsers() { $sql = "SELECT usename, usesuper, usecreatedb, valuntil AS useexpires, useconfig 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 AS useexpires, useconfig FROM pg_user WHERE usename='{$username}'"; return $this->selectSet($sql); } /** * Creates a new role * @param $rolename The name of the role to create * @param $password A password for the role * @param $superuser Boolean whether or not the role is a superuser * @param $createdb Boolean whether or not the role can create databases * @param $createrole Boolean whether or not the role can create other roles * @param $inherits Boolean whether or not the role inherits the privileges from parent roles * @param $login Boolean whether or not the role will be allowed to login * @param $connlimit Number of concurrent connections the role can make * @param $expiry String Format 'YYYY-MM-DD HH:MM:SS'. '' means never expire * @param $memberof (array) Roles to which the new role will be immediately added as a new member * @param $members (array) Roles which are automatically added as members of the new role * @param $adminmembers (array) Roles which are automatically added as admin members of the new role * @return 0 success */ function createRole($rolename, $password, $superuser, $createdb, $createrole, $inherits, $login, $connlimit, $expiry, $memberof, $members, $adminmembers) { $enc = $this->_encryptPassword($rolename, $password); $this->fieldClean($rolename); $this->clean($enc); $this->clean($connlimit); $this->clean($expiry); $this->fieldArrayClean($memberof); $this->fieldArrayClean($members); $this->fieldArrayClean($adminmembers); $sql = "CREATE ROLE \"{$rolename}\""; if ($password != '') $sql .= " WITH ENCRYPTED PASSWORD '{$enc}'"; $sql .= ($superuser) ? ' SUPERUSER' : ' NOSUPERUSER'; $sql .= ($createdb) ? ' CREATEDB' : ' NOCREATEDB'; $sql .= ($createrole) ? ' CREATEROLE' : ' NOCREATEROLE'; $sql .= ($inherits) ? ' INHERIT' : ' NOINHERIT'; $sql .= ($login) ? ' LOGIN' : ' NOLOGIN'; if ($connlimit != '') $sql .= " CONNECTION LIMIT {$connlimit}"; else $sql .= ' CONNECTION LIMIT -1'; if ($expiry != '') $sql .= " VALID UNTIL '{$expiry}'"; else $sql .= " VALID UNTIL 'infinity'"; if (is_array($memberof) && sizeof($memberof) > 0) $sql .= ' IN ROLE "' . join('", "', $memberof) . '"'; if (is_array($members) && sizeof($members) > 0) $sql .= ' ROLE "' . join('", "', $members) . '"'; if (is_array($adminmembers) && sizeof($adminmembers) > 0) $sql .= ' ADMIN "' . join('", "', $adminmembers) . '"'; return $this->execute($sql); } /** * Adjusts a role's info * @param $rolename The name of the role to adjust * @param $password A password for the role * @param $superuser Boolean whether or not the role is a superuser * @param $createdb Boolean whether or not the role can create databases * @param $createrole Boolean whether or not the role can create other roles * @param $inherits Boolean whether or not the role inherits the privileges from parent roles * @param $login Boolean whether or not the role will be allowed to login * @param $connlimit Number of concurrent connections the role can make * @param $expiry string Format 'YYYY-MM-DD HH:MM:SS'. '' means never expire * @param $memberof (array) Roles to which the role will be immediately added as a new member * @param $members (array) Roles which are automatically added as members of the role * @param $adminmembers (array) Roles which are automatically added as admin members of the role * @param $memberofold (array) Original roles whose the role belongs to * @param $membersold (array) Original roles that are members of the role * @param $adminmembersold (array) Original roles that are admin members of the role * @return 0 success */ function setRole($rolename, $password, $superuser, $createdb, $createrole, $inherits, $login, $connlimit, $expiry, $memberof, $members, $adminmembers, $memberofold, $membersold, $adminmembersold) { $enc = $this->_encryptPassword($rolename, $password); $this->fieldClean($rolename); $this->clean($enc); $this->clean($connlimit); $this->clean($expiry); $this->fieldArrayClean($memberof); $this->fieldArrayClean($members); $this->fieldArrayClean($adminmembers); $sql = "ALTER ROLE \"{$rolename}\""; if ($password != '') $sql .= " WITH ENCRYPTED PASSWORD '{$enc}'"; $sql .= ($superuser) ? ' SUPERUSER' : ' NOSUPERUSER'; $sql .= ($createdb) ? ' CREATEDB' : ' NOCREATEDB'; $sql .= ($createrole) ? ' CREATEROLE' : ' NOCREATEROLE'; $sql .= ($inherits) ? ' INHERIT' : ' NOINHERIT'; $sql .= ($login) ? ' LOGIN' : ' NOLOGIN'; if ($connlimit != '') $sql .= " CONNECTION LIMIT {$connlimit}"; else $sql .= ' CONNECTION LIMIT -1'; if ($expiry != '') $sql .= " VALID UNTIL '{$expiry}'"; else $sql .= " VALID UNTIL 'infinity'"; $status = $this->execute($sql); if ($status != 0) return -1; //memberof $old = explode(',', $memberofold); foreach ($memberof as $m) { if (!in_array($m, $old)) { $status = $this->grantRole($m, $rolename); if ($status != 0) return -1; } } if($memberofold) { foreach ($old as $o) { if (!in_array($o, $memberof)) { $status = $this->revokeRole($o, $rolename, 0, 'CASCADE'); if ($status != 0) return -1; } } } //members $old = explode(',', $membersold); foreach ($members as $m) { if (!in_array($m, $old)) { $status = $this->grantRole($rolename, $m); if ($status != 0) return -1; } } if($membersold) { foreach ($old as $o) { if (!in_array($o, $members)) { $status = $this->revokeRole($rolename, $o, 0, 'CASCADE'); if ($status != 0) return -1; } } } //adminmembers $old = explode(',', $adminmembersold); foreach ($adminmembers as $m) { if (!in_array($m, $old)) { $status = $this->grantRole($rolename, $m, 1); if ($status != 0) return -1; } } if($adminmembersold) { foreach ($old as $o) { if (!in_array($o, $adminmembers)) { $status = $this->revokeRole($rolename, $o, 1, 'CASCADE'); if ($status != 0) return -1; } } } return $status; } /** * Renames a role * @param $rolename The name of the role to rename * @param $newrolename The new name of the role * @return 0 success */ function renameRole($rolename, $newrolename){ $this->fieldClean($rolename); $this->fieldClean($newrolename); $sql = "ALTER ROLE \"{$rolename}\" RENAME TO \"{$newrolename}\""; return $this->execute($sql); } /** * Adjusts a role's info and renames it * @param $rolename The name of the role to adjust * @param $password A password for the role * @param $superuser Boolean whether or not the role is a superuser * @param $createdb Boolean whether or not the role can create databases * @param $createrole Boolean whether or not the role can create other roles * @param $inherits Boolean whether or not the role inherits the privileges from parent roles * @param $login Boolean whether or not the role will be allowed to login * @param $connlimit Number of concurrent connections the role can make * @param $expiry string Format 'YYYY-MM-DD HH:MM:SS'. '' means never expire * @param $memberof (array) Roles to which the role will be immediately added as a new member * @param $members (array) Roles which are automatically added as members of the role * @param $adminmembers (array) Roles which are automatically added as admin members of the role * @param $memberofold (array) Original roles whose the role belongs to * @param $membersold (array) Original roles that are members of the role * @param $adminmembersold (array) Original roles that are admin members of the role * @param $newrolename The new name of the role * @return 0 success * @return -1 transaction error * @return -2 set role attributes error * @return -3 rename error */ function setRenameRole($rolename, $password, $superuser, $createdb, $createrole, $inherits, $login, $connlimit, $expiry, $memberof, $members, $adminmembers, $memberofold, $membersold, $adminmembersold, $newrolename) { $status = $this->beginTransaction(); if ($status != 0) return -1; if ($rolename != $newrolename){ $status = $this->renameRole($rolename, $newrolename); if ($status != 0) { $this->rollbackTransaction(); return -3; } $rolename = $newrolename; } $status = $this->setRole($rolename, $password, $superuser, $createdb, $createrole, $inherits, $login, $connlimit, $expiry, $memberof, $members, $adminmembers, $memberofold, $membersold, $adminmembersold); if ($status != 0) { $this->rollbackTransaction(); return -2; } return $this->endTransaction(); } /** * Removes a role * @param $rolename The name of the role to drop * @return 0 success */ function dropRole($rolename) { $this->fieldClean($rolename); $sql = "DROP ROLE \"{$rolename}\""; return $this->execute($sql); } /** * 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) { $enc = $this->_encryptPassword($username, $password); $this->fieldClean($username); $this->clean($enc); $this->clean($expiry); $this->fieldArrayClean($groups); $sql = "CREATE USER \"{$username}\""; if ($password != '') $sql .= " WITH ENCRYPTED PASSWORD '{$enc}'"; $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); } /** * Renames a user * @param $username The username of the user to rename * @param $newname The new name of the user * @return 0 success */ function renameUser($username, $newname){ $this->fieldClean($username); $this->fieldClean($newname); $sql = "ALTER USER \"{$username}\" RENAME TO \"{$newname}\""; 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) { $enc = $this->_encryptPassword($username, $password); $this->fieldClean($username); $this->clean($enc); $this->clean($expiry); $sql = "ALTER USER \"{$username}\""; if ($password != '') $sql .= " WITH ENCRYPTED PASSWORD '{$enc}'"; $sql .= ($createdb) ? ' CREATEDB' : ' NOCREATEDB'; $sql .= ($createuser) ? ' CREATEUSER' : ' NOCREATEUSER'; if ($expiry != '') $sql .= " VALID UNTIL '{$expiry}'"; else $sql .= " VALID UNTIL 'infinity'"; return $this->execute($sql); } /** * Adjusts a user's info and renames the user * @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. * @param $newname The new name of the user * @return 0 success * @return -1 transaction error * @return -2 set user attributes error * @return -3 rename error */ function setRenameUser($username, $password, $createdb, $createuser, $expiry, $newname) { $status = $this->beginTransaction(); if ($status != 0) return -1; if ($username != $newname){ $status = $this->renameUser($username, $newname); if ($status != 0) { $this->rollbackTransaction(); return -3; } $username = $newname; } $status = $this->setUser($username, $password, $createdb, $createuser, $expiry); if ($status != 0) { $this->rollbackTransaction(); return -2; } return $this->endTransaction(); } /** * 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); } /** * 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); if (function_exists('pg_parameter_status')) { $val = pg_parameter_status($this->conn->_connectionID, 'is_superuser'); if ($val !== false) return $val == 'on'; } $sql = "SELECT usesuper FROM pg_user WHERE usename='{$username}'"; $usesuper = $this->selectField($sql, 'usesuper'); if ($usesuper == -1) return false; else return $usesuper == 't'; } /** * Changes a role's password * @param $rolename The role name * @param $password The new password * @return 0 success */ function changePassword($rolename, $password) { $enc = $this->_encryptPassword($rolename, $password); $this->fieldClean($rolename); $this->clean($enc); $sql = "ALTER ROLE \"{$rolename}\" WITH ENCRYPTED PASSWORD '{$enc}'"; 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); } /** * Returns all role names which the role belongs to * @param $rolename The role name * @return All role names */ function getMemberOf($rolename) { $this->clean($rolename); $sql = " SELECT rolname FROM pg_catalog.pg_roles R, pg_auth_members M WHERE R.oid=M.roleid AND member IN ( SELECT oid FROM pg_catalog.pg_roles WHERE rolname='{$rolename}') ORDER BY rolname"; return $this->selectSet($sql); } /** * Returns all role names that are members of a role * @param $rolename The role name * @param $admin (optional) Find only admin members * @return All role names */ function getMembers($rolename, $admin = 'f') { $this->clean($rolename); $sql = " SELECT rolname FROM pg_catalog.pg_roles R, pg_auth_members M WHERE R.oid=M.member AND admin_option='{$admin}' AND roleid IN (SELECT oid FROM pg_catalog.pg_roles WHERE rolname='{$rolename}') ORDER BY rolname"; return $this->selectSet($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); } /** * Return 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 s.usename FROM pg_catalog.pg_user s, pg_catalog.pg_group g WHERE g.groname='{$groname}' AND s.usesysid = ANY (g.grolist) ORDER BY s.usename"; return $this->selectSet($sql); } /** * 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); } /** * 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); } /** * 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'; else if ($this->hasRoles()) { $atype = 'role'; } else if (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. database, schema, relation, function or language) * @param $table Optional, column's table if type = column * @return Privileges array * @return -1 invalid type * @return -2 object not found * @return -3 unknown privilege type */ function getPrivileges($object, $type, $table = null) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($object); switch ($type) { case 'column': $this->clean($table); $sql = " SELECT E'{' || pg_catalog.array_to_string(attacl, E',') || E'}' as acl FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_class c ON (a.attrelid = c.oid) LEFT JOIN pg_catalog.pg_namespace n ON (c.relnamespace=n.oid) WHERE n.nspname='{$c_schema}' AND c.relname='{$table}' AND a.attname='{$object}'"; break; case 'table': case 'view': case 'sequence': $sql = " SELECT relacl AS acl FROM pg_catalog.pg_class WHERE relname='{$object}' AND relnamespace=(SELECT oid FROM pg_catalog.pg_namespace WHERE nspname='{$c_schema}')"; break; case 'database': $sql = "SELECT datacl AS acl FROM pg_catalog.pg_database WHERE datname='{$object}'"; break; case 'function': // Since we fetch functions by oid, they are already constrained to // the current schema. $sql = "SELECT proacl AS acl FROM pg_catalog.pg_proc WHERE oid='{$object}'"; break; case 'language': $sql = "SELECT lanacl AS acl FROM pg_catalog.pg_language WHERE lanname='{$object}'"; break; case 'schema': $sql = "SELECT nspacl AS acl FROM pg_catalog.pg_namespace WHERE nspname='{$object}'"; break; case 'tablespace': $sql = "SELECT spcacl AS acl FROM pg_catalog.pg_tablespace WHERE spcname='{$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 * @param $table the column's table if type=column * @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, $table ) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $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'; } else { if ($type == 'column') { $this->fieldClean($object); $sql .= ' ' . join(" (\"{$object}\"), ", $privileges); } else { $sql .= ' ' . join(', ', $privileges); } } switch ($type) { case 'column': $sql .= " (\"{$object}\")"; $object = $table; case 'table': case 'view': case 'sequence': $this->fieldClean($object); $sql .= " ON \"{$f_schema}\".\"{$object}\""; break; case 'database': $this->fieldClean($object); $sql .= " ON DATABASE \"{$object}\""; break; case 'function': // Function comes in with $object as function OID $fn = $this->getFunction($object); $this->fieldClean($fn->fields['proname']); $sql .= " ON FUNCTION \"{$f_schema}\".\"{$fn->fields['proname']}\"({$fn->fields['proarguments']})"; break; case 'language': $this->fieldClean($object); $sql .= " ON LANGUAGE \"{$object}\""; break; case 'schema': $this->fieldClean($object); $sql .= " ON SCHEMA \"{$object}\""; break; case 'tablespace': $this->fieldClean($object); $sql .= " ON TABLESPACE \"{$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); } /** * Helper function that computes encypted PostgreSQL passwords * @param $username The username * @param $password The password */ function _encryptPassword($username, $password) { return 'md5' . md5($password . $username); } // Tablespace functions /** * Retrieves information for all tablespaces * @param $all Include all tablespaces (necessary when moving objects back to the default space) * @return A recordset */ function getTablespaces($all = false) { global $conf; $sql = "SELECT spcname, pg_catalog.pg_get_userbyid(spcowner) AS spcowner, spclocation, (SELECT description FROM pg_catalog.pg_shdescription pd WHERE pg_tablespace.oid=pd.objoid) AS spccomment FROM pg_catalog.pg_tablespace"; if (!$conf['show_system'] && !$all) { $sql .= ' WHERE spcname NOT LIKE $$pg\_%$$'; } $sql .= " ORDER BY spcname"; return $this->selectSet($sql); } /** * Retrieves a tablespace's information * @return A recordset */ function getTablespace($spcname) { $this->clean($spcname); $sql = "SELECT spcname, pg_catalog.pg_get_userbyid(spcowner) AS spcowner, spclocation, (SELECT description FROM pg_catalog.pg_shdescription pd WHERE pg_tablespace.oid=pd.objoid) AS spccomment FROM pg_catalog.pg_tablespace WHERE spcname='{$spcname}'"; return $this->selectSet($sql); } /** * Creates a tablespace * @param $spcname The name of the tablespace to create * @param $spcowner The owner of the tablespace. '' for current * @param $spcloc The directory in which to create the tablespace * @return 0 success */ function createTablespace($spcname, $spcowner, $spcloc, $comment='') { $this->fieldClean($spcname); $this->clean($spcloc); $sql = "CREATE TABLESPACE \"{$spcname}\""; if ($spcowner != '') { $this->fieldClean($spcowner); $sql .= " OWNER \"{$spcowner}\""; } $sql .= " LOCATION '{$spcloc}'"; $status = $this->execute($sql); if ($status != 0) return -1; if ($comment != '' && $this->hasSharedComments()) { $status = $this->setComment('TABLESPACE',$spcname,'',$comment); if ($status != 0) return -2; } return 0; } /** * Alters a tablespace * @param $spcname The name of the tablespace * @param $name The new name for the tablespace * @param $owner The new owner for the tablespace * @return 0 success * @return -1 transaction error * @return -2 owner error * @return -3 rename error * @return -4 comment error */ function alterTablespace($spcname, $name, $owner, $comment='') { $this->fieldClean($spcname); $this->fieldClean($name); $this->fieldClean($owner); // Begin transaction $status = $this->beginTransaction(); if ($status != 0) return -1; // Owner $sql = "ALTER TABLESPACE \"{$spcname}\" OWNER TO \"{$owner}\""; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -2; } // Rename (only if name has changed) if ($name != $spcname) { $sql = "ALTER TABLESPACE \"{$spcname}\" RENAME TO \"{$name}\""; $status = $this->execute($sql); if ($status != 0) { $this->rollbackTransaction(); return -3; } } // Set comment if it has changed if (trim($comment) != '' && $this->hasSharedComments()) { $status = $this->setComment('TABLESPACE',$spcname,'',$comment); if ($status != 0) return -4; } return $this->endTransaction(); } /** * Drops a tablespace * @param $spcname The name of the domain to drop * @return 0 success */ function dropTablespace($spcname) { $this->fieldClean($spcname); $sql = "DROP TABLESPACE \"{$spcname}\""; return $this->execute($sql); } // Administration functions /** * Analyze a database * @param $table (optional) The table to analyze */ function analyzeDB($table = '') { if ($table != '') { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $sql = "ANALYZE \"{$f_schema}\".\"{$table}\""; } else $sql = "ANALYZE"; return $this->execute($sql); } /** * Vacuums a database * @param $table The table to vacuum * @param $analyze If true, also does analyze * @param $full If true, selects "full" vacuum * @param $freeze If true, selects aggressive "freezing" of tuples */ function vacuumDB($table = '', $analyze = false, $full = false, $freeze = false) { $sql = "VACUUM"; if ($full) $sql .= " FULL"; if ($freeze) $sql .= " FREEZE"; if ($analyze) $sql .= " ANALYZE"; if ($table != '') { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $sql .= " \"{$f_schema}\".\"{$table}\""; } return $this->execute($sql); } /** * Returns all autovacuum global configuration * @return associative array array( param => value, ...) */ function getAutovacuum() { $_defaults = $this->selectSet("SELECT name, setting FROM pg_catalog.pg_settings WHERE name = 'autovacuum' OR name = 'autovacuum_vacuum_threshold' OR name = 'autovacuum_vacuum_scale_factor' OR name = 'autovacuum_analyze_threshold' OR name = 'autovacuum_analyze_scale_factor' OR name = 'autovacuum_vacuum_cost_delay' OR name = 'autovacuum_vacuum_cost_limit' OR name = 'vacuum_freeze_min_age' OR name = 'autovacuum_freeze_max_age' " ); $ret = array(); while (!$_defaults->EOF) { $ret[$_defaults->fields['name']] = $_defaults->fields['setting']; $_defaults->moveNext(); } return $ret; } /** * Returns all available autovacuum per table information. * @return A recordset */ function saveAutovacuum($table, $vacenabled, $vacthreshold, $vacscalefactor, $anathresold, $anascalefactor, $vaccostdelay, $vaccostlimit) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $sql = "ALTER TABLE \"{$f_schema}\".\"{$table}\" SET ("; if (!empty($vacenabled)) { $this->clean($vacenabled); $params[] = "autovacuum_enabled='{$vacenabled}'"; } if (!empty($vacthreshold)) { $this->clean($vacthreshold); $params[] = "autovacuum_vacuum_threshold='{$vacthreshold}'"; } if (!empty($vacscalefactor)) { $this->clean($vacscalefactor); $params[] = "autovacuum_vacuum_scale_factor='{$vacscalefactor}'"; } if (!empty($anathresold)) { $this->clean($anathresold); $params[] = "autovacuum_analyze_threshold='{$anathresold}'"; } if (!empty($anascalefactor)) { $this->clean($anascalefactor); $params[] = "autovacuum_analyze_scale_factor='{$anascalefactor}'"; } if (!empty($vaccostdelay)) { $this->clean($vaccostdelay); $params[] = "autovacuum_vacuum_cost_delay='{$vaccostdelay}'"; } if (!empty($vaccostlimit)) { $this->clean($vaccostlimit); $params[] = "autovacuum_vacuum_cost_limit='{$vaccostlimit}'"; } $sql = $sql . implode(',', $params) . ');'; return $this->execute($sql); } function dropAutovacuum($table) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); return $this->execute(" ALTER TABLE \"{$f_schema}\".\"{$table}\" RESET (autovacuum_enabled, autovacuum_vacuum_threshold, autovacuum_vacuum_scale_factor, autovacuum_analyze_threshold, autovacuum_analyze_scale_factor, autovacuum_vacuum_cost_delay, autovacuum_vacuum_cost_limit );" ); } /** * Returns all available process information. * @param $database (optional) Find only connections to specified database * @return A recordset */ function getProcesses($database = null) { if ($database === null) $sql = "SELECT * FROM pg_catalog.pg_stat_activity ORDER BY datname, usename, procpid"; else { $this->clean($database); $sql = " SELECT * FROM pg_catalog.pg_stat_activity WHERE datname='{$database}' ORDER BY usename, procpid"; } return $this->selectSet($sql); } /** * Returns table locks information in the current database * @return A recordset */ function getLocks() { global $conf; if (!$conf['show_system']) $where = 'AND pn.nspname NOT LIKE $$pg\_%$$'; else $where = "AND nspname !~ '^pg_t(emp_[0-9]+|oast)$'"; $sql = " SELECT pn.nspname, pc.relname AS tablename, pl.pid, pl.mode, pl.granted, pl.virtualtransaction, (select transactionid from pg_catalog.pg_locks l2 where l2.locktype='transactionid' and l2.mode='ExclusiveLock' and l2.virtualtransaction=pl.virtualtransaction) as transaction FROM pg_catalog.pg_locks pl, pg_catalog.pg_class pc, pg_catalog.pg_namespace pn WHERE pl.relation = pc.oid AND pc.relnamespace=pn.oid {$where} ORDER BY pid,nspname,tablename"; return $this->selectSet($sql); } /** * Sends a cancel or kill command to a process * @param $pid The ID of the backend process * @param $signal 'CANCEL' * @return 0 success * @return -1 invalid signal type */ function sendSignal($pid, $signal) { // Clean $pid = (int)$pid; if ($signal == 'CANCEL') $sql = "SELECT pg_catalog.pg_cancel_backend({$pid}) AS val"; elseif ($signal == 'KILL') $sql = "SELECT pg_catalog.pg_terminate_backend({$pid}) AS val"; else return -1; // Execute the query $val = $this->selectField($sql, 'val'); if ($val === 'f') return -1; elseif ($val === 't') return 0; else return -1; } // Misc functions /** * Sets the comment for an object in the database * @pre All parameters must already be cleaned * @param $obj_type One of 'TABLE' | 'COLUMN' | 'VIEW' | 'SCHEMA' | 'SEQUENCE' | 'TYPE' | 'FUNCTION' | 'AGGREGATE' * @param $obj_name The name of the object for which to attach a comment. * @param $table Name of table that $obj_name belongs to. Ignored unless $obj_type is 'TABLE' or 'COLUMN'. * @param $comment The comment to add. * @return 0 success */ function setComment($obj_type, $obj_name, $table, $comment, $basetype = NULL) { $sql = "COMMENT ON {$obj_type} " ; $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->clean($comment); // Passing in an already cleaned comment will lead to double escaped data // So, while counter-intuitive, it is important to not clean comments before // calling setComment. We will clean it here instead. /* $this->fieldClean($table); $this->fieldClean($obj_name); */ switch ($obj_type) { case 'TABLE': $sql .= "\"{$f_schema}\".\"{$table}\" IS "; break; case 'COLUMN': $sql .= "\"{$f_schema}\".\"{$table}\".\"{$obj_name}\" IS "; break; case 'SEQUENCE': case 'VIEW': case 'TEXT SEARCH CONFIGURATION': case 'TEXT SEARCH DICTIONARY': case 'TEXT SEARCH TEMPLATE': case 'TEXT SEARCH PARSER': case 'TYPE': $sql .= "\"{$f_schema}\"."; case 'DATABASE': case 'ROLE': case 'SCHEMA': case 'TABLESPACE': $sql .= "\"{$obj_name}\" IS "; break; case 'FUNCTION': $sql .= "\"{$f_schema}\".{$obj_name} IS "; break; case 'AGGREGATE': $sql .= "\"{$f_schema}\".\"{$obj_name}\" (\"{$basetype}\") IS "; break; default: // Unknown object type return -1; } if ($comment != '') $sql .= "'{$comment}';"; else $sql .= 'NULL;'; return $this->execute($sql); } /** * Sets the client encoding * @param $encoding The encoding to for the client * @return 0 success */ function setClientEncoding($encoding) { $this->clean($encoding); $sql = "SET CLIENT_ENCODING TO '{$encoding}'"; return $this->execute($sql); } /** * A private helper method for executeScript that advances the * character by 1. In psql this is careful to take into account * multibyte languages, but we don't at the moment, so this function * is someone redundant, since it will always advance by 1 * @param &$i The current character position in the line * @param &$prevlen Length of previous character (ie. 1) * @param &$thislen Length of current character (ie. 1) */ private function advance_1(&$i, &$prevlen, &$thislen) { $prevlen = $thislen; $i += $thislen; $thislen = 1; } /** * Private helper method to detect a valid $foo$ quote delimiter at * the start of the parameter dquote * @return True if valid, false otherwise */ private function valid_dolquote($dquote) { // XXX: support multibyte return (preg_match('/^[$][$]/', $dquote) || preg_match('/^[$][_[:alpha:]][_[:alnum:]]*[$]/', $dquote)); } /** * Executes an SQL script as a series of SQL statements. Returns * the result of the final step. This is a very complicated lexer * based on the REL7_4_STABLE src/bin/psql/mainloop.c lexer in * the PostgreSQL source code. * XXX: It does not handle multibyte languages properly. * @param $name Entry in $_FILES to use * @param $callback (optional) Callback function to call with each query, its result and line number. * @return True for general success, false on any failure. */ function executeScript($name, $callback = null) { global $data; // This whole function isn't very encapsulated, but hey... $conn = $data->conn->_connectionID; if (!is_uploaded_file($_FILES[$name]['tmp_name'])) return false; $fd = fopen($_FILES[$name]['tmp_name'], 'r'); if (!$fd) return false; // Build up each SQL statement, they can be multiline $query_buf = null; $query_start = 0; $in_quote = 0; $in_xcomment = 0; $bslash_count = 0; $dol_quote = null; $paren_level = 0; $len = 0; $i = 0; $prevlen = 0; $thislen = 0; $lineno = 0; // Loop over each line in the file while (!feof($fd)) { $line = fgets($fd); $lineno++; // Nothing left on line? Then ignore... if (trim($line) == '') continue; $len = strlen($line); $query_start = 0; /* * Parse line, looking for command separators. * * The current character is at line[i], the prior character at line[i * - prevlen], the next character at line[i + thislen]. */ $prevlen = 0; $thislen = ($len > 0) ? 1 : 0; for ($i = 0; $i < $len; $this->advance_1($i, $prevlen, $thislen)) { /* was the previous character a backslash? */ if ($i > 0 && substr($line, $i - $prevlen, 1) == '\\') $bslash_count++; else $bslash_count = 0; /* * It is important to place the in_* test routines before the * in_* detection routines. i.e. we have to test if we are in * a quote before testing for comments. */ /* in quote? */ if ($in_quote !== 0) { /* * end of quote if matching non-backslashed character. * backslashes don't count for double quotes, though. */ if (substr($line, $i, 1) == $in_quote && ($bslash_count % 2 == 0 || $in_quote == '"')) $in_quote = 0; } /* in or end of $foo$ type quote? */ else if ($dol_quote) { if (strncmp(substr($line, $i), $dol_quote, strlen($dol_quote)) == 0) { $this->advance_1($i, $prevlen, $thislen); while(substr($line, $i, 1) != '$') $this->advance_1($i, $prevlen, $thislen); $dol_quote = null; } } /* start of extended comment? */ else if (substr($line, $i, 2) == '/*') { $in_xcomment++; if ($in_xcomment == 1) $this->advance_1($i, $prevlen, $thislen); } /* in or end of extended comment? */ else if ($in_xcomment) { if (substr($line, $i, 2) == '*/' && !--$in_xcomment) $this->advance_1($i, $prevlen, $thislen); } /* start of quote? */ else if (substr($line, $i, 1) == '\'' || substr($line, $i, 1) == '"') { $in_quote = substr($line, $i, 1); } /* * start of $foo$ type quote? */ else if (!$dol_quote && $this->valid_dolquote(substr($line, $i))) { $dol_end = strpos(substr($line, $i + 1), '$'); $dol_quote = substr($line, $i, $dol_end + 1); $this->advance_1($i, $prevlen, $thislen); while (substr($line, $i, 1) != '$') { $this->advance_1($i, $prevlen, $thislen); } } /* single-line comment? truncate line */ else if (substr($line, $i, 2) == '--') { $line = substr($line, 0, $i); /* remove comment */ break; } /* count nested parentheses */ else if (substr($line, $i, 1) == '(') { $paren_level++; } else if (substr($line, $i, 1) == ')' && $paren_level > 0) { $paren_level--; } /* semicolon? then send query */ else if (substr($line, $i, 1) == ';' && !$bslash_count && !$paren_level) { $subline = substr(substr($line, 0, $i), $query_start); /* is there anything else on the line? */ if (strspn($subline, " \t\n\r") != strlen($subline)) { /* * insert a cosmetic newline, if this is not the first * line in the buffer */ if (strlen($query_buf) > 0) $query_buf .= "\n"; /* append the line to the query buffer */ $query_buf .= $subline; $query_buf .= ';'; // Execute the query (supporting 4.1.x PHP...). PHP cannot execute // empty queries, unlike libpq if (function_exists('pg_query')) $res = @pg_query($conn, $query_buf); else $res = @pg_exec($conn, $query_buf); // Call the callback function for display if ($callback !== null) $callback($query_buf, $res, $lineno); // Check for COPY request if (pg_result_status($res) == 4) { // 4 == PGSQL_COPY_FROM while (!feof($fd)) { $copy = fgets($fd, 32768); $lineno++; pg_put_line($conn, $copy); if ($copy == "\\.\n" || $copy == "\\.\r\n") { pg_end_copy($conn); break; } } } } $query_buf = null; $query_start = $i + $thislen; } /* * keyword or identifier? * We grab the whole string so that we don't * mistakenly see $foo$ inside an identifier as the start * of a dollar quote. */ // XXX: multibyte here else if (preg_match('/^[_[:alpha:]]$/', substr($line, $i, 1))) { $sub = substr($line, $i, $thislen); while (preg_match('/^[\$_A-Za-z0-9]$/', $sub)) { /* keep going while we still have identifier chars */ $this->advance_1($i, $prevlen, $thislen); $sub = substr($line, $i, $thislen); } // Since we're now over the next character to be examined, it is necessary // to move back one space. $i-=$prevlen; } } // end for /* Put the rest of the line in the query buffer. */ $subline = substr($line, $query_start); if ($in_quote || $dol_quote || strspn($subline, " \t\n\r") != strlen($subline)) { if (strlen($query_buf) > 0) $query_buf .= "\n"; $query_buf .= $subline; } $line = null; } // end while /* * Process query at the end of file without a semicolon, so long as * it's non-empty. */ if (strlen($query_buf) > 0 && strspn($query_buf, " \t\n\r") != strlen($query_buf)) { // Execute the query (supporting 4.1.x PHP...) if (function_exists('pg_query')) $res = @pg_query($conn, $query_buf); else $res = @pg_exec($conn, $query_buf); // Call the callback function for display if ($callback !== null) $callback($query_buf, $res, $lineno); // Check for COPY request if (pg_result_status($res) == 4) { // 4 == PGSQL_COPY_FROM while (!feof($fd)) { $copy = fgets($fd, 32768); $lineno++; pg_put_line($conn, $copy); if ($copy == "\\.\n" || $copy == "\\.\r\n") { pg_end_copy($conn); break; } } } } fclose($fd); return true; } /** * Generates the SQL for the 'select' function * @param $table The table from which to select * @param $show An array of columns to show. Empty array means all columns. * @param $values An array mapping columns to values * @param $ops An array of the operators to use * @param $orderby (optional) An array of column numbers or names (one based) * mapped to sort direction (asc or desc or '' or null) to order by * @return The SQL query */ function getSelectSQL($table, $show, $values, $ops, $orderby = array()) { $this->fieldArrayClean($show); // If an empty array is passed in, then show all columns if (sizeof($show) == 0) { if ($this->hasObjectID($table)) $sql = "SELECT \"{$this->id}\", * FROM "; else $sql = "SELECT * FROM "; } else { // Add oid column automatically to results for editing purposes if (!in_array($this->id, $show) && $this->hasObjectID($table)) $sql = "SELECT \"{$this->id}\", \""; else $sql = "SELECT \""; $sql .= join('","', $show) . "\" FROM "; } $this->fieldClean($table); if (isset($_REQUEST['schema'])) { $f_schema = $_REQUEST['schema']; $this->fieldClean($f_schema); $sql .= "\"{$f_schema}\"."; } $sql .= "\"{$table}\""; // If we have values specified, add them to the WHERE clause $first = true; if (is_array($values) && sizeof($values) > 0) { foreach ($values as $k => $v) { if ($v != '' || $this->selectOps[$ops[$k]] == 'p') { $this->fieldClean($k); if ($first) { $sql .= " WHERE "; $first = false; } else { $sql .= " AND "; } // Different query format depending on operator type switch ($this->selectOps[$ops[$k]]) { case 'i': // Only clean the field for the inline case // this is because (x), subqueries need to // to allow 'a','b' as input. $this->clean($v); $sql .= "\"{$k}\" {$ops[$k]} '{$v}'"; break; case 'p': $sql .= "\"{$k}\" {$ops[$k]}"; break; case 'x': $sql .= "\"{$k}\" {$ops[$k]} ({$v})"; break; case 't': $sql .= "\"{$k}\" {$ops[$k]}('{$v}')"; break; default: // Shouldn't happen } } } } // ORDER BY if (is_array($orderby) && sizeof($orderby) > 0) { $sql .= " ORDER BY "; $first = true; foreach ($orderby as $k => $v) { if ($first) $first = false; else $sql .= ', '; if (preg_match('/^[0-9]+$/', $k)) { $sql .= $k; } else { $this->fieldClean($k); $sql .= '"' . $k . '"'; } if (strtoupper($v) == 'DESC') $sql .= " DESC"; } } return $sql; } /** * 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 * @return -5 failed setting transaction read only */ 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 (preg_match('/^[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; // If backend supports read only queries, then specify read only mode // to avoid side effects from repeating queries that do writes. if ($this->hasReadOnlyQueries()) { $status = $this->execute("SET TRANSACTION READ ONLY"); if ($status != 0) { $this->rollbackTransaction(); return -5; } } // 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' && preg_match('/^[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 $rs = $this->selectSet("SELECT * FROM ({$query}) AS sub {$orderby} LIMIT {$page_size} OFFSET " . ($page - 1) * $page_size); $status = $this->endTransaction(); if ($status != 0) { $this->rollbackTransaction(); return -1; } return $rs; } /** * 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) { return $this->selectField($count, 'total'); } /** * 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) { $f_schema = $this->_schema; $this->fieldClean($f_schema); $this->fieldClean($table); $sql = "SELECT * FROM \"{$f_schema}\".\"{$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); } // 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; } // interfaces Statistics collector functions /** * Fetches statistics for a database * @param $database The database to fetch stats for * @return A recordset */ function getStatsDatabase($database) { $this->clean($database); $sql = "SELECT * FROM pg_stat_database WHERE datname='{$database}'"; return $this->selectSet($sql); } /** * Fetches tuple statistics for a table * @param $table The table to fetch stats for * @return A recordset */ function getStatsTableTuples($table) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); $sql = "SELECT * FROM pg_stat_all_tables WHERE schemaname='{$c_schema}' AND relname='{$table}'"; return $this->selectSet($sql); } /** * Fetches I/0 statistics for a table * @param $table The table to fetch stats for * @return A recordset */ function getStatsTableIO($table) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); $sql = "SELECT * FROM pg_statio_all_tables WHERE schemaname='{$c_schema}' AND relname='{$table}'"; return $this->selectSet($sql); } /** * Fetches tuple statistics for all indexes on a table * @param $table The table to fetch index stats for * @return A recordset */ function getStatsIndexTuples($table) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); $sql = "SELECT * FROM pg_stat_all_indexes WHERE schemaname='{$c_schema}' AND relname='{$table}' ORDER BY indexrelname"; return $this->selectSet($sql); } /** * Fetches I/0 statistics for all indexes on a table * @param $table The table to fetch index stats for * @return A recordset */ function getStatsIndexIO($table) { $c_schema = $this->_schema; $this->clean($c_schema); $this->clean($table); $sql = "SELECT * FROM pg_statio_all_indexes WHERE schemaname='{$c_schema}' AND relname='{$table}' ORDER BY indexrelname"; return $this->selectSet($sql); } // Capabilities function hasAggregateSortOp() { return true; } function hasAlterAggregate() { return true; } function hasAlterColumnType() { return true; } function hasAlterDatabaseOwner() { return true; } function hasAlterDatabaseRename() { return true; } function hasAlterSchema() { return true; } function hasAlterSchemaOwner() { return true; } function hasAlterSequenceSchema() { return true; } function hasAlterSequenceStart() { return true; } function hasAlterTableSchema() { return true; } function hasAutovacuum() { return true; } function hasCreateTableLike() { return true; } function hasCreateTableLikeWithConstraints() { return true; } function hasCreateTableLikeWithIndexes() { return true; } function hasCreateFieldWithConstraints() { return true; } function hasDisableTriggers() { return true; } function hasAlterDomains() { return true; } function hasDomainConstraints() { return true; } function hasEnumTypes() { return true; } function hasFTS() { return true; } function hasFunctionAlterOwner() { return true; } function hasFunctionAlterSchema() { return true; } function hasFunctionCosting() { return true; } function hasFunctionGUC() { return true; } function hasGrantOption() { return true; } function hasNamedParams() { return true; } function hasPrepare() { return true; } function hasPreparedXacts() { return true; } function hasReadOnlyQueries() { return true; } function hasRecluster() { return true; } function hasRoles() { return true; } function hasServerAdminFuncs() { return true; } function hasSharedComments() { return true; } function hasQueryCancel() { return true; } function hasTablespaces() { return true; } function hasUserRename() { return true; } function hasVirtualTransactionId() { return true; } function hasAlterDatabase() { return $this->hasAlterDatabaseRename(); } function hasDatabaseCollation() { return true; } function hasMagicTypes() { return true; } function hasQueryKill() { return true; } function hasConcurrentIndexBuild() { return true; } function hasForceReindex() { return false; } } ?>