PostgresSchema.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Database\Schema;
  16. use Cake\Database\Exception;
  17. use Cake\Database\Schema\Table;
  18. /**
  19. * Schema management/reflection features for Postgres.
  20. */
  21. class PostgresSchema extends BaseSchema
  22. {
  23. /**
  24. * {@inheritDoc}
  25. */
  26. public function listTablesSql($config)
  27. {
  28. $sql = 'SELECT table_name as name FROM information_schema.tables WHERE table_schema = ? ORDER BY name';
  29. $schema = empty($config['schema']) ? 'public' : $config['schema'];
  30. return [$sql, [$schema]];
  31. }
  32. /**
  33. * {@inheritDoc}
  34. */
  35. public function describeColumnSql($tableName, $config)
  36. {
  37. $sql =
  38. 'SELECT DISTINCT table_schema AS schema, column_name AS name, data_type AS type,
  39. is_nullable AS null, column_default AS default,
  40. character_maximum_length AS char_length,
  41. d.description as comment,
  42. ordinal_position
  43. FROM information_schema.columns c
  44. INNER JOIN pg_catalog.pg_namespace ns ON (ns.nspname = table_schema)
  45. INNER JOIN pg_catalog.pg_class cl ON (cl.relnamespace = ns.oid AND cl.relname = table_name)
  46. LEFT JOIN pg_catalog.pg_index i ON (i.indrelid = cl.oid AND i.indkey[0] = c.ordinal_position)
  47. LEFT JOIN pg_catalog.pg_description d on (cl.oid = d.objoid AND d.objsubid = c.ordinal_position)
  48. WHERE table_name = ? AND table_schema = ? AND table_catalog = ?
  49. ORDER BY ordinal_position';
  50. $schema = empty($config['schema']) ? 'public' : $config['schema'];
  51. return [$sql, [$tableName, $schema, $config['database']]];
  52. }
  53. /**
  54. * Convert a column definition to the abstract types.
  55. *
  56. * The returned type will be a type that
  57. * Cake\Database\Type can handle.
  58. *
  59. * @param string $column The column type + length
  60. * @throws \Cake\Database\Exception when column cannot be parsed.
  61. * @return array Array of column information.
  62. */
  63. protected function _convertColumn($column)
  64. {
  65. preg_match('/([a-z\s]+)(?:\(([0-9,]+)\))?/i', $column, $matches);
  66. if (empty($matches)) {
  67. throw new Exception(sprintf('Unable to parse column type from "%s"', $column));
  68. }
  69. $col = strtolower($matches[1]);
  70. $length = null;
  71. if (isset($matches[2])) {
  72. $length = (int)$matches[2];
  73. }
  74. if (in_array($col, ['date', 'time', 'boolean'])) {
  75. return ['type' => $col, 'length' => null];
  76. }
  77. if (strpos($col, 'timestamp') !== false) {
  78. return ['type' => 'timestamp', 'length' => null];
  79. }
  80. if ($col === 'serial' || $col === 'integer') {
  81. return ['type' => 'integer', 'length' => 10];
  82. }
  83. if ($col === 'bigserial' || $col === 'bigint') {
  84. return ['type' => 'biginteger', 'length' => 20];
  85. }
  86. if ($col === 'smallint') {
  87. return ['type' => 'integer', 'length' => 5];
  88. }
  89. if ($col === 'inet') {
  90. return ['type' => 'string', 'length' => 39];
  91. }
  92. if ($col === 'uuid') {
  93. return ['type' => 'uuid', 'length' => null];
  94. }
  95. if ($col === 'char' || $col === 'character') {
  96. return ['type' => 'string', 'fixed' => true, 'length' => $length];
  97. }
  98. // money is 'string' as it includes arbitrary text content
  99. // before the number value.
  100. if (strpos($col, 'char') !== false ||
  101. strpos($col, 'money') !== false
  102. ) {
  103. return ['type' => 'string', 'length' => $length];
  104. }
  105. if (strpos($col, 'text') !== false) {
  106. return ['type' => 'text', 'length' => null];
  107. }
  108. if ($col === 'bytea') {
  109. return ['type' => 'binary', 'length' => null];
  110. }
  111. if ($col === 'real' || strpos($col, 'double') !== false) {
  112. return ['type' => 'float', 'length' => null];
  113. }
  114. if (strpos($col, 'numeric') !== false ||
  115. strpos($col, 'decimal') !== false
  116. ) {
  117. return ['type' => 'decimal', 'length' => null];
  118. }
  119. return ['type' => 'text', 'length' => null];
  120. }
  121. /**
  122. * {@inheritDoc}
  123. */
  124. public function convertColumnDescription(Table $table, $row)
  125. {
  126. $field = $this->_convertColumn($row['type']);
  127. if ($field['type'] === 'boolean') {
  128. if ($row['default'] === 'true') {
  129. $row['default'] = 1;
  130. }
  131. if ($row['default'] === 'false') {
  132. $row['default'] = 0;
  133. }
  134. }
  135. // Sniff out serial types.
  136. if (in_array($field['type'], ['integer', 'biginteger']) && strpos($row['default'], 'nextval(') === 0) {
  137. $field['autoIncrement'] = true;
  138. }
  139. $field += [
  140. 'default' => $this->_defaultValue($row['default']),
  141. 'null' => $row['null'] === 'YES' ? true : false,
  142. 'comment' => $row['comment']
  143. ];
  144. $field['length'] = $row['char_length'] ?: $field['length'];
  145. $table->addColumn($row['name'], $field);
  146. }
  147. /**
  148. * Manipulate the default value.
  149. *
  150. * Postgres includes sequence data and casting information in default values.
  151. * We need to remove those.
  152. *
  153. * @param string|null $default The default value.
  154. * @return string|null
  155. */
  156. protected function _defaultValue($default)
  157. {
  158. if (is_numeric($default) || $default === null) {
  159. return $default;
  160. }
  161. // Sequences
  162. if (strpos($default, 'nextval') === 0) {
  163. return null;
  164. }
  165. // Remove quotes and postgres casts
  166. return preg_replace(
  167. "/^'(.*)'(?:::.*)$/",
  168. "$1",
  169. $default
  170. );
  171. }
  172. /**
  173. * {@inheritDoc}
  174. */
  175. public function describeIndexSql($tableName, $config)
  176. {
  177. $sql = 'SELECT
  178. c2.relname,
  179. i.indisprimary,
  180. i.indisunique,
  181. i.indisvalid,
  182. pg_catalog.pg_get_indexdef(i.indexrelid, 0, true) AS statement
  183. FROM pg_catalog.pg_class AS c,
  184. pg_catalog.pg_class AS c2,
  185. pg_catalog.pg_index AS i
  186. WHERE c.oid = (
  187. SELECT c.oid
  188. FROM pg_catalog.pg_class c
  189. LEFT JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
  190. WHERE c.relname = ?
  191. AND n.nspname = ?
  192. )
  193. AND c.oid = i.indrelid
  194. AND i.indexrelid = c2.oid
  195. ORDER BY i.indisprimary DESC, i.indisunique DESC, c2.relname';
  196. $schema = 'public';
  197. if (!empty($config['schema'])) {
  198. $schema = $config['schema'];
  199. }
  200. return [$sql, [$tableName, $schema]];
  201. }
  202. /**
  203. * {@inheritDoc}
  204. */
  205. public function convertIndexDescription(Table $table, $row)
  206. {
  207. $type = Table::INDEX_INDEX;
  208. $name = $row['relname'];
  209. if ($row['indisprimary']) {
  210. $name = $type = Table::CONSTRAINT_PRIMARY;
  211. }
  212. if ($row['indisunique'] && $type === Table::INDEX_INDEX) {
  213. $type = Table::CONSTRAINT_UNIQUE;
  214. }
  215. preg_match('/\(([^\)]+)\)/', $row['statement'], $matches);
  216. $columns = $this->_convertColumnList($matches[1]);
  217. if ($type === Table::CONSTRAINT_PRIMARY || $type === Table::CONSTRAINT_UNIQUE) {
  218. $table->addConstraint($name, [
  219. 'type' => $type,
  220. 'columns' => $columns
  221. ]);
  222. // If there is only one column in the primary key and it is integery,
  223. // make it autoincrement.
  224. $columnDef = $table->column($columns[0]);
  225. if ($type === Table::CONSTRAINT_PRIMARY &&
  226. count($columns) === 1 &&
  227. in_array($columnDef['type'], ['integer', 'biginteger'])
  228. ) {
  229. $columnDef['autoIncrement'] = true;
  230. $table->addColumn($columns[0], $columnDef);
  231. }
  232. return;
  233. }
  234. $table->addIndex($name, [
  235. 'type' => $type,
  236. 'columns' => $columns
  237. ]);
  238. }
  239. /**
  240. * Convert a column list into a clean array.
  241. *
  242. * @param string $columns comma separated column list.
  243. * @return array
  244. */
  245. protected function _convertColumnList($columns)
  246. {
  247. $columns = explode(', ', $columns);
  248. foreach ($columns as &$column) {
  249. $column = trim($column, '"');
  250. }
  251. return $columns;
  252. }
  253. /**
  254. * {@inheritDoc}
  255. */
  256. public function describeForeignKeySql($tableName, $config)
  257. {
  258. $sql = "SELECT
  259. rc.constraint_name AS name,
  260. tc.constraint_type AS type,
  261. kcu.column_name,
  262. rc.match_option AS match_type,
  263. rc.update_rule AS on_update,
  264. rc.delete_rule AS on_delete,
  265. kc.table_name AS references_table,
  266. kc.column_name AS references_field
  267. FROM information_schema.referential_constraints rc
  268. JOIN information_schema.table_constraints tc
  269. ON tc.constraint_name = rc.constraint_name
  270. AND tc.constraint_schema = rc.constraint_schema
  271. AND tc.constraint_name = rc.constraint_name
  272. JOIN information_schema.key_column_usage kcu
  273. ON kcu.constraint_name = rc.constraint_name
  274. AND kcu.constraint_schema = rc.constraint_schema
  275. AND kcu.constraint_name = rc.constraint_name
  276. JOIN information_schema.key_column_usage kc
  277. ON kc.ordinal_position = kcu.position_in_unique_constraint
  278. AND kc.constraint_name = rc.unique_constraint_name
  279. WHERE kcu.table_name = ?
  280. AND kc.table_schema = ?
  281. AND tc.constraint_type = 'FOREIGN KEY'
  282. ORDER BY rc.constraint_name, kcu.ordinal_position";
  283. $schema = empty($config['schema']) ? 'public' : $config['schema'];
  284. return [$sql, [$tableName, $schema]];
  285. }
  286. /**
  287. * {@inheritDoc}
  288. */
  289. public function convertForeignKeyDescription(Table $table, $row)
  290. {
  291. $data = [
  292. 'type' => Table::CONSTRAINT_FOREIGN,
  293. 'columns' => $row['column_name'],
  294. 'references' => [$row['references_table'], $row['references_field']],
  295. 'update' => $this->_convertOnClause($row['on_update']),
  296. 'delete' => $this->_convertOnClause($row['on_delete']),
  297. ];
  298. $table->addConstraint($row['name'], $data);
  299. }
  300. /**
  301. * {@inheritDoc}
  302. */
  303. protected function _convertOnClause($clause)
  304. {
  305. if ($clause === 'RESTRICT') {
  306. return Table::ACTION_RESTRICT;
  307. }
  308. if ($clause === 'NO ACTION') {
  309. return Table::ACTION_NO_ACTION;
  310. }
  311. if ($clause === 'CASCADE') {
  312. return Table::ACTION_CASCADE;
  313. }
  314. return Table::ACTION_SET_NULL;
  315. }
  316. /**
  317. * {@inheritDoc}
  318. */
  319. public function columnSql(Table $table, $name)
  320. {
  321. $data = $table->column($name);
  322. $out = $this->_driver->quoteIdentifier($name);
  323. $typeMap = [
  324. 'boolean' => ' BOOLEAN',
  325. 'binary' => ' BYTEA',
  326. 'float' => ' FLOAT',
  327. 'decimal' => ' DECIMAL',
  328. 'text' => ' TEXT',
  329. 'date' => ' DATE',
  330. 'time' => ' TIME',
  331. 'datetime' => ' TIMESTAMP',
  332. 'timestamp' => ' TIMESTAMP',
  333. 'uuid' => ' UUID',
  334. ];
  335. if (isset($typeMap[$data['type']])) {
  336. $out .= $typeMap[$data['type']];
  337. }
  338. if ($data['type'] === 'integer' || $data['type'] === 'biginteger') {
  339. $type = $data['type'] === 'integer' ? ' INTEGER' : ' BIGINT';
  340. if ([$name] === $table->primaryKey() || $data['autoIncrement'] === true) {
  341. $type = $data['type'] === 'integer' ? ' SERIAL' : ' BIGSERIAL';
  342. unset($data['null'], $data['default']);
  343. }
  344. $out .= $type;
  345. }
  346. if ($data['type'] === 'string') {
  347. $isFixed = !empty($data['fixed']);
  348. $type = ' VARCHAR';
  349. if ($isFixed) {
  350. $type = ' CHAR';
  351. }
  352. $out .= $type;
  353. if (isset($data['length']) && $data['length'] != 36) {
  354. $out .= '(' . (int)$data['length'] . ')';
  355. }
  356. }
  357. if ($data['type'] === 'float' && isset($data['precision'])) {
  358. $out .= '(' . (int)$data['precision'] . ')';
  359. }
  360. if ($data['type'] === 'decimal' &&
  361. (isset($data['length']) || isset($data['precision']))
  362. ) {
  363. $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')';
  364. }
  365. if (isset($data['null']) && $data['null'] === false) {
  366. $out .= ' NOT NULL';
  367. }
  368. if (isset($data['null']) && $data['null'] === true) {
  369. $out .= ' DEFAULT NULL';
  370. unset($data['default']);
  371. }
  372. if (isset($data['default']) && $data['type'] !== 'timestamp') {
  373. $defaultValue = $data['default'];
  374. if ($data['type'] === 'boolean') {
  375. $defaultValue = (bool)$defaultValue;
  376. }
  377. $out .= ' DEFAULT ' . $this->_driver->schemaValue($defaultValue);
  378. }
  379. return $out;
  380. }
  381. /**
  382. * {@inheritDoc}
  383. */
  384. public function indexSql(Table $table, $name)
  385. {
  386. $data = $table->index($name);
  387. $columns = array_map(
  388. [$this->_driver, 'quoteIdentifier'],
  389. $data['columns']
  390. );
  391. return sprintf(
  392. 'CREATE INDEX %s ON %s (%s)',
  393. $this->_driver->quoteIdentifier($name),
  394. $this->_driver->quoteIdentifier($table->name()),
  395. implode(', ', $columns)
  396. );
  397. }
  398. /**
  399. * {@inheritDoc}
  400. */
  401. public function constraintSql(Table $table, $name)
  402. {
  403. $data = $table->constraint($name);
  404. $out = 'CONSTRAINT ' . $this->_driver->quoteIdentifier($name);
  405. if ($data['type'] === Table::CONSTRAINT_PRIMARY) {
  406. $out = 'PRIMARY KEY';
  407. }
  408. if ($data['type'] === Table::CONSTRAINT_UNIQUE) {
  409. $out .= ' UNIQUE';
  410. }
  411. return $this->_keySql($out, $data);
  412. }
  413. /**
  414. * Helper method for generating key SQL snippets.
  415. *
  416. * @param string $prefix The key prefix
  417. * @param array $data Key data.
  418. * @return string
  419. */
  420. protected function _keySql($prefix, $data)
  421. {
  422. $columns = array_map(
  423. [$this->_driver, 'quoteIdentifier'],
  424. $data['columns']
  425. );
  426. if ($data['type'] === Table::CONSTRAINT_FOREIGN) {
  427. return $prefix . sprintf(
  428. ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s DEFERRABLE INITIALLY IMMEDIATE',
  429. implode(', ', $columns),
  430. $this->_driver->quoteIdentifier($data['references'][0]),
  431. $this->_convertFkColumnsReference($data['references'][1]),
  432. $this->_foreignOnClause($data['update']),
  433. $this->_foreignOnClause($data['delete'])
  434. );
  435. }
  436. return $prefix . ' (' . implode(', ', $columns) . ')';
  437. }
  438. /**
  439. * {@inheritDoc}
  440. */
  441. public function createTableSql(Table $table, $columns, $constraints, $indexes)
  442. {
  443. $content = array_merge($columns, $constraints);
  444. $content = implode(",\n", array_filter($content));
  445. $tableName = $this->_driver->quoteIdentifier($table->name());
  446. $temporary = $table->temporary() ? ' TEMPORARY ' : ' ';
  447. $out = [];
  448. $out[] = sprintf("CREATE%sTABLE %s (\n%s\n)", $temporary, $tableName, $content);
  449. foreach ($indexes as $index) {
  450. $out[] = $index;
  451. }
  452. foreach ($table->columns() as $column) {
  453. $columnData = $table->column($column);
  454. if (isset($columnData['comment'])) {
  455. $out[] = sprintf(
  456. 'COMMENT ON COLUMN %s.%s IS %s',
  457. $tableName,
  458. $this->_driver->quoteIdentifier($column),
  459. $this->_driver->schemaValue($columnData['comment'])
  460. );
  461. }
  462. }
  463. return $out;
  464. }
  465. /**
  466. * {@inheritDoc}
  467. */
  468. public function truncateTableSql(Table $table)
  469. {
  470. $name = $this->_driver->quoteIdentifier($table->name());
  471. return [
  472. sprintf('TRUNCATE %s RESTART IDENTITY CASCADE', $name)
  473. ];
  474. }
  475. /**
  476. * Generate the SQL to drop a table.
  477. *
  478. * @param \Cake\Database\Schema\Table $table Table instance
  479. * @return array SQL statements to drop a table.
  480. */
  481. public function dropTableSql(Table $table)
  482. {
  483. $sql = sprintf(
  484. 'DROP TABLE %s CASCADE',
  485. $this->_driver->quoteIdentifier($table->name())
  486. );
  487. return [$sql];
  488. }
  489. }