PageRenderTime 185ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php

http://github.com/doctrine/dbal
PHP | 1274 lines | 787 code | 165 blank | 322 comment | 81 complexity | 738a28b43d2ae18e56fae0d62ff67add MD5 | raw file
Possible License(s): Unlicense
  1. <?php
  2. namespace Doctrine\DBAL\Platforms;
  3. use Doctrine\DBAL\Schema\Column;
  4. use Doctrine\DBAL\Schema\ColumnDiff;
  5. use Doctrine\DBAL\Schema\ForeignKeyConstraint;
  6. use Doctrine\DBAL\Schema\Identifier;
  7. use Doctrine\DBAL\Schema\Index;
  8. use Doctrine\DBAL\Schema\Sequence;
  9. use Doctrine\DBAL\Schema\TableDiff;
  10. use Doctrine\DBAL\Types\BigIntType;
  11. use Doctrine\DBAL\Types\BinaryType;
  12. use Doctrine\DBAL\Types\BlobType;
  13. use Doctrine\DBAL\Types\IntegerType;
  14. use Doctrine\DBAL\Types\Type;
  15. use UnexpectedValueException;
  16. use function array_diff;
  17. use function array_merge;
  18. use function array_unique;
  19. use function array_values;
  20. use function count;
  21. use function explode;
  22. use function implode;
  23. use function in_array;
  24. use function is_array;
  25. use function is_bool;
  26. use function is_numeric;
  27. use function is_string;
  28. use function sprintf;
  29. use function strpos;
  30. use function strtolower;
  31. use function trim;
  32. /**
  33. * PostgreSqlPlatform.
  34. *
  35. * @todo Rename: PostgreSQLPlatform
  36. */
  37. class PostgreSqlPlatform extends AbstractPlatform
  38. {
  39. /** @var bool */
  40. private $useBooleanTrueFalseStrings = true;
  41. /** @var string[][] PostgreSQL booleans literals */
  42. private $booleanLiterals = [
  43. 'true' => [
  44. 't',
  45. 'true',
  46. 'y',
  47. 'yes',
  48. 'on',
  49. '1',
  50. ],
  51. 'false' => [
  52. 'f',
  53. 'false',
  54. 'n',
  55. 'no',
  56. 'off',
  57. '0',
  58. ],
  59. ];
  60. /**
  61. * PostgreSQL has different behavior with some drivers
  62. * with regard to how booleans have to be handled.
  63. *
  64. * Enables use of 'true'/'false' or otherwise 1 and 0 instead.
  65. *
  66. * @param bool $flag
  67. *
  68. * @return void
  69. */
  70. public function setUseBooleanTrueFalseStrings($flag)
  71. {
  72. $this->useBooleanTrueFalseStrings = (bool) $flag;
  73. }
  74. /**
  75. * {@inheritDoc}
  76. */
  77. public function getSubstringExpression($value, $from, $length = null)
  78. {
  79. if ($length === null) {
  80. return 'SUBSTRING(' . $value . ' FROM ' . $from . ')';
  81. }
  82. return 'SUBSTRING(' . $value . ' FROM ' . $from . ' FOR ' . $length . ')';
  83. }
  84. /**
  85. * {@inheritDoc}
  86. */
  87. public function getNowExpression()
  88. {
  89. return 'LOCALTIMESTAMP(0)';
  90. }
  91. /**
  92. * {@inheritDoc}
  93. */
  94. public function getRegexpExpression()
  95. {
  96. return 'SIMILAR TO';
  97. }
  98. /**
  99. * {@inheritDoc}
  100. */
  101. public function getLocateExpression($str, $substr, $startPos = false)
  102. {
  103. if ($startPos !== false) {
  104. $str = $this->getSubstringExpression($str, $startPos);
  105. return 'CASE WHEN (POSITION(' . $substr . ' IN ' . $str . ') = 0) THEN 0 ELSE (POSITION(' . $substr . ' IN ' . $str . ') + ' . ($startPos-1) . ') END';
  106. }
  107. return 'POSITION(' . $substr . ' IN ' . $str . ')';
  108. }
  109. /**
  110. * {@inheritdoc}
  111. */
  112. protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
  113. {
  114. if ($unit === DateIntervalUnit::QUARTER) {
  115. $interval *= 3;
  116. $unit = DateIntervalUnit::MONTH;
  117. }
  118. return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)";
  119. }
  120. /**
  121. * {@inheritDoc}
  122. */
  123. public function getDateDiffExpression($date1, $date2)
  124. {
  125. return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))';
  126. }
  127. /**
  128. * {@inheritDoc}
  129. */
  130. public function supportsSequences()
  131. {
  132. return true;
  133. }
  134. /**
  135. * {@inheritDoc}
  136. */
  137. public function supportsSchemas()
  138. {
  139. return true;
  140. }
  141. /**
  142. * {@inheritdoc}
  143. */
  144. public function getDefaultSchemaName()
  145. {
  146. return 'public';
  147. }
  148. /**
  149. * {@inheritDoc}
  150. */
  151. public function supportsIdentityColumns()
  152. {
  153. return true;
  154. }
  155. /**
  156. * {@inheritdoc}
  157. */
  158. public function supportsPartialIndexes()
  159. {
  160. return true;
  161. }
  162. /**
  163. * {@inheritdoc}
  164. */
  165. public function usesSequenceEmulatedIdentityColumns()
  166. {
  167. return true;
  168. }
  169. /**
  170. * {@inheritdoc}
  171. */
  172. public function getIdentitySequenceName($tableName, $columnName)
  173. {
  174. return $tableName . '_' . $columnName . '_seq';
  175. }
  176. /**
  177. * {@inheritDoc}
  178. */
  179. public function supportsCommentOnStatement()
  180. {
  181. return true;
  182. }
  183. /**
  184. * {@inheritDoc}
  185. */
  186. public function prefersSequences()
  187. {
  188. return true;
  189. }
  190. /**
  191. * {@inheritDoc}
  192. */
  193. public function hasNativeGuidType()
  194. {
  195. return true;
  196. }
  197. /**
  198. * {@inheritDoc}
  199. */
  200. public function getListDatabasesSQL()
  201. {
  202. return 'SELECT datname FROM pg_database';
  203. }
  204. /**
  205. * {@inheritDoc}
  206. */
  207. public function getListNamespacesSQL()
  208. {
  209. return "SELECT schema_name AS nspname
  210. FROM information_schema.schemata
  211. WHERE schema_name NOT LIKE 'pg\_%'
  212. AND schema_name != 'information_schema'";
  213. }
  214. /**
  215. * {@inheritDoc}
  216. */
  217. public function getListSequencesSQL($database)
  218. {
  219. return "SELECT sequence_name AS relname,
  220. sequence_schema AS schemaname
  221. FROM information_schema.sequences
  222. WHERE sequence_schema NOT LIKE 'pg\_%'
  223. AND sequence_schema != 'information_schema'";
  224. }
  225. /**
  226. * {@inheritDoc}
  227. */
  228. public function getListTablesSQL()
  229. {
  230. return "SELECT quote_ident(table_name) AS table_name,
  231. table_schema AS schema_name
  232. FROM information_schema.tables
  233. WHERE table_schema NOT LIKE 'pg\_%'
  234. AND table_schema != 'information_schema'
  235. AND table_name != 'geometry_columns'
  236. AND table_name != 'spatial_ref_sys'
  237. AND table_type != 'VIEW'";
  238. }
  239. /**
  240. * {@inheritDoc}
  241. */
  242. public function getListViewsSQL($database)
  243. {
  244. return 'SELECT quote_ident(table_name) AS viewname,
  245. table_schema AS schemaname,
  246. view_definition AS definition
  247. FROM information_schema.views
  248. WHERE view_definition IS NOT NULL';
  249. }
  250. /**
  251. * @param string $table
  252. * @param string|null $database
  253. *
  254. * @return string
  255. */
  256. public function getListTableForeignKeysSQL($table, $database = null)
  257. {
  258. return 'SELECT quote_ident(r.conname) as conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef
  259. FROM pg_catalog.pg_constraint r
  260. WHERE r.conrelid =
  261. (
  262. SELECT c.oid
  263. FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n
  264. WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace
  265. )
  266. AND r.contype = 'f'";
  267. }
  268. /**
  269. * {@inheritDoc}
  270. */
  271. public function getCreateViewSQL($name, $sql)
  272. {
  273. return 'CREATE VIEW ' . $name . ' AS ' . $sql;
  274. }
  275. /**
  276. * {@inheritDoc}
  277. */
  278. public function getDropViewSQL($name)
  279. {
  280. return 'DROP VIEW ' . $name;
  281. }
  282. /**
  283. * {@inheritDoc}
  284. */
  285. public function getListTableConstraintsSQL($table)
  286. {
  287. $table = new Identifier($table);
  288. $table = $this->quoteStringLiteral($table->getName());
  289. return sprintf(
  290. <<<'SQL'
  291. SELECT
  292. quote_ident(relname) as relname
  293. FROM
  294. pg_class
  295. WHERE oid IN (
  296. SELECT indexrelid
  297. FROM pg_index, pg_class
  298. WHERE pg_class.relname = %s
  299. AND pg_class.oid = pg_index.indrelid
  300. AND (indisunique = 't' OR indisprimary = 't')
  301. )
  302. SQL
  303. ,
  304. $table
  305. );
  306. }
  307. /**
  308. * {@inheritDoc}
  309. *
  310. * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
  311. */
  312. public function getListTableIndexesSQL($table, $currentDatabase = null)
  313. {
  314. return 'SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary,
  315. pg_index.indkey, pg_index.indrelid,
  316. pg_get_expr(indpred, indrelid) AS where
  317. FROM pg_class, pg_index
  318. WHERE oid IN (
  319. SELECT indexrelid
  320. FROM pg_index si, pg_class sc, pg_namespace sn
  321. WHERE ' . $this->getTableWhereClause($table, 'sc', 'sn') . ' AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid
  322. ) AND pg_index.indexrelid = oid';
  323. }
  324. /**
  325. * @param string $table
  326. * @param string $classAlias
  327. * @param string $namespaceAlias
  328. *
  329. * @return string
  330. */
  331. private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n')
  332. {
  333. $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND ";
  334. if (strpos($table, '.') !== false) {
  335. [$schema, $table] = explode('.', $table);
  336. $schema = $this->quoteStringLiteral($schema);
  337. } else {
  338. $schema = 'ANY(current_schemas(false))';
  339. }
  340. $table = new Identifier($table);
  341. $table = $this->quoteStringLiteral($table->getName());
  342. return $whereClause . sprintf(
  343. '%s.relname = %s AND %s.nspname = %s',
  344. $classAlias,
  345. $table,
  346. $namespaceAlias,
  347. $schema
  348. );
  349. }
  350. /**
  351. * {@inheritDoc}
  352. */
  353. public function getListTableColumnsSQL($table, $database = null)
  354. {
  355. return "SELECT
  356. a.attnum,
  357. quote_ident(a.attname) AS field,
  358. t.typname AS type,
  359. format_type(a.atttypid, a.atttypmod) AS complete_type,
  360. (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
  361. (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
  362. pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
  363. a.attnotnull AS isnotnull,
  364. (SELECT 't'
  365. FROM pg_index
  366. WHERE c.oid = pg_index.indrelid
  367. AND pg_index.indkey[0] = a.attnum
  368. AND pg_index.indisprimary = 't'
  369. ) AS pri,
  370. (SELECT pg_get_expr(adbin, adrelid)
  371. FROM pg_attrdef
  372. WHERE c.oid = pg_attrdef.adrelid
  373. AND pg_attrdef.adnum=a.attnum
  374. ) AS default,
  375. (SELECT pg_description.description
  376. FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
  377. ) AS comment
  378. FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n
  379. WHERE " . $this->getTableWhereClause($table, 'c', 'n') . '
  380. AND a.attnum > 0
  381. AND a.attrelid = c.oid
  382. AND a.atttypid = t.oid
  383. AND n.oid = c.relnamespace
  384. ORDER BY a.attnum';
  385. }
  386. /**
  387. * {@inheritDoc}
  388. */
  389. public function getCreateDatabaseSQL($name)
  390. {
  391. return 'CREATE DATABASE ' . $name;
  392. }
  393. /**
  394. * Returns the SQL statement for disallowing new connections on the given database.
  395. *
  396. * This is useful to force DROP DATABASE operations which could fail because of active connections.
  397. *
  398. * @param string $database The name of the database to disallow new connections for.
  399. *
  400. * @return string
  401. */
  402. public function getDisallowDatabaseConnectionsSQL($database)
  403. {
  404. return "UPDATE pg_database SET datallowconn = 'false' WHERE datname = " . $this->quoteStringLiteral($database);
  405. }
  406. /**
  407. * Returns the SQL statement for closing currently active connections on the given database.
  408. *
  409. * This is useful to force DROP DATABASE operations which could fail because of active connections.
  410. *
  411. * @param string $database The name of the database to close currently active connections for.
  412. *
  413. * @return string
  414. */
  415. public function getCloseActiveDatabaseConnectionsSQL($database)
  416. {
  417. return 'SELECT pg_terminate_backend(procpid) FROM pg_stat_activity WHERE datname = '
  418. . $this->quoteStringLiteral($database);
  419. }
  420. /**
  421. * {@inheritDoc}
  422. */
  423. public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
  424. {
  425. $query = '';
  426. if ($foreignKey->hasOption('match')) {
  427. $query .= ' MATCH ' . $foreignKey->getOption('match');
  428. }
  429. $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
  430. if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) {
  431. $query .= ' DEFERRABLE';
  432. } else {
  433. $query .= ' NOT DEFERRABLE';
  434. }
  435. if (($foreignKey->hasOption('feferred') && $foreignKey->getOption('feferred') !== false)
  436. || ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false)
  437. ) {
  438. $query .= ' INITIALLY DEFERRED';
  439. } else {
  440. $query .= ' INITIALLY IMMEDIATE';
  441. }
  442. return $query;
  443. }
  444. /**
  445. * {@inheritDoc}
  446. */
  447. public function getAlterTableSQL(TableDiff $diff)
  448. {
  449. $sql = [];
  450. $commentsSQL = [];
  451. $columnSql = [];
  452. foreach ($diff->addedColumns as $column) {
  453. if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
  454. continue;
  455. }
  456. $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
  457. $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
  458. $comment = $this->getColumnComment($column);
  459. if ($comment === null || $comment === '') {
  460. continue;
  461. }
  462. $commentsSQL[] = $this->getCommentOnColumnSQL(
  463. $diff->getName($this)->getQuotedName($this),
  464. $column->getQuotedName($this),
  465. $comment
  466. );
  467. }
  468. foreach ($diff->removedColumns as $column) {
  469. if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
  470. continue;
  471. }
  472. $query = 'DROP ' . $column->getQuotedName($this);
  473. $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
  474. }
  475. foreach ($diff->changedColumns as $columnDiff) {
  476. if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
  477. continue;
  478. }
  479. if ($this->isUnchangedBinaryColumn($columnDiff)) {
  480. continue;
  481. }
  482. $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($this);
  483. $column = $columnDiff->column;
  484. if ($columnDiff->hasChanged('type') || $columnDiff->hasChanged('precision') || $columnDiff->hasChanged('scale') || $columnDiff->hasChanged('fixed')) {
  485. $type = $column->getType();
  486. // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type
  487. $columnDefinition = $column->toArray();
  488. $columnDefinition['autoincrement'] = false;
  489. // here was a server version check before, but DBAL API does not support this anymore.
  490. $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this);
  491. $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
  492. }
  493. if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) {
  494. $defaultClause = $column->getDefault() === null
  495. ? ' DROP DEFAULT'
  496. : ' SET' . $this->getDefaultValueDeclarationSQL($column->toArray());
  497. $query = 'ALTER ' . $oldColumnName . $defaultClause;
  498. $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
  499. }
  500. if ($columnDiff->hasChanged('notnull')) {
  501. $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL';
  502. $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
  503. }
  504. if ($columnDiff->hasChanged('autoincrement')) {
  505. if ($column->getAutoincrement()) {
  506. // add autoincrement
  507. $seqName = $this->getIdentitySequenceName($diff->name, $oldColumnName);
  508. $sql[] = 'CREATE SEQUENCE ' . $seqName;
  509. $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM ' . $diff->getName($this)->getQuotedName($this) . '))';
  510. $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')";
  511. $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
  512. } else {
  513. // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have
  514. $query = 'ALTER ' . $oldColumnName . ' DROP DEFAULT';
  515. $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
  516. }
  517. }
  518. $newComment = $this->getColumnComment($column);
  519. $oldComment = $this->getOldColumnComment($columnDiff);
  520. if ($columnDiff->hasChanged('comment') || ($columnDiff->fromColumn !== null && $oldComment !== $newComment)) {
  521. $commentsSQL[] = $this->getCommentOnColumnSQL(
  522. $diff->getName($this)->getQuotedName($this),
  523. $column->getQuotedName($this),
  524. $newComment
  525. );
  526. }
  527. if (! $columnDiff->hasChanged('length')) {
  528. continue;
  529. }
  530. $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $column->getType()->getSQLDeclaration($column->toArray(), $this);
  531. $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
  532. }
  533. foreach ($diff->renamedColumns as $oldColumnName => $column) {
  534. if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
  535. continue;
  536. }
  537. $oldColumnName = new Identifier($oldColumnName);
  538. $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) .
  539. ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this);
  540. }
  541. $tableSql = [];
  542. if (! $this->onSchemaAlterTable($diff, $tableSql)) {
  543. $sql = array_merge($sql, $commentsSQL);
  544. $newName = $diff->getNewName();
  545. if ($newName !== false) {
  546. $sql[] = sprintf(
  547. 'ALTER TABLE %s RENAME TO %s',
  548. $diff->getName($this)->getQuotedName($this),
  549. $newName->getQuotedName($this)
  550. );
  551. }
  552. $sql = array_merge(
  553. $this->getPreAlterTableIndexForeignKeySQL($diff),
  554. $sql,
  555. $this->getPostAlterTableIndexForeignKeySQL($diff)
  556. );
  557. }
  558. return array_merge($sql, $tableSql, $columnSql);
  559. }
  560. /**
  561. * Checks whether a given column diff is a logically unchanged binary type column.
  562. *
  563. * Used to determine whether a column alteration for a binary type column can be skipped.
  564. * Doctrine's {@link \Doctrine\DBAL\Types\BinaryType} and {@link \Doctrine\DBAL\Types\BlobType}
  565. * are mapped to the same database column type on this platform as this platform
  566. * does not have a native VARBINARY/BINARY column type. Therefore the {@link \Doctrine\DBAL\Schema\Comparator}
  567. * might detect differences for binary type columns which do not have to be propagated
  568. * to database as there actually is no difference at database level.
  569. *
  570. * @param ColumnDiff $columnDiff The column diff to check against.
  571. *
  572. * @return bool True if the given column diff is an unchanged binary type column, false otherwise.
  573. */
  574. private function isUnchangedBinaryColumn(ColumnDiff $columnDiff)
  575. {
  576. $columnType = $columnDiff->column->getType();
  577. if (! $columnType instanceof BinaryType && ! $columnType instanceof BlobType) {
  578. return false;
  579. }
  580. $fromColumn = $columnDiff->fromColumn instanceof Column ? $columnDiff->fromColumn : null;
  581. if ($fromColumn) {
  582. $fromColumnType = $fromColumn->getType();
  583. if (! $fromColumnType instanceof BinaryType && ! $fromColumnType instanceof BlobType) {
  584. return false;
  585. }
  586. return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0;
  587. }
  588. if ($columnDiff->hasChanged('type')) {
  589. return false;
  590. }
  591. return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0;
  592. }
  593. /**
  594. * {@inheritdoc}
  595. */
  596. protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
  597. {
  598. if (strpos($tableName, '.') !== false) {
  599. [$schema] = explode('.', $tableName);
  600. $oldIndexName = $schema . '.' . $oldIndexName;
  601. }
  602. return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)];
  603. }
  604. /**
  605. * {@inheritdoc}
  606. */
  607. public function getCommentOnColumnSQL($tableName, $columnName, $comment)
  608. {
  609. $tableName = new Identifier($tableName);
  610. $columnName = new Identifier($columnName);
  611. $comment = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment);
  612. return sprintf(
  613. 'COMMENT ON COLUMN %s.%s IS %s',
  614. $tableName->getQuotedName($this),
  615. $columnName->getQuotedName($this),
  616. $comment
  617. );
  618. }
  619. /**
  620. * {@inheritDoc}
  621. */
  622. public function getCreateSequenceSQL(Sequence $sequence)
  623. {
  624. return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
  625. ' INCREMENT BY ' . $sequence->getAllocationSize() .
  626. ' MINVALUE ' . $sequence->getInitialValue() .
  627. ' START ' . $sequence->getInitialValue() .
  628. $this->getSequenceCacheSQL($sequence);
  629. }
  630. /**
  631. * {@inheritDoc}
  632. */
  633. public function getAlterSequenceSQL(Sequence $sequence)
  634. {
  635. return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
  636. ' INCREMENT BY ' . $sequence->getAllocationSize() .
  637. $this->getSequenceCacheSQL($sequence);
  638. }
  639. /**
  640. * Cache definition for sequences
  641. *
  642. * @return string
  643. */
  644. private function getSequenceCacheSQL(Sequence $sequence)
  645. {
  646. if ($sequence->getCache() > 1) {
  647. return ' CACHE ' . $sequence->getCache();
  648. }
  649. return '';
  650. }
  651. /**
  652. * {@inheritDoc}
  653. */
  654. public function getDropSequenceSQL($sequence)
  655. {
  656. if ($sequence instanceof Sequence) {
  657. $sequence = $sequence->getQuotedName($this);
  658. }
  659. return 'DROP SEQUENCE ' . $sequence . ' CASCADE';
  660. }
  661. /**
  662. * {@inheritDoc}
  663. */
  664. public function getCreateSchemaSQL($schemaName)
  665. {
  666. return 'CREATE SCHEMA ' . $schemaName;
  667. }
  668. /**
  669. * {@inheritDoc}
  670. */
  671. public function getDropForeignKeySQL($foreignKey, $table)
  672. {
  673. return $this->getDropConstraintSQL($foreignKey, $table);
  674. }
  675. /**
  676. * {@inheritDoc}
  677. */
  678. protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
  679. {
  680. $queryFields = $this->getColumnDeclarationListSQL($columns);
  681. if (isset($options['primary']) && ! empty($options['primary'])) {
  682. $keyColumns = array_unique(array_values($options['primary']));
  683. $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
  684. }
  685. $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')';
  686. $sql = [$query];
  687. if (isset($options['indexes']) && ! empty($options['indexes'])) {
  688. foreach ($options['indexes'] as $index) {
  689. $sql[] = $this->getCreateIndexSQL($index, $tableName);
  690. }
  691. }
  692. if (isset($options['foreignKeys'])) {
  693. foreach ((array) $options['foreignKeys'] as $definition) {
  694. $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
  695. }
  696. }
  697. return $sql;
  698. }
  699. /**
  700. * Converts a single boolean value.
  701. *
  702. * First converts the value to its native PHP boolean type
  703. * and passes it to the given callback function to be reconverted
  704. * into any custom representation.
  705. *
  706. * @param mixed $value The value to convert.
  707. * @param callable $callback The callback function to use for converting the real boolean value.
  708. *
  709. * @return mixed
  710. *
  711. * @throws UnexpectedValueException
  712. */
  713. private function convertSingleBooleanValue($value, $callback)
  714. {
  715. if ($value === null) {
  716. return $callback(null);
  717. }
  718. if (is_bool($value) || is_numeric($value)) {
  719. return $callback((bool) $value);
  720. }
  721. if (! is_string($value)) {
  722. return $callback(true);
  723. }
  724. /**
  725. * Better safe than sorry: http://php.net/in_array#106319
  726. */
  727. if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) {
  728. return $callback(false);
  729. }
  730. if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) {
  731. return $callback(true);
  732. }
  733. throw new UnexpectedValueException("Unrecognized boolean literal '${value}'");
  734. }
  735. /**
  736. * Converts one or multiple boolean values.
  737. *
  738. * First converts the value(s) to their native PHP boolean type
  739. * and passes them to the given callback function to be reconverted
  740. * into any custom representation.
  741. *
  742. * @param mixed $item The value(s) to convert.
  743. * @param callable $callback The callback function to use for converting the real boolean value(s).
  744. *
  745. * @return mixed
  746. */
  747. private function doConvertBooleans($item, $callback)
  748. {
  749. if (is_array($item)) {
  750. foreach ($item as $key => $value) {
  751. $item[$key] = $this->convertSingleBooleanValue($value, $callback);
  752. }
  753. return $item;
  754. }
  755. return $this->convertSingleBooleanValue($item, $callback);
  756. }
  757. /**
  758. * {@inheritDoc}
  759. *
  760. * Postgres wants boolean values converted to the strings 'true'/'false'.
  761. */
  762. public function convertBooleans($item)
  763. {
  764. if (! $this->useBooleanTrueFalseStrings) {
  765. return parent::convertBooleans($item);
  766. }
  767. return $this->doConvertBooleans(
  768. $item,
  769. static function ($boolean) {
  770. if ($boolean === null) {
  771. return 'NULL';
  772. }
  773. return $boolean === true ? 'true' : 'false';
  774. }
  775. );
  776. }
  777. /**
  778. * {@inheritDoc}
  779. */
  780. public function convertBooleansToDatabaseValue($item)
  781. {
  782. if (! $this->useBooleanTrueFalseStrings) {
  783. return parent::convertBooleansToDatabaseValue($item);
  784. }
  785. return $this->doConvertBooleans(
  786. $item,
  787. static function ($boolean) {
  788. return $boolean === null ? null : (int) $boolean;
  789. }
  790. );
  791. }
  792. /**
  793. * {@inheritDoc}
  794. */
  795. public function convertFromBoolean($item)
  796. {
  797. if (in_array(strtolower($item), $this->booleanLiterals['false'], true)) {
  798. return false;
  799. }
  800. return parent::convertFromBoolean($item);
  801. }
  802. /**
  803. * {@inheritDoc}
  804. */
  805. public function getSequenceNextValSQL($sequenceName)
  806. {
  807. return "SELECT NEXTVAL('" . $sequenceName . "')";
  808. }
  809. /**
  810. * {@inheritDoc}
  811. */
  812. public function getSetTransactionIsolationSQL($level)
  813. {
  814. return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
  815. . $this->_getTransactionIsolationLevelSQL($level);
  816. }
  817. /**
  818. * {@inheritDoc}
  819. */
  820. public function getBooleanTypeDeclarationSQL(array $field)
  821. {
  822. return 'BOOLEAN';
  823. }
  824. /**
  825. * {@inheritDoc}
  826. */
  827. public function getIntegerTypeDeclarationSQL(array $field)
  828. {
  829. if (! empty($field['autoincrement'])) {
  830. return 'SERIAL';
  831. }
  832. return 'INT';
  833. }
  834. /**
  835. * {@inheritDoc}
  836. */
  837. public function getBigIntTypeDeclarationSQL(array $field)
  838. {
  839. if (! empty($field['autoincrement'])) {
  840. return 'BIGSERIAL';
  841. }
  842. return 'BIGINT';
  843. }
  844. /**
  845. * {@inheritDoc}
  846. */
  847. public function getSmallIntTypeDeclarationSQL(array $field)
  848. {
  849. return 'SMALLINT';
  850. }
  851. /**
  852. * {@inheritDoc}
  853. */
  854. public function getGuidTypeDeclarationSQL(array $field)
  855. {
  856. return 'UUID';
  857. }
  858. /**
  859. * {@inheritDoc}
  860. */
  861. public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
  862. {
  863. return 'TIMESTAMP(0) WITHOUT TIME ZONE';
  864. }
  865. /**
  866. * {@inheritDoc}
  867. */
  868. public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
  869. {
  870. return 'TIMESTAMP(0) WITH TIME ZONE';
  871. }
  872. /**
  873. * {@inheritDoc}
  874. */
  875. public function getDateTypeDeclarationSQL(array $fieldDeclaration)
  876. {
  877. return 'DATE';
  878. }
  879. /**
  880. * {@inheritDoc}
  881. */
  882. public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
  883. {
  884. return 'TIME(0) WITHOUT TIME ZONE';
  885. }
  886. /**
  887. * {@inheritDoc}
  888. *
  889. * @deprecated Use application-generated UUIDs instead
  890. */
  891. public function getGuidExpression()
  892. {
  893. return 'UUID_GENERATE_V4()';
  894. }
  895. /**
  896. * {@inheritDoc}
  897. */
  898. protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
  899. {
  900. return '';
  901. }
  902. /**
  903. * {@inheritDoc}
  904. */
  905. protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
  906. {
  907. return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
  908. : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
  909. }
  910. /**
  911. * {@inheritdoc}
  912. */
  913. protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
  914. {
  915. return 'BYTEA';
  916. }
  917. /**
  918. * {@inheritDoc}
  919. */
  920. public function getClobTypeDeclarationSQL(array $field)
  921. {
  922. return 'TEXT';
  923. }
  924. /**
  925. * {@inheritDoc}
  926. */
  927. public function getName()
  928. {
  929. return 'postgresql';
  930. }
  931. /**
  932. * {@inheritDoc}
  933. *
  934. * PostgreSQL returns all column names in SQL result sets in lowercase.
  935. */
  936. public function getSQLResultCasing($column)
  937. {
  938. return strtolower($column);
  939. }
  940. /**
  941. * {@inheritDoc}
  942. */
  943. public function getDateTimeTzFormatString()
  944. {
  945. return 'Y-m-d H:i:sO';
  946. }
  947. /**
  948. * {@inheritDoc}
  949. */
  950. public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
  951. {
  952. return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)';
  953. }
  954. /**
  955. * {@inheritDoc}
  956. */
  957. public function getTruncateTableSQL($tableName, $cascade = false)
  958. {
  959. $tableIdentifier = new Identifier($tableName);
  960. $sql = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this);
  961. if ($cascade) {
  962. $sql .= ' CASCADE';
  963. }
  964. return $sql;
  965. }
  966. /**
  967. * {@inheritDoc}
  968. */
  969. public function getReadLockSQL()
  970. {
  971. return 'FOR SHARE';
  972. }
  973. /**
  974. * {@inheritDoc}
  975. */
  976. protected function initializeDoctrineTypeMappings()
  977. {
  978. $this->doctrineTypeMapping = [
  979. 'smallint' => 'smallint',
  980. 'int2' => 'smallint',
  981. 'serial' => 'integer',
  982. 'serial4' => 'integer',
  983. 'int' => 'integer',
  984. 'int4' => 'integer',
  985. 'integer' => 'integer',
  986. 'bigserial' => 'bigint',
  987. 'serial8' => 'bigint',
  988. 'bigint' => 'bigint',
  989. 'int8' => 'bigint',
  990. 'bool' => 'boolean',
  991. 'boolean' => 'boolean',
  992. 'text' => 'text',
  993. 'tsvector' => 'text',
  994. 'varchar' => 'string',
  995. 'interval' => 'string',
  996. '_varchar' => 'string',
  997. 'char' => 'string',
  998. 'bpchar' => 'string',
  999. 'inet' => 'string',
  1000. 'date' => 'date',
  1001. 'datetime' => 'datetime',
  1002. 'timestamp' => 'datetime',
  1003. 'timestamptz' => 'datetimetz',
  1004. 'time' => 'time',
  1005. 'timetz' => 'time',
  1006. 'float' => 'float',
  1007. 'float4' => 'float',
  1008. 'float8' => 'float',
  1009. 'double' => 'float',
  1010. 'double precision' => 'float',
  1011. 'real' => 'float',
  1012. 'decimal' => 'decimal',
  1013. 'money' => 'decimal',
  1014. 'numeric' => 'decimal',
  1015. 'year' => 'date',
  1016. 'uuid' => 'guid',
  1017. 'bytea' => 'blob',
  1018. ];
  1019. }
  1020. /**
  1021. * {@inheritDoc}
  1022. */
  1023. public function getVarcharMaxLength()
  1024. {
  1025. return 65535;
  1026. }
  1027. /**
  1028. * {@inheritdoc}
  1029. */
  1030. public function getBinaryMaxLength()
  1031. {
  1032. return 0;
  1033. }
  1034. /**
  1035. * {@inheritdoc}
  1036. */
  1037. public function getBinaryDefaultLength()
  1038. {
  1039. return 0;
  1040. }
  1041. /**
  1042. * {@inheritDoc}
  1043. */
  1044. protected function getReservedKeywordsClass()
  1045. {
  1046. return Keywords\PostgreSQLKeywords::class;
  1047. }
  1048. /**
  1049. * {@inheritDoc}
  1050. */
  1051. public function getBlobTypeDeclarationSQL(array $field)
  1052. {
  1053. return 'BYTEA';
  1054. }
  1055. /**
  1056. * {@inheritdoc}
  1057. */
  1058. public function getDefaultValueDeclarationSQL($field)
  1059. {
  1060. if ($this->isSerialField($field)) {
  1061. return '';
  1062. }
  1063. return parent::getDefaultValueDeclarationSQL($field);
  1064. }
  1065. /**
  1066. * @param mixed[] $field
  1067. */
  1068. private function isSerialField(array $field) : bool
  1069. {
  1070. return isset($field['type'], $field['autoincrement'])
  1071. && $field['autoincrement'] === true
  1072. && $this->isNumericType($field['type']);
  1073. }
  1074. /**
  1075. * Check whether the type of a column is changed in a way that invalidates the default value for the column
  1076. */
  1077. private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff) : bool
  1078. {
  1079. if (! $columnDiff->fromColumn) {
  1080. return $columnDiff->hasChanged('type');
  1081. }
  1082. $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType());
  1083. $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType());
  1084. // default should not be changed when switching between numeric types and the default comes from a sequence
  1085. return $columnDiff->hasChanged('type')
  1086. && ! ($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement());
  1087. }
  1088. private function isNumericType(Type $type) : bool
  1089. {
  1090. return $type instanceof IntegerType || $type instanceof BigIntType;
  1091. }
  1092. private function getOldColumnComment(ColumnDiff $columnDiff) : ?string
  1093. {
  1094. return $columnDiff->fromColumn ? $this->getColumnComment($columnDiff->fromColumn) : null;
  1095. }
  1096. public function getListTableMetadataSQL(string $table, ?string $schema = null) : string
  1097. {
  1098. if ($schema !== null) {
  1099. $table = $schema . '.' . $table;
  1100. }
  1101. return sprintf(
  1102. <<<'SQL'
  1103. SELECT obj_description(%s::regclass) AS table_comment;
  1104. SQL
  1105. ,
  1106. $this->quoteStringLiteral($table)
  1107. );
  1108. }
  1109. }