vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php line 48

Open in your IDE?
  1. <?php
  2. namespace Doctrine\DBAL\Schema;
  3. use Doctrine\DBAL\Exception;
  4. use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
  5. use Doctrine\DBAL\Result;
  6. use Doctrine\DBAL\Types\JsonType;
  7. use Doctrine\DBAL\Types\Type;
  8. use Doctrine\DBAL\Types\Types;
  9. use Doctrine\Deprecations\Deprecation;
  10. use function array_change_key_case;
  11. use function array_filter;
  12. use function array_map;
  13. use function array_merge;
  14. use function array_shift;
  15. use function assert;
  16. use function explode;
  17. use function get_class;
  18. use function implode;
  19. use function in_array;
  20. use function preg_match;
  21. use function preg_replace;
  22. use function sprintf;
  23. use function str_replace;
  24. use function strpos;
  25. use function strtolower;
  26. use function trim;
  27. use const CASE_LOWER;
  28. /**
  29.  * PostgreSQL Schema Manager.
  30.  *
  31.  * @extends AbstractSchemaManager<PostgreSQLPlatform>
  32.  */
  33. class PostgreSQLSchemaManager extends AbstractSchemaManager
  34. {
  35.     /** @var string[]|null */
  36.     private ?array $existingSchemaPaths null;
  37.     /**
  38.      * {@inheritDoc}
  39.      */
  40.     public function listTableNames()
  41.     {
  42.         return $this->doListTableNames();
  43.     }
  44.     /**
  45.      * {@inheritDoc}
  46.      */
  47.     public function listTables()
  48.     {
  49.         return $this->doListTables();
  50.     }
  51.     /**
  52.      * {@inheritDoc}
  53.      *
  54.      * @deprecated Use {@see introspectTable()} instead.
  55.      */
  56.     public function listTableDetails($name)
  57.     {
  58.         Deprecation::triggerIfCalledFromOutside(
  59.             'doctrine/dbal',
  60.             'https://github.com/doctrine/dbal/pull/5595',
  61.             '%s is deprecated. Use introspectTable() instead.',
  62.             __METHOD__,
  63.         );
  64.         return $this->doListTableDetails($name);
  65.     }
  66.     /**
  67.      * {@inheritDoc}
  68.      */
  69.     public function listTableColumns($table$database null)
  70.     {
  71.         return $this->doListTableColumns($table$database);
  72.     }
  73.     /**
  74.      * {@inheritDoc}
  75.      */
  76.     public function listTableIndexes($table)
  77.     {
  78.         return $this->doListTableIndexes($table);
  79.     }
  80.     /**
  81.      * {@inheritDoc}
  82.      */
  83.     public function listTableForeignKeys($table$database null)
  84.     {
  85.         return $this->doListTableForeignKeys($table$database);
  86.     }
  87.     /**
  88.      * Gets all the existing schema names.
  89.      *
  90.      * @deprecated Use {@see listSchemaNames()} instead.
  91.      *
  92.      * @return string[]
  93.      *
  94.      * @throws Exception
  95.      */
  96.     public function getSchemaNames()
  97.     {
  98.         Deprecation::trigger(
  99.             'doctrine/dbal',
  100.             'https://github.com/doctrine/dbal/issues/4503',
  101.             'PostgreSQLSchemaManager::getSchemaNames() is deprecated,'
  102.                 ' use PostgreSQLSchemaManager::listSchemaNames() instead.',
  103.         );
  104.         return $this->listNamespaceNames();
  105.     }
  106.     /**
  107.      * {@inheritDoc}
  108.      */
  109.     public function listSchemaNames(): array
  110.     {
  111.         return $this->_conn->fetchFirstColumn(
  112.             <<<'SQL'
  113. SELECT schema_name
  114. FROM   information_schema.schemata
  115. WHERE  schema_name NOT LIKE 'pg\_%'
  116. AND    schema_name != 'information_schema'
  117. SQL,
  118.         );
  119.     }
  120.     /**
  121.      * {@inheritDoc}
  122.      *
  123.      * @deprecated
  124.      */
  125.     public function getSchemaSearchPaths()
  126.     {
  127.         Deprecation::triggerIfCalledFromOutside(
  128.             'doctrine/dbal',
  129.             'https://github.com/doctrine/dbal/pull/4821',
  130.             'PostgreSQLSchemaManager::getSchemaSearchPaths() is deprecated.',
  131.         );
  132.         $params $this->_conn->getParams();
  133.         $searchPaths $this->_conn->fetchOne('SHOW search_path');
  134.         assert($searchPaths !== false);
  135.         $schema explode(','$searchPaths);
  136.         if (isset($params['user'])) {
  137.             $schema str_replace('"$user"'$params['user'], $schema);
  138.         }
  139.         return array_map('trim'$schema);
  140.     }
  141.     /**
  142.      * Gets names of all existing schemas in the current users search path.
  143.      *
  144.      * This is a PostgreSQL only function.
  145.      *
  146.      * @internal The method should be only used from within the PostgreSQLSchemaManager class hierarchy.
  147.      *
  148.      * @return string[]
  149.      *
  150.      * @throws Exception
  151.      */
  152.     public function getExistingSchemaSearchPaths()
  153.     {
  154.         if ($this->existingSchemaPaths === null) {
  155.             $this->determineExistingSchemaSearchPaths();
  156.         }
  157.         assert($this->existingSchemaPaths !== null);
  158.         return $this->existingSchemaPaths;
  159.     }
  160.     /**
  161.      * Returns the name of the current schema.
  162.      *
  163.      * @return string|null
  164.      *
  165.      * @throws Exception
  166.      */
  167.     protected function getCurrentSchema()
  168.     {
  169.         $schemas $this->getExistingSchemaSearchPaths();
  170.         return array_shift($schemas);
  171.     }
  172.     /**
  173.      * Sets or resets the order of the existing schemas in the current search path of the user.
  174.      *
  175.      * This is a PostgreSQL only function.
  176.      *
  177.      * @internal The method should be only used from within the PostgreSQLSchemaManager class hierarchy.
  178.      *
  179.      * @return void
  180.      *
  181.      * @throws Exception
  182.      */
  183.     public function determineExistingSchemaSearchPaths()
  184.     {
  185.         $names $this->listSchemaNames();
  186.         $paths $this->getSchemaSearchPaths();
  187.         $this->existingSchemaPaths array_filter($paths, static function ($v) use ($names): bool {
  188.             return in_array($v$namestrue);
  189.         });
  190.     }
  191.     /**
  192.      * {@inheritDoc}
  193.      */
  194.     protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
  195.     {
  196.         $onUpdate null;
  197.         $onDelete null;
  198.         if (
  199.             preg_match(
  200.                 '(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))',
  201.                 $tableForeignKey['condef'],
  202.                 $match,
  203.             ) === 1
  204.         ) {
  205.             $onUpdate $match[1];
  206.         }
  207.         if (
  208.             preg_match(
  209.                 '(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))',
  210.                 $tableForeignKey['condef'],
  211.                 $match,
  212.             ) === 1
  213.         ) {
  214.             $onDelete $match[1];
  215.         }
  216.         $result preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/'$tableForeignKey['condef'], $values);
  217.         assert($result === 1);
  218.         // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
  219.         // the idea to trim them here.
  220.         $localColumns   array_map('trim'explode(','$values[1]));
  221.         $foreignColumns array_map('trim'explode(','$values[3]));
  222.         $foreignTable   $values[2];
  223.         return new ForeignKeyConstraint(
  224.             $localColumns,
  225.             $foreignTable,
  226.             $foreignColumns,
  227.             $tableForeignKey['conname'],
  228.             ['onUpdate' => $onUpdate'onDelete' => $onDelete],
  229.         );
  230.     }
  231.     /**
  232.      * {@inheritDoc}
  233.      */
  234.     protected function _getPortableViewDefinition($view)
  235.     {
  236.         return new View($view['schemaname'] . '.' $view['viewname'], $view['definition']);
  237.     }
  238.     /**
  239.      * {@inheritDoc}
  240.      */
  241.     protected function _getPortableTableDefinition($table)
  242.     {
  243.         $currentSchema $this->getCurrentSchema();
  244.         if ($table['schema_name'] === $currentSchema) {
  245.             return $table['table_name'];
  246.         }
  247.         return $table['schema_name'] . '.' $table['table_name'];
  248.     }
  249.     /**
  250.      * {@inheritDoc}
  251.      */
  252.     protected function _getPortableTableIndexesList($tableIndexes$tableName null)
  253.     {
  254.         $buffer = [];
  255.         foreach ($tableIndexes as $row) {
  256.             $colNumbers    array_map('intval'explode(' '$row['indkey']));
  257.             $columnNameSql sprintf(
  258.                 'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC',
  259.                 $row['indrelid'],
  260.                 implode(' ,'$colNumbers),
  261.             );
  262.             $indexColumns $this->_conn->fetchAllAssociative($columnNameSql);
  263.             // required for getting the order of the columns right.
  264.             foreach ($colNumbers as $colNum) {
  265.                 foreach ($indexColumns as $colRow) {
  266.                     if ($colNum !== $colRow['attnum']) {
  267.                         continue;
  268.                     }
  269.                     $buffer[] = [
  270.                         'key_name' => $row['relname'],
  271.                         'column_name' => trim($colRow['attname']),
  272.                         'non_unique' => ! $row['indisunique'],
  273.                         'primary' => $row['indisprimary'],
  274.                         'where' => $row['where'],
  275.                     ];
  276.                 }
  277.             }
  278.         }
  279.         return parent::_getPortableTableIndexesList($buffer$tableName);
  280.     }
  281.     /**
  282.      * {@inheritDoc}
  283.      */
  284.     protected function _getPortableDatabaseDefinition($database)
  285.     {
  286.         return $database['datname'];
  287.     }
  288.     /**
  289.      * {@inheritDoc}
  290.      *
  291.      * @deprecated Use {@see listSchemaNames()} instead.
  292.      */
  293.     protected function getPortableNamespaceDefinition(array $namespace)
  294.     {
  295.         Deprecation::triggerIfCalledFromOutside(
  296.             'doctrine/dbal',
  297.             'https://github.com/doctrine/dbal/issues/4503',
  298.             'PostgreSQLSchemaManager::getPortableNamespaceDefinition() is deprecated,'
  299.                 ' use PostgreSQLSchemaManager::listSchemaNames() instead.',
  300.         );
  301.         return $namespace['nspname'];
  302.     }
  303.     /**
  304.      * {@inheritDoc}
  305.      */
  306.     protected function _getPortableSequenceDefinition($sequence)
  307.     {
  308.         if ($sequence['schemaname'] !== 'public') {
  309.             $sequenceName $sequence['schemaname'] . '.' $sequence['relname'];
  310.         } else {
  311.             $sequenceName $sequence['relname'];
  312.         }
  313.         return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
  314.     }
  315.     /**
  316.      * {@inheritDoc}
  317.      */
  318.     protected function _getPortableTableColumnDefinition($tableColumn)
  319.     {
  320.         $tableColumn array_change_key_case($tableColumnCASE_LOWER);
  321.         if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') {
  322.             // get length from varchar definition
  323.             $length                preg_replace('~.*\(([0-9]*)\).*~''$1'$tableColumn['complete_type']);
  324.             $tableColumn['length'] = $length;
  325.         }
  326.         $matches = [];
  327.         $autoincrement false;
  328.         if (
  329.             $tableColumn['default'] !== null
  330.             && preg_match("/^nextval\('(.*)'(::.*)?\)$/"$tableColumn['default'], $matches) === 1
  331.         ) {
  332.             $tableColumn['sequence'] = $matches[1];
  333.             $tableColumn['default']  = null;
  334.             $autoincrement           true;
  335.         }
  336.         if ($tableColumn['default'] !== null) {
  337.             if (preg_match("/^['(](.*)[')]::/"$tableColumn['default'], $matches) === 1) {
  338.                 $tableColumn['default'] = $matches[1];
  339.             } elseif (preg_match('/^NULL::/'$tableColumn['default']) === 1) {
  340.                 $tableColumn['default'] = null;
  341.             }
  342.         }
  343.         $length $tableColumn['length'] ?? null;
  344.         if ($length === '-1' && isset($tableColumn['atttypmod'])) {
  345.             $length $tableColumn['atttypmod'] - 4;
  346.         }
  347.         if ((int) $length <= 0) {
  348.             $length null;
  349.         }
  350.         $fixed null;
  351.         if (! isset($tableColumn['name'])) {
  352.             $tableColumn['name'] = '';
  353.         }
  354.         $precision null;
  355.         $scale     null;
  356.         $jsonb     null;
  357.         $dbType strtolower($tableColumn['type']);
  358.         if (
  359.             $tableColumn['domain_type'] !== null
  360.             && $tableColumn['domain_type'] !== ''
  361.             && ! $this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])
  362.         ) {
  363.             $dbType                       strtolower($tableColumn['domain_type']);
  364.             $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
  365.         }
  366.         $type                   $this->_platform->getDoctrineTypeMapping($dbType);
  367.         $type                   $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
  368.         $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
  369.         switch ($dbType) {
  370.             case 'smallint':
  371.             case 'int2':
  372.                 $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
  373.                 $length                 null;
  374.                 break;
  375.             case 'int':
  376.             case 'int4':
  377.             case 'integer':
  378.                 $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
  379.                 $length                 null;
  380.                 break;
  381.             case 'bigint':
  382.             case 'int8':
  383.                 $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
  384.                 $length                 null;
  385.                 break;
  386.             case 'bool':
  387.             case 'boolean':
  388.                 if ($tableColumn['default'] === 'true') {
  389.                     $tableColumn['default'] = true;
  390.                 }
  391.                 if ($tableColumn['default'] === 'false') {
  392.                     $tableColumn['default'] = false;
  393.                 }
  394.                 $length null;
  395.                 break;
  396.             case 'json':
  397.             case 'text':
  398.             case '_varchar':
  399.             case 'varchar':
  400.                 $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']);
  401.                 $fixed                  false;
  402.                 break;
  403.             case 'interval':
  404.                 $fixed false;
  405.                 break;
  406.             case 'char':
  407.             case 'bpchar':
  408.                 $fixed true;
  409.                 break;
  410.             case 'float':
  411.             case 'float4':
  412.             case 'float8':
  413.             case 'double':
  414.             case 'double precision':
  415.             case 'real':
  416.             case 'decimal':
  417.             case 'money':
  418.             case 'numeric':
  419.                 $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
  420.                 if (
  421.                     preg_match(
  422.                         '([A-Za-z]+\(([0-9]+),([0-9]+)\))',
  423.                         $tableColumn['complete_type'],
  424.                         $match,
  425.                     ) === 1
  426.                 ) {
  427.                     $precision $match[1];
  428.                     $scale     $match[2];
  429.                     $length    null;
  430.                 }
  431.                 break;
  432.             case 'year':
  433.                 $length null;
  434.                 break;
  435.             // PostgreSQL 9.4+ only
  436.             case 'jsonb':
  437.                 $jsonb true;
  438.                 break;
  439.         }
  440.         if (
  441.             $tableColumn['default'] !== null && preg_match(
  442.                 "('([^']+)'::)",
  443.                 $tableColumn['default'],
  444.                 $match,
  445.             ) === 1
  446.         ) {
  447.             $tableColumn['default'] = $match[1];
  448.         }
  449.         $options = [
  450.             'length'        => $length,
  451.             'notnull'       => (bool) $tableColumn['isnotnull'],
  452.             'default'       => $tableColumn['default'],
  453.             'precision'     => $precision,
  454.             'scale'         => $scale,
  455.             'fixed'         => $fixed,
  456.             'autoincrement' => $autoincrement,
  457.             'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
  458.                 $tableColumn['comment']
  459.                 : null,
  460.         ];
  461.         $column = new Column($tableColumn['field'], Type::getType($type), $options);
  462.         if (! empty($tableColumn['collation'])) {
  463.             $column->setPlatformOption('collation'$tableColumn['collation']);
  464.         }
  465.         if ($column->getType()->getName() === Types::JSON) {
  466.             if (! $column->getType() instanceof JsonType) {
  467.                 Deprecation::trigger(
  468.                     'doctrine/dbal',
  469.                     'https://github.com/doctrine/dbal/pull/5049',
  470.                     <<<'DEPRECATION'
  471.                     %s not extending %s while being named %s is deprecated,
  472.                     and will lead to jsonb never to being used in 4.0.,
  473.                     DEPRECATION,
  474.                     get_class($column->getType()),
  475.                     JsonType::class,
  476.                     Types::JSON,
  477.                 );
  478.             }
  479.             $column->setPlatformOption('jsonb'$jsonb);
  480.         }
  481.         return $column;
  482.     }
  483.     /**
  484.      * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
  485.      *
  486.      * @param mixed $defaultValue
  487.      *
  488.      * @return mixed
  489.      */
  490.     private function fixVersion94NegativeNumericDefaultValue($defaultValue)
  491.     {
  492.         if ($defaultValue !== null && strpos($defaultValue'(') === 0) {
  493.             return trim($defaultValue'()');
  494.         }
  495.         return $defaultValue;
  496.     }
  497.     /**
  498.      * Parses a default value expression as given by PostgreSQL
  499.      */
  500.     private function parseDefaultExpression(?string $default): ?string
  501.     {
  502.         if ($default === null) {
  503.             return $default;
  504.         }
  505.         return str_replace("''""'"$default);
  506.     }
  507.     protected function selectTableNames(string $databaseName): Result
  508.     {
  509.         $sql = <<<'SQL'
  510. SELECT quote_ident(table_name) AS table_name,
  511.        table_schema AS schema_name
  512. FROM information_schema.tables
  513. WHERE table_catalog = ?
  514.   AND table_schema NOT LIKE 'pg\_%'
  515.   AND table_schema != 'information_schema'
  516.   AND table_name != 'geometry_columns'
  517.   AND table_name != 'spatial_ref_sys'
  518.   AND table_type = 'BASE TABLE'
  519. SQL;
  520.         return $this->_conn->executeQuery($sql, [$databaseName]);
  521.     }
  522.     protected function selectTableColumns(string $databaseName, ?string $tableName null): Result
  523.     {
  524.         $sql 'SELECT';
  525.         if ($tableName === null) {
  526.             $sql .= ' c.relname AS table_name, n.nspname AS schema_name,';
  527.         }
  528.         $sql .= sprintf(<<<'SQL'
  529.             a.attnum,
  530.             quote_ident(a.attname) AS field,
  531.             t.typname AS type,
  532.             format_type(a.atttypid, a.atttypmod) AS complete_type,
  533.             (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation,
  534.             (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
  535.             (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
  536.               pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
  537.             a.attnotnull AS isnotnull,
  538.             (SELECT 't'
  539.              FROM pg_index
  540.              WHERE c.oid = pg_index.indrelid
  541.                 AND pg_index.indkey[0] = a.attnum
  542.                 AND pg_index.indisprimary = 't'
  543.             ) AS pri,
  544.             (%s) AS default,
  545.             (SELECT pg_description.description
  546.                 FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
  547.             ) AS comment
  548.             FROM pg_attribute a
  549.                 INNER JOIN pg_class c
  550.                     ON c.oid = a.attrelid
  551.                 INNER JOIN pg_type t
  552.                     ON t.oid = a.atttypid
  553.                 INNER JOIN pg_namespace n
  554.                     ON n.oid = c.relnamespace
  555.                 LEFT JOIN pg_depend d
  556.                     ON d.objid = c.oid
  557.                         AND d.deptype = 'e'
  558.                         AND d.classid = (SELECT oid FROM pg_class WHERE relname = 'pg_class')
  559. SQL, $this->_platform->getDefaultColumnValueSQLSnippet());
  560.         $conditions array_merge([
  561.             'a.attnum > 0',
  562.             "c.relkind = 'r'",
  563.             'd.refobjid IS NULL',
  564.         ], $this->buildQueryConditions($tableName));
  565.         $sql .= ' WHERE ' implode(' AND '$conditions) . ' ORDER BY a.attnum';
  566.         return $this->_conn->executeQuery($sql);
  567.     }
  568.     protected function selectIndexColumns(string $databaseName, ?string $tableName null): Result
  569.     {
  570.         $sql 'SELECT';
  571.         if ($tableName === null) {
  572.             $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,';
  573.         }
  574.         $sql .= <<<'SQL'
  575.                    quote_ident(ic.relname) AS relname,
  576.                    i.indisunique,
  577.                    i.indisprimary,
  578.                    i.indkey,
  579.                    i.indrelid,
  580.                    pg_get_expr(indpred, indrelid) AS "where"
  581.               FROM pg_index i
  582.                    JOIN pg_class AS tc ON tc.oid = i.indrelid
  583.                    JOIN pg_namespace tn ON tn.oid = tc.relnamespace
  584.                    JOIN pg_class AS ic ON ic.oid = i.indexrelid
  585.              WHERE ic.oid IN (
  586.                 SELECT indexrelid
  587.                 FROM pg_index i, pg_class c, pg_namespace n
  588. SQL;
  589.         $conditions array_merge([
  590.             'c.oid = i.indrelid',
  591.             'c.relnamespace = n.oid',
  592.         ], $this->buildQueryConditions($tableName));
  593.         $sql .= ' WHERE ' implode(' AND '$conditions) . ')';
  594.         return $this->_conn->executeQuery($sql);
  595.     }
  596.     protected function selectForeignKeyColumns(string $databaseName, ?string $tableName null): Result
  597.     {
  598.         $sql 'SELECT';
  599.         if ($tableName === null) {
  600.             $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,';
  601.         }
  602.         $sql .= <<<'SQL'
  603.                   quote_ident(r.conname) as conname,
  604.                   pg_get_constraintdef(r.oid, true) as condef
  605.                   FROM pg_constraint r
  606.                       JOIN pg_class AS tc ON tc.oid = r.conrelid
  607.                       JOIN pg_namespace tn ON tn.oid = tc.relnamespace
  608.                   WHERE r.conrelid IN
  609.                   (
  610.                       SELECT c.oid
  611.                       FROM pg_class c, pg_namespace n
  612. SQL;
  613.         $conditions array_merge(['n.oid = c.relnamespace'], $this->buildQueryConditions($tableName));
  614.         $sql .= ' WHERE ' implode(' AND '$conditions) . ") AND r.contype = 'f'";
  615.         return $this->_conn->executeQuery($sql);
  616.     }
  617.     /**
  618.      * {@inheritDoc}
  619.      */
  620.     protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName null): array
  621.     {
  622.         $sql = <<<'SQL'
  623. SELECT c.relname,
  624.        CASE c.relpersistence WHEN 'u' THEN true ELSE false END as unlogged,
  625.        obj_description(c.oid, 'pg_class') AS comment
  626. FROM pg_class c
  627.      INNER JOIN pg_namespace n
  628.          ON n.oid = c.relnamespace
  629. SQL;
  630.         $conditions array_merge(["c.relkind = 'r'"], $this->buildQueryConditions($tableName));
  631.         $sql .= ' WHERE ' implode(' AND '$conditions);
  632.         return $this->_conn->fetchAllAssociativeIndexed($sql);
  633.     }
  634.     /**
  635.      * @param string|null $tableName
  636.      *
  637.      * @return list<string>
  638.      */
  639.     private function buildQueryConditions($tableName): array
  640.     {
  641.         $conditions = [];
  642.         if ($tableName !== null) {
  643.             if (strpos($tableName'.') !== false) {
  644.                 [$schemaName$tableName] = explode('.'$tableName);
  645.                 $conditions[]             = 'n.nspname = ' $this->_platform->quoteStringLiteral($schemaName);
  646.             } else {
  647.                 $conditions[] = 'n.nspname = ANY(current_schemas(false))';
  648.             }
  649.             $identifier   = new Identifier($tableName);
  650.             $conditions[] = 'c.relname = ' $this->_platform->quoteStringLiteral($identifier->getName());
  651.         }
  652.         $conditions[] = "n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')";
  653.         return $conditions;
  654.     }
  655. }