ReferentialAction::NO_ACTION, 'c' => ReferentialAction::CASCADE, 'd' => ReferentialAction::SET_DEFAULT, 'n' => ReferentialAction::SET_NULL, 'r' => ReferentialAction::RESTRICT, ]; /** @internal This class can be instantiated only by a database platform. */ public function __construct(private Connection $connection, private PostgreSQLPlatform $platform) { } /** * {@inheritDoc} * * @link https://www.postgresql.org/docs/current/catalog-pg-database.html */ public function getAllDatabaseNames(): iterable { $sql = <<<'SQL' SELECT datname FROM pg_database ORDER BY datname SQL; foreach ($this->connection->iterateColumn($sql) as $databaseName) { yield new DatabaseMetadataRow($databaseName); } } /** * {@inheritDoc} * * @link https://www.postgresql.org/docs/current/catalog-pg-namespace.html */ public function getAllSchemaNames(): iterable { $sql = sprintf( <<<'SQL' SELECT nspname FROM pg_namespace WHERE %s ORDER BY nspname SQL, $this->buildNamespaceNamePredicate('nspname'), ); foreach ($this->connection->iterateColumn($sql) as $schemaName) { yield new SchemaMetadataRow($schemaName); } } /** * {@inheritDoc} * * @link https://www.postgresql.org/docs/current/catalog-pg-class.html * @link https://www.postgresql.org/docs/current/catalog-pg-namespace.html */ public function getAllTableNames(): iterable { $sql = sprintf( <<<'SQL' SELECT n.nspname, c.relname FROM pg_class c INNER JOIN pg_namespace n ON n.oid = c.relnamespace WHERE %s AND %s ORDER BY n.nspname, c.relname SQL, $this->buildNamespaceNamePredicate('n.nspname'), $this->buildTablePredicate('c.relkind', 'c.relname'), ); foreach ($this->connection->iterateNumeric($sql) as $row) { yield new TableMetadataRow($row[0], $row[1], []); } } /** {@inheritDoc} */ public function getTableColumnsForAllTables(): iterable { return $this->getTableColumns(null, null); } /** {@inheritDoc} */ public function getTableColumnsForTable(?string $schemaName, string $tableName): iterable { if ($schemaName === null) { throw UnsupportedName::fromNullSchemaName(__METHOD__); } return $this->getTableColumns($schemaName, $tableName); } /** * @link https://www.postgresql.org/docs/current/catalog-pg-attrdef.html * @link https://www.postgresql.org/docs/current/catalog-pg-attribute.html * @link https://www.postgresql.org/docs/current/catalog-pg-class.html * @link https://www.postgresql.org/docs/current/catalog-pg-namespace.html * @link https://www.postgresql.org/docs/current/catalog-pg-type.html * @link https://www.postgresql.org/docs/current/catalog-pg-collation.html * @link https://www.postgresql.org/docs/current/catalog-pg-depend.html * @link https://www.postgresql.org/docs/current/catalog-pg-description.html * @link https://www.postgresql.org/docs/current/catalog-pg-inherits.html * * @return iterable * * @throws Exception */ private function getTableColumns(?string $schemaName, ?string $tableName): iterable { $params = []; $sql = sprintf( <<<'SQL' SELECT n.nspname, c.relname, a.attname, t.typname, format_type(a.atttypid, a.atttypmod), bt.typname, format_type(bt.oid, t.typtypmod), a.attnotnull, a.attidentity, (%s), dsc.description, CASE WHEN coll.collprovider = 'c' THEN coll.collcollate WHEN coll.collprovider = 'd' THEN NULL ELSE coll.collname END FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid JOIN pg_namespace n ON n.oid = c.relnamespace JOIN pg_type t ON t.oid = a.atttypid LEFT JOIN pg_type bt ON t.typtype = 'd' AND bt.oid = t.typbasetype LEFT JOIN pg_collation coll ON coll.oid = a.attcollation LEFT JOIN pg_depend dep ON dep.objid = c.oid AND dep.deptype = 'e' AND dep.classid = (SELECT oid FROM pg_class WHERE relname = 'pg_class') LEFT JOIN pg_description dsc ON dsc.objoid = c.oid AND dsc.objsubid = a.attnum LEFT JOIN pg_inherits i ON i.inhrelid = c.oid LEFT JOIN pg_class p ON i.inhparent = p.oid AND p.relkind = 'p' WHERE %s AND a.attnum > 0 AND dep.refobjid IS NULL -- exclude partitions (tables that inherit from partitioned tables) AND p.oid IS NULL ORDER BY n.nspname, c.relname, a.attnum SQL, $this->platform->getDefaultColumnValueSQLSnippet(), $this->buildTableQueryPredicate('n', $schemaName, 'c', $tableName, $params), ); foreach ($this->connection->iterateNumeric($sql, $params) as $row) { yield $this->createTableColumn($row); } } /** * @param list $row * * @throws TypesException */ private function createTableColumn(array $row): TableColumnMetadataRow { [ $namespaceName, $relationName, $attributeName, $typeName, $completeType, $domainTypeName, $domainCompleteType, $isNotNull, $identity, $defaultExpression, $description, $collationName, ] = $row; $editor = Column::editor() ->setQuotedName($attributeName); if ($domainTypeName !== null && ! $this->platform->hasDoctrineTypeMappingFor($typeName)) { $typeName = $domainTypeName; $completeType = $domainCompleteType; } $editor->setTypeName( $this->platform->getDoctrineTypeMapping($typeName), ); switch ($typeName) { case 'bpchar': case 'varchar': $parameters = $this->parseColumnTypeParameters($completeType); if (count($parameters) > 0) { $editor->setLength($parameters[0]); } break; case 'double': case 'decimal': case 'money': case 'numeric': $parameters = $this->parseColumnTypeParameters($completeType); if (count($parameters) > 0) { $editor->setPrecision($parameters[0]); } if (count($parameters) > 1) { $editor->setScale($parameters[1]); } break; } if ($typeName === 'bpchar') { $editor->setFixed(true); } $editor ->setNotNull((bool) $isNotNull) ->setDefaultValue($this->parseDefaultExpression($defaultExpression)) ->setAutoincrement($identity === 'd'); if ($description !== null) { $editor->setComment($description); } $editor->setCollation($collationName); return new TableColumnMetadataRow($namespaceName, $relationName, $editor->create()); } /** * Parses the parameters between parenthesis in the data type. * * @return list */ private function parseColumnTypeParameters(string $type): array { if (preg_match('/\((\d+)(?:,(\d+))?\)/', $type, $matches) !== 1) { return []; } $parameters = [(int) $matches[1]]; if (isset($matches[2])) { $parameters[] = (int) $matches[2]; } return $parameters; } /** * Parses a default value expression as given by PostgreSQL */ private function parseDefaultExpression(?string $expression): mixed { if ($expression === null || str_starts_with($expression, 'NULL::')) { return null; } if ($expression === 'true') { return true; } if ($expression === 'false') { return false; } if (preg_match("/^'(.*)'::/s", $expression, $matches) === 1) { return str_replace("''", "'", $matches[1]); } return $expression; } /** {@inheritDoc} */ public function getIndexColumnsForAllTables(): iterable { return $this->getIndexColumns(null, null); } /** {@inheritDoc} */ public function getIndexColumnsForTable(?string $schemaName, string $tableName): iterable { if ($schemaName === null) { throw UnsupportedName::fromNullSchemaName(__METHOD__); } return $this->getIndexColumns($schemaName, $tableName); } /** * @link https://www.postgresql.org/docs/current/catalog-pg-index.html * @link https://www.postgresql.org/docs/current/catalog-pg-class.html * @link https://www.postgresql.org/docs/current/catalog-pg-namespace.html * @link https://www.postgresql.org/docs/current/catalog-pg-class.html * * @return iterable * * @throws Exception */ private function getIndexColumns(?string $schemaName, ?string $tableName): iterable { $params = []; $sql = sprintf( <<<'SQL' SELECT n.nspname, c.relname, ic.relname, i.indisunique, pg_get_expr(indpred, indrelid), attname FROM pg_index i JOIN pg_class AS c ON c.oid = i.indrelid JOIN pg_namespace n ON n.oid = c.relnamespace JOIN pg_class AS ic ON ic.oid = i.indexrelid JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS keys(attnum, ord) ON TRUE JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = keys.attnum WHERE %s AND i.indisprimary = false ORDER BY n.nspname, c.relname, ic.relname, keys.ord SQL, $this->buildTableQueryPredicate('n', $schemaName, 'c', $tableName, $params), ); foreach ($this->connection->iterateNumeric($sql, $params) as $row) { yield new IndexColumnMetadataRow( schemaName: $row[0], tableName: $row[1], indexName: $row[2], type: $row[3] ? IndexType::UNIQUE : IndexType::REGULAR, isClustered: false, predicate: $row[4], columnName: $row[5], columnLength: null, ); } } /** {@inheritDoc} */ public function getPrimaryKeyConstraintColumnsForAllTables(): iterable { return $this->getPrimaryKeyConstraintColumns(null, null); } /** {@inheritDoc} */ public function getPrimaryKeyConstraintColumnsForTable( ?string $schemaName, string $tableName, ): iterable { if ($schemaName === null) { throw UnsupportedName::fromNullSchemaName(__METHOD__); } return $this->getPrimaryKeyConstraintColumns($schemaName, $tableName); } /** * @link https://www.postgresql.org/docs/current/catalog-pg-namespace.html * @link https://www.postgresql.org/docs/current/catalog-pg-class.html * @link https://www.postgresql.org/docs/current/catalog-pg-constraint.html * @link https://www.postgresql.org/docs/current/catalog-pg-index.html * * @return iterable * * @throws Exception */ private function getPrimaryKeyConstraintColumns(?string $schemaName, ?string $tableName): iterable { $params = []; $sql = sprintf( <<<'SQL' SELECT n.nspname, c.relname, ct.conname, a.attname FROM pg_namespace n INNER JOIN pg_class c ON c.relnamespace = n.oid INNER JOIN pg_constraint ct ON ct.conrelid = c.oid INNER JOIN pg_index i ON i.indrelid = c.oid AND i.indexrelid = ct.conindid INNER JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS keys(attnum, ord) ON true INNER JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = keys.attnum WHERE %s AND ct.contype = 'p' ORDER BY n.nspname, c.relname, ct.conname, keys.ord SQL, $this->buildTableQueryPredicate('n', $schemaName, 'c', $tableName, $params), ); foreach ($this->connection->iterateNumeric($sql, $params) as $row) { yield new PrimaryKeyConstraintColumnRow( schemaName: $row[0], tableName: $row[1], constraintName: $row[2], isClustered: true, columnName: $row[3], ); } } /** {@inheritDoc} */ public function getForeignKeyConstraintColumnsForAllTables(): iterable { return $this->getForeignKeyConstraintColumns(null, null); } /** {@inheritDoc} */ public function getForeignKeyConstraintColumnsForTable( ?string $schemaName, string $tableName, ): iterable { if ($schemaName === null) { throw UnsupportedName::fromNullSchemaName(__METHOD__); } return $this->getForeignKeyConstraintColumns($schemaName, $tableName); } /** * @link https://www.postgresql.org/docs/current/catalog-pg-constraint.html * @link https://www.postgresql.org/docs/current/catalog-pg-class.html * @link https://www.postgresql.org/docs/current/catalog-pg-namespace.html * @link https://www.postgresql.org/docs/current/catalog-pg-attribute.html * * @return iterable * * @throws Exception */ private function getForeignKeyConstraintColumns(?string $schemaName, ?string $tableName): iterable { $params = []; $sql = sprintf( <<<'SQL' SELECT pkn.nspname, pkc.relname, r.conname, fkn.nspname, fkc.relname, r.confupdtype, r.confdeltype, r.condeferrable, r.condeferred, pka.attname, fka.attname FROM pg_constraint r JOIN pg_class fkc ON fkc.oid = r.confrelid JOIN pg_namespace fkn ON fkn.oid = fkc.relnamespace JOIN unnest(r.confkey) WITH ORDINALITY AS fk_attnum(attnum, ord) ON TRUE JOIN pg_attribute fka ON fka.attrelid = fkc.oid AND fka.attnum = fk_attnum.attnum JOIN pg_class pkc ON pkc.oid = r.conrelid JOIN pg_namespace pkn ON pkn.oid = pkc.relnamespace JOIN unnest(r.conkey) WITH ORDINALITY AS pk_attnum(attnum, ord) ON pk_attnum.ord = fk_attnum.ord JOIN pg_attribute pka ON pka.attrelid = pkc.oid AND pka.attnum = pk_attnum.attnum WHERE %s AND r.contype = 'f' ORDER BY pkn.nspname, pkc.relname, r.conname SQL, $this->buildTableQueryPredicate('pkn', $schemaName, 'pkc', $tableName, $params), ); foreach ($this->connection->iterateNumeric($sql, $params) as $row) { yield new ForeignKeyConstraintColumnMetadataRow( $row[0], $row[1], null, $row[2], $row[3], $row[4], MatchType::SIMPLE, self::REFERENTIAL_ACTIONS[$row[5]], self::REFERENTIAL_ACTIONS[$row[6]], (bool) $row[7], (bool) $row[8], $row[9], $row[10], ); } } /** {@inheritDoc} */ public function getTableOptionsForAllTables(): iterable { return $this->getTableOptions(null, null); } /** {@inheritDoc} */ public function getTableOptionsForTable( ?string $schemaName, string $tableName, ): iterable { if ($schemaName === null) { throw UnsupportedName::fromNullSchemaName(__METHOD__); } return $this->getTableOptions($schemaName, $tableName); } /** * @link https://www.postgresql.org/docs/current/catalog-pg-class.html * @link https://www.postgresql.org/docs/current/catalog-pg-namespace.html * * @return iterable * * @throws Exception */ private function getTableOptions(?string $schemaName, ?string $tableName): iterable { $params = []; $sql = sprintf( <<<'SQL' SELECT n.nspname, c.relname, CASE c.relpersistence WHEN 'u' THEN true ELSE false END, obj_description(c.oid, 'pg_class') FROM pg_class c INNER JOIN pg_namespace n ON n.oid = c.relnamespace WHERE %s SQL, $this->buildTableQueryPredicate('n', $schemaName, 'c', $tableName, $params), ); foreach ($this->connection->iterateNumeric($sql, $params) as $row) { yield new TableMetadataRow($row[0], $row[1], [ 'unlogged' => $row[2], 'comment' => $row[3], ]); } } /** * @param list $params * * @return non-empty-string */ private function buildTableQueryPredicate( string $namespaceRelation, ?string $schemaName, string $tableRelation, ?string $tableName, array &$params, ): string { $conditions = []; assert(($tableName === null) === ($schemaName === null)); if ($tableName !== null && $schemaName !== null) { $conditions[] = sprintf('%s.nspname = ?', $namespaceRelation); $params[] = $schemaName; $conditions[] = sprintf('%s.relname = ?', $tableRelation); $params[] = $tableName; } $conditions[] = $this->buildNamespaceNamePredicate($namespaceRelation . '.nspname'); $conditions[] = $this->buildTablePredicate($tableRelation . '.relkind', $tableRelation . '.relname'); return implode(' AND ', $conditions); } /** * {@inheritDoc} * * @link https://www.postgresql.org/docs/current/catalog-pg-views.html */ public function getAllViews(): iterable { $sql = sprintf( <<<'SQL' SELECT schemaname, viewname, definition FROM pg_views WHERE %s ORDER BY schemaname, viewname SQL, $this->buildNamespaceNamePredicate('schemaname'), ); foreach ($this->connection->iterateNumeric($sql) as $row) { yield new ViewMetadataRow(...$row); } } /** {@inheritDoc} */ public function getAllSequences(): iterable { // Using information_schema here instead of pg_sequences since the latter also includes sequences that are owned // by serial columns. We want to include only the user-defined ones. $sql = sprintf( <<<'SQL' SELECT sequence_schema, sequence_name, increment, minimum_value FROM information_schema.sequences WHERE sequence_catalog = CURRENT_DATABASE() AND %s ORDER BY sequence_schema, sequence_name SQL, $this->buildNamespaceNamePredicate('sequence_schema'), ); foreach ($this->connection->iterateNumeric($sql) as $row) { yield new SequenceMetadataRow( schemaName: $row[0], sequenceName: $row[1], allocationSize: (int) $row[2], initialValue: (int) $row[3], cacheSize: null, ); } } private function buildNamespaceNamePredicate(string $columnName): string { return sprintf("%1\$s NOT LIKE 'pg\_%%' AND %1\$s != 'information_schema'", $columnName); } /** * * @link https://www.postgresql.org/docs/current/catalog-pg-class.html */ private function buildTablePredicate(string $kindColumnName, string $nameColumnName): string { return sprintf( // r = ordinary table, p = partitioned table, "%s IN ('r', 'p') AND %s NOT IN ('geometry_columns', 'spatial_ref_sys')", $kindColumnName, $nameColumnName, ); } }