diff --git a/CHANGELOG.md b/CHANGELOG.md index 982fb4413..af9086ebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ - Enh #415: Implement `CaseExpressionBuilder` class (@Tigrov) - Enh #420: Provide `yiisoft/db-implementation` virtual package (@vjik) - Enh #424, #425, #428: Adapt to conditions refactoring in `yiisoft/db` package (@vjik) +- Enh #431: Remove `TableSchema` class and refactor `Schema` class (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/src/DDLQueryBuilder.php b/src/DDLQueryBuilder.php index ceead8cef..e409ee444 100644 --- a/src/DDLQueryBuilder.php +++ b/src/DDLQueryBuilder.php @@ -86,7 +86,7 @@ public function checkIntegrity(string $schema = '', string $table = '', bool $ch $tableNames = []; $viewNames = []; - if ($schema !== null) { + if ($schema !== '') { $tableNames = $table ? [$table] : $schemaInstance->getTableNames($schema); $viewNames = $schemaInstance->getViewNames($schema); } diff --git a/src/Schema.php b/src/Schema.php index 6e079917c..210d97855 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -14,6 +14,8 @@ use Yiisoft\Db\Helper\DbArrayHelper; use Yiisoft\Db\Pgsql\Column\SequenceColumnInterface; use Yiisoft\Db\Schema\Column\ColumnInterface; +use Yiisoft\Db\Schema\SchemaInterface; +use Yiisoft\Db\Schema\TableSchema; use Yiisoft\Db\Schema\TableSchemaInterface; use function array_change_key_case; @@ -22,11 +24,9 @@ use function array_unique; use function array_values; use function explode; -use function in_array; use function is_string; use function preg_match; use function str_replace; -use function str_starts_with; use function substr; /** @@ -36,7 +36,7 @@ * column_name: string, * data_type: string, * type_type: string|null, - * type_scheme: string|null, + * type_scheme: string, * character_maximum_length: int|string, * column_comment: string|null, * is_nullable: bool|string, @@ -81,27 +81,20 @@ */ final class Schema extends AbstractPdoSchema { - /** - * @var string|null The default schema used for the current session. - */ - protected string|null $defaultSchema = 'public'; - - protected function resolveTableName(string $name): TableSchemaInterface + protected function findConstraints(TableSchemaInterface $table): void { - $resolvedName = new TableSchema(); - - $parts = $this->db->getQuoter()->getTableNameParts($name); - $resolvedName->name($parts['name']); - $resolvedName->schemaName($parts['schemaName'] ?? $this->defaultSchema); - - $resolvedName->fullName( - $resolvedName->getSchemaName() !== $this->defaultSchema ? - implode('.', $parts) : $resolvedName->getName() - ); + $tableName = $this->resolveFullName($table->getName(), $table->getSchemaName()); - return $resolvedName; + $table->checks(...$this->getTableMetadata($tableName, SchemaInterface::CHECKS)); + $table->foreignKeys(...$this->getTableMetadata($tableName, SchemaInterface::FOREIGN_KEYS)); + $table->indexes(...$this->getTableMetadata($tableName, SchemaInterface::INDEXES)); } + /** + * @var string The default schema used for the current session. + */ + protected string $defaultSchema = 'public'; + protected function findSchemaNames(): array { $sql = <<db->createCommand($sql, [ - ':schemaName' => $tableSchema->getSchemaName(), + ':schemaName' => $tableSchema->getSchemaName() ?: $this->defaultSchema, ':tableName' => $tableSchema->getName(), ])->queryScalar(); @@ -154,10 +147,10 @@ protected function findTableNames(string $schema = ''): array protected function loadTableSchema(string $name): TableSchemaInterface|null { - $table = $this->resolveTableName($name); - $this->findTableComment($table); + $table = new TableSchema(...$this->db->getQuoter()->getTableNameParts($name)); if ($this->findColumns($table)) { + $this->findTableComment($table); $this->findConstraints($table); return $table; } @@ -165,12 +158,6 @@ protected function loadTableSchema(string $name): TableSchemaInterface|null return null; } - protected function loadTablePrimaryKey(string $tableName): Index|null - { - /** @var Index|null */ - return $this->loadTableConstraints($tableName, self::PRIMARY_KEY); - } - protected function loadTableForeignKeys(string $tableName): array { /** @var ForeignKey[] */ @@ -224,7 +211,7 @@ protected function loadTableIndexes(string $tableName): array * > $index */ foreach ($indexes as $name => $index) { - $result[] = new Index( + $result[$name] = new Index( $name, array_column($index, 'column_name'), $index[0]['is_unique'], @@ -235,12 +222,6 @@ protected function loadTableIndexes(string $tableName): array return $result; } - protected function loadTableUniques(string $tableName): array - { - /** @var Index[] */ - return $this->loadTableConstraints($tableName, self::UNIQUES); - } - protected function loadTableChecks(string $tableName): array { /** @var Check[] */ @@ -270,143 +251,6 @@ protected function findViewNames(string $schema = ''): array return $this->db->createCommand($sql, [':schemaName' => $schema])->queryColumn(); } - /** - * Collects the foreign key column details for the given table. - * - * @param TableSchemaInterface $table The table metadata - */ - protected function findConstraints(TableSchemaInterface $table): void - { - /** - * We need to extract the constraints de hard way since: - * {@see https://www.postgresql.org/message-id/26677.1086673982@sss.pgh.pa.us} - */ - - $sql = << $rows */ - $rows = $this->db->createCommand($sql, [ - ':schemaName' => $table->getSchemaName(), - ':tableName' => $table->getName(), - ])->queryAll(); - - foreach ($rows as $constraint) { - /** @psalm-var FindConstraintArray $constraint */ - $constraint = array_change_key_case($constraint); - - if ($constraint['foreign_table_schema'] !== $this->defaultSchema) { - $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name']; - } else { - $foreignTable = $constraint['foreign_table_name']; - } - - $name = $constraint['constraint_name']; - - if (!isset($constraints[$name])) { - $constraints[$name] = [ - 'tableName' => $foreignTable, - 'columns' => [], - ]; - } - - $constraints[$name]['columns'][$constraint['column_name']] = $constraint['foreign_column_name']; - } - - /** - * @psalm-var array{tableName: string, columns: array} $constraint - */ - foreach ($constraints as $foreingKeyName => $constraint) { - $table->foreignKey( - (string) $foreingKeyName, - [$constraint['tableName'], ...$constraint['columns']] - ); - } - } - - /** - * Gets information about given table unique indexes. - * - * @param TableSchemaInterface $table The table metadata. - * - * @return array With index and column names. - */ - protected function getUniqueIndexInformation(TableSchemaInterface $table): array - { - $sql = <<<'SQL' - SELECT - i.relname as indexname, - pg_get_indexdef(idx.indexrelid, k + 1, TRUE) AS columnname - FROM ( - SELECT *, generate_subscripts(indkey, 1) AS k - FROM pg_index - ) idx - INNER JOIN pg_class i ON i.oid = idx.indexrelid - INNER JOIN pg_class c ON c.oid = idx.indrelid - INNER JOIN pg_namespace ns ON c.relnamespace = ns.oid - WHERE idx.indisprimary = FALSE AND idx.indisunique = TRUE - AND c.relname = :tableName AND ns.nspname = :schemaName - ORDER BY i.relname, k - SQL; - - return $this->db->createCommand($sql, [ - ':schemaName' => $table->getSchemaName(), - ':tableName' => $table->getName(), - ])->queryAll(); - } - - public function findUniqueIndexes(TableSchemaInterface $table): array - { - $uniqueIndexes = []; - - /** @psalm-var array{indexname: string, columnname: string} $row */ - foreach ($this->getUniqueIndexInformation($table) as $row) { - /** @psalm-var array{indexname: string, columnname: string} $row */ - $row = array_change_key_case($row); - - $column = $row['columnname']; - - if (str_starts_with($column, '"') && str_ends_with($column, '"')) { - /** - * postgres will quote names that aren't lowercase-only. - * - * {@see https://github.com/yiisoft/yii2/issues/10613} - */ - $column = substr($column, 1, -1); - } - - $uniqueIndexes[$row['indexname']][] = $column; - } - - return $uniqueIndexes; - } - /** * Collects the metadata of table columns. * @@ -492,7 +336,7 @@ protected function findColumns(TableSchemaInterface $table): bool a.attnum; SQL; - $schemaName = $table->getSchemaName(); + $schemaName = $table->getSchemaName() ?: $this->defaultSchema; $tableName = $table->getName(); $columns = $this->db->createCommand($sql, [ @@ -516,18 +360,28 @@ protected function findColumns(TableSchemaInterface $table): bool $table->column($info['column_name'], $column); - if ($column->isPrimaryKey()) { - $table->primaryKey($info['column_name']); - - if ($column instanceof SequenceColumnInterface && $table->getSequenceName() === null) { - $table->sequenceName($column->getSequenceName()); - } + if ($column instanceof SequenceColumnInterface + && $column->isPrimaryKey() + && $table->getSequenceName() === null + ) { + $table->sequenceName($column->getSequenceName()); } } return true; } + protected function resolveFullName(string $name, string $schemaName = ''): string + { + $quoter = $this->db->getQuoter(); + $rawName = $quoter->getRawTableName($name); + + return match ($schemaName) { + '', 'pg_catalog', $this->defaultSchema => $rawName, + default => $quoter->getRawTableName($schemaName) . ".$rawName", + }; + } + /** * @psalm-param array{ * "pgsql:oid": int, @@ -604,13 +458,10 @@ protected function loadResultColumn(array $metadata): ColumnInterface|null [':oid' => $metadata['pgsql:oid']] )->queryOne(); - $dbType = match ($typeInfo['schema']) { - $this->defaultSchema, 'pg_catalog' => $typeInfo['schema'] . '.' . $typeInfo['typname'], - default => $typeInfo['typname'], - }; + $dbType = $this->resolveFullName($typeInfo['typname'], $typeInfo['schema']); if ($typeInfo['typtype'] === 'c') { - $structured = $this->resolveTableName($dbType); + $structured = new TableSchema($typeInfo['typname'], $typeInfo['schema']); if ($this->findColumns($structured)) { $columnInfo['columns'] = $structured->getColumns(); @@ -645,11 +496,7 @@ protected function loadResultColumn(array $metadata): ColumnInterface|null private function loadColumn(array $info): ColumnInterface { $columnFactory = $this->db->getColumnFactory(); - $dbType = $info['data_type']; - - if (!in_array($info['type_scheme'], [$this->defaultSchema, 'pg_catalog'], true)) { - $dbType = $info['type_scheme'] . '.' . $dbType; - } + $dbType = $this->resolveFullName($info['data_type'], $info['type_scheme']); $columnInfo = [ 'autoIncrement' => (bool) $info['is_autoinc'], @@ -669,7 +516,7 @@ private function loadColumn(array $info): ColumnInterface ]; if ($info['type_type'] === 'c') { - $structured = $this->resolveTableName($dbType); + $structured = new TableSchema($info['data_type'], $info['type_scheme']); if ($this->findColumns($structured)) { $columnInfo['columns'] = $structured->getColumns(); @@ -698,7 +545,7 @@ private function loadColumn(array $info): ColumnInterface $defaultValue = null; $columnInfo['sequenceName'] = $matches[1]; } elseif ($info['sequence_name'] !== null) { - $columnInfo['sequenceName'] = $this->resolveTableName($info['sequence_name'])->getFullName(); + $columnInfo['sequenceName'] = $this->clearFullName($info['sequence_name']); } $columnInfo['defaultValueRaw'] = $defaultValue; @@ -712,14 +559,12 @@ private function loadColumn(array $info): ColumnInterface * * @param string $tableName The table name. * @param string $returnType The return type: - * - primaryKey * - foreignKeys - * - uniques * - checks * - * @return Check[]|ForeignKey[]|Index|Index[]|null Constraints. + * @return Check[]|ForeignKey[] Constraints. */ - private function loadTableConstraints(string $tableName, string $returnType): array|Index|null + private function loadTableConstraints(string $tableName, string $returnType): array { $sql = << null, self::FOREIGN_KEYS => [], - self::UNIQUES => [], self::CHECKS => [], ]; + /** + * @var string $type + * @psalm-var array $names + */ foreach ($constraints as $type => $names) { - /** - * @var string $name - * @psalm-var ConstraintArray $constraint - */ foreach ($names as $name => $constraint) { match ($type) { - 'p' => $result[self::PRIMARY_KEY] = new Index( - $name, - array_column($constraint, 'column_name'), - true, - true, - ), - 'f' => $result[self::FOREIGN_KEYS][] = new ForeignKey( + 'f' => $result[self::FOREIGN_KEYS][$name] = new ForeignKey( $name, array_values(array_unique(array_column($constraint, 'column_name'))), $constraint[0]['foreign_table_schema'], @@ -803,12 +640,7 @@ private function loadTableConstraints(string $tableName, string $returnType): ar $actionTypes[$constraint[0]['on_delete']] ?? null, $actionTypes[$constraint[0]['on_update']] ?? null, ), - 'u' => $result[self::UNIQUES][] = new Index( - $name, - array_column($constraint, 'column_name'), - true, - ), - 'c' => $result[self::CHECKS][] = new Check( + 'c' => $result[self::CHECKS][$name] = new Check( $name, array_column($constraint, 'column_name'), $constraint[0]['check_expr'], diff --git a/src/TableSchema.php b/src/TableSchema.php deleted file mode 100644 index f55ae789d..000000000 --- a/src/TableSchema.php +++ /dev/null @@ -1,14 +0,0 @@ -getConnection(true); @@ -57,11 +48,7 @@ public function testBooleanDefaultValues(): void $db->close(); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\SchemaProvider::columns - * - * @throws Exception - */ + #[DataProviderExternal(SchemaProvider::class, 'columns')] public function testColumns(array $columns, string $tableName): void { $db = $this->getConnection(); @@ -77,11 +64,6 @@ public function testColumns(array $columns, string $tableName): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - * @throws Throwable - */ public function testColumnTypeMapNoExist(): void { $db = $this->getConnection(); @@ -104,10 +86,6 @@ public function testColumnTypeMapNoExist(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - */ public function testGeneratedValues(): void { $this->fixture = 'pgsql12.sql'; @@ -130,10 +108,6 @@ public function testGeneratedValues(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - */ public function testGetDefaultSchema(): void { $db = $this->getConnection(); @@ -145,10 +119,6 @@ public function testGetDefaultSchema(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - */ public function testGetSchemaDefaultValues(): void { $db = $this->getConnection(); @@ -161,11 +131,6 @@ public function testGetSchemaDefaultValues(): void $db->getSchema()->getSchemaDefaultValues(); } - /** - * @throws Exception - * @throws InvalidConfigException - * @throws NotSupportedException - */ public function testGetSchemaNames(): void { $db = $this->getConnection(true); @@ -183,11 +148,6 @@ public function testGetSchemaNames(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - * @throws NotSupportedException - */ public function testGetTableSchemasNotSchemaDefault(): void { $db = $this->getConnection(true); @@ -206,9 +166,6 @@ public function testGetTableSchemasNotSchemaDefault(): void /** * @link https://github.com/yiisoft/yii2/issues/12483 - * - * @throws Exception - * @throws Throwable */ public function testParenthesisDefaultValue(): void { @@ -244,10 +201,6 @@ public function testParenthesisDefaultValue(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - */ public function testPartitionedTable(): void { $this->fixture = 'pgsql10.sql'; @@ -265,11 +218,6 @@ public function testPartitionedTable(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - * @throws Throwable - */ public function testSequenceName(): void { $db = $this->getConnection(true); @@ -306,11 +254,7 @@ public function testSequenceName(): void $db->close(); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\SchemaProvider::tableSchemaCacheWithTablePrefixes - * - * @throws Exception - */ + #[DataProviderExternal(SchemaProvider::class, 'tableSchemaCacheWithTablePrefixes')] public function testTableSchemaCacheWithTablePrefixes( string $tablePrefix, string $tableName, @@ -351,47 +295,26 @@ public function testTableSchemaCacheWithTablePrefixes( $db->close(); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\SchemaProvider::constraints - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\SchemaProvider::constraintsOfView - * - * @throws Exception - * @throws JsonException - */ + #[DataProviderExternal(SchemaProvider::class, 'constraints')] + #[DataProviderExternal(SchemaProvider::class, 'constraintsOfView')] public function testTableSchemaConstraints(string $tableName, string $type, mixed $expected): void { parent::testTableSchemaConstraints($tableName, $type, $expected); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\SchemaProvider::constraints - * - * @throws Exception - * @throws InvalidConfigException - * @throws JsonException - * @throws NotSupportedException - */ + #[DataProviderExternal(SchemaProvider::class, 'constraints')] public function testTableSchemaConstraintsWithPdoLowercase(string $tableName, string $type, mixed $expected): void { parent::testTableSchemaConstraintsWithPdoLowercase($tableName, $type, $expected); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\SchemaProvider::constraints - * - * @throws Exception - * @throws JsonException - */ + #[DataProviderExternal(SchemaProvider::class, 'constraints')] public function testTableSchemaConstraintsWithPdoUppercase(string $tableName, string $type, mixed $expected): void { parent::testTableSchemaConstraintsWithPdoUppercase($tableName, $type, $expected); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\SchemaProvider::tableSchemaWithDbSchemes - * - * @throws Exception - */ + #[DataProviderExternal(SchemaProvider::class, 'tableSchemaWithDbSchemes')] public function testTableSchemaWithDbSchemes( string $tableName, string $expectedTableName, @@ -426,9 +349,6 @@ function ($params) use ($expectedTableName, $expectedSchemaName) { /** * @link https://github.com/yiisoft/yii2/issues/14192 - * - * @throws Exception - * @throws Throwable */ public function testTimestampNullDefaultValue(): void { @@ -574,7 +494,7 @@ public function testGetViewNames(): void $db->close(); } - /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\StructuredTypeProvider::columns */ + #[DataProviderExternal(StructuredTypeProvider::class, 'columns')] public function testStructuredTypeColumn(array $columns, string $tableName): void { $this->assertTableColumns($columns, $tableName); @@ -591,16 +511,15 @@ public function testTableIndexes(): void $db = $this->getConnection(true); $schema = $db->getSchema(); - /** @var Index[] $tableIndexes */ $tableIndexes = $schema->getTableIndexes('table_index'); $this->assertEquals( [ - new Index('table_index_pkey', ['id'], true, true), - new Index('table_index_one_unique_key', ['one_unique'], true), - new Index('table_index_two_unique_1_two_unique_2_key', ['two_unique_1', 'two_unique_2'], true), - new Index('table_index_unique_index_non_unique_index_idx', ['unique_index'], true), - new Index('table_index_non_unique_index_unique_index_idx', ['non_unique_index']), + 'table_index_pkey' => new Index('table_index_pkey', ['id'], true, true), + 'table_index_one_unique_key' => new Index('table_index_one_unique_key', ['one_unique'], true), + 'table_index_two_unique_1_two_unique_2_key' => new Index('table_index_two_unique_1_two_unique_2_key', ['two_unique_1', 'two_unique_2'], true), + 'table_index_unique_index_non_unique_index_idx' => new Index('table_index_unique_index_non_unique_index_idx', ['unique_index'], true), + 'table_index_non_unique_index_unique_index_idx' => new Index('table_index_non_unique_index_unique_index_idx', ['non_unique_index']), ], $tableIndexes ); diff --git a/tests/Support/Fixture/pgsql.sql b/tests/Support/Fixture/pgsql.sql index e81b00bcb..5bb5cc4bb 100644 --- a/tests/Support/Fixture/pgsql.sql +++ b/tests/Support/Fixture/pgsql.sql @@ -127,7 +127,7 @@ CREATE TABLE "composite_fk" ( order_id integer NOT NULL, item_id integer NOT NULL, PRIMARY KEY (id), - CONSTRAINT FK_composite_fk_order_item FOREIGN KEY (order_id, item_id) REFERENCES "order_item" (order_id, item_id) ON DELETE CASCADE + CONSTRAINT "FK_composite_fk_order_item" FOREIGN KEY (order_id, item_id) REFERENCES "order_item" (order_id, item_id) ON DELETE CASCADE ); CREATE TABLE "null_values" (