CPgsqlSchema.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. <?php
  2. /**
  3. * CPgsqlSchema class file.
  4. *
  5. * @author Qiang Xue <qiang.xue@gmail.com>
  6. * @link http://www.yiiframework.com/
  7. * @copyright 2008-2013 Yii Software LLC
  8. * @license http://www.yiiframework.com/license/
  9. */
  10. /**
  11. * CPgsqlSchema is the class for retrieving metadata information from a PostgreSQL database.
  12. *
  13. * @author Qiang Xue <qiang.xue@gmail.com>
  14. * @package system.db.schema.pgsql
  15. * @since 1.0
  16. */
  17. class CPgsqlSchema extends CDbSchema
  18. {
  19. const DEFAULT_SCHEMA='public';
  20. /**
  21. * @var array the abstract column types mapped to physical column types.
  22. * @since 1.1.6
  23. */
  24. public $columnTypes=array(
  25. 'pk' => 'serial NOT NULL PRIMARY KEY',
  26. 'bigpk' => 'bigserial NOT NULL PRIMARY KEY',
  27. 'string' => 'character varying (255)',
  28. 'text' => 'text',
  29. 'integer' => 'integer',
  30. 'bigint' => 'bigint',
  31. 'float' => 'double precision',
  32. 'decimal' => 'numeric',
  33. 'datetime' => 'timestamp',
  34. 'timestamp' => 'timestamp',
  35. 'time' => 'time',
  36. 'date' => 'date',
  37. 'binary' => 'bytea',
  38. 'boolean' => 'boolean',
  39. 'money' => 'decimal(19,4)',
  40. );
  41. private $_sequences=array();
  42. /**
  43. * Quotes a table name for use in a query.
  44. * A simple table name does not schema prefix.
  45. * @param string $name table name
  46. * @return string the properly quoted table name
  47. * @since 1.1.6
  48. */
  49. public function quoteSimpleTableName($name)
  50. {
  51. return '"'.$name.'"';
  52. }
  53. /**
  54. * Resets the sequence value of a table's primary key.
  55. * The sequence will be reset such that the primary key of the next new row inserted
  56. * will have the specified value or max value of a primary key plus one (i.e. sequence trimming).
  57. * @param CDbTableSchema $table the table schema whose primary key sequence will be reset
  58. * @param integer|null $value the value for the primary key of the next new row inserted.
  59. * If this is not set, the next new row's primary key will have the max value of a primary
  60. * key plus one (i.e. sequence trimming).
  61. * @since 1.1
  62. */
  63. public function resetSequence($table,$value=null)
  64. {
  65. if($table->sequenceName===null)
  66. return;
  67. $sequence='"'.$table->sequenceName.'"';
  68. if(strpos($sequence,'.')!==false)
  69. $sequence=str_replace('.','"."',$sequence);
  70. if($value!==null)
  71. $value=(int)$value;
  72. else
  73. $value="(SELECT COALESCE(MAX(\"{$table->primaryKey}\"),0) FROM {$table->rawName})+1";
  74. $this->getDbConnection()
  75. ->createCommand("SELECT SETVAL('$sequence',$value,false)")
  76. ->execute();
  77. }
  78. /**
  79. * Enables or disables integrity check.
  80. * @param boolean $check whether to turn on or off the integrity check.
  81. * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
  82. * @since 1.1
  83. */
  84. public function checkIntegrity($check=true,$schema='')
  85. {
  86. $enable=$check ? 'ENABLE' : 'DISABLE';
  87. $tableNames=$this->getTableNames($schema);
  88. $db=$this->getDbConnection();
  89. foreach($tableNames as $tableName)
  90. {
  91. $tableName='"'.$tableName.'"';
  92. if(strpos($tableName,'.')!==false)
  93. $tableName=str_replace('.','"."',$tableName);
  94. $db->createCommand("ALTER TABLE $tableName $enable TRIGGER ALL")->execute();
  95. }
  96. }
  97. /**
  98. * Loads the metadata for the specified table.
  99. * @param string $name table name
  100. * @return CDbTableSchema driver dependent table metadata.
  101. */
  102. protected function loadTable($name)
  103. {
  104. $table=new CPgsqlTableSchema;
  105. $this->resolveTableNames($table,$name);
  106. if(!$this->findColumns($table))
  107. return null;
  108. $this->findConstraints($table);
  109. if(is_string($table->primaryKey) && isset($this->_sequences[$table->rawName.'.'.$table->primaryKey]))
  110. $table->sequenceName=$this->_sequences[$table->rawName.'.'.$table->primaryKey];
  111. elseif(is_array($table->primaryKey))
  112. {
  113. foreach($table->primaryKey as $pk)
  114. {
  115. if(isset($this->_sequences[$table->rawName.'.'.$pk]))
  116. {
  117. $table->sequenceName=$this->_sequences[$table->rawName.'.'.$pk];
  118. break;
  119. }
  120. }
  121. }
  122. return $table;
  123. }
  124. /**
  125. * Generates various kinds of table names.
  126. * @param CPgsqlTableSchema $table the table instance
  127. * @param string $name the unquoted table name
  128. */
  129. protected function resolveTableNames($table,$name)
  130. {
  131. $parts=explode('.',str_replace('"','',$name));
  132. if(isset($parts[1]))
  133. {
  134. $schemaName=$parts[0];
  135. $tableName=$parts[1];
  136. }
  137. else
  138. {
  139. $schemaName=self::DEFAULT_SCHEMA;
  140. $tableName=$parts[0];
  141. }
  142. $table->name=$tableName;
  143. $table->schemaName=$schemaName;
  144. if($schemaName===self::DEFAULT_SCHEMA)
  145. $table->rawName=$this->quoteTableName($tableName);
  146. else
  147. $table->rawName=$this->quoteTableName($schemaName).'.'.$this->quoteTableName($tableName);
  148. }
  149. /**
  150. * Collects the table column metadata.
  151. * @param CPgsqlTableSchema $table the table metadata
  152. * @return boolean whether the table exists in the database
  153. */
  154. protected function findColumns($table)
  155. {
  156. $sql=<<<EOD
  157. SELECT a.attname, LOWER(format_type(a.atttypid, a.atttypmod)) AS type, d.adsrc, a.attnotnull, a.atthasdef,
  158. pg_catalog.col_description(a.attrelid, a.attnum) AS comment
  159. FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
  160. WHERE a.attnum > 0 AND NOT a.attisdropped
  161. AND a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname=:table
  162. AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = :schema))
  163. ORDER BY a.attnum
  164. EOD;
  165. $command=$this->getDbConnection()->createCommand($sql);
  166. $command->bindValue(':table',$table->name);
  167. $command->bindValue(':schema',$table->schemaName);
  168. if(($columns=$command->queryAll())===array())
  169. return false;
  170. foreach($columns as $column)
  171. {
  172. $c=$this->createColumn($column);
  173. $table->columns[$c->name]=$c;
  174. if(stripos($column['adsrc'],'nextval')===0 && preg_match('/nextval\([^\']*\'([^\']+)\'[^\)]*\)/i',$column['adsrc'],$matches))
  175. {
  176. if(strpos($matches[1],'.')!==false || $table->schemaName===self::DEFAULT_SCHEMA)
  177. $this->_sequences[$table->rawName.'.'.$c->name]=$matches[1];
  178. else
  179. $this->_sequences[$table->rawName.'.'.$c->name]=$table->schemaName.'.'.$matches[1];
  180. $c->autoIncrement=true;
  181. }
  182. }
  183. return true;
  184. }
  185. /**
  186. * Creates a table column.
  187. * @param array $column column metadata
  188. * @return CDbColumnSchema normalized column metadata
  189. */
  190. protected function createColumn($column)
  191. {
  192. $c=new CPgsqlColumnSchema;
  193. $c->name=$column['attname'];
  194. $c->rawName=$this->quoteColumnName($c->name);
  195. $c->allowNull=!$column['attnotnull'];
  196. $c->isPrimaryKey=false;
  197. $c->isForeignKey=false;
  198. $c->comment=$column['comment']===null ? '' : $column['comment'];
  199. $c->init($column['type'],$column['atthasdef'] ? $column['adsrc'] : null);
  200. return $c;
  201. }
  202. /**
  203. * Collects the primary and foreign key column details for the given table.
  204. * @param CPgsqlTableSchema $table the table metadata
  205. */
  206. protected function findConstraints($table)
  207. {
  208. $sql=<<<EOD
  209. SELECT conname, consrc, contype, indkey FROM (
  210. SELECT
  211. conname,
  212. CASE WHEN contype='f' THEN
  213. pg_catalog.pg_get_constraintdef(oid)
  214. ELSE
  215. 'CHECK (' || consrc || ')'
  216. END AS consrc,
  217. contype,
  218. conrelid AS relid,
  219. NULL AS indkey
  220. FROM
  221. pg_catalog.pg_constraint
  222. WHERE
  223. contype IN ('f', 'c')
  224. UNION ALL
  225. SELECT
  226. pc.relname,
  227. NULL,
  228. CASE WHEN indisprimary THEN
  229. 'p'
  230. ELSE
  231. 'u'
  232. END,
  233. pi.indrelid,
  234. indkey
  235. FROM
  236. pg_catalog.pg_class pc,
  237. pg_catalog.pg_index pi
  238. WHERE
  239. pc.oid=pi.indexrelid
  240. AND EXISTS (
  241. SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c
  242. ON (d.refclassid = c.tableoid AND d.refobjid = c.oid)
  243. WHERE d.classid = pc.tableoid AND d.objid = pc.oid AND d.deptype = 'i' AND c.contype IN ('u', 'p')
  244. )
  245. ) AS sub
  246. WHERE relid = (SELECT oid FROM pg_catalog.pg_class WHERE relname=:table
  247. AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace
  248. WHERE nspname=:schema))
  249. EOD;
  250. $command=$this->getDbConnection()->createCommand($sql);
  251. $command->bindValue(':table',$table->name);
  252. $command->bindValue(':schema',$table->schemaName);
  253. foreach($command->queryAll() as $row)
  254. {
  255. if($row['contype']==='p') // primary key
  256. $this->findPrimaryKey($table,$row['indkey']);
  257. elseif($row['contype']==='f') // foreign key
  258. $this->findForeignKey($table,$row['consrc']);
  259. }
  260. }
  261. /**
  262. * Collects primary key information.
  263. * @param CPgsqlTableSchema $table the table metadata
  264. * @param string $indices pgsql primary key index list
  265. */
  266. protected function findPrimaryKey($table,$indices)
  267. {
  268. $indices=implode(', ',preg_split('/\s+/',$indices));
  269. $sql=<<<EOD
  270. SELECT attnum, attname FROM pg_catalog.pg_attribute WHERE
  271. attrelid=(
  272. SELECT oid FROM pg_catalog.pg_class WHERE relname=:table AND relnamespace=(
  273. SELECT oid FROM pg_catalog.pg_namespace WHERE nspname=:schema
  274. )
  275. )
  276. AND attnum IN ({$indices})
  277. EOD;
  278. $command=$this->getDbConnection()->createCommand($sql);
  279. $command->bindValue(':table',$table->name);
  280. $command->bindValue(':schema',$table->schemaName);
  281. foreach($command->queryAll() as $row)
  282. {
  283. $name=$row['attname'];
  284. if(isset($table->columns[$name]))
  285. {
  286. $table->columns[$name]->isPrimaryKey=true;
  287. if($table->primaryKey===null)
  288. $table->primaryKey=$name;
  289. elseif(is_string($table->primaryKey))
  290. $table->primaryKey=array($table->primaryKey,$name);
  291. else
  292. $table->primaryKey[]=$name;
  293. }
  294. }
  295. }
  296. /**
  297. * Collects foreign key information.
  298. * @param CPgsqlTableSchema $table the table metadata
  299. * @param string $src pgsql foreign key definition
  300. */
  301. protected function findForeignKey($table,$src)
  302. {
  303. $matches=array();
  304. $brackets='\(([^\)]+)\)';
  305. $pattern="/FOREIGN\s+KEY\s+{$brackets}\s+REFERENCES\s+([^\(]+){$brackets}/i";
  306. if(preg_match($pattern,str_replace('"','',$src),$matches))
  307. {
  308. $keys=preg_split('/,\s+/', $matches[1]);
  309. $tableName=$matches[2];
  310. $fkeys=preg_split('/,\s+/', $matches[3]);
  311. foreach($keys as $i=>$key)
  312. {
  313. $table->foreignKeys[$key]=array($tableName,$fkeys[$i]);
  314. if(isset($table->columns[$key]))
  315. $table->columns[$key]->isForeignKey=true;
  316. }
  317. }
  318. }
  319. /**
  320. * Returns all table names in the database.
  321. * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
  322. * If not empty, the returned table names will be prefixed with the schema name.
  323. * @return array all table names in the database.
  324. */
  325. protected function findTableNames($schema='')
  326. {
  327. if($schema==='')
  328. $schema=self::DEFAULT_SCHEMA;
  329. $sql=<<<EOD
  330. SELECT table_name, table_schema FROM information_schema.tables
  331. WHERE table_schema=:schema AND table_type='BASE TABLE'
  332. EOD;
  333. $command=$this->getDbConnection()->createCommand($sql);
  334. $command->bindParam(':schema',$schema);
  335. $rows=$command->queryAll();
  336. $names=array();
  337. foreach($rows as $row)
  338. {
  339. if($schema===self::DEFAULT_SCHEMA)
  340. $names[]=$row['table_name'];
  341. else
  342. $names[]=$row['table_schema'].'.'.$row['table_name'];
  343. }
  344. return $names;
  345. }
  346. /**
  347. * Builds a SQL statement for renaming a DB table.
  348. * @param string $table the table to be renamed. The name will be properly quoted by the method.
  349. * @param string $newName the new table name. The name will be properly quoted by the method.
  350. * @return string the SQL statement for renaming a DB table.
  351. * @since 1.1.6
  352. */
  353. public function renameTable($table, $newName)
  354. {
  355. return 'ALTER TABLE ' . $this->quoteTableName($table) . ' RENAME TO ' . $this->quoteTableName($newName);
  356. }
  357. /**
  358. * Builds a SQL statement for adding a new DB column.
  359. * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method.
  360. * @param string $column the name of the new column. The name will be properly quoted by the method.
  361. * @param string $type the column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any)
  362. * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL.
  363. * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'.
  364. * @return string the SQL statement for adding a new column.
  365. * @since 1.1.6
  366. */
  367. public function addColumn($table, $column, $type)
  368. {
  369. $type=$this->getColumnType($type);
  370. $sql='ALTER TABLE ' . $this->quoteTableName($table)
  371. . ' ADD COLUMN ' . $this->quoteColumnName($column) . ' '
  372. . $type;
  373. return $sql;
  374. }
  375. /**
  376. * Builds a SQL statement for changing the definition of a column.
  377. * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
  378. * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
  379. * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any)
  380. * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL.
  381. * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'.
  382. * @return string the SQL statement for changing the definition of a column.
  383. * @since 1.1.6
  384. */
  385. public function alterColumn($table, $column, $type)
  386. {
  387. $type=$this->getColumnType($type);
  388. $sql='ALTER TABLE ' . $this->quoteTableName($table) . ' ALTER COLUMN '
  389. . $this->quoteColumnName($column) . ' TYPE ' . $this->getColumnType($type);
  390. return $sql;
  391. }
  392. /**
  393. * Builds a SQL statement for creating a new index.
  394. * @param string $name the name of the index. The name will be properly quoted by the method.
  395. * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method.
  396. * @param string $columns the column(s) that should be included in the index. If there are multiple columns, please separate them
  397. * by commas. Each column name will be properly quoted by the method, unless a parenthesis is found in the name.
  398. * @param boolean $unique whether to add UNIQUE constraint on the created index.
  399. * @return string the SQL statement for creating a new index.
  400. * @since 1.1.6
  401. */
  402. public function createIndex($name, $table, $columns, $unique=false)
  403. {
  404. $cols=array();
  405. if (is_string($columns))
  406. $columns=preg_split('/\s*,\s*/',$columns,-1,PREG_SPLIT_NO_EMPTY);
  407. foreach($columns as $col)
  408. {
  409. if(strpos($col,'(')!==false)
  410. $cols[]=$col;
  411. else
  412. $cols[]=$this->quoteColumnName($col);
  413. }
  414. if ($unique)
  415. {
  416. return 'ALTER TABLE ONLY '
  417. . $this->quoteTableName($table).' ADD CONSTRAINT '
  418. . $this->quoteTableName($name).' UNIQUE ('.implode(', ',$cols).')';
  419. }
  420. else
  421. {
  422. return 'CREATE INDEX '
  423. . $this->quoteTableName($name).' ON '
  424. . $this->quoteTableName($table).' ('.implode(', ',$cols).')';
  425. }
  426. }
  427. /**
  428. * Builds a SQL statement for dropping an index.
  429. * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
  430. * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
  431. * @return string the SQL statement for dropping an index.
  432. * @since 1.1.6
  433. */
  434. public function dropIndex($name, $table)
  435. {
  436. return 'DROP INDEX '.$this->quoteTableName($name);
  437. }
  438. /**
  439. * Creates a command builder for the database.
  440. * This method may be overridden by child classes to create a DBMS-specific command builder.
  441. * @return CPgsqlCommandBuilder command builder instance.
  442. */
  443. protected function createCommandBuilder()
  444. {
  445. return new CPgsqlCommandBuilder($this);
  446. }
  447. }