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

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.      * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
  253.      */
  254.     protected function _getPortableTableIndexesList($tableIndexes$tableName null)
  255.     {
  256.         $buffer = [];
  257.         foreach ($tableIndexes as $row) {
  258.             $colNumbers    array_map('intval'explode(' '$row['indkey']));
  259.             $columnNameSql sprintf(
  260.                 'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC',
  261.                 $row['indrelid'],
  262.                 implode(' ,'$colNumbers),
  263.             );
  264.             $indexColumns $this->_conn->fetchAllAssociative($columnNameSql);
  265.             // required for getting the order of the columns right.
  266.             foreach ($colNumbers as $colNum) {
  267.                 foreach ($indexColumns as $colRow) {
  268.                     if ($colNum !== $colRow['attnum']) {
  269.                         continue;
  270.                     }
  271.                     $buffer[] = [
  272.                         'key_name' => $row['relname'],
  273.                         'column_name' => trim($colRow['attname']),
  274.                         'non_unique' => ! $row['indisunique'],
  275.                         'primary' => $row['indisprimary'],
  276.                         'where' => $row['where'],
  277.                     ];
  278.                 }
  279.             }
  280.         }
  281.         return parent::_getPortableTableIndexesList($buffer$tableName);
  282.     }
  283.     /**
  284.      * {@inheritDoc}
  285.      */
  286.     protected function _getPortableDatabaseDefinition($database)
  287.     {
  288.         return $database['datname'];
  289.     }
  290.     /**
  291.      * {@inheritDoc}
  292.      *
  293.      * @deprecated Use {@see listSchemaNames()} instead.
  294.      */
  295.     protected function getPortableNamespaceDefinition(array $namespace)
  296.     {
  297.         Deprecation::triggerIfCalledFromOutside(
  298.             'doctrine/dbal',
  299.             'https://github.com/doctrine/dbal/issues/4503',
  300.             'PostgreSQLSchemaManager::getPortableNamespaceDefinition() is deprecated,'
  301.                 ' use PostgreSQLSchemaManager::listSchemaNames() instead.',
  302.         );
  303.         return $namespace['nspname'];
  304.     }
  305.     /**
  306.      * {@inheritDoc}
  307.      */
  308.     protected function _getPortableSequenceDefinition($sequence)
  309.     {
  310.         if ($sequence['schemaname'] !== 'public') {
  311.             $sequenceName $sequence['schemaname'] . '.' $sequence['relname'];
  312.         } else {
  313.             $sequenceName $sequence['relname'];
  314.         }
  315.         return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
  316.     }
  317.     /**
  318.      * {@inheritDoc}
  319.      */
  320.     protected function _getPortableTableColumnDefinition($tableColumn)
  321.     {
  322.         $tableColumn array_change_key_case($tableColumnCASE_LOWER);
  323.         if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') {
  324.             // get length from varchar definition
  325.             $length                preg_replace('~.*\(([0-9]*)\).*~''$1'$tableColumn['complete_type']);
  326.             $tableColumn['length'] = $length;
  327.         }
  328.         $matches = [];
  329.         $autoincrement false;
  330.         if (
  331.             $tableColumn['default'] !== null
  332.             && preg_match("/^nextval\('(.*)'(::.*)?\)$/"$tableColumn['default'], $matches) === 1
  333.         ) {
  334.             $tableColumn['sequence'] = $matches[1];
  335.             $tableColumn['default']  = null;
  336.             $autoincrement           true;
  337.         }
  338.         if ($tableColumn['default'] !== null) {
  339.             if (preg_match("/^['(](.*)[')]::/"$tableColumn['default'], $matches) === 1) {
  340.                 $tableColumn['default'] = $matches[1];
  341.             } elseif (preg_match('/^NULL::/'$tableColumn['default']) === 1) {
  342.                 $tableColumn['default'] = null;
  343.             }
  344.         }
  345.         $length $tableColumn['length'] ?? null;
  346.         if ($length === '-1' && isset($tableColumn['atttypmod'])) {
  347.             $length $tableColumn['atttypmod'] - 4;
  348.         }
  349.         if ((int) $length <= 0) {
  350.             $length null;
  351.         }
  352.         $fixed null;
  353.         if (! isset($tableColumn['name'])) {
  354.             $tableColumn['name'] = '';
  355.         }
  356.         $precision null;
  357.         $scale     null;
  358.         $jsonb     null;
  359.         $dbType strtolower($tableColumn['type']);
  360.         if (
  361.             $tableColumn['domain_type'] !== null
  362.             && $tableColumn['domain_type'] !== ''
  363.             && ! $this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])
  364.         ) {
  365.             $dbType                       strtolower($tableColumn['domain_type']);
  366.             $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
  367.         }
  368.         $type                   $this->_platform->getDoctrineTypeMapping($dbType);
  369.         $type                   $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
  370.         $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
  371.         switch ($dbType) {
  372.             case 'smallint':
  373.             case 'int2':
  374.                 $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
  375.                 $length                 null;
  376.                 break;
  377.             case 'int':
  378.             case 'int4':
  379.             case 'integer':
  380.                 $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
  381.                 $length                 null;
  382.                 break;
  383.             case 'bigint':
  384.             case 'int8':
  385.                 $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
  386.                 $length                 null;
  387.                 break;
  388.             case 'bool':
  389.             case 'boolean':
  390.                 if ($tableColumn['default'] === 'true') {
  391.                     $tableColumn['default'] = true;
  392.                 }
  393.                 if ($tableColumn['default'] === 'false') {
  394.                     $tableColumn['default'] = false;
  395.                 }
  396.                 $length null;
  397.                 break;
  398.             case 'json':
  399.             case 'text':
  400.             case '_varchar':
  401.             case 'varchar':
  402.                 $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']);
  403.                 $fixed                  false;
  404.                 break;
  405.             case 'interval':
  406.                 $fixed false;
  407.                 break;
  408.             case 'char':
  409.             case 'bpchar':
  410.                 $fixed true;
  411.                 break;
  412.             case 'float':
  413.             case 'float4':
  414.             case 'float8':
  415.             case 'double':
  416.             case 'double precision':
  417.             case 'real':
  418.             case 'decimal':
  419.             case 'money':
  420.             case 'numeric':
  421.                 $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
  422.                 if (
  423.                     preg_match(
  424.                         '([A-Za-z]+\(([0-9]+),([0-9]+)\))',
  425.                         $tableColumn['complete_type'],
  426.                         $match,
  427.                     ) === 1
  428.                 ) {
  429.                     $precision $match[1];
  430.                     $scale     $match[2];
  431.                     $length    null;
  432.                 }
  433.                 break;
  434.             case 'year':
  435.                 $length null;
  436.                 break;
  437.             // PostgreSQL 9.4+ only
  438.             case 'jsonb':
  439.                 $jsonb true;
  440.                 break;
  441.         }
  442.         if (
  443.             $tableColumn['default'] !== null && preg_match(
  444.                 "('([^']+)'::)",
  445.                 $tableColumn['default'],
  446.                 $match,
  447.             ) === 1
  448.         ) {
  449.             $tableColumn['default'] = $match[1];
  450.         }
  451.         $options = [
  452.             'length'        => $length,
  453.             'notnull'       => (bool) $tableColumn['isnotnull'],
  454.             'default'       => $tableColumn['default'],
  455.             'precision'     => $precision,
  456.             'scale'         => $scale,
  457.             'fixed'         => $fixed,
  458.             'autoincrement' => $autoincrement,
  459.             'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
  460.                 $tableColumn['comment']
  461.                 : null,
  462.         ];
  463.         $column = new Column($tableColumn['field'], Type::getType($type), $options);
  464.         if (! empty($tableColumn['collation'])) {
  465.             $column->setPlatformOption('collation'$tableColumn['collation']);
  466.         }
  467.         if ($column->getType()->getName() === Types::JSON) {
  468.             if (! $column->getType() instanceof JsonType) {
  469.                 Deprecation::trigger(
  470.                     'doctrine/dbal',
  471.                     'https://github.com/doctrine/dbal/pull/5049',
  472.                     <<<'DEPRECATION'
  473.                     %s not extending %s while being named %s is deprecated,
  474.                     and will lead to jsonb never to being used in 4.0.,
  475.                     DEPRECATION,
  476.                     get_class($column->getType()),
  477.                     JsonType::class,
  478.                     Types::JSON,
  479.                 );
  480.             }
  481.             $column->setPlatformOption('jsonb'$jsonb);
  482.         }
  483.         return $column;
  484.     }
  485.     /**
  486.      * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
  487.      *
  488.      * @param mixed $defaultValue
  489.      *
  490.      * @return mixed
  491.      */
  492.     private function fixVersion94NegativeNumericDefaultValue($defaultValue)
  493.     {
  494.         if ($defaultValue !== null && strpos($defaultValue'(') === 0) {
  495.             return trim($defaultValue'()');
  496.         }
  497.         return $defaultValue;
  498.     }
  499.     /**
  500.      * Parses a default value expression as given by PostgreSQL
  501.      */
  502.     private function parseDefaultExpression(?string $default): ?string
  503.     {
  504.         if ($default === null) {
  505.             return $default;
  506.         }
  507.         return str_replace("''""'"$default);
  508.     }
  509.     protected function selectTableNames(string $databaseName): Result
  510.     {
  511.         $sql = <<<'SQL'
  512. SELECT quote_ident(table_name) AS table_name,
  513.        table_schema AS schema_name
  514. FROM information_schema.tables
  515. WHERE table_catalog = ?
  516.   AND table_schema NOT LIKE 'pg\_%'
  517.   AND table_schema != 'information_schema'
  518.   AND table_name != 'geometry_columns'
  519.   AND table_name != 'spatial_ref_sys'
  520.   AND table_type = 'BASE TABLE'
  521. SQL;
  522.         return $this->_conn->executeQuery($sql, [$databaseName]);
  523.     }
  524.     protected function selectTableColumns(string $databaseName, ?string $tableName null): Result
  525.     {
  526.         $sql 'SELECT';
  527.         if ($tableName === null) {
  528.             $sql .= ' c.relname AS table_name, n.nspname AS schema_name,';
  529.         }
  530.         $sql .= sprintf(<<<'SQL'
  531.             a.attnum,
  532.             quote_ident(a.attname) AS field,
  533.             t.typname AS type,
  534.             format_type(a.atttypid, a.atttypmod) AS complete_type,
  535.             (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation,
  536.             (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
  537.             (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
  538.               pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
  539.             a.attnotnull AS isnotnull,
  540.             (SELECT 't'
  541.              FROM pg_index
  542.              WHERE c.oid = pg_index.indrelid
  543.                 AND pg_index.indkey[0] = a.attnum
  544.                 AND pg_index.indisprimary = 't'
  545.             ) AS pri,
  546.             (%s) AS default,
  547.             (SELECT pg_description.description
  548.                 FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
  549.             ) AS comment
  550.             FROM pg_attribute a
  551.                 INNER JOIN pg_class c
  552.                     ON c.oid = a.attrelid
  553.                 INNER JOIN pg_type t
  554.                     ON t.oid = a.atttypid
  555.                 INNER JOIN pg_namespace n
  556.                     ON n.oid = c.relnamespace
  557.                 LEFT JOIN pg_depend d
  558.                     ON d.objid = c.oid
  559.                         AND d.deptype = 'e'
  560.                         AND d.classid = (SELECT oid FROM pg_class WHERE relname = 'pg_class')
  561. SQL, $this->_platform->getDefaultColumnValueSQLSnippet());
  562.         $conditions array_merge([
  563.             'a.attnum > 0',
  564.             "c.relkind = 'r'",
  565.             'd.refobjid IS NULL',
  566.         ], $this->buildQueryConditions($tableName));
  567.         $sql .= ' WHERE ' implode(' AND '$conditions) . ' ORDER BY a.attnum';
  568.         return $this->_conn->executeQuery($sql);
  569.     }
  570.     protected function selectIndexColumns(string $databaseName, ?string $tableName null): Result
  571.     {
  572.         $sql 'SELECT';
  573.         if ($tableName === null) {
  574.             $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,';
  575.         }
  576.         $sql .= <<<'SQL'
  577.                    quote_ident(ic.relname) AS relname,
  578.                    i.indisunique,
  579.                    i.indisprimary,
  580.                    i.indkey,
  581.                    i.indrelid,
  582.                    pg_get_expr(indpred, indrelid) AS "where"
  583.               FROM pg_index i
  584.                    JOIN pg_class AS tc ON tc.oid = i.indrelid
  585.                    JOIN pg_namespace tn ON tn.oid = tc.relnamespace
  586.                    JOIN pg_class AS ic ON ic.oid = i.indexrelid
  587.              WHERE ic.oid IN (
  588.                 SELECT indexrelid
  589.                 FROM pg_index i, pg_class c, pg_namespace n
  590. SQL;
  591.         $conditions array_merge([
  592.             'c.oid = i.indrelid',
  593.             'c.relnamespace = n.oid',
  594.         ], $this->buildQueryConditions($tableName));
  595.         $sql .= ' WHERE ' implode(' AND '$conditions) . ')';
  596.         return $this->_conn->executeQuery($sql);
  597.     }
  598.     protected function selectForeignKeyColumns(string $databaseName, ?string $tableName null): Result
  599.     {
  600.         $sql 'SELECT';
  601.         if ($tableName === null) {
  602.             $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,';
  603.         }
  604.         $sql .= <<<'SQL'
  605.                   quote_ident(r.conname) as conname,
  606.                   pg_get_constraintdef(r.oid, true) as condef
  607.                   FROM pg_constraint r
  608.                       JOIN pg_class AS tc ON tc.oid = r.conrelid
  609.                       JOIN pg_namespace tn ON tn.oid = tc.relnamespace
  610.                   WHERE r.conrelid IN
  611.                   (
  612.                       SELECT c.oid
  613.                       FROM pg_class c, pg_namespace n
  614. SQL;
  615.         $conditions array_merge(['n.oid = c.relnamespace'], $this->buildQueryConditions($tableName));
  616.         $sql .= ' WHERE ' implode(' AND '$conditions) . ") AND r.contype = 'f'";
  617.         return $this->_conn->executeQuery($sql);
  618.     }
  619.     /**
  620.      * {@inheritDoc}
  621.      */
  622.     protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName null): array
  623.     {
  624.         $sql = <<<'SQL'
  625. SELECT c.relname,
  626.        CASE c.relpersistence WHEN 'u' THEN true ELSE false END as unlogged,
  627.        obj_description(c.oid, 'pg_class') AS comment
  628. FROM pg_class c
  629.      INNER JOIN pg_namespace n
  630.          ON n.oid = c.relnamespace
  631. SQL;
  632.         $conditions array_merge(["c.relkind = 'r'"], $this->buildQueryConditions($tableName));
  633.         $sql .= ' WHERE ' implode(' AND '$conditions);
  634.         return $this->_conn->fetchAllAssociativeIndexed($sql);
  635.     }
  636.     /**
  637.      * @param string|null $tableName
  638.      *
  639.      * @return list<string>
  640.      */
  641.     private function buildQueryConditions($tableName): array
  642.     {
  643.         $conditions = [];
  644.         if ($tableName !== null) {
  645.             if (strpos($tableName'.') !== false) {
  646.                 [$schemaName$tableName] = explode('.'$tableName);
  647.                 $conditions[]             = 'n.nspname = ' $this->_platform->quoteStringLiteral($schemaName);
  648.             } else {
  649.                 $conditions[] = 'n.nspname = ANY(current_schemas(false))';
  650.             }
  651.             $identifier   = new Identifier($tableName);
  652.             $conditions[] = 'c.relname = ' $this->_platform->quoteStringLiteral($identifier->getName());
  653.         }
  654.         $conditions[] = "n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')";
  655.         return $conditions;
  656.     }
  657. }