diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 814f402e1a8..3e132f30040 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -42,6 +42,7 @@ jobs: dependencies: - "highest" extension: + - "sqlite3" - "pdo_sqlite" include: - os: "ubuntu-20.04" @@ -49,7 +50,7 @@ jobs: dependencies: "lowest" extension: "pdo_sqlite" - os: "ubuntu-22.04" - php-version: "8.1" + php-version: "8.4" dependencies: "highest" extension: "sqlite3" diff --git a/UPGRADE.md b/UPGRADE.md index 32d5945e257..b1810b7adb3 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -965,6 +965,12 @@ The following methods have been removed. | `QueryCacheProfile` | `setResultCacheDriver()` | `setResultCache()` | | `QueryCacheProfile` | `getResultCacheDriver()` | `getResultCache()` | +# Upgrade to 3.10 + +The `doctrine/cache` package is now an optional dependency. If you are using the +`Doctrine\DBAL\Cache` classes, you need to require the `doctrine/cache` package +explicitly. + # Upgrade to 3.8 ## Deprecated lock-related `AbstractPlatform` methods diff --git a/src/Platforms/SQLServerPlatform.php b/src/Platforms/SQLServerPlatform.php index 18324eb11e4..fb0de8acd4e 100644 --- a/src/Platforms/SQLServerPlatform.php +++ b/src/Platforms/SQLServerPlatform.php @@ -1198,11 +1198,17 @@ protected function getLikeWildcardCharacters(): string protected function getCommentOnTableSQL(string $tableName, string $comment): string { + if (str_contains($tableName, '.')) { + [$schemaName, $tableName] = explode('.', $tableName); + } else { + $schemaName = 'dbo'; + } + return $this->getAddExtendedPropertySQL( 'MS_Description', $comment, 'SCHEMA', - $this->quoteStringLiteral('dbo'), + $this->quoteStringLiteral($schemaName), 'TABLE', $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), ); diff --git a/src/Schema/PostgreSQLSchemaManager.php b/src/Schema/PostgreSQLSchemaManager.php index ae09979d6a5..fd323872994 100644 --- a/src/Schema/PostgreSQLSchemaManager.php +++ b/src/Schema/PostgreSQLSchemaManager.php @@ -161,9 +161,16 @@ protected function _getPortableTableIndexesList(array $tableIndexes, string $tab foreach ($tableIndexes as $row) { $colNumbers = array_map('intval', explode(' ', $row['indkey'])); $columnNameSql = sprintf( - 'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC', + <<<'SQL' + SELECT attnum, + quote_ident(attname) AS attname + FROM pg_attribute + WHERE attrelid = %d + AND attnum IN (%s) + ORDER BY attnum + SQL, $row['indrelid'], - implode(' ,', $colNumbers), + implode(', ', $colNumbers), ); $indexColumns = $this->connection->fetchAllAssociative($columnNameSql); @@ -412,7 +419,7 @@ protected function selectTableColumns(string $databaseName, ?string $tableName = $sql = 'SELECT '; if ($tableName === null) { - $sql .= 'c.relname AS table_name, n.nspname AS schema_name,'; + $sql .= 'quote_ident(c.relname) AS table_name, quote_ident(n.nspname) AS schema_name,'; } $sql .= sprintf(<<<'SQL' @@ -478,7 +485,7 @@ protected function selectIndexColumns(string $databaseName, ?string $tableName = $sql = 'SELECT'; if ($tableName === null) { - $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,'; + $sql .= ' quote_ident(tc.relname) AS table_name, quote_ident(tn.nspname) AS schema_name,'; } $sql .= <<<'SQL' @@ -512,7 +519,7 @@ protected function selectForeignKeyColumns(string $databaseName, ?string $tableN $sql = 'SELECT'; if ($tableName === null) { - $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,'; + $sql .= ' quote_ident(tc.relname) AS table_name, quote_ident(tn.nspname) AS schema_name,'; } $sql .= <<<'SQL' @@ -540,7 +547,8 @@ protected function selectForeignKeyColumns(string $databaseName, ?string $tableN protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array { $sql = <<<'SQL' -SELECT c.relname, +SELECT quote_ident(n.nspname) AS schema_name, + quote_ident(c.relname) AS table_name, CASE c.relpersistence WHEN 'u' THEN true ELSE false END as unlogged, obj_description(c.oid, 'pg_class') AS comment FROM pg_class c @@ -552,7 +560,12 @@ protected function fetchTableOptionsByTable(string $databaseName, ?string $table $sql .= ' WHERE ' . implode(' AND ', $conditions); - return $this->connection->fetchAllAssociativeIndexed($sql); + $tableOptions = []; + foreach ($this->connection->iterateAssociative($sql) as $row) { + $tableOptions[$this->_getPortableTableDefinition($row)] = $row; + } + + return $tableOptions; } /** @return list */ diff --git a/src/Schema/SQLServerSchemaManager.php b/src/Schema/SQLServerSchemaManager.php index e0a74ce2a81..18b9254e9b6 100644 --- a/src/Schema/SQLServerSchemaManager.php +++ b/src/Schema/SQLServerSchemaManager.php @@ -186,9 +186,15 @@ protected function _getPortableTableForeignKeysList(array $tableForeignKeys): ar $name = $tableForeignKey['ForeignKey']; if (! isset($foreignKeys[$name])) { + $referencedTableName = $tableForeignKey['ReferenceTableName']; + + if ($tableForeignKey['ReferenceSchemaName'] !== 'dbo') { + $referencedTableName = $tableForeignKey['ReferenceSchemaName'] . '.' . $referencedTableName; + } + $foreignKeys[$name] = [ 'local_columns' => [$tableForeignKey['ColumnName']], - 'foreign_table' => $tableForeignKey['ReferenceTableName'], + 'foreign_table' => $referencedTableName, 'foreign_columns' => [$tableForeignKey['ReferenceColumnName']], 'name' => $name, 'options' => [ @@ -443,31 +449,29 @@ protected function fetchTableOptionsByTable(string $databaseName, ?string $table { $sql = <<<'SQL' SELECT - tbl.name, + scm.name AS schema_name, + tbl.name AS table_name, p.value AS [table_comment] FROM sys.tables AS tbl + JOIN sys.schemas AS scm + ON tbl.schema_id = scm.schema_id INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1 SQL; - $conditions = ["SCHEMA_NAME(tbl.schema_id) = N'dbo'", "p.name = N'MS_Description'"]; - $params = []; + $conditions = ["p.name = N'MS_Description'"]; if ($tableName !== null) { - $conditions[] = "tbl.name = N'" . $tableName . "'"; + $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'tbl.name'); } $sql .= ' WHERE ' . implode(' AND ', $conditions); - /** @var array> $metadata */ - $metadata = $this->connection->executeQuery($sql, $params) - ->fetchAllAssociativeIndexed(); - $tableOptions = []; - foreach ($metadata as $table => $data) { + foreach ($this->connection->iterateAssociative($sql) as $data) { $data = array_change_key_case($data, CASE_LOWER); - $tableOptions[$table] = [ + $tableOptions[$this->_getPortableTableDefinition($data)] = [ 'comment' => $data['table_comment'], ]; } diff --git a/tests/Functional/Schema/OracleSchemaManagerTest.php b/tests/Functional/Schema/OracleSchemaManagerTest.php index 8f57cd0f74a..967a169039d 100644 --- a/tests/Functional/Schema/OracleSchemaManagerTest.php +++ b/tests/Functional/Schema/OracleSchemaManagerTest.php @@ -69,118 +69,6 @@ public function testAlterTableColumnNotNull(): void self::assertTrue($columns['bar']->getNotnull()); } - public function testListTableDetailsWithDifferentIdentifierQuotingRequirements(): void - { - $primaryTableName = '"Primary_Table"'; - $offlinePrimaryTable = new Table($primaryTableName); - $offlinePrimaryTable->addColumn( - '"Id"', - Types::INTEGER, - ['autoincrement' => true], - ); - $offlinePrimaryTable->addColumn('select', Types::INTEGER); - $offlinePrimaryTable->addColumn('foo', Types::INTEGER); - $offlinePrimaryTable->addColumn('BAR', Types::INTEGER); - $offlinePrimaryTable->addColumn('"BAZ"', Types::INTEGER); - $offlinePrimaryTable->addIndex(['select'], 'from'); - $offlinePrimaryTable->addIndex(['foo'], 'foo_index'); - $offlinePrimaryTable->addIndex(['BAR'], 'BAR_INDEX'); - $offlinePrimaryTable->addIndex(['"BAZ"'], 'BAZ_INDEX'); - $offlinePrimaryTable->setPrimaryKey(['"Id"']); - - $foreignTableName = 'foreign'; - $offlineForeignTable = new Table($foreignTableName); - $offlineForeignTable->addColumn('id', Types::INTEGER, ['autoincrement' => true]); - $offlineForeignTable->addColumn('"Fk"', Types::INTEGER); - $offlineForeignTable->addIndex(['"Fk"'], '"Fk_index"'); - $offlineForeignTable->addForeignKeyConstraint( - $primaryTableName, - ['"Fk"'], - ['"Id"'], - [], - '"Primary_Table_Fk"', - ); - $offlineForeignTable->setPrimaryKey(['id']); - - $this->dropTableIfExists($foreignTableName); - $this->dropTableIfExists($primaryTableName); - - $this->schemaManager->createTable($offlinePrimaryTable); - $this->schemaManager->createTable($offlineForeignTable); - - $onlinePrimaryTable = $this->schemaManager->introspectTable($primaryTableName); - $onlineForeignTable = $this->schemaManager->introspectTable($foreignTableName); - - $platform = $this->connection->getDatabasePlatform(); - - // Primary table assertions - self::assertSame($primaryTableName, $onlinePrimaryTable->getQuotedName($platform)); - - self::assertTrue($onlinePrimaryTable->hasColumn('"Id"')); - self::assertSame('"Id"', $onlinePrimaryTable->getColumn('"Id"')->getQuotedName($platform)); - - $onlinePrimaryTablePrimaryKey = $onlinePrimaryTable->getPrimaryKey(); - self::assertNotNull($onlinePrimaryTablePrimaryKey); - self::assertSame(['"Id"'], $onlinePrimaryTablePrimaryKey->getQuotedColumns($platform)); - - self::assertTrue($onlinePrimaryTable->hasColumn('select')); - self::assertSame('"select"', $onlinePrimaryTable->getColumn('select')->getQuotedName($platform)); - - self::assertTrue($onlinePrimaryTable->hasColumn('foo')); - self::assertSame('FOO', $onlinePrimaryTable->getColumn('foo')->getQuotedName($platform)); - - self::assertTrue($onlinePrimaryTable->hasColumn('BAR')); - self::assertSame('BAR', $onlinePrimaryTable->getColumn('BAR')->getQuotedName($platform)); - - self::assertTrue($onlinePrimaryTable->hasColumn('"BAZ"')); - self::assertSame('BAZ', $onlinePrimaryTable->getColumn('"BAZ"')->getQuotedName($platform)); - - self::assertTrue($onlinePrimaryTable->hasIndex('from')); - self::assertTrue($onlinePrimaryTable->getIndex('from')->hasColumnAtPosition('"select"')); - self::assertSame(['"select"'], $onlinePrimaryTable->getIndex('from')->getQuotedColumns($platform)); - - self::assertTrue($onlinePrimaryTable->hasIndex('foo_index')); - self::assertTrue($onlinePrimaryTable->getIndex('foo_index')->hasColumnAtPosition('foo')); - self::assertSame(['FOO'], $onlinePrimaryTable->getIndex('foo_index')->getQuotedColumns($platform)); - - self::assertTrue($onlinePrimaryTable->hasIndex('BAR_INDEX')); - self::assertTrue($onlinePrimaryTable->getIndex('BAR_INDEX')->hasColumnAtPosition('BAR')); - self::assertSame(['BAR'], $onlinePrimaryTable->getIndex('BAR_INDEX')->getQuotedColumns($platform)); - - self::assertTrue($onlinePrimaryTable->hasIndex('BAZ_INDEX')); - self::assertTrue($onlinePrimaryTable->getIndex('BAZ_INDEX')->hasColumnAtPosition('"BAZ"')); - self::assertSame(['BAZ'], $onlinePrimaryTable->getIndex('BAZ_INDEX')->getQuotedColumns($platform)); - - // Foreign table assertions - self::assertTrue($onlineForeignTable->hasColumn('id')); - self::assertSame('ID', $onlineForeignTable->getColumn('id')->getQuotedName($platform)); - - $onlineForeignTablePrimaryKey = $onlineForeignTable->getPrimaryKey(); - self::assertNotNull($onlineForeignTablePrimaryKey); - self::assertSame(['ID'], $onlineForeignTablePrimaryKey->getQuotedColumns($platform)); - - self::assertTrue($onlineForeignTable->hasColumn('"Fk"')); - self::assertSame('"Fk"', $onlineForeignTable->getColumn('"Fk"')->getQuotedName($platform)); - - self::assertTrue($onlineForeignTable->hasIndex('"Fk_index"')); - self::assertTrue($onlineForeignTable->getIndex('"Fk_index"')->hasColumnAtPosition('"Fk"')); - self::assertSame(['"Fk"'], $onlineForeignTable->getIndex('"Fk_index"')->getQuotedColumns($platform)); - - self::assertTrue($onlineForeignTable->hasForeignKey('"Primary_Table_Fk"')); - self::assertSame( - $primaryTableName, - $onlineForeignTable->getForeignKey('"Primary_Table_Fk"')->getQuotedForeignTableName($platform), - ); - self::assertSame( - ['"Fk"'], - $onlineForeignTable->getForeignKey('"Primary_Table_Fk"')->getQuotedLocalColumns($platform), - ); - self::assertSame( - ['"Id"'], - $onlineForeignTable->getForeignKey('"Primary_Table_Fk"')->getQuotedForeignColumns($platform), - ); - } - public function testListTableColumnsSameTableNamesInDifferentSchemas(): void { $table = $this->createListTableColumns(); diff --git a/tests/Functional/Schema/PostgreSQLSchemaManagerTest.php b/tests/Functional/Schema/PostgreSQLSchemaManagerTest.php index 591340c8864..dba9d6be230 100644 --- a/tests/Functional/Schema/PostgreSQLSchemaManagerTest.php +++ b/tests/Functional/Schema/PostgreSQLSchemaManagerTest.php @@ -22,7 +22,6 @@ use PHPUnit\Framework\Attributes\DataProvider; use function array_map; -use function array_pop; use function count; use function sprintf; use function strtolower; @@ -121,44 +120,6 @@ public function testAlterTableAutoIncrementDrop(): void self::assertFalse($tableFinal->getColumn('id')->getAutoincrement()); } - public function testTableWithSchema(): void - { - $this->connection->executeStatement('CREATE SCHEMA nested'); - - $nestedRelatedTable = new Table('nested.schemarelated'); - $column = $nestedRelatedTable->addColumn('id', Types::INTEGER); - $column->setAutoincrement(true); - $nestedRelatedTable->setPrimaryKey(['id']); - - $nestedSchemaTable = new Table('nested.schematable'); - $column = $nestedSchemaTable->addColumn('id', Types::INTEGER); - $column->setAutoincrement(true); - $nestedSchemaTable->setPrimaryKey(['id']); - $nestedSchemaTable->addForeignKeyConstraint($nestedRelatedTable->getName(), ['id'], ['id']); - - $this->schemaManager->createTable($nestedRelatedTable); - $this->schemaManager->createTable($nestedSchemaTable); - - $tableNames = $this->schemaManager->listTableNames(); - self::assertContains('nested.schematable', $tableNames); - - $tables = $this->schemaManager->listTables(); - self::assertNotNull($this->findTableByName($tables, 'nested.schematable')); - - $nestedSchemaTable = $this->schemaManager->introspectTable('nested.schematable'); - self::assertTrue($nestedSchemaTable->hasColumn('id')); - - $primaryKey = $nestedSchemaTable->getPrimaryKey(); - self::assertNotNull($primaryKey); - self::assertEquals(['id'], $primaryKey->getColumns()); - - $relatedFks = $nestedSchemaTable->getForeignKeys(); - self::assertCount(1, $relatedFks); - $relatedFk = array_pop($relatedFks); - self::assertNotNull($relatedFk); - self::assertEquals('nested.schemarelated', $relatedFk->getForeignTableName()); - } - public function testListSameTableNameColumnsWithDifferentSchema(): void { $this->connection->executeStatement('CREATE SCHEMA another'); diff --git a/tests/Functional/Schema/SchemaManagerFunctionalTestCase.php b/tests/Functional/Schema/SchemaManagerFunctionalTestCase.php index 7ca35dee0d4..ef03a6c6d70 100644 --- a/tests/Functional/Schema/SchemaManagerFunctionalTestCase.php +++ b/tests/Functional/Schema/SchemaManagerFunctionalTestCase.php @@ -7,6 +7,7 @@ use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException; use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\DB2Platform; use Doctrine\DBAL\Platforms\OraclePlatform; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Platforms\SQLitePlatform; @@ -1335,6 +1336,96 @@ private function createReservedKeywordTables(): void $schemaManager->createSchemaObjects($schema); } + /** @throws Exception */ + public function testQuotedIdentifiers(): void + { + $platform = $this->connection->getDatabasePlatform(); + + if ($platform instanceof DB2Platform) { + self::markTestIncomplete( + 'Introspection of lower-case identifiers as quoted is currently not implemented on IBM DB2.', + ); + } + + if (! $platform instanceof OraclePlatform && ! $platform instanceof PostgreSQLPlatform) { + self::markTestSkipped('The current platform does not auto-quote introspected identifiers.'); + } + + $artists = new Table('"Artists"'); + $artists->addColumn('"Id"', Types::INTEGER); + $artists->addColumn('"Name"', Types::INTEGER); + $artists->addIndex(['"Name"'], '"Idx_Name"'); + $artists->setPrimaryKey(['"Id"']); + + $tracks = new Table('"Tracks"'); + $tracks->addColumn('"Id"', Types::INTEGER); + $tracks->addColumn('"Artist_Id"', Types::INTEGER); + $tracks->addIndex(['"Artist_Id"'], '"Idx_Artist_Id"'); + $tracks->addForeignKeyConstraint( + '"Artists"', + ['"Artist_Id"'], + ['"Id"'], + [], + '"Artists_Fk"', + ); + $tracks->setPrimaryKey(['"Id"']); + + $this->dropTableIfExists('"Tracks"'); + $this->dropTableIfExists('"Artists"'); + + $this->schemaManager->createTable($artists); + $this->schemaManager->createTable($tracks); + + $artists = $this->schemaManager->introspectTable('"Artists"'); + $tracks = $this->schemaManager->introspectTable('"Tracks"'); + + $platform = $this->connection->getDatabasePlatform(); + + // Primary table assertions + self::assertSame('"Artists"', $artists->getQuotedName($platform)); + self::assertSame('"Id"', $artists->getColumn('"Id"')->getQuotedName($platform)); + self::assertSame('"Name"', $artists->getColumn('"Name"')->getQuotedName($platform)); + self::assertSame(['"Name"'], $artists->getIndex('"Idx_Name"')->getQuotedColumns($platform)); + + $primaryKey = $artists->getPrimaryKey(); + self::assertNotNull($primaryKey); + self::assertSame(['"Id"'], $primaryKey->getQuotedColumns($platform)); + + // Foreign table assertions + self::assertTrue($tracks->hasColumn('"Id"')); + self::assertSame('"Id"', $tracks->getColumn('"Id"')->getQuotedName($platform)); + + $primaryKey = $tracks->getPrimaryKey(); + self::assertNotNull($primaryKey); + self::assertSame(['"Id"'], $primaryKey->getQuotedColumns($platform)); + + self::assertTrue($tracks->hasColumn('"Artist_Id"')); + self::assertSame( + '"Artist_Id"', + $tracks->getColumn('"Artist_Id"')->getQuotedName($platform), + ); + + self::assertTrue($tracks->hasIndex('"Idx_Artist_Id"')); + self::assertSame( + ['"Artist_Id"'], + $tracks->getIndex('"Idx_Artist_Id"')->getQuotedColumns($platform), + ); + + self::assertTrue($tracks->hasForeignKey('"Artists_Fk"')); + self::assertSame( + '"Artists"', + $tracks->getForeignKey('"Artists_Fk"')->getQuotedForeignTableName($platform), + ); + self::assertSame( + ['"Artist_Id"'], + $tracks->getForeignKey('"Artists_Fk"')->getQuotedLocalColumns($platform), + ); + self::assertSame( + ['"Id"'], + $tracks->getForeignKey('"Artists_Fk"')->getQuotedForeignColumns($platform), + ); + } + public function testChangeIndexWithForeignKeys(): void { $this->dropTableIfExists('child'); @@ -1430,6 +1521,49 @@ protected function findTableByName(array $tables, string $name): ?Table return null; } + + public function testTableWithSchema(): void + { + if (! $this->connection->getDatabasePlatform()->supportsSchemas()) { + self::markTestSkipped('The currently used database platform does not support schemas.'); + } + + $this->connection->executeStatement('CREATE SCHEMA nested'); + + $nestedRelatedTable = new Table('nested.schemarelated'); + $column = $nestedRelatedTable->addColumn('id', Types::INTEGER); + $column->setAutoincrement(true); + $nestedRelatedTable->setPrimaryKey(['id']); + + $nestedSchemaTable = new Table('nested.schematable'); + $column = $nestedSchemaTable->addColumn('id', Types::INTEGER); + $column->setAutoincrement(true); + $nestedSchemaTable->setPrimaryKey(['id']); + $nestedSchemaTable->addForeignKeyConstraint($nestedRelatedTable->getName(), ['id'], ['id']); + $nestedSchemaTable->setComment('This is a comment'); + + $this->schemaManager->createTable($nestedRelatedTable); + $this->schemaManager->createTable($nestedSchemaTable); + + $tableNames = $this->schemaManager->listTableNames(); + self::assertContains('nested.schematable', $tableNames); + + $tables = $this->schemaManager->listTables(); + self::assertNotNull($this->findTableByName($tables, 'nested.schematable')); + + $nestedSchemaTable = $this->schemaManager->introspectTable('nested.schematable'); + self::assertTrue($nestedSchemaTable->hasColumn('id')); + + $primaryKey = $nestedSchemaTable->getPrimaryKey(); + self::assertNotNull($primaryKey); + self::assertEquals(['id'], $primaryKey->getColumns()); + + $relatedFks = array_values($nestedSchemaTable->getForeignKeys()); + self::assertCount(1, $relatedFks); + $relatedFk = $relatedFks[0]; + self::assertEquals('nested.schemarelated', $relatedFk->getForeignTableName()); + self::assertEquals('This is a comment', $nestedSchemaTable->getComment()); + } } interface ListTableColumnsDispatchEventListener