vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php line 874

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