PostgresSchema.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  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. // Remove quotes and postgres casts
  182. return preg_replace(
  183. "/^'(.*)'(?:::.*)$/",
  184. '$1',
  185. $default
  186. );
  187. }
  188. /**
  189. * {@inheritDoc}
  190. */
  191. public function describeIndexSql($tableName, $config)
  192. {
  193. $sql = 'SELECT
  194. c2.relname,
  195. a.attname,
  196. i.indisprimary,
  197. i.indisunique
  198. FROM pg_catalog.pg_namespace n
  199. INNER JOIN pg_catalog.pg_class c ON (n.oid = c.relnamespace)
  200. INNER JOIN pg_catalog.pg_index i ON (c.oid = i.indrelid)
  201. INNER JOIN pg_catalog.pg_class c2 ON (c2.oid = i.indexrelid)
  202. INNER JOIN pg_catalog.pg_attribute a ON (a.attrelid = c.oid AND i.indrelid::regclass = a.attrelid::regclass)
  203. WHERE n.nspname = ?
  204. AND a.attnum = ANY(i.indkey)
  205. AND c.relname = ?
  206. ORDER BY i.indisprimary DESC, i.indisunique DESC, c.relname, a.attnum';
  207. $schema = 'public';
  208. if (!empty($config['schema'])) {
  209. $schema = $config['schema'];
  210. }
  211. return [$sql, [$schema, $tableName]];
  212. }
  213. /**
  214. * {@inheritDoc}
  215. */
  216. public function convertIndexDescription(TableSchema $schema, $row)
  217. {
  218. $type = TableSchema::INDEX_INDEX;
  219. $name = $row['relname'];
  220. if ($row['indisprimary']) {
  221. $name = $type = TableSchema::CONSTRAINT_PRIMARY;
  222. }
  223. if ($row['indisunique'] && $type === TableSchema::INDEX_INDEX) {
  224. $type = TableSchema::CONSTRAINT_UNIQUE;
  225. }
  226. if ($type === TableSchema::CONSTRAINT_PRIMARY || $type === TableSchema::CONSTRAINT_UNIQUE) {
  227. $this->_convertConstraint($schema, $name, $type, $row);
  228. return;
  229. }
  230. $index = $schema->getIndex($name);
  231. if (!$index) {
  232. $index = [
  233. 'type' => $type,
  234. 'columns' => []
  235. ];
  236. }
  237. $index['columns'][] = $row['attname'];
  238. $schema->addIndex($name, $index);
  239. }
  240. /**
  241. * Add/update a constraint into the schema object.
  242. *
  243. * @param \Cake\Database\Schema\TableSchema $schema The table to update.
  244. * @param string $name The index name.
  245. * @param string $type The index type.
  246. * @param array $row The metadata record to update with.
  247. * @return void
  248. */
  249. protected function _convertConstraint($schema, $name, $type, $row)
  250. {
  251. $constraint = $schema->getConstraint($name);
  252. if (!$constraint) {
  253. $constraint = [
  254. 'type' => $type,
  255. 'columns' => []
  256. ];
  257. }
  258. $constraint['columns'][] = $row['attname'];
  259. $schema->addConstraint($name, $constraint);
  260. }
  261. /**
  262. * {@inheritDoc}
  263. */
  264. public function describeForeignKeySql($tableName, $config)
  265. {
  266. $sql = 'SELECT
  267. c.conname AS name,
  268. c.contype AS type,
  269. a.attname AS column_name,
  270. c.confmatchtype AS match_type,
  271. c.confupdtype AS on_update,
  272. c.confdeltype AS on_delete,
  273. c.confrelid::regclass AS references_table,
  274. ab.attname AS references_field
  275. FROM pg_catalog.pg_namespace n
  276. INNER JOIN pg_catalog.pg_class cl ON (n.oid = cl.relnamespace)
  277. INNER JOIN pg_catalog.pg_constraint c ON (n.oid = c.connamespace)
  278. INNER JOIN pg_catalog.pg_attribute a ON (a.attrelid = cl.oid AND c.conrelid = a.attrelid AND a.attnum = ANY(c.conkey))
  279. INNER JOIN pg_catalog.pg_attribute ab ON (a.attrelid = cl.oid AND c.confrelid = ab.attrelid AND ab.attnum = ANY(c.confkey))
  280. WHERE n.nspname = ?
  281. AND cl.relname = ?
  282. ORDER BY name, a.attnum, ab.attnum DESC';
  283. $schema = empty($config['schema']) ? 'public' : $config['schema'];
  284. return [$sql, [$schema, $tableName]];
  285. }
  286. /**
  287. * {@inheritDoc}
  288. */
  289. public function convertForeignKeyDescription(TableSchema $schema, $row)
  290. {
  291. $data = [
  292. 'type' => TableSchema::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. $schema->addConstraint($row['name'], $data);
  299. }
  300. /**
  301. * {@inheritDoc}
  302. */
  303. protected function _convertOnClause($clause)
  304. {
  305. if ($clause === 'r') {
  306. return TableSchema::ACTION_RESTRICT;
  307. }
  308. if ($clause === 'a') {
  309. return TableSchema::ACTION_NO_ACTION;
  310. }
  311. if ($clause === 'c') {
  312. return TableSchema::ACTION_CASCADE;
  313. }
  314. return TableSchema::ACTION_SET_NULL;
  315. }
  316. /**
  317. * {@inheritDoc}
  318. */
  319. public function columnSql(TableSchema $schema, $name)
  320. {
  321. $data = $schema->getColumn($name);
  322. $out = $this->_driver->quoteIdentifier($name);
  323. $typeMap = [
  324. TableSchema::TYPE_TINYINTEGER => ' SMALLINT',
  325. TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
  326. TableSchema::TYPE_BINARY => ' BYTEA',
  327. TableSchema::TYPE_BOOLEAN => ' BOOLEAN',
  328. TableSchema::TYPE_FLOAT => ' FLOAT',
  329. TableSchema::TYPE_DECIMAL => ' DECIMAL',
  330. TableSchema::TYPE_DATE => ' DATE',
  331. TableSchema::TYPE_TIME => ' TIME',
  332. TableSchema::TYPE_DATETIME => ' TIMESTAMP',
  333. TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP',
  334. TableSchema::TYPE_UUID => ' UUID',
  335. TableSchema::TYPE_JSON => ' JSONB'
  336. ];
  337. if (isset($typeMap[$data['type']])) {
  338. $out .= $typeMap[$data['type']];
  339. }
  340. if ($data['type'] === TableSchema::TYPE_INTEGER || $data['type'] === TableSchema::TYPE_BIGINTEGER) {
  341. $type = $data['type'] === TableSchema::TYPE_INTEGER ? ' INTEGER' : ' BIGINT';
  342. if ([$name] === $schema->primaryKey() || $data['autoIncrement'] === true) {
  343. $type = $data['type'] === TableSchema::TYPE_INTEGER ? ' SERIAL' : ' BIGSERIAL';
  344. unset($data['null'], $data['default']);
  345. }
  346. $out .= $type;
  347. }
  348. if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) {
  349. $out .= ' TEXT';
  350. }
  351. if ($data['type'] === TableSchema::TYPE_STRING ||
  352. ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] === TableSchema::LENGTH_TINY)
  353. ) {
  354. $isFixed = !empty($data['fixed']);
  355. $type = ' VARCHAR';
  356. if ($isFixed) {
  357. $type = ' CHAR';
  358. }
  359. $out .= $type;
  360. if (isset($data['length']) && $data['length'] != 36) {
  361. $out .= '(' . (int)$data['length'] . ')';
  362. }
  363. }
  364. $hasCollate = [TableSchema::TYPE_TEXT, TableSchema::TYPE_STRING];
  365. if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') {
  366. $out .= ' COLLATE "' . $data['collate'] . '"';
  367. }
  368. if ($data['type'] === TableSchema::TYPE_FLOAT && isset($data['precision'])) {
  369. $out .= '(' . (int)$data['precision'] . ')';
  370. }
  371. if ($data['type'] === TableSchema::TYPE_DECIMAL &&
  372. (isset($data['length']) || isset($data['precision']))
  373. ) {
  374. $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')';
  375. }
  376. if (isset($data['null']) && $data['null'] === false) {
  377. $out .= ' NOT NULL';
  378. }
  379. if (isset($data['default']) &&
  380. in_array($data['type'], [TableSchema::TYPE_TIMESTAMP, TableSchema::TYPE_DATETIME]) &&
  381. strtolower($data['default']) === 'current_timestamp'
  382. ) {
  383. $out .= ' DEFAULT CURRENT_TIMESTAMP';
  384. } elseif (isset($data['default'])) {
  385. $defaultValue = $data['default'];
  386. if ($data['type'] === 'boolean') {
  387. $defaultValue = (bool)$defaultValue;
  388. }
  389. $out .= ' DEFAULT ' . $this->_driver->schemaValue($defaultValue);
  390. } elseif (isset($data['null']) && $data['null'] !== false) {
  391. $out .= ' DEFAULT NULL';
  392. }
  393. return $out;
  394. }
  395. /**
  396. * {@inheritDoc}
  397. */
  398. public function addConstraintSql(TableSchema $schema)
  399. {
  400. $sqlPattern = 'ALTER TABLE %s ADD %s;';
  401. $sql = [];
  402. foreach ($schema->constraints() as $name) {
  403. $constraint = $schema->getConstraint($name);
  404. if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
  405. $tableName = $this->_driver->quoteIdentifier($schema->name());
  406. $sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name));
  407. }
  408. }
  409. return $sql;
  410. }
  411. /**
  412. * {@inheritDoc}
  413. */
  414. public function dropConstraintSql(TableSchema $schema)
  415. {
  416. $sqlPattern = 'ALTER TABLE %s DROP CONSTRAINT %s;';
  417. $sql = [];
  418. foreach ($schema->constraints() as $name) {
  419. $constraint = $schema->getConstraint($name);
  420. if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
  421. $tableName = $this->_driver->quoteIdentifier($schema->name());
  422. $constraintName = $this->_driver->quoteIdentifier($name);
  423. $sql[] = sprintf($sqlPattern, $tableName, $constraintName);
  424. }
  425. }
  426. return $sql;
  427. }
  428. /**
  429. * {@inheritDoc}
  430. */
  431. public function indexSql(TableSchema $schema, $name)
  432. {
  433. $data = $schema->getIndex($name);
  434. $columns = array_map(
  435. [$this->_driver, 'quoteIdentifier'],
  436. $data['columns']
  437. );
  438. return sprintf(
  439. 'CREATE INDEX %s ON %s (%s)',
  440. $this->_driver->quoteIdentifier($name),
  441. $this->_driver->quoteIdentifier($schema->name()),
  442. implode(', ', $columns)
  443. );
  444. }
  445. /**
  446. * {@inheritDoc}
  447. */
  448. public function constraintSql(TableSchema $schema, $name)
  449. {
  450. $data = $schema->getConstraint($name);
  451. $out = 'CONSTRAINT ' . $this->_driver->quoteIdentifier($name);
  452. if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) {
  453. $out = 'PRIMARY KEY';
  454. }
  455. if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) {
  456. $out .= ' UNIQUE';
  457. }
  458. return $this->_keySql($out, $data);
  459. }
  460. /**
  461. * Helper method for generating key SQL snippets.
  462. *
  463. * @param string $prefix The key prefix
  464. * @param array $data Key data.
  465. * @return string
  466. */
  467. protected function _keySql($prefix, $data)
  468. {
  469. $columns = array_map(
  470. [$this->_driver, 'quoteIdentifier'],
  471. $data['columns']
  472. );
  473. if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
  474. return $prefix . sprintf(
  475. ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s DEFERRABLE INITIALLY IMMEDIATE',
  476. implode(', ', $columns),
  477. $this->_driver->quoteIdentifier($data['references'][0]),
  478. $this->_convertConstraintColumns($data['references'][1]),
  479. $this->_foreignOnClause($data['update']),
  480. $this->_foreignOnClause($data['delete'])
  481. );
  482. }
  483. return $prefix . ' (' . implode(', ', $columns) . ')';
  484. }
  485. /**
  486. * {@inheritDoc}
  487. */
  488. public function createTableSql(TableSchema $schema, $columns, $constraints, $indexes)
  489. {
  490. $content = array_merge($columns, $constraints);
  491. $content = implode(",\n", array_filter($content));
  492. $tableName = $this->_driver->quoteIdentifier($schema->name());
  493. $temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' ';
  494. $out = [];
  495. $out[] = sprintf("CREATE%sTABLE %s (\n%s\n)", $temporary, $tableName, $content);
  496. foreach ($indexes as $index) {
  497. $out[] = $index;
  498. }
  499. foreach ($schema->columns() as $column) {
  500. $columnData = $schema->getColumn($column);
  501. if (isset($columnData['comment'])) {
  502. $out[] = sprintf(
  503. 'COMMENT ON COLUMN %s.%s IS %s',
  504. $tableName,
  505. $this->_driver->quoteIdentifier($column),
  506. $this->_driver->schemaValue($columnData['comment'])
  507. );
  508. }
  509. }
  510. return $out;
  511. }
  512. /**
  513. * {@inheritDoc}
  514. */
  515. public function truncateTableSql(TableSchema $schema)
  516. {
  517. $name = $this->_driver->quoteIdentifier($schema->name());
  518. return [
  519. sprintf('TRUNCATE %s RESTART IDENTITY CASCADE', $name)
  520. ];
  521. }
  522. /**
  523. * Generate the SQL to drop a table.
  524. *
  525. * @param \Cake\Database\Schema\TableSchema $schema Table instance
  526. * @return array SQL statements to drop a table.
  527. */
  528. public function dropTableSql(TableSchema $schema)
  529. {
  530. $sql = sprintf(
  531. 'DROP TABLE %s CASCADE',
  532. $this->_driver->quoteIdentifier($schema->name())
  533. );
  534. return [$sql];
  535. }
  536. }