3 * TPgsqlMetaData class file.
5 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
6 * @link https://github.com/pradosoft/prado
7 * @copyright Copyright © 2005-2016 The PRADO Group
8 * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
9 * @package System.Data.Common.Pgsql
13 * Load the base TDbMetaData class.
15 Prado::using('System.Data.Common.TDbMetaData');
16 Prado::using('System.Data.Common.Pgsql.TPgsqlTableInfo');
19 * TPgsqlMetaData loads PostgreSQL database table and column information.
21 * @author Wei Zhuo <weizho[at]gmail[dot]com>
22 * @package System.Data.Common.Pgsql
25 class TPgsqlMetaData extends TDbMetaData
27 private $_defaultSchema = 'public';
30 * @return string TDbTableInfo class name.
32 protected function getTableInfoClass()
34 return 'TPgsqlTableInfo';
38 * Quotes a table name for use in a query.
39 * @param string $name table name
40 * @return string the properly quoted table name
42 public function quoteTableName($name)
44 return parent::quoteTableName($name, '"', '"');
48 * Quotes a column name for use in a query.
49 * @param string $name column name
50 * @return string the properly quoted column name
52 public function quoteColumnName($name)
54 return parent::quoteColumnName($name, '"', '"');
58 * Quotes a column alias for use in a query.
59 * @param string $name column alias
60 * @return string the properly quoted column alias
62 public function quoteColumnAlias($name)
64 return parent::quoteColumnAlias($name, '"', '"');
68 * @param string default schema.
70 public function setDefaultSchema($schema)
72 $this->_defaultSchema=$schema;
76 * @return string default schema.
78 public function getDefaultSchema()
80 return $this->_defaultSchema;
84 * @param string table name with optional schema name prefix, uses default schema name prefix is not provided.
85 * @return array tuple as ($schemaName,$tableName)
87 protected function getSchemaTableName($table)
89 if(count($parts= explode('.', str_replace('"','',$table))) > 1)
90 return array($parts[0], $parts[1]);
92 return array($this->getDefaultSchema(),$parts[0]);
96 * Get the column definitions for given table.
97 * @param string table name.
98 * @return TPgsqlTableInfo table information.
100 protected function createTableInfo($table)
102 list($schemaName,$tableName) = $this->getSchemaTableName($table);
104 // This query is made much more complex by the addition of the 'attisserial' field.
105 // The subquery to get that field checks to see if there is an internally dependent
106 // sequence on the field.
111 pg_catalog.format_type(a.atttypid, a.atttypmod) as type,
113 a.attnotnull, a.atthasdef, adef.adsrc,
115 SELECT 1 FROM pg_catalog.pg_depend pd, pg_catalog.pg_class pc
116 WHERE pd.objid=pc.oid
117 AND pd.classid=pc.tableoid
118 AND pd.refclassid=pc.tableoid
119 AND pd.refobjid=a.attrelid
120 AND pd.refobjsubid=a.attnum
123 ) IS NOT NULL AS attisserial
126 pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_attrdef adef
127 ON a.attrelid=adef.adrelid
128 AND a.attnum=adef.adnum
129 LEFT JOIN pg_catalog.pg_type t ON a.atttypid=t.oid
131 a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname=:table
132 AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE
134 AND a.attnum > 0 AND NOT a.attisdropped
137 $this->getDbConnection()->setActive(true);
138 $command = $this->getDbConnection()->createCommand($sql);
139 $command->bindValue(':table', $tableName);
140 $command->bindValue(':schema', $schemaName);
141 $tableInfo = $this->createNewTableInfo($schemaName, $tableName);
143 foreach($command->query() as $col)
145 $col['index'] = $index++;
146 $this->processColumn($tableInfo, $col);
149 throw new TDbException('dbmetadata_invalid_table_view', $table);
154 * @param string table schema name
155 * @param string table name.
156 * @return TPgsqlTableInfo
158 protected function createNewTableInfo($schemaName,$tableName)
160 $info['SchemaName'] = $this->assertIdentifier($schemaName);
161 $info['TableName'] = $this->assertIdentifier($tableName);
162 if($this->getIsView($schemaName,$tableName))
163 $info['IsView'] = true;
164 list($primary, $foreign) = $this->getConstraintKeys($schemaName, $tableName);
165 $class = $this->getTableInfoClass();
166 return new $class($info,$primary,$foreign);
170 * @param string table name, schema name or column name.
171 * @return string a valid identifier.
172 * @throws TDbException when table name contains a double quote (").
174 protected function assertIdentifier($name)
176 if(strpos($name, '"')!==false)
178 $ref = 'http://www.postgresql.org/docs/7.4/static/sql-syntax.html#SQL-SYNTAX-IDENTIFIERS';
179 throw new TDbException('dbcommon_invalid_identifier_name', $name, $ref);
185 * @param string table schema name
186 * @param string table name.
187 * @return boolean true if the table is a view.
189 protected function getIsView($schemaName,$tableName)
193 SELECT count(c.relname) FROM pg_catalog.pg_class c
194 LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace)
195 WHERE (n.nspname=:schema) AND (c.relkind = 'v'::"char") AND c.relname = :table
197 $this->getDbConnection()->setActive(true);
198 $command = $this->getDbConnection()->createCommand($sql);
199 $command->bindValue(':schema',$schemaName);
200 $command->bindValue(':table', $tableName);
201 return intval($command->queryScalar()) === 1;
205 * @param TPgsqlTableInfo table information.
206 * @param array column information.
208 protected function processColumn($tableInfo, $col)
210 $columnId = $col['attname']; //use column name as column Id
212 $info['ColumnName'] = '"'.$columnId.'"'; //quote the column names!
213 $info['ColumnId'] = $columnId;
214 $info['ColumnIndex'] = $col['index'];
215 if(!$col['attnotnull'])
216 $info['AllowNull'] = true;
217 if(in_array($columnId, $tableInfo->getPrimaryKeys()))
218 $info['IsPrimaryKey'] = true;
219 if($this->isForeignKeyColumn($columnId, $tableInfo))
220 $info['IsForeignKey'] = true;
222 if($col['atttypmod'] > 0)
223 $info['ColumnSize'] = $col['atttypmod'] - 4;
224 if($col['atthasdef'])
225 $info['DefaultValue'] = $col['adsrc'];
226 if($col['attisserial'] || substr($col['adsrc'],0,8) === 'nextval(')
228 if(($sequence = $this->getSequenceName($tableInfo, $col['adsrc']))!==null)
230 $info['SequenceName'] = $sequence;
231 unset($info['DefaultValue']);
235 if(preg_match('/\((\d+)(?:,(\d+))?+\)/', $col['type'], $matches))
237 $info['DbType'] = preg_replace('/\(\d+(?:,\d+)?\)/','',$col['type']);
238 if($this->isPrecisionType($info['DbType']))
240 $info['NumericPrecision'] = intval($matches[1]);
241 if(count($matches) > 2)
242 $info['NumericScale'] = intval($matches[2]);
245 $info['ColumnSize'] = intval($matches[1]);
248 $info['DbType'] = $col['type'];
250 $tableInfo->Columns[$columnId] = new TPgsqlTableColumn($info);
254 * @return string serial name if found, null otherwise.
256 protected function getSequenceName($tableInfo,$src)
259 if(preg_match('/nextval\([^\']*\'([^\']+)\'[^\)]*\)/i',$src,$matches))
261 if(is_int(strpos($matches[1], '.')))
264 return $tableInfo->getSchemaName().'.'.$matches[1];
269 * @return boolean true if column type if "numeric", "interval" or begins with "time".
271 protected function isPrecisionType($type)
273 $type = strtolower(trim($type));
274 return $type==='numeric' || $type==='interval' || strpos($type, 'time')===0;
278 * Gets the primary and foreign key column details for the given table.
279 * @param string schema name
280 * @param string table name.
281 * @return array tuple ($primary, $foreign)
283 protected function getConstraintKeys($schemaName, $tableName)
287 SELECT conname, consrc, contype, indkey, indisclustered FROM (
290 CASE WHEN contype='f' THEN
291 pg_catalog.pg_get_constraintdef(oid)
293 'CHECK (' || consrc || ')'
298 FALSE AS indisclustered
300 pg_catalog.pg_constraint
302 contype IN ('f', 'c')
307 CASE WHEN indisprimary THEN
316 pg_catalog.pg_class pc,
317 pg_catalog.pg_index pi
321 SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c
322 ON (d.refclassid = c.tableoid AND d.refobjid = c.oid)
323 WHERE d.classid = pc.tableoid AND d.objid = pc.oid AND d.deptype = 'i' AND c.contype IN ('u', 'p')
326 WHERE relid = (SELECT oid FROM pg_catalog.pg_class WHERE relname=:table
327 AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace
328 WHERE nspname=:schema))
332 $this->getDbConnection()->setActive(true);
333 $command = $this->getDbConnection()->createCommand($sql);
334 $command->bindValue(':table', $tableName);
335 $command->bindValue(':schema', $schemaName);
338 foreach($command->query() as $row)
340 switch($row['contype'])
343 $primary = $this->getPrimaryKeys($tableName, $schemaName, $row['indkey']);
346 if(($fkey = $this->getForeignKeys($row['consrc']))!==null)
351 return array($primary,$foreign);
355 * Gets the primary key field names
356 * @param string pgsql primary key definition
357 * @return array primary key field names.
359 protected function getPrimaryKeys($tableName, $schemaName, $columnIndex)
361 $index = join(', ', explode(' ', $columnIndex));
364 SELECT attnum, attname FROM pg_catalog.pg_attribute WHERE
366 SELECT oid FROM pg_catalog.pg_class WHERE relname=:table AND relnamespace=(
367 SELECT oid FROM pg_catalog.pg_namespace WHERE nspname=:schema
370 AND attnum IN ({$index})
372 $command = $this->getDbConnection()->createCommand($sql);
373 $command->bindValue(':table', $tableName);
374 $command->bindValue(':schema', $schemaName);
375 // $command->bindValue(':columnIndex', join(', ', explode(' ', $columnIndex)));
377 foreach($command->query() as $row)
379 $primary[] = $row['attname'];
386 * Gets foreign relationship constraint keys and table name
387 * @param string pgsql foreign key definition
388 * @return array foreign relationship table name and keys, null otherwise
390 protected function getForeignKeys($src)
393 $brackets = '\(([^\)]+)\)';
394 $find = "/FOREIGN\s+KEY\s+{$brackets}\s+REFERENCES\s+([^\(]+){$brackets}/i";
395 if(preg_match($find, $src, $matches))
397 $keys = preg_split('/,\s+/', $matches[1]);
399 foreach(preg_split('/,\s+/', $matches[3]) as $i => $fkey)
400 $fkeys[$keys[$i]] = $fkey;
401 return array('table' => str_replace('"','',$matches[2]), 'keys' => $fkeys);
406 * @param string column name.
407 * @param TPgsqlTableInfo table information.
408 * @return boolean true if column is a foreign key.
410 protected function isForeignKeyColumn($columnId, $tableInfo)
412 foreach($tableInfo->getForeignKeys() as $fk)
414 if(in_array($columnId, array_keys($fk['keys'])))
421 * Returns all table names in the database.
422 * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
423 * If not empty, the returned table names will be prefixed with the schema name.
424 * @return array all table names in the database.
426 public function findTableNames($schema='public')
429 $schema=self::DEFAULT_SCHEMA;
431 SELECT table_name, table_schema FROM information_schema.tables
432 WHERE table_schema=:schema AND table_type='BASE TABLE'
434 $command=$this->getDbConnection()->createCommand($sql);
435 $command->bindParam(':schema',$schema);
436 $rows=$command->queryAll();
438 foreach($rows as $row)
440 if($schema===self::DEFAULT_SCHEMA)
441 $names[]=$row['table_name'];
443 $names[]=$row['table_schema'].'.'.$row['table_name'];