PostgresSchema.php 19 KB

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