PostgresSchema.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (https://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. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Database\Schema;
  16. use Cake\Database\Exception;
  17. use Cake\Database\Schema\TableSchema;
  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 = 'SELECT DISTINCT table_schema AS schema,
  38. column_name AS name,
  39. data_type AS type,
  40. is_nullable AS null, column_default AS default,
  41. character_maximum_length AS char_length,
  42. c.collation_name,
  43. d.description as comment,
  44. ordinal_position,
  45. c.numeric_precision as column_precision,
  46. c.numeric_scale as column_scale,
  47. pg_get_serial_sequence(attr.attrelid::regclass::text, attr.attname) IS NOT NULL AS has_serial
  48. FROM information_schema.columns c
  49. INNER JOIN pg_catalog.pg_namespace ns ON (ns.nspname = table_schema)
  50. INNER JOIN pg_catalog.pg_class cl ON (cl.relnamespace = ns.oid AND cl.relname = table_name)
  51. LEFT JOIN pg_catalog.pg_index i ON (i.indrelid = cl.oid AND i.indkey[0] = c.ordinal_position)
  52. LEFT JOIN pg_catalog.pg_description d on (cl.oid = d.objoid AND d.objsubid = c.ordinal_position)
  53. LEFT JOIN pg_catalog.pg_attribute attr ON (cl.oid = attr.attrelid AND column_name = attr.attname)
  54. WHERE table_name = ? AND table_schema = ? AND table_catalog = ?
  55. ORDER BY ordinal_position';
  56. $schema = empty($config['schema']) ? 'public' : $config['schema'];
  57. return [$sql, [$tableName, $schema, $config['database']]];
  58. }
  59. /**
  60. * Convert a column definition to the abstract types.
  61. *
  62. * The returned type will be a type that
  63. * Cake\Database\Type can handle.
  64. *
  65. * @param string $column The column type + length
  66. * @throws \Cake\Database\Exception when column cannot be parsed.
  67. * @return array Array of column information.
  68. */
  69. protected function _convertColumn($column)
  70. {
  71. preg_match('/([a-z\s]+)(?:\(([0-9,]+)\))?/i', $column, $matches);
  72. if (empty($matches)) {
  73. throw new Exception(sprintf('Unable to parse column type from "%s"', $column));
  74. }
  75. $col = strtolower($matches[1]);
  76. $length = null;
  77. if (isset($matches[2])) {
  78. $length = (int)$matches[2];
  79. }
  80. if (in_array($col, ['date', 'time', 'boolean'])) {
  81. return ['type' => $col, 'length' => null];
  82. }
  83. if (strpos($col, 'timestamp') !== false) {
  84. return ['type' => TableSchema::TYPE_TIMESTAMP, 'length' => null];
  85. }
  86. if (strpos($col, 'time') !== false) {
  87. return ['type' => TableSchema::TYPE_TIME, 'length' => null];
  88. }
  89. if ($col === 'serial' || $col === 'integer') {
  90. return ['type' => TableSchema::TYPE_INTEGER, 'length' => 10];
  91. }
  92. if ($col === 'bigserial' || $col === 'bigint') {
  93. return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => 20];
  94. }
  95. if ($col === 'smallint') {
  96. return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => 5];
  97. }
  98. if ($col === 'inet') {
  99. return ['type' => TableSchema::TYPE_STRING, 'length' => 39];
  100. }
  101. if ($col === 'uuid') {
  102. return ['type' => TableSchema::TYPE_UUID, 'length' => null];
  103. }
  104. if ($col === 'char' || $col === 'character') {
  105. return ['type' => TableSchema::TYPE_STRING, 'fixed' => true, 'length' => $length];
  106. }
  107. // money is 'string' as it includes arbitrary text content
  108. // before the number value.
  109. if (strpos($col, 'char') !== false ||
  110. strpos($col, 'money') !== false
  111. ) {
  112. return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
  113. }
  114. if (strpos($col, 'text') !== false) {
  115. return ['type' => TableSchema::TYPE_TEXT, 'length' => null];
  116. }
  117. if ($col === 'bytea') {
  118. return ['type' => TableSchema::TYPE_BINARY, 'length' => null];
  119. }
  120. if ($col === 'real' || strpos($col, 'double') !== false) {
  121. return ['type' => TableSchema::TYPE_FLOAT, 'length' => null];
  122. }
  123. if (strpos($col, 'numeric') !== false ||
  124. strpos($col, 'decimal') !== false
  125. ) {
  126. return ['type' => TableSchema::TYPE_DECIMAL, 'length' => null];
  127. }
  128. if (strpos($col, 'json') !== false) {
  129. return ['type' => TableSchema::TYPE_JSON, 'length' => null];
  130. }
  131. return ['type' => TableSchema::TYPE_STRING, 'length' => null];
  132. }
  133. /**
  134. * {@inheritDoc}
  135. */
  136. public function convertColumnDescription(TableSchema $schema, $row)
  137. {
  138. $field = $this->_convertColumn($row['type']);
  139. if ($field['type'] === TableSchema::TYPE_BOOLEAN) {
  140. if ($row['default'] === 'true') {
  141. $row['default'] = 1;
  142. }
  143. if ($row['default'] === 'false') {
  144. $row['default'] = 0;
  145. }
  146. }
  147. if (!empty($row['has_serial'])) {
  148. $field['autoIncrement'] = true;
  149. }
  150. $field += [
  151. 'default' => $this->_defaultValue($row['default']),
  152. 'null' => $row['null'] === 'YES',
  153. 'collate' => $row['collation_name'],
  154. 'comment' => $row['comment']
  155. ];
  156. $field['length'] = $row['char_length'] ?: $field['length'];
  157. if ($field['type'] === 'numeric' || $field['type'] === 'decimal') {
  158. $field['length'] = $row['column_precision'];
  159. $field['precision'] = $row['column_scale'] ?: null;
  160. }
  161. $schema->addColumn($row['name'], $field);
  162. }
  163. /**
  164. * Manipulate the default value.
  165. *
  166. * Postgres includes sequence data and casting information in default values.
  167. * We need to remove those.
  168. *
  169. * @param string|null $default The default value.
  170. * @return string|null
  171. */
  172. protected function _defaultValue($default)
  173. {
  174. if (is_numeric($default) || $default === null) {
  175. return $default;
  176. }
  177. // Sequences
  178. if (strpos($default, 'nextval') === 0) {
  179. return null;
  180. }
  181. if (strpos($default, 'NULL::') === 0) {
  182. return null;
  183. }
  184. // Remove quotes and postgres casts
  185. return preg_replace(
  186. "/^'(.*)'(?:::.*)$/",
  187. '$1',
  188. $default
  189. );
  190. }
  191. /**
  192. * {@inheritDoc}
  193. */
  194. public function describeIndexSql($tableName, $config)
  195. {
  196. $sql = 'SELECT
  197. c2.relname,
  198. a.attname,
  199. i.indisprimary,
  200. i.indisunique
  201. FROM pg_catalog.pg_namespace n
  202. INNER JOIN pg_catalog.pg_class c ON (n.oid = c.relnamespace)
  203. INNER JOIN pg_catalog.pg_index i ON (c.oid = i.indrelid)
  204. INNER JOIN pg_catalog.pg_class c2 ON (c2.oid = i.indexrelid)
  205. INNER JOIN pg_catalog.pg_attribute a ON (a.attrelid = c.oid AND i.indrelid::regclass = a.attrelid::regclass)
  206. WHERE n.nspname = ?
  207. AND a.attnum = ANY(i.indkey)
  208. AND c.relname = ?
  209. ORDER BY i.indisprimary DESC, i.indisunique DESC, c.relname, a.attnum';
  210. $schema = 'public';
  211. if (!empty($config['schema'])) {
  212. $schema = $config['schema'];
  213. }
  214. return [$sql, [$schema, $tableName]];
  215. }
  216. /**
  217. * {@inheritDoc}
  218. */
  219. public function convertIndexDescription(TableSchema $schema, $row)
  220. {
  221. $type = TableSchema::INDEX_INDEX;
  222. $name = $row['relname'];
  223. if ($row['indisprimary']) {
  224. $name = $type = TableSchema::CONSTRAINT_PRIMARY;
  225. }
  226. if ($row['indisunique'] && $type === TableSchema::INDEX_INDEX) {
  227. $type = TableSchema::CONSTRAINT_UNIQUE;
  228. }
  229. if ($type === TableSchema::CONSTRAINT_PRIMARY || $type === TableSchema::CONSTRAINT_UNIQUE) {
  230. $this->_convertConstraint($schema, $name, $type, $row);
  231. return;
  232. }
  233. $index = $schema->getIndex($name);
  234. if (!$index) {
  235. $index = [
  236. 'type' => $type,
  237. 'columns' => []
  238. ];
  239. }
  240. $index['columns'][] = $row['attname'];
  241. $schema->addIndex($name, $index);
  242. }
  243. /**
  244. * Add/update a constraint into the schema object.
  245. *
  246. * @param \Cake\Database\Schema\TableSchema $schema The table to update.
  247. * @param string $name The index name.
  248. * @param string $type The index type.
  249. * @param array $row The metadata record to update with.
  250. * @return void
  251. */
  252. protected function _convertConstraint($schema, $name, $type, $row)
  253. {
  254. $constraint = $schema->getConstraint($name);
  255. if (!$constraint) {
  256. $constraint = [
  257. 'type' => $type,
  258. 'columns' => []
  259. ];
  260. }
  261. $constraint['columns'][] = $row['attname'];
  262. $schema->addConstraint($name, $constraint);
  263. }
  264. /**
  265. * {@inheritDoc}
  266. */
  267. public function describeForeignKeySql($tableName, $config)
  268. {
  269. $sql = 'SELECT
  270. c.conname AS name,
  271. c.contype AS type,
  272. a.attname AS column_name,
  273. c.confmatchtype AS match_type,
  274. c.confupdtype AS on_update,
  275. c.confdeltype AS on_delete,
  276. c.confrelid::regclass AS references_table,
  277. ab.attname AS references_field
  278. FROM pg_catalog.pg_namespace n
  279. INNER JOIN pg_catalog.pg_class cl ON (n.oid = cl.relnamespace)
  280. INNER JOIN pg_catalog.pg_constraint c ON (n.oid = c.connamespace)
  281. INNER JOIN pg_catalog.pg_attribute a ON (a.attrelid = cl.oid AND c.conrelid = a.attrelid AND a.attnum = ANY(c.conkey))
  282. INNER JOIN pg_catalog.pg_attribute ab ON (a.attrelid = cl.oid AND c.confrelid = ab.attrelid AND ab.attnum = ANY(c.confkey))
  283. WHERE n.nspname = ?
  284. AND cl.relname = ?
  285. ORDER BY name, a.attnum, ab.attnum DESC';
  286. $schema = empty($config['schema']) ? 'public' : $config['schema'];
  287. return [$sql, [$schema, $tableName]];
  288. }
  289. /**
  290. * {@inheritDoc}
  291. */
  292. public function convertForeignKeyDescription(TableSchema $schema, $row)
  293. {
  294. $data = [
  295. 'type' => TableSchema::CONSTRAINT_FOREIGN,
  296. 'columns' => $row['column_name'],
  297. 'references' => [$row['references_table'], $row['references_field']],
  298. 'update' => $this->_convertOnClause($row['on_update']),
  299. 'delete' => $this->_convertOnClause($row['on_delete']),
  300. ];
  301. $schema->addConstraint($row['name'], $data);
  302. }
  303. /**
  304. * {@inheritDoc}
  305. */
  306. protected function _convertOnClause($clause)
  307. {
  308. if ($clause === 'r') {
  309. return TableSchema::ACTION_RESTRICT;
  310. }
  311. if ($clause === 'a') {
  312. return TableSchema::ACTION_NO_ACTION;
  313. }
  314. if ($clause === 'c') {
  315. return TableSchema::ACTION_CASCADE;
  316. }
  317. return TableSchema::ACTION_SET_NULL;
  318. }
  319. /**
  320. * {@inheritDoc}
  321. */
  322. public function columnSql(TableSchema $schema, $name)
  323. {
  324. $data = $schema->getColumn($name);
  325. $out = $this->_driver->quoteIdentifier($name);
  326. $typeMap = [
  327. TableSchema::TYPE_TINYINTEGER => ' SMALLINT',
  328. TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
  329. TableSchema::TYPE_BINARY_UUID => ' UUID',
  330. TableSchema::TYPE_BOOLEAN => ' BOOLEAN',
  331. TableSchema::TYPE_FLOAT => ' FLOAT',
  332. TableSchema::TYPE_DECIMAL => ' DECIMAL',
  333. TableSchema::TYPE_DATE => ' DATE',
  334. TableSchema::TYPE_TIME => ' TIME',
  335. TableSchema::TYPE_DATETIME => ' TIMESTAMP',
  336. TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP',
  337. TableSchema::TYPE_UUID => ' UUID',
  338. TableSchema::TYPE_JSON => ' JSONB'
  339. ];
  340. if (isset($typeMap[$data['type']])) {
  341. $out .= $typeMap[$data['type']];
  342. }
  343. if ($data['type'] === TableSchema::TYPE_INTEGER || $data['type'] === TableSchema::TYPE_BIGINTEGER) {
  344. $type = $data['type'] === TableSchema::TYPE_INTEGER ? ' INTEGER' : ' BIGINT';
  345. if ([$name] === $schema->primaryKey() || $data['autoIncrement'] === true) {
  346. $type = $data['type'] === TableSchema::TYPE_INTEGER ? ' SERIAL' : ' BIGSERIAL';
  347. unset($data['null'], $data['default']);
  348. }
  349. $out .= $type;
  350. }
  351. if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) {
  352. $out .= ' TEXT';
  353. }
  354. if ($data['type'] === TableSchema::TYPE_BINARY) {
  355. $out .= ' BYTEA';
  356. }
  357. if ($data['type'] === TableSchema::TYPE_STRING ||
  358. ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] === TableSchema::LENGTH_TINY)
  359. ) {
  360. $isFixed = !empty($data['fixed']);
  361. $type = ' VARCHAR';
  362. if ($isFixed) {
  363. $type = ' CHAR';
  364. }
  365. $out .= $type;
  366. if (isset($data['length']) && $data['length'] != 36) {
  367. $out .= '(' . (int)$data['length'] . ')';
  368. }
  369. }
  370. $hasCollate = [TableSchema::TYPE_TEXT, TableSchema::TYPE_STRING];
  371. if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') {
  372. $out .= ' COLLATE "' . $data['collate'] . '"';
  373. }
  374. if ($data['type'] === TableSchema::TYPE_FLOAT && isset($data['precision'])) {
  375. $out .= '(' . (int)$data['precision'] . ')';
  376. }
  377. if ($data['type'] === TableSchema::TYPE_DECIMAL &&
  378. (isset($data['length']) || isset($data['precision']))
  379. ) {
  380. $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')';
  381. }
  382. if (isset($data['null']) && $data['null'] === false) {
  383. $out .= ' NOT NULL';
  384. }
  385. if (isset($data['default']) &&
  386. in_array($data['type'], [TableSchema::TYPE_TIMESTAMP, TableSchema::TYPE_DATETIME]) &&
  387. strtolower($data['default']) === 'current_timestamp'
  388. ) {
  389. $out .= ' DEFAULT CURRENT_TIMESTAMP';
  390. } elseif (isset($data['default'])) {
  391. $defaultValue = $data['default'];
  392. if ($data['type'] === 'boolean') {
  393. $defaultValue = (bool)$defaultValue;
  394. }
  395. $out .= ' DEFAULT ' . $this->_driver->schemaValue($defaultValue);
  396. } elseif (isset($data['null']) && $data['null'] !== false) {
  397. $out .= ' DEFAULT NULL';
  398. }
  399. return $out;
  400. }
  401. /**
  402. * {@inheritDoc}
  403. */
  404. public function addConstraintSql(TableSchema $schema)
  405. {
  406. $sqlPattern = 'ALTER TABLE %s ADD %s;';
  407. $sql = [];
  408. foreach ($schema->constraints() as $name) {
  409. $constraint = $schema->getConstraint($name);
  410. if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
  411. $tableName = $this->_driver->quoteIdentifier($schema->name());
  412. $sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name));
  413. }
  414. }
  415. return $sql;
  416. }
  417. /**
  418. * {@inheritDoc}
  419. */
  420. public function dropConstraintSql(TableSchema $schema)
  421. {
  422. $sqlPattern = 'ALTER TABLE %s DROP CONSTRAINT %s;';
  423. $sql = [];
  424. foreach ($schema->constraints() as $name) {
  425. $constraint = $schema->getConstraint($name);
  426. if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
  427. $tableName = $this->_driver->quoteIdentifier($schema->name());
  428. $constraintName = $this->_driver->quoteIdentifier($name);
  429. $sql[] = sprintf($sqlPattern, $tableName, $constraintName);
  430. }
  431. }
  432. return $sql;
  433. }
  434. /**
  435. * {@inheritDoc}
  436. */
  437. public function indexSql(TableSchema $schema, $name)
  438. {
  439. $data = $schema->getIndex($name);
  440. $columns = array_map(
  441. [$this->_driver, 'quoteIdentifier'],
  442. $data['columns']
  443. );
  444. return sprintf(
  445. 'CREATE INDEX %s ON %s (%s)',
  446. $this->_driver->quoteIdentifier($name),
  447. $this->_driver->quoteIdentifier($schema->name()),
  448. implode(', ', $columns)
  449. );
  450. }
  451. /**
  452. * {@inheritDoc}
  453. */
  454. public function constraintSql(TableSchema $schema, $name)
  455. {
  456. $data = $schema->getConstraint($name);
  457. $out = 'CONSTRAINT ' . $this->_driver->quoteIdentifier($name);
  458. if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) {
  459. $out = 'PRIMARY KEY';
  460. }
  461. if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) {
  462. $out .= ' UNIQUE';
  463. }
  464. return $this->_keySql($out, $data);
  465. }
  466. /**
  467. * Helper method for generating key SQL snippets.
  468. *
  469. * @param string $prefix The key prefix
  470. * @param array $data Key data.
  471. * @return string
  472. */
  473. protected function _keySql($prefix, $data)
  474. {
  475. $columns = array_map(
  476. [$this->_driver, 'quoteIdentifier'],
  477. $data['columns']
  478. );
  479. if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
  480. return $prefix . sprintf(
  481. ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s DEFERRABLE INITIALLY IMMEDIATE',
  482. implode(', ', $columns),
  483. $this->_driver->quoteIdentifier($data['references'][0]),
  484. $this->_convertConstraintColumns($data['references'][1]),
  485. $this->_foreignOnClause($data['update']),
  486. $this->_foreignOnClause($data['delete'])
  487. );
  488. }
  489. return $prefix . ' (' . implode(', ', $columns) . ')';
  490. }
  491. /**
  492. * {@inheritDoc}
  493. */
  494. public function createTableSql(TableSchema $schema, $columns, $constraints, $indexes)
  495. {
  496. $content = array_merge($columns, $constraints);
  497. $content = implode(",\n", array_filter($content));
  498. $tableName = $this->_driver->quoteIdentifier($schema->name());
  499. $temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' ';
  500. $out = [];
  501. $out[] = sprintf("CREATE%sTABLE %s (\n%s\n)", $temporary, $tableName, $content);
  502. foreach ($indexes as $index) {
  503. $out[] = $index;
  504. }
  505. foreach ($schema->columns() as $column) {
  506. $columnData = $schema->getColumn($column);
  507. if (isset($columnData['comment'])) {
  508. $out[] = sprintf(
  509. 'COMMENT ON COLUMN %s.%s IS %s',
  510. $tableName,
  511. $this->_driver->quoteIdentifier($column),
  512. $this->_driver->schemaValue($columnData['comment'])
  513. );
  514. }
  515. }
  516. return $out;
  517. }
  518. /**
  519. * {@inheritDoc}
  520. */
  521. public function truncateTableSql(TableSchema $schema)
  522. {
  523. $name = $this->_driver->quoteIdentifier($schema->name());
  524. return [
  525. sprintf('TRUNCATE %s RESTART IDENTITY CASCADE', $name)
  526. ];
  527. }
  528. /**
  529. * Generate the SQL to drop a table.
  530. *
  531. * @param \Cake\Database\Schema\TableSchema $schema Table instance
  532. * @return array SQL statements to drop a table.
  533. */
  534. public function dropTableSql(TableSchema $schema)
  535. {
  536. $sql = sprintf(
  537. 'DROP TABLE %s CASCADE',
  538. $this->_driver->quoteIdentifier($schema->name())
  539. );
  540. return [$sql];
  541. }
  542. }