PostgresSchema.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  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. if (strpos($col, 'char') !== false) {
  99. return ['type' => 'string', 'length' => $length];
  100. }
  101. if (strpos($col, 'text') !== false) {
  102. return ['type' => 'text', 'length' => null];
  103. }
  104. if ($col === 'bytea') {
  105. return ['type' => 'binary', 'length' => null];
  106. }
  107. if ($col === 'real' || strpos($col, 'double') !== false) {
  108. return ['type' => 'float', 'length' => null];
  109. }
  110. if (strpos($col, 'numeric') !== false ||
  111. strpos($col, 'money') !== false ||
  112. strpos($col, 'decimal') !== false
  113. ) {
  114. return ['type' => 'decimal', 'length' => null];
  115. }
  116. return ['type' => 'text', 'length' => null];
  117. }
  118. /**
  119. * {@inheritDoc}
  120. */
  121. public function convertColumnDescription(Table $table, $row)
  122. {
  123. $field = $this->_convertColumn($row['type']);
  124. if ($field['type'] === 'boolean') {
  125. if ($row['default'] === 'true') {
  126. $row['default'] = 1;
  127. }
  128. if ($row['default'] === 'false') {
  129. $row['default'] = 0;
  130. }
  131. }
  132. // Sniff out serial types.
  133. if (in_array($field['type'], ['integer', 'biginteger']) && strpos($row['default'], 'nextval(') === 0) {
  134. $field['autoIncrement'] = true;
  135. }
  136. $field += [
  137. 'default' => $this->_defaultValue($row['default']),
  138. 'null' => $row['null'] === 'YES' ? true : false,
  139. 'comment' => $row['comment']
  140. ];
  141. $field['length'] = $row['char_length'] ?: $field['length'];
  142. $table->addColumn($row['name'], $field);
  143. }
  144. /**
  145. * Manipulate the default value.
  146. *
  147. * Postgres includes sequence data and casting information in default values.
  148. * We need to remove those.
  149. *
  150. * @param string|null $default The default value.
  151. * @return string|null
  152. */
  153. protected function _defaultValue($default)
  154. {
  155. if (is_numeric($default) || $default === null) {
  156. return $default;
  157. }
  158. // Sequences
  159. if (strpos($default, 'nextval') === 0) {
  160. return null;
  161. }
  162. // Remove quotes and postgres casts
  163. return preg_replace(
  164. "/^'(.*)'(?:::.*)$/",
  165. "$1",
  166. $default
  167. );
  168. }
  169. /**
  170. * {@inheritDoc}
  171. */
  172. public function describeIndexSql($tableName, $config)
  173. {
  174. $sql = 'SELECT
  175. c2.relname,
  176. i.indisprimary,
  177. i.indisunique,
  178. i.indisvalid,
  179. pg_catalog.pg_get_indexdef(i.indexrelid, 0, true) AS statement
  180. FROM pg_catalog.pg_class AS c,
  181. pg_catalog.pg_class AS c2,
  182. pg_catalog.pg_index AS i
  183. WHERE c.oid = (
  184. SELECT c.oid
  185. FROM pg_catalog.pg_class c
  186. LEFT JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
  187. WHERE c.relname = ?
  188. AND n.nspname = ?
  189. )
  190. AND c.oid = i.indrelid
  191. AND i.indexrelid = c2.oid
  192. ORDER BY i.indisprimary DESC, i.indisunique DESC, c2.relname';
  193. $schema = 'public';
  194. if (!empty($config['schema'])) {
  195. $schema = $config['schema'];
  196. }
  197. return [$sql, [$tableName, $schema]];
  198. }
  199. /**
  200. * {@inheritDoc}
  201. */
  202. public function convertIndexDescription(Table $table, $row)
  203. {
  204. $type = Table::INDEX_INDEX;
  205. $name = $row['relname'];
  206. if ($row['indisprimary']) {
  207. $name = $type = Table::CONSTRAINT_PRIMARY;
  208. }
  209. if ($row['indisunique'] && $type === Table::INDEX_INDEX) {
  210. $type = Table::CONSTRAINT_UNIQUE;
  211. }
  212. preg_match('/\(([^\)]+)\)/', $row['statement'], $matches);
  213. $columns = $this->_convertColumnList($matches[1]);
  214. if ($type === Table::CONSTRAINT_PRIMARY || $type === Table::CONSTRAINT_UNIQUE) {
  215. $table->addConstraint($name, [
  216. 'type' => $type,
  217. 'columns' => $columns
  218. ]);
  219. // If there is only one column in the primary key and it is integery,
  220. // make it autoincrement.
  221. $columnDef = $table->column($columns[0]);
  222. if (
  223. $type === Table::CONSTRAINT_PRIMARY &&
  224. count($columns) === 1 &&
  225. in_array($columnDef['type'], ['integer', 'biginteger'])
  226. ) {
  227. $columnDef['autoIncrement'] = true;
  228. $table->addColumn($columns[0], $columnDef);
  229. }
  230. return;
  231. }
  232. $table->addIndex($name, [
  233. 'type' => $type,
  234. 'columns' => $columns
  235. ]);
  236. }
  237. /**
  238. * Convert a column list into a clean array.
  239. *
  240. * @param string $columns comma separated column list.
  241. * @return array
  242. */
  243. protected function _convertColumnList($columns)
  244. {
  245. $columns = explode(', ', $columns);
  246. foreach ($columns as &$column) {
  247. $column = trim($column, '"');
  248. }
  249. return $columns;
  250. }
  251. /**
  252. * {@inheritDoc}
  253. */
  254. public function describeForeignKeySql($tableName, $config)
  255. {
  256. $sql = "SELECT
  257. r.conname AS name,
  258. r.confupdtype AS update_type,
  259. r.confdeltype AS delete_type,
  260. pg_catalog.pg_get_constraintdef(r.oid, true) AS definition
  261. FROM pg_catalog.pg_constraint AS r
  262. WHERE r.conrelid = (
  263. SELECT c.oid
  264. FROM pg_catalog.pg_class AS c,
  265. pg_catalog.pg_namespace AS n
  266. WHERE c.relname = ?
  267. AND n.nspname = ?
  268. AND n.oid = c.relnamespace
  269. )
  270. AND r.contype = 'f'";
  271. $schema = empty($config['schema']) ? 'public' : $config['schema'];
  272. return [$sql, [$tableName, $schema]];
  273. }
  274. /**
  275. * {@inheritDoc}
  276. */
  277. public function convertForeignKeyDescription(Table $table, $row)
  278. {
  279. preg_match('/REFERENCES ([^\)]+)\(([^\)]+)\)/', $row['definition'], $matches);
  280. $tableName = $matches[1];
  281. $column = $matches[2];
  282. preg_match('/FOREIGN KEY \(([^\)]+)\) REFERENCES/', $row['definition'], $matches);
  283. $columns = $this->_convertColumnList($matches[1]);
  284. $data = [
  285. 'type' => Table::CONSTRAINT_FOREIGN,
  286. 'columns' => $columns,
  287. 'references' => [$tableName, $column],
  288. 'update' => $this->_convertOnClause($row['update_type']),
  289. 'delete' => $this->_convertOnClause($row['delete_type']),
  290. ];
  291. $name = $row['name'];
  292. $table->addConstraint($name, $data);
  293. }
  294. /**
  295. * {@inheritDoc}
  296. */
  297. protected function _convertOnClause($clause)
  298. {
  299. if ($clause === 'r') {
  300. return Table::ACTION_RESTRICT;
  301. }
  302. if ($clause === 'a') {
  303. return Table::ACTION_NO_ACTION;
  304. }
  305. if ($clause === 'c') {
  306. return Table::ACTION_CASCADE;
  307. }
  308. return Table::ACTION_SET_NULL;
  309. }
  310. /**
  311. * {@inheritDoc}
  312. */
  313. public function columnSql(Table $table, $name)
  314. {
  315. $data = $table->column($name);
  316. $out = $this->_driver->quoteIdentifier($name);
  317. $typeMap = [
  318. 'boolean' => ' BOOLEAN',
  319. 'binary' => ' BYTEA',
  320. 'float' => ' FLOAT',
  321. 'decimal' => ' DECIMAL',
  322. 'text' => ' TEXT',
  323. 'date' => ' DATE',
  324. 'time' => ' TIME',
  325. 'datetime' => ' TIMESTAMP',
  326. 'timestamp' => ' TIMESTAMP',
  327. 'uuid' => ' UUID',
  328. ];
  329. if (isset($typeMap[$data['type']])) {
  330. $out .= $typeMap[$data['type']];
  331. }
  332. if ($data['type'] === 'integer' || $data['type'] === 'biginteger') {
  333. $type = $data['type'] === 'integer' ? ' INTEGER' : ' BIGINT';
  334. if ([$name] === $table->primaryKey() || $data['autoIncrement'] === true) {
  335. $type = $data['type'] === 'integer' ? ' SERIAL' : ' BIGSERIAL';
  336. unset($data['null'], $data['default']);
  337. }
  338. $out .= $type;
  339. }
  340. if ($data['type'] === 'string') {
  341. $isFixed = !empty($data['fixed']);
  342. $type = ' VARCHAR';
  343. if ($isFixed) {
  344. $type = ' CHAR';
  345. }
  346. $out .= $type;
  347. if (isset($data['length']) && $data['length'] != 36) {
  348. $out .= '(' . (int)$data['length'] . ')';
  349. }
  350. }
  351. if ($data['type'] === 'float' && isset($data['precision'])) {
  352. $out .= '(' . (int)$data['precision'] . ')';
  353. }
  354. if ($data['type'] === 'decimal' &&
  355. (isset($data['length']) || isset($data['precision']))
  356. ) {
  357. $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')';
  358. }
  359. if (isset($data['null']) && $data['null'] === false) {
  360. $out .= ' NOT NULL';
  361. }
  362. if (isset($data['null']) && $data['null'] === true) {
  363. $out .= ' DEFAULT NULL';
  364. unset($data['default']);
  365. }
  366. if (isset($data['default']) && $data['type'] !== 'timestamp') {
  367. $defaultValue = $data['default'];
  368. if ($data['type'] === 'boolean') {
  369. $defaultValue = (bool)$defaultValue;
  370. }
  371. $out .= ' DEFAULT ' . $this->_driver->schemaValue($defaultValue);
  372. }
  373. return $out;
  374. }
  375. /**
  376. * {@inheritDoc}
  377. */
  378. public function indexSql(Table $table, $name)
  379. {
  380. $data = $table->index($name);
  381. $columns = array_map(
  382. [$this->_driver, 'quoteIdentifier'],
  383. $data['columns']
  384. );
  385. return sprintf(
  386. 'CREATE INDEX %s ON %s (%s)',
  387. $this->_driver->quoteIdentifier($name),
  388. $this->_driver->quoteIdentifier($table->name()),
  389. implode(', ', $columns)
  390. );
  391. }
  392. /**
  393. * {@inheritDoc}
  394. */
  395. public function constraintSql(Table $table, $name)
  396. {
  397. $data = $table->constraint($name);
  398. $out = 'CONSTRAINT ' . $this->_driver->quoteIdentifier($name);
  399. if ($data['type'] === Table::CONSTRAINT_PRIMARY) {
  400. $out = 'PRIMARY KEY';
  401. }
  402. if ($data['type'] === Table::CONSTRAINT_UNIQUE) {
  403. $out .= ' UNIQUE';
  404. }
  405. return $this->_keySql($out, $data);
  406. }
  407. /**
  408. * Helper method for generating key SQL snippets.
  409. *
  410. * @param string $prefix The key prefix
  411. * @param array $data Key data.
  412. * @return string
  413. */
  414. protected function _keySql($prefix, $data)
  415. {
  416. $columns = array_map(
  417. [$this->_driver, 'quoteIdentifier'],
  418. $data['columns']
  419. );
  420. if ($data['type'] === Table::CONSTRAINT_FOREIGN) {
  421. return $prefix . sprintf(
  422. ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s DEFERRABLE INITIALLY IMMEDIATE',
  423. implode(', ', $columns),
  424. $this->_driver->quoteIdentifier($data['references'][0]),
  425. $this->_driver->quoteIdentifier($data['references'][1]),
  426. $this->_foreignOnClause($data['update']),
  427. $this->_foreignOnClause($data['delete'])
  428. );
  429. }
  430. return $prefix . ' (' . implode(', ', $columns) . ')';
  431. }
  432. /**
  433. * {@inheritDoc}
  434. */
  435. public function createTableSql(Table $table, $columns, $constraints, $indexes)
  436. {
  437. $content = array_merge($columns, $constraints);
  438. $content = implode(",\n", array_filter($content));
  439. $tableName = $this->_driver->quoteIdentifier($table->name());
  440. $temporary = $table->temporary() ? ' TEMPORARY ' : ' ';
  441. $out = [];
  442. $out[] = sprintf("CREATE%sTABLE %s (\n%s\n)", $temporary, $tableName, $content);
  443. foreach ($indexes as $index) {
  444. $out[] = $index;
  445. }
  446. foreach ($table->columns() as $column) {
  447. $columnData = $table->column($column);
  448. if (isset($columnData['comment'])) {
  449. $out[] = sprintf(
  450. 'COMMENT ON COLUMN %s.%s IS %s',
  451. $tableName,
  452. $this->_driver->quoteIdentifier($column),
  453. $this->_driver->schemaValue($columnData['comment'])
  454. );
  455. }
  456. }
  457. return $out;
  458. }
  459. /**
  460. * {@inheritDoc}
  461. */
  462. public function truncateTableSql(Table $table)
  463. {
  464. $name = $this->_driver->quoteIdentifier($table->name());
  465. return [
  466. sprintf('TRUNCATE %s RESTART IDENTITY CASCADE', $name)
  467. ];
  468. }
  469. /**
  470. * Generate the SQL to drop a table.
  471. *
  472. * @param \Cake\Database\Schema\Table $table Table instance
  473. * @return array SQL statements to drop a table.
  474. */
  475. public function dropTableSql(Table $table)
  476. {
  477. $sql = sprintf(
  478. 'DROP TABLE %s CASCADE',
  479. $this->_driver->quoteIdentifier($table->name())
  480. );
  481. return [$sql];
  482. }
  483. }