PostgresSchema.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  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. d.description as comment,
  42. ordinal_position,
  43. pg_get_serial_sequence(attr.attrelid::regclass::text, attr.attname) IS NOT NULL AS has_serial
  44. FROM information_schema.columns c
  45. INNER JOIN pg_catalog.pg_namespace ns ON (ns.nspname = table_schema)
  46. INNER JOIN pg_catalog.pg_class cl ON (cl.relnamespace = ns.oid AND cl.relname = table_name)
  47. LEFT JOIN pg_catalog.pg_index i ON (i.indrelid = cl.oid AND i.indkey[0] = c.ordinal_position)
  48. LEFT JOIN pg_catalog.pg_description d on (cl.oid = d.objoid AND d.objsubid = c.ordinal_position)
  49. LEFT JOIN pg_catalog.pg_attribute attr ON (cl.oid = attr.attrelid AND column_name = attr.attname)
  50. WHERE table_name = ? AND table_schema = ? AND table_catalog = ?
  51. ORDER BY ordinal_position';
  52. $schema = empty($config['schema']) ? 'public' : $config['schema'];
  53. return [$sql, [$tableName, $schema, $config['database']]];
  54. }
  55. /**
  56. * Convert a column definition to the abstract types.
  57. *
  58. * The returned type will be a type that
  59. * Cake\Database\Type can handle.
  60. *
  61. * @param string $column The column type + length
  62. * @throws \Cake\Database\Exception when column cannot be parsed.
  63. * @return array Array of column information.
  64. */
  65. protected function _convertColumn($column)
  66. {
  67. preg_match('/([a-z\s]+)(?:\(([0-9,]+)\))?/i', $column, $matches);
  68. if (empty($matches)) {
  69. throw new Exception(sprintf('Unable to parse column type from "%s"', $column));
  70. }
  71. $col = strtolower($matches[1]);
  72. $length = null;
  73. if (isset($matches[2])) {
  74. $length = (int)$matches[2];
  75. }
  76. if (in_array($col, ['date', 'time', 'boolean'])) {
  77. return ['type' => $col, 'length' => null];
  78. }
  79. if (strpos($col, 'timestamp') !== false) {
  80. return ['type' => 'timestamp', 'length' => null];
  81. }
  82. if (strpos($col, 'time') !== false) {
  83. return ['type' => 'time', 'length' => null];
  84. }
  85. if ($col === 'serial' || $col === 'integer') {
  86. return ['type' => 'integer', 'length' => 10];
  87. }
  88. if ($col === 'bigserial' || $col === 'bigint') {
  89. return ['type' => 'biginteger', 'length' => 20];
  90. }
  91. if ($col === 'smallint') {
  92. return ['type' => 'integer', 'length' => 5];
  93. }
  94. if ($col === 'inet') {
  95. return ['type' => 'string', 'length' => 39];
  96. }
  97. if ($col === 'uuid') {
  98. return ['type' => 'uuid', 'length' => null];
  99. }
  100. if ($col === 'char' || $col === 'character') {
  101. return ['type' => 'string', 'fixed' => true, 'length' => $length];
  102. }
  103. // money is 'string' as it includes arbitrary text content
  104. // before the number value.
  105. if (strpos($col, 'char') !== false ||
  106. strpos($col, 'money') !== false
  107. ) {
  108. return ['type' => 'string', 'length' => $length];
  109. }
  110. if (strpos($col, 'text') !== false) {
  111. return ['type' => 'text', 'length' => null];
  112. }
  113. if ($col === 'bytea') {
  114. return ['type' => 'binary', 'length' => null];
  115. }
  116. if ($col === 'real' || strpos($col, 'double') !== false) {
  117. return ['type' => 'float', 'length' => null];
  118. }
  119. if (strpos($col, 'numeric') !== false ||
  120. strpos($col, 'decimal') !== false
  121. ) {
  122. return ['type' => 'decimal', 'length' => null];
  123. }
  124. return ['type' => 'text', 'length' => null];
  125. }
  126. /**
  127. * {@inheritDoc}
  128. */
  129. public function convertColumnDescription(Table $table, $row)
  130. {
  131. $field = $this->_convertColumn($row['type']);
  132. if ($field['type'] === 'boolean') {
  133. if ($row['default'] === 'true') {
  134. $row['default'] = 1;
  135. }
  136. if ($row['default'] === 'false') {
  137. $row['default'] = 0;
  138. }
  139. }
  140. if (!empty($row['has_serial'])) {
  141. $field['autoIncrement'] = true;
  142. }
  143. $field += [
  144. 'default' => $this->_defaultValue($row['default']),
  145. 'null' => $row['null'] === 'YES' ? true : false,
  146. 'comment' => $row['comment']
  147. ];
  148. $field['length'] = $row['char_length'] ?: $field['length'];
  149. $table->addColumn($row['name'], $field);
  150. }
  151. /**
  152. * Manipulate the default value.
  153. *
  154. * Postgres includes sequence data and casting information in default values.
  155. * We need to remove those.
  156. *
  157. * @param string|null $default The default value.
  158. * @return string|null
  159. */
  160. protected function _defaultValue($default)
  161. {
  162. if (is_numeric($default) || $default === null) {
  163. return $default;
  164. }
  165. // Sequences
  166. if (strpos($default, 'nextval') === 0) {
  167. return null;
  168. }
  169. // Remove quotes and postgres casts
  170. return preg_replace(
  171. "/^'(.*)'(?:::.*)$/",
  172. "$1",
  173. $default
  174. );
  175. }
  176. /**
  177. * {@inheritDoc}
  178. */
  179. public function describeIndexSql($tableName, $config)
  180. {
  181. $sql = 'SELECT
  182. c2.relname,
  183. a.attname,
  184. i.indisprimary,
  185. i.indisunique
  186. FROM pg_catalog.pg_class AS c,
  187. pg_catalog.pg_class AS c2,
  188. pg_catalog.pg_index AS i,
  189. pg_catalog.pg_attribute AS a
  190. WHERE c.oid = (
  191. SELECT c.oid
  192. FROM pg_catalog.pg_class c
  193. LEFT JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
  194. WHERE c.relname = ?
  195. AND n.nspname = ?
  196. )
  197. AND c.oid = i.indrelid
  198. AND c.oid = a.attrelid
  199. AND a.attnum = ANY(i.indkey)
  200. AND i.indexrelid = c2.oid
  201. ORDER BY i.indisprimary DESC, i.indisunique DESC, c2.relname, a.attnum';
  202. $schema = 'public';
  203. if (!empty($config['schema'])) {
  204. $schema = $config['schema'];
  205. }
  206. return [$sql, [$tableName, $schema]];
  207. }
  208. /**
  209. * {@inheritDoc}
  210. */
  211. public function convertIndexDescription(Table $table, $row)
  212. {
  213. $type = Table::INDEX_INDEX;
  214. $name = $row['relname'];
  215. if ($row['indisprimary']) {
  216. $name = $type = Table::CONSTRAINT_PRIMARY;
  217. }
  218. if ($row['indisunique'] && $type === Table::INDEX_INDEX) {
  219. $type = Table::CONSTRAINT_UNIQUE;
  220. }
  221. if ($type === Table::CONSTRAINT_PRIMARY || $type === Table::CONSTRAINT_UNIQUE) {
  222. $this->_convertConstraint($table, $name, $type, $row);
  223. return;
  224. }
  225. $index = $table->index($name);
  226. if (!$index) {
  227. $index = [
  228. 'type' => $type,
  229. 'columns' => []
  230. ];
  231. }
  232. $index['columns'][] = $row['attname'];
  233. $table->addIndex($name, $index);
  234. }
  235. /**
  236. * Add/update a constraint into the schema object.
  237. *
  238. * @param \Cake\Database\Schema\Table $table The table to update.
  239. * @param string $name The index name.
  240. * @param string $type The index type.
  241. * @param array $row The metadata record to update with.
  242. * @return void
  243. */
  244. protected function _convertConstraint($table, $name, $type, $row)
  245. {
  246. $constraint = $table->constraint($name);
  247. if (!$constraint) {
  248. $constraint = [
  249. 'type' => $type,
  250. 'columns' => []
  251. ];
  252. }
  253. $constraint['columns'][] = $row['attname'];
  254. $table->addConstraint($name, $constraint);
  255. }
  256. /**
  257. * {@inheritDoc}
  258. */
  259. public function describeForeignKeySql($tableName, $config)
  260. {
  261. $sql = "SELECT
  262. c.conname AS name,
  263. c.contype AS type,
  264. a.attname AS column_name,
  265. c.confmatchtype AS match_type,
  266. c.confupdtype AS on_update,
  267. c.confdeltype AS on_delete,
  268. c.confrelid::regclass AS references_table,
  269. ab.attname AS references_field
  270. FROM pg_catalog.pg_namespace n, pg_catalog.pg_class cl, pg_catalog.pg_constraint c, pg_catalog.pg_attribute a, pg_catalog.pg_attribute ab
  271. WHERE n.oid = c.connamespace
  272. AND cl.oid = c.conrelid
  273. AND c.conrelid::regclass = a.attrelid::regclass
  274. AND c.conkey[1] = a.attnum
  275. AND a.attrelid = cl.oid
  276. AND ab.attrelid = cl.oid
  277. AND a.attrelid = ab.attrelid
  278. AND a.attnum = ab.attnum
  279. AND c.conrelid = ?::regclass
  280. AND c.contype='f'
  281. AND n.nspname = ?
  282. ORDER BY c.conname, a.attnum";
  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 === 'r') {
  306. return Table::ACTION_RESTRICT;
  307. }
  308. if ($clause === 'a') {
  309. return Table::ACTION_NO_ACTION;
  310. }
  311. if ($clause === 'c') {
  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 addConstraintSql(Table $table)
  385. {
  386. $sqlPattern = 'ALTER TABLE %s ADD %s;';
  387. $sql = [];
  388. foreach ($table->constraints() as $name) {
  389. $constraint = $table->constraint($name);
  390. if ($constraint['type'] === Table::CONSTRAINT_FOREIGN) {
  391. $tableName = $this->_driver->quoteIdentifier($table->name());
  392. $sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($table, $name));
  393. }
  394. }
  395. return $sql;
  396. }
  397. /**
  398. * {@inheritDoc}
  399. */
  400. public function dropConstraintSql(Table $table)
  401. {
  402. $sqlPattern = 'ALTER TABLE %s DROP CONSTRAINT %s;';
  403. $sql = [];
  404. foreach ($table->constraints() as $name) {
  405. $constraint = $table->constraint($name);
  406. if ($constraint['type'] === Table::CONSTRAINT_FOREIGN) {
  407. $tableName = $this->_driver->quoteIdentifier($table->name());
  408. $constraintName = $this->_driver->quoteIdentifier($name);
  409. $sql[] = sprintf($sqlPattern, $tableName, $constraintName);
  410. }
  411. }
  412. return $sql;
  413. }
  414. /**
  415. * {@inheritDoc}
  416. */
  417. public function indexSql(Table $table, $name)
  418. {
  419. $data = $table->index($name);
  420. $columns = array_map(
  421. [$this->_driver, 'quoteIdentifier'],
  422. $data['columns']
  423. );
  424. return sprintf(
  425. 'CREATE INDEX %s ON %s (%s)',
  426. $this->_driver->quoteIdentifier($name),
  427. $this->_driver->quoteIdentifier($table->name()),
  428. implode(', ', $columns)
  429. );
  430. }
  431. /**
  432. * {@inheritDoc}
  433. */
  434. public function constraintSql(Table $table, $name)
  435. {
  436. $data = $table->constraint($name);
  437. $out = 'CONSTRAINT ' . $this->_driver->quoteIdentifier($name);
  438. if ($data['type'] === Table::CONSTRAINT_PRIMARY) {
  439. $out = 'PRIMARY KEY';
  440. }
  441. if ($data['type'] === Table::CONSTRAINT_UNIQUE) {
  442. $out .= ' UNIQUE';
  443. }
  444. return $this->_keySql($out, $data);
  445. }
  446. /**
  447. * Helper method for generating key SQL snippets.
  448. *
  449. * @param string $prefix The key prefix
  450. * @param array $data Key data.
  451. * @return string
  452. */
  453. protected function _keySql($prefix, $data)
  454. {
  455. $columns = array_map(
  456. [$this->_driver, 'quoteIdentifier'],
  457. $data['columns']
  458. );
  459. if ($data['type'] === Table::CONSTRAINT_FOREIGN) {
  460. return $prefix . sprintf(
  461. ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s DEFERRABLE INITIALLY IMMEDIATE',
  462. implode(', ', $columns),
  463. $this->_driver->quoteIdentifier($data['references'][0]),
  464. $this->_convertConstraintColumns($data['references'][1]),
  465. $this->_foreignOnClause($data['update']),
  466. $this->_foreignOnClause($data['delete'])
  467. );
  468. }
  469. return $prefix . ' (' . implode(', ', $columns) . ')';
  470. }
  471. /**
  472. * {@inheritDoc}
  473. */
  474. public function createTableSql(Table $table, $columns, $constraints, $indexes)
  475. {
  476. $content = array_merge($columns, $constraints);
  477. $content = implode(",\n", array_filter($content));
  478. $tableName = $this->_driver->quoteIdentifier($table->name());
  479. $temporary = $table->temporary() ? ' TEMPORARY ' : ' ';
  480. $out = [];
  481. $out[] = sprintf("CREATE%sTABLE %s (\n%s\n)", $temporary, $tableName, $content);
  482. foreach ($indexes as $index) {
  483. $out[] = $index;
  484. }
  485. foreach ($table->columns() as $column) {
  486. $columnData = $table->column($column);
  487. if (isset($columnData['comment'])) {
  488. $out[] = sprintf(
  489. 'COMMENT ON COLUMN %s.%s IS %s',
  490. $tableName,
  491. $this->_driver->quoteIdentifier($column),
  492. $this->_driver->schemaValue($columnData['comment'])
  493. );
  494. }
  495. }
  496. return $out;
  497. }
  498. /**
  499. * {@inheritDoc}
  500. */
  501. public function truncateTableSql(Table $table)
  502. {
  503. $name = $this->_driver->quoteIdentifier($table->name());
  504. return [
  505. sprintf('TRUNCATE %s RESTART IDENTITY CASCADE', $name)
  506. ];
  507. }
  508. /**
  509. * Generate the SQL to drop a table.
  510. *
  511. * @param \Cake\Database\Schema\Table $table Table instance
  512. * @return array SQL statements to drop a table.
  513. */
  514. public function dropTableSql(Table $table)
  515. {
  516. $sql = sprintf(
  517. 'DROP TABLE %s CASCADE',
  518. $this->_driver->quoteIdentifier($table->name())
  519. );
  520. return [$sql];
  521. }
  522. }