From 6c34ebb747e33384a8c94488357ba81a953ae5c3 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 6 Aug 2026 16:51:17 +0200 Subject: [PATCH] chore: Bump doctrine to v4 Signed-off-by: Carl Schwan --- composer.json | 2 +- composer.lock | 131 +- composer/InstalledVersions.php | 4 +- composer/LICENSE | 2 - composer/autoload_classmap.php | 246 +- composer/autoload_psr4.php | 1 - composer/autoload_static.php | 251 +- composer/installed.json | 136 +- composer/installed.php | 17 +- doctrine/dbal/src/ArrayParameterType.php | 33 +- .../dbal/src/ArrayParameters/Exception.php | 2 + .../Exception/MissingNamedParameter.php | 2 + .../Exception/MissingPositionalParameter.php | 2 + doctrine/dbal/src/Cache/ArrayResult.php | 96 +- doctrine/dbal/src/Cache/CacheException.php | 15 +- .../dbal/src/Cache/Exception/NoCacheKey.php | 15 + .../Exception/NoResultDriverConfigured.php | 15 + doctrine/dbal/src/Cache/QueryCacheProfile.php | 142 +- doctrine/dbal/src/ColumnCase.php | 17 +- doctrine/dbal/src/Configuration.php | 155 +- doctrine/dbal/src/Connection.php | 1292 ++--- .../StaticServerVersionProvider.php | 20 + doctrine/dbal/src/ConnectionException.php | 27 +- .../PrimaryReadReplicaConnection.php | 162 +- doctrine/dbal/src/Driver.php | 20 +- .../Driver/API/IBMDB2/ExceptionConverter.php | 50 +- .../Driver/API/MySQL/ExceptionConverter.php | 160 +- .../src/Driver/API/OCI/ExceptionConverter.php | 98 +- .../API/PostgreSQL/ExceptionConverter.php | 15 +- .../Driver/API/SQLSrv/ExceptionConverter.php | 54 +- .../Driver/API/SQLite/ExceptionConverter.php | 32 +- .../API/SQLite/UserDefinedFunctions.php | 80 - .../dbal/src/Driver/AbstractDB2Driver.php | 85 +- .../dbal/src/Driver/AbstractException.php | 24 +- .../dbal/src/Driver/AbstractMySQLDriver.php | 211 +- .../dbal/src/Driver/AbstractOracleDriver.php | 45 +- .../EasyConnectString.php | 29 +- .../src/Driver/AbstractPostgreSQLDriver.php | 70 +- .../src/Driver/AbstractSQLServerDriver.php | 36 +- .../dbal/src/Driver/AbstractSQLiteDriver.php | 47 +- .../Middleware/EnableForeignKeys.php | 6 +- doctrine/dbal/src/Driver/Connection.php | 55 +- doctrine/dbal/src/Driver/Exception.php | 10 +- .../Exception/IdentityColumnsNotSupported.php | 17 + .../src/Driver/Exception/NoIdentityValue.php | 17 + .../Driver/Exception/UnknownParameterType.php | 19 - doctrine/dbal/src/Driver/FetchUtils.php | 8 +- .../dbal/src/Driver/IBMDB2/Connection.php | 84 +- .../dbal/src/Driver/IBMDB2/DataSourceName.php | 24 +- doctrine/dbal/src/Driver/IBMDB2/Driver.php | 8 +- .../Exception/CannotCopyStreamToStream.php | 2 +- .../Exception/CannotCreateTemporaryFile.php | 2 +- .../Driver/IBMDB2/Exception/PrepareFailed.php | 2 +- doctrine/dbal/src/Driver/IBMDB2/Result.php | 42 +- doctrine/dbal/src/Driver/IBMDB2/Statement.php | 86 +- .../AbstractConnectionMiddleware.php | 82 +- .../Middleware/AbstractDriverMiddleware.php | 54 +- .../Middleware/AbstractResultMiddleware.php | 41 +- .../AbstractStatementMiddleware.php | 59 +- .../dbal/src/Driver/Mysqli/Connection.php | 89 +- doctrine/dbal/src/Driver/Mysqli/Driver.php | 30 +- .../Mysqli/Exception/ConnectionError.php | 7 +- .../Mysqli/Exception/ConnectionFailed.php | 7 +- .../Mysqli/Exception/InvalidCharset.php | 7 +- .../Driver/Mysqli/Exception/InvalidOption.php | 3 +- .../Mysqli/Exception/StatementError.php | 7 +- .../src/Driver/Mysqli/Initializer/Charset.php | 11 +- .../src/Driver/Mysqli/Initializer/Options.php | 6 +- .../src/Driver/Mysqli/Initializer/Secure.php | 21 +- doctrine/dbal/src/Driver/Mysqli/Result.php | 58 +- doctrine/dbal/src/Driver/Mysqli/Statement.php | 141 +- doctrine/dbal/src/Driver/OCI8/Connection.php | 101 +- .../ConvertPositionalToNamedPlaceholders.php | 2 + doctrine/dbal/src/Driver/OCI8/Driver.php | 8 +- .../OCI8/Exception/SequenceDoesNotExist.php | 16 - .../OCI8/Middleware/InitializeSession.php | 6 +- doctrine/dbal/src/Driver/OCI8/Result.php | 45 +- doctrine/dbal/src/Driver/OCI8/Statement.php | 131 +- doctrine/dbal/src/Driver/PDO/Connection.php | 111 +- .../PDO/Exception/InvalidConfiguration.php | 22 + doctrine/dbal/src/Driver/PDO/MySQL/Driver.php | 19 +- doctrine/dbal/src/Driver/PDO/OCI/Driver.php | 19 +- doctrine/dbal/src/Driver/PDO/PDOConnect.php | 2 +- doctrine/dbal/src/Driver/PDO/PDOException.php | 29 - .../dbal/src/Driver/PDO/ParameterTypeMap.php | 49 - doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php | 41 +- doctrine/dbal/src/Driver/PDO/Result.php | 44 +- .../dbal/src/Driver/PDO/SQLSrv/Connection.php | 49 +- .../dbal/src/Driver/PDO/SQLSrv/Driver.php | 18 +- .../dbal/src/Driver/PDO/SQLSrv/Statement.php | 101 +- .../dbal/src/Driver/PDO/SQLite/Driver.php | 37 +- doctrine/dbal/src/Driver/PDO/Statement.php | 133 +- doctrine/dbal/src/Driver/PgSQL/Connection.php | 91 +- doctrine/dbal/src/Driver/PgSQL/Driver.php | 12 +- doctrine/dbal/src/Driver/PgSQL/Exception.php | 5 +- .../PgSQL/Exception/UnexpectedValue.php | 3 +- .../PgSQL/Exception/UnknownParameter.php | 2 + doctrine/dbal/src/Driver/PgSQL/Result.php | 107 +- doctrine/dbal/src/Driver/PgSQL/Statement.php | 121 +- doctrine/dbal/src/Driver/Result.php | 16 +- .../dbal/src/Driver/SQLSrv/Connection.php | 78 +- doctrine/dbal/src/Driver/SQLSrv/Driver.php | 8 +- .../src/Driver/SQLSrv/Exception/Error.php | 6 +- doctrine/dbal/src/Driver/SQLSrv/Result.php | 39 +- doctrine/dbal/src/Driver/SQLSrv/Statement.php | 107 +- .../dbal/src/Driver/SQLite3/Connection.php | 44 +- doctrine/dbal/src/Driver/SQLite3/Driver.php | 9 +- .../dbal/src/Driver/SQLite3/Exception.php | 2 + doctrine/dbal/src/Driver/SQLite3/Result.php | 33 +- .../dbal/src/Driver/SQLite3/Statement.php | 117 +- .../src/Driver/ServerInfoAwareConnection.php | 21 - doctrine/dbal/src/Driver/Statement.php | 59 +- doctrine/dbal/src/DriverManager.php | 163 +- .../dbal/src/Event/ConnectionEventArgs.php | 27 - .../src/Event/Listeners/OracleSessionInit.php | 77 - .../src/Event/Listeners/SQLSessionInit.php | 43 - .../src/Event/Listeners/SQLiteSessionInit.php | 30 - .../SchemaAlterTableAddColumnEventArgs.php | 81 - .../SchemaAlterTableChangeColumnEventArgs.php | 71 - .../src/Event/SchemaAlterTableEventArgs.php | 62 - .../SchemaAlterTableRemoveColumnEventArgs.php | 71 - .../SchemaAlterTableRenameColumnEventArgs.php | 82 - .../Event/SchemaColumnDefinitionEventArgs.php | 87 - .../SchemaCreateTableColumnEventArgs.php | 71 - .../src/Event/SchemaCreateTableEventArgs.php | 87 - .../src/Event/SchemaDropTableEventArgs.php | 59 - doctrine/dbal/src/Event/SchemaEventArgs.php | 29 - .../Event/SchemaIndexDefinitionEventArgs.php | 75 - .../src/Event/TransactionBeginEventArgs.php | 10 - .../src/Event/TransactionCommitEventArgs.php | 10 - .../dbal/src/Event/TransactionEventArgs.php | 24 - .../Event/TransactionRollBackEventArgs.php | 10 - doctrine/dbal/src/Events.php | 64 - doctrine/dbal/src/Exception.php | 138 +- .../Exception/CommitFailedRollbackOnly.php | 15 + .../src/Exception/ConnectionException.php | 2 + .../dbal/src/Exception/ConnectionLost.php | 2 + .../ConstraintViolationException.php | 2 + .../src/Exception/DatabaseDoesNotExist.php | 2 + .../DatabaseObjectExistsException.php | 2 + .../DatabaseObjectNotFoundException.php | 2 + .../dbal/src/Exception/DatabaseRequired.php | 9 +- .../dbal/src/Exception/DeadlockException.php | 2 + .../dbal/src/Exception/DriverException.php | 32 +- .../dbal/src/Exception/DriverRequired.php | 29 + ...ForeignKeyConstraintViolationException.php | 2 + .../Exception/InvalidArgumentException.php | 9 +- .../Exception/InvalidColumnDeclaration.php | 18 + .../dbal/src/Exception/InvalidColumnIndex.php | 19 + .../dbal/src/Exception/InvalidColumnType.php | 12 + .../ColumnLengthRequired.php | 30 + .../ColumnPrecisionRequired.php | 16 + .../InvalidColumnType/ColumnScaleRequired.php | 16 + .../ColumnValuesRequired.php | 29 + .../dbal/src/Exception/InvalidDriverClass.php | 23 + .../Exception/InvalidFieldNameException.php | 2 + .../dbal/src/Exception/InvalidLockMode.php | 21 - .../src/Exception/InvalidWrapperClass.php | 23 + .../Exception/LockWaitTimeoutException.php | 2 + .../src/Exception/MalformedDsnException.php | 4 +- .../src/Exception/NoActiveTransaction.php | 15 + doctrine/dbal/src/Exception/NoKeyValue.php | 4 +- .../Exception/NonUniqueFieldNameException.php | 2 + .../NotNullConstraintViolationException.php | 2 + doctrine/dbal/src/Exception/ParseError.php | 17 + .../dbal/src/Exception/ReadOnlyException.php | 2 + .../dbal/src/Exception/RetryableException.php | 2 + .../src/Exception/SavepointsNotSupported.php | 15 + .../dbal/src/Exception/SchemaDoesNotExist.php | 2 + .../dbal/src/Exception/ServerException.php | 2 + .../src/Exception/SyntaxErrorException.php | 2 + .../src/Exception/TableExistsException.php | 2 + .../src/Exception/TableNotFoundException.php | 2 + .../src/Exception/TransactionRolledBack.php | 2 + .../UniqueConstraintViolationException.php | 2 + doctrine/dbal/src/Exception/UnknownDriver.php | 23 + doctrine/dbal/src/ExpandArrayParameters.php | 57 +- doctrine/dbal/src/FetchMode.php | 20 - doctrine/dbal/src/Id/TableGenerator.php | 169 - .../src/Id/TableGeneratorSchemaVisitor.php | 77 - doctrine/dbal/src/LockMode.php | 23 +- doctrine/dbal/src/Logging/Connection.php | 29 +- doctrine/dbal/src/Logging/DebugStack.php | 75 - doctrine/dbal/src/Logging/Driver.php | 16 +- doctrine/dbal/src/Logging/LoggerChain.php | 48 - doctrine/dbal/src/Logging/Middleware.php | 5 +- doctrine/dbal/src/Logging/SQLLogger.php | 32 - doctrine/dbal/src/Logging/Statement.php | 74 +- doctrine/dbal/src/ParameterType.php | 33 +- .../src/Platforms/AbstractMySQLPlatform.php | 1053 +--- .../dbal/src/Platforms/AbstractPlatform.php | 4840 +++++------------ .../dbal/src/Platforms/DB2111Platform.php | 40 - doctrine/dbal/src/Platforms/DB2Platform.php | 771 +-- .../dbal/src/Platforms/DateIntervalUnit.php | 30 +- .../src/Platforms/Db2/Db2MetadataProvider.php | 518 ++ .../Exception/InvalidPlatformVersion.php | 27 + .../Exception/NoColumnsSpecifiedForTable.php | 17 + .../src/Platforms/Exception/NotSupported.php | 17 + .../Platforms/Exception/PlatformException.php | 11 + .../src/Platforms/Keywords/DB2Keywords.php | 24 +- .../src/Platforms/Keywords/KeywordList.php | 36 +- .../Platforms/Keywords/MariaDB117Keywords.php | 26 + .../Platforms/Keywords/MariaDB123Keywords.php | 32 + .../Platforms/Keywords/MariaDBKeywords.php | 17 +- .../Platforms/Keywords/MariaDb102Keywords.php | 27 - .../Platforms/Keywords/MariaDb117Keywords.php | 46 - .../Platforms/Keywords/MySQL57Keywords.php | 275 - .../Platforms/Keywords/MySQL80Keywords.php | 26 +- .../Platforms/Keywords/MySQL84Keywords.php | 22 +- .../src/Platforms/Keywords/MySQLKeywords.php | 44 +- .../src/Platforms/Keywords/OracleKeywords.php | 24 +- .../Keywords/PostgreSQL100Keywords.php | 27 - .../Keywords/PostgreSQL94Keywords.php | 12 - .../Platforms/Keywords/PostgreSQLKeywords.php | 24 +- .../Keywords/ReservedKeywordsValidator.php | 130 - .../Keywords/SQLServer2012Keywords.php | 12 - .../Platforms/Keywords/SQLServerKeywords.php | 25 +- .../src/Platforms/Keywords/SQLiteKeywords.php | 24 +- ...10Platform.php => MariaDB1010Platform.php} | 5 +- ...52Platform.php => MariaDB1052Platform.php} | 16 +- ...60Platform.php => MariaDB1060Platform.php} | 6 +- .../src/Platforms/MariaDB110700Platform.php | 30 + .../src/Platforms/MariaDB120300Platform.php | 30 + .../dbal/src/Platforms/MariaDBPlatform.php | 163 +- .../src/Platforms/MariaDb1027Platform.php | 13 - .../src/Platforms/MariaDb1043Platform.php | 131 - .../src/Platforms/MariaDb110700Platform.php | 26 - .../MySQL/CharsetMetadataProvider.php | 12 + .../CachingCharsetMetadataProvider.php | 29 + .../ConnectionCharsetMetadataProvider.php | 37 + .../MySQL/CollationMetadataProvider.php | 5 + .../CachingCollationMetadataProvider.php | 10 +- .../ConnectionCollationMetadataProvider.php | 6 +- .../dbal/src/Platforms/MySQL/Comparator.php | 75 +- .../Platforms/MySQL/DefaultTableOptions.php | 23 + .../Platforms/MySQL/MySQLMetadataProvider.php | 686 +++ .../dbal/src/Platforms/MySQL57Platform.php | 99 - .../dbal/src/Platforms/MySQL80Platform.php | 29 +- .../dbal/src/Platforms/MySQL84Platform.php | 22 +- doctrine/dbal/src/Platforms/MySQLPlatform.php | 54 + .../Oracle/OracleMetadataProvider.php | 581 ++ .../dbal/src/Platforms/OraclePlatform.php | 964 +--- .../PostgreSQL/PostgreSQLMetadataProvider.php | 729 +++ .../src/Platforms/PostgreSQL100Platform.php | 36 - .../src/Platforms/PostgreSQL120Platform.php | 4 +- .../src/Platforms/PostgreSQL94Platform.php | 12 - .../dbal/src/Platforms/PostgreSQLPlatform.php | 997 +--- .../src/Platforms/SQLServer/Comparator.php | 33 +- .../SQL/Builder/SQLServerSelectSQLBuilder.php | 8 +- .../SQLServer/SQLServerMetadataProvider.php | 658 +++ .../src/Platforms/SQLServer2012Platform.php | 13 - .../dbal/src/Platforms/SQLServerPlatform.php | 1295 ++--- .../dbal/src/Platforms/SQLite/Comparator.php | 26 +- .../SQLite/SQLiteMetadataProvider.php | 710 +++ .../ForeignKeyConstraintDetails.php | 38 + .../dbal/src/Platforms/SQLitePlatform.php | 976 ++++ .../dbal/src/Platforms/SqlitePlatform.php | 1545 ------ doctrine/dbal/src/Platforms/TrimMode.php | 18 +- doctrine/dbal/src/Portability/Connection.php | 14 +- doctrine/dbal/src/Portability/Converter.php | 124 +- doctrine/dbal/src/Portability/Driver.php | 61 +- doctrine/dbal/src/Portability/Middleware.php | 15 +- .../dbal/src/Portability/OptimizeFlags.php | 4 +- doctrine/dbal/src/Portability/Result.php | 28 +- doctrine/dbal/src/Portability/Statement.php | 15 +- doctrine/dbal/src/Query.php | 39 +- .../dbal/src/Query/CommonTableExpression.php | 27 + .../src/Query/Exception/NonUniqueAlias.php | 26 + .../dbal/src/Query/Exception/UnknownAlias.php | 26 + .../Query/Expression/CompositeExpression.php | 137 +- .../Query/Expression/ExpressionBuilder.php | 196 +- doctrine/dbal/src/Query/ForUpdate.php | 14 +- .../ForUpdate/ConflictResolutionMode.php | 15 +- doctrine/dbal/src/Query/From.php | 15 + doctrine/dbal/src/Query/Join.php | 32 + doctrine/dbal/src/Query/Limit.php | 14 +- doctrine/dbal/src/Query/QueryBuilder.php | 1251 ++--- doctrine/dbal/src/Query/QueryException.php | 31 +- doctrine/dbal/src/Query/QueryType.php | 15 + doctrine/dbal/src/Query/SelectQuery.php | 51 +- doctrine/dbal/src/Query/Union.php | 15 + doctrine/dbal/src/Query/UnionQuery.php | 38 + doctrine/dbal/src/Query/UnionType.php | 11 + doctrine/dbal/src/Result.php | 146 +- .../Builder/CreateSchemaObjectsSQLBuilder.php | 20 +- .../SQL/Builder/DefaultSelectSQLBuilder.php | 21 +- .../SQL/Builder/DefaultUnionSQLBuilder.php | 48 + .../Builder/DropSchemaObjectsSQLBuilder.php | 18 +- .../dbal/src/SQL/Builder/SelectSQLBuilder.php | 2 + .../dbal/src/SQL/Builder/UnionSQLBuilder.php | 14 + .../dbal/src/SQL/Builder/WithSQLBuilder.php | 31 + doctrine/dbal/src/SQL/Parser.php | 6 +- doctrine/dbal/src/SQL/Parser/Visitor.php | 2 + doctrine/dbal/src/Schema/AbstractAsset.php | 382 +- .../dbal/src/Schema/AbstractNamedObject.php | 58 + .../Schema/AbstractOptionallyNamedObject.php | 46 + .../dbal/src/Schema/AbstractSchemaManager.php | 1739 +++--- .../dbal/src/Schema/Collections/Exception.php | 12 + .../Exception/ObjectAlreadyExists.php | 30 + .../Exception/ObjectDoesNotExist.php | 30 + .../dbal/src/Schema/Collections/ObjectSet.php | 71 + .../OptionallyUnqualifiedNamedObjectSet.php | 169 + .../Collections/UnqualifiedNamedObjectSet.php | 148 + doctrine/dbal/src/Schema/Column.php | 480 +- doctrine/dbal/src/Schema/ColumnDiff.php | 188 +- doctrine/dbal/src/Schema/ColumnEditor.php | 286 + doctrine/dbal/src/Schema/Comparator.php | 658 +-- doctrine/dbal/src/Schema/ComparatorConfig.php | 58 + doctrine/dbal/src/Schema/Constraint.php | 41 - doctrine/dbal/src/Schema/DB2SchemaManager.php | 285 +- .../dbal/src/Schema/DefaultExpression.php | 18 + .../Schema/DefaultExpression/CurrentDate.php | 19 + .../Schema/DefaultExpression/CurrentTime.php | 19 + .../DefaultExpression/CurrentTimestamp.php | 19 + .../Schema/Exception/ColumnAlreadyExists.php | 8 +- .../Schema/Exception/ColumnDoesNotExist.php | 8 +- .../Exception/ForeignKeyDoesNotExist.php | 4 +- .../Schema/Exception/IncomparableNames.php | 25 + .../Schema/Exception/IndexAlreadyExists.php | 4 +- .../Schema/Exception/IndexDoesNotExist.php | 8 +- .../src/Schema/Exception/IndexNameInvalid.php | 8 +- .../Exception/InvalidColumnDefinition.php | 25 + .../InvalidForeignKeyConstraintDefinition.php | 43 + .../Schema/Exception/InvalidIdentifier.php | 16 + .../Exception/InvalidIndexDefinition.php | 33 + .../dbal/src/Schema/Exception/InvalidName.php | 16 + .../InvalidPrimaryKeyConstraintDefinition.php | 16 + .../Exception/InvalidSequenceDefinition.php | 23 + .../src/Schema/Exception/InvalidState.php | 97 + .../Exception/InvalidTableDefinition.php | 24 + .../Exception/InvalidTableModification.php | 184 + .../src/Schema/Exception/InvalidTableName.php | 3 +- .../InvalidUniqueConstraintDefinition.php | 22 + .../Exception/InvalidViewDefinition.php | 24 + .../Exception/NamedForeignKeyRequired.php | 29 - .../Exception/NamespaceAlreadyExists.php | 8 +- .../src/Schema/Exception/NotImplemented.php | 18 + .../Exception/PrimaryKeyAlreadyExists.php | 20 + .../Exception/SequenceAlreadyExists.php | 8 +- .../Schema/Exception/SequenceDoesNotExist.php | 8 +- .../Schema/Exception/TableAlreadyExists.php | 8 +- .../Schema/Exception/TableDoesNotExist.php | 8 +- .../UniqueConstraintDoesNotExist.php | 4 +- .../Schema/Exception/UnknownColumnOption.php | 3 +- .../src/Schema/Exception/UnsupportedName.php | 23 + .../Schema/Exception/UnsupportedSchema.php | 33 + .../dbal/src/Schema/ForeignKeyConstraint.php | 679 ++- .../ForeignKeyConstraint/Deferrability.php | 23 + .../Schema/ForeignKeyConstraint/MatchType.php | 33 + .../ReferentialAction.php | 38 + .../src/Schema/ForeignKeyConstraintEditor.php | 275 + doctrine/dbal/src/Schema/Identifier.php | 25 +- doctrine/dbal/src/Schema/Index.php | 590 +- doctrine/dbal/src/Schema/Index/IndexType.php | 13 + .../dbal/src/Schema/Index/IndexedColumn.php | 34 + doctrine/dbal/src/Schema/IndexEditor.php | 187 + .../IntrospectingSchemaProvider.php | 413 ++ ...gnKeyConstraintColumnMetadataProcessor.php | 71 + .../IndexColumnMetadataProcessor.php | 40 + ...ryKeyConstraintColumnMetadataProcessor.php | 33 + .../SequenceMetadataProcessor.php | 26 + .../ViewMetadataProcessor.php | 24 + .../src/Schema/LegacySchemaManagerFactory.php | 19 - .../Schema/Metadata/DatabaseMetadataRow.php | 22 + .../ForeignKeyConstraintColumnMetadataRow.php | 131 + .../Metadata/IndexColumnMetadataRow.php | 79 + .../src/Schema/Metadata/MetadataProvider.php | 241 + .../PrimaryKeyConstraintColumnRow.php | 55 + .../src/Schema/Metadata/SchemaMetadataRow.php | 22 + .../Schema/Metadata/SequenceMetadataRow.php | 53 + .../Metadata/TableColumnMetadataRow.php | 46 + .../src/Schema/Metadata/TableMetadataRow.php | 41 + .../src/Schema/Metadata/ViewMetadataRow.php | 39 + .../dbal/src/Schema/MySQLSchemaManager.php | 524 +- doctrine/dbal/src/Schema/Name.php | 25 + doctrine/dbal/src/Schema/Name/GenericName.php | 51 + doctrine/dbal/src/Schema/Name/Identifier.php | 103 + .../Schema/Name/OptionallyQualifiedName.php | 103 + doctrine/dbal/src/Schema/Name/Parser.php | 25 + .../dbal/src/Schema/Name/Parser/Exception.php | 11 + .../Name/Parser/Exception/ExpectedDot.php | 19 + .../Exception/ExpectedNextIdentifier.php | 17 + .../Name/Parser/Exception/InvalidName.php | 26 + .../Exception/UnableToParseIdentifier.php | 19 + .../Schema/Name/Parser/GenericNameParser.php | 100 + .../Parser/OptionallyQualifiedNameParser.php | 35 + .../Name/Parser/UnqualifiedNameParser.php | 35 + doctrine/dbal/src/Schema/Name/Parsers.php | 43 + .../dbal/src/Schema/Name/UnqualifiedName.php | 65 + .../Schema/Name/UnquotedIdentifierFolding.php | 45 + doctrine/dbal/src/Schema/NamedObject.php | 22 + .../dbal/src/Schema/OptionallyNamedObject.php | 22 + .../dbal/src/Schema/OracleSchemaManager.php | 354 +- .../src/Schema/PostgreSQLSchemaManager.php | 722 +-- .../dbal/src/Schema/PrimaryKeyConstraint.php | 76 + .../src/Schema/PrimaryKeyConstraintEditor.php | 115 + .../src/Schema/SQLServerSchemaManager.php | 473 +- .../dbal/src/Schema/SQLiteSchemaManager.php | 616 +++ doctrine/dbal/src/Schema/Schema.php | 492 +- doctrine/dbal/src/Schema/SchemaConfig.php | 98 +- doctrine/dbal/src/Schema/SchemaDiff.php | 259 +- doctrine/dbal/src/Schema/SchemaException.php | 198 +- doctrine/dbal/src/Schema/SchemaProvider.php | 154 + doctrine/dbal/src/Schema/Sequence.php | 170 +- doctrine/dbal/src/Schema/SequenceEditor.php | 94 + .../dbal/src/Schema/SqliteSchemaManager.php | 807 --- doctrine/dbal/src/Schema/Table.php | 1274 +++-- .../dbal/src/Schema/TableConfiguration.php | 30 + doctrine/dbal/src/Schema/TableDiff.php | 396 +- doctrine/dbal/src/Schema/TableEditor.php | 559 ++ doctrine/dbal/src/Schema/UniqueConstraint.php | 277 +- .../src/Schema/UniqueConstraintEditor.php | 110 + doctrine/dbal/src/Schema/View.php | 46 +- doctrine/dbal/src/Schema/ViewEditor.php | 72 + .../src/Schema/Visitor/AbstractVisitor.php | 49 - .../Visitor/CreateSchemaSqlCollector.php | 104 - .../Schema/Visitor/DropSchemaSqlCollector.php | 107 - doctrine/dbal/src/Schema/Visitor/Graphviz.php | 164 - .../src/Schema/Visitor/NamespaceVisitor.php | 20 - .../Schema/Visitor/RemoveNamespacedAssets.php | 103 - doctrine/dbal/src/Schema/Visitor/Visitor.php | 45 - doctrine/dbal/src/ServerVersionProvider.php | 13 + doctrine/dbal/src/Statement.php | 208 +- .../Console/Command/CommandCompatibility.php | 63 - .../Console/Command/ReservedWordsCommand.php | 221 - .../Tools/Console/Command/RunSqlCommand.php | 22 +- .../src/Tools/Console/ConnectionNotFound.php | 2 + .../src/Tools/Console/ConnectionProvider.php | 2 + .../SingleConnectionProvider.php | 14 +- .../dbal/src/Tools/Console/ConsoleRunner.php | 81 - doctrine/dbal/src/Tools/DsnParser.php | 27 +- .../dbal/src/TransactionIsolationLevel.php | 30 +- doctrine/dbal/src/Types/ArrayType.php | 91 - doctrine/dbal/src/Types/AsciiStringType.php | 7 +- doctrine/dbal/src/Types/BigIntType.php | 48 +- doctrine/dbal/src/Types/BinaryType.php | 40 +- doctrine/dbal/src/Types/BlobType.php | 25 +- doctrine/dbal/src/Types/BooleanType.php | 47 +- .../dbal/src/Types/ConversionException.php | 113 +- doctrine/dbal/src/Types/DateImmutableType.php | 44 +- doctrine/dbal/src/Types/DateIntervalType.php | 46 +- .../dbal/src/Types/DateTimeImmutableType.php | 44 +- doctrine/dbal/src/Types/DateTimeType.php | 65 +- .../src/Types/DateTimeTzImmutableType.php | 44 +- doctrine/dbal/src/Types/DateTimeTzType.php | 63 +- doctrine/dbal/src/Types/DateType.php | 63 +- doctrine/dbal/src/Types/DecimalType.php | 24 +- doctrine/dbal/src/Types/EnumType.php | 18 + .../src/Types/Exception/InvalidFormat.php | 37 + .../dbal/src/Types/Exception/InvalidType.php | 51 + .../Types/Exception/SerializationFailed.php | 28 + .../Types/Exception/TypeAlreadyRegistered.php | 24 + .../Exception/TypeArgumentCountError.php | 24 + .../dbal/src/Types/Exception/TypeNotFound.php | 17 + .../src/Types/Exception/TypeNotRegistered.php | 24 + .../Types/Exception/TypesAlreadyExists.php | 17 + .../src/Types/Exception/TypesException.php | 11 + .../src/Types/Exception/UnknownColumnType.php | 28 + .../Types/Exception/ValueNotConvertible.php | 42 + doctrine/dbal/src/Types/FloatType.php | 16 +- doctrine/dbal/src/Types/GuidType.php | 30 +- doctrine/dbal/src/Types/IntegerType.php | 21 +- doctrine/dbal/src/Types/JsonObjectType.php | 28 + doctrine/dbal/src/Types/JsonType.php | 82 +- doctrine/dbal/src/Types/JsonTypeConvert.php | 61 + doctrine/dbal/src/Types/JsonbObjectType.php | 28 + doctrine/dbal/src/Types/JsonbType.php | 21 + doctrine/dbal/src/Types/NumberType.php | 54 + doctrine/dbal/src/Types/ObjectType.php | 88 - .../dbal/src/Types/PhpDateMappingType.php | 14 + .../dbal/src/Types/PhpDateTimeMappingType.php | 2 + .../dbal/src/Types/PhpIntegerMappingType.php | 2 + .../dbal/src/Types/PhpTimeMappingType.php | 14 + doctrine/dbal/src/Types/SimpleArrayType.php | 49 +- doctrine/dbal/src/Types/SmallFloatType.php | 30 + doctrine/dbal/src/Types/SmallIntType.php | 21 +- doctrine/dbal/src/Types/StringType.php | 12 +- doctrine/dbal/src/Types/TextType.php | 17 +- doctrine/dbal/src/Types/TimeImmutableType.php | 44 +- doctrine/dbal/src/Types/TimeType.php | 63 +- doctrine/dbal/src/Types/Type.php | 179 +- doctrine/dbal/src/Types/TypeRegistry.php | 31 +- doctrine/dbal/src/Types/Types.php | 25 +- .../src/Types/VarDateTimeImmutableType.php | 46 +- doctrine/dbal/src/Types/VarDateTimeType.php | 12 +- .../dbal/src/VersionAwarePlatformDriver.php | 30 - doctrine/event-manager/LICENSE | 19 - doctrine/event-manager/src/EventArgs.php | 37 - doctrine/event-manager/src/EventManager.php | 129 - .../event-manager/src/EventSubscriber.php | 21 - 490 files changed, 24190 insertions(+), 26855 deletions(-) create mode 100644 doctrine/dbal/src/Cache/Exception/NoCacheKey.php create mode 100644 doctrine/dbal/src/Cache/Exception/NoResultDriverConfigured.php create mode 100644 doctrine/dbal/src/Connection/StaticServerVersionProvider.php delete mode 100644 doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php create mode 100644 doctrine/dbal/src/Driver/Exception/IdentityColumnsNotSupported.php create mode 100644 doctrine/dbal/src/Driver/Exception/NoIdentityValue.php delete mode 100644 doctrine/dbal/src/Driver/Exception/UnknownParameterType.php delete mode 100644 doctrine/dbal/src/Driver/OCI8/Exception/SequenceDoesNotExist.php create mode 100644 doctrine/dbal/src/Driver/PDO/Exception/InvalidConfiguration.php delete mode 100644 doctrine/dbal/src/Driver/PDO/PDOException.php delete mode 100644 doctrine/dbal/src/Driver/PDO/ParameterTypeMap.php delete mode 100644 doctrine/dbal/src/Driver/ServerInfoAwareConnection.php delete mode 100644 doctrine/dbal/src/Event/ConnectionEventArgs.php delete mode 100644 doctrine/dbal/src/Event/Listeners/OracleSessionInit.php delete mode 100644 doctrine/dbal/src/Event/Listeners/SQLSessionInit.php delete mode 100644 doctrine/dbal/src/Event/Listeners/SQLiteSessionInit.php delete mode 100644 doctrine/dbal/src/Event/SchemaAlterTableAddColumnEventArgs.php delete mode 100644 doctrine/dbal/src/Event/SchemaAlterTableChangeColumnEventArgs.php delete mode 100644 doctrine/dbal/src/Event/SchemaAlterTableEventArgs.php delete mode 100644 doctrine/dbal/src/Event/SchemaAlterTableRemoveColumnEventArgs.php delete mode 100644 doctrine/dbal/src/Event/SchemaAlterTableRenameColumnEventArgs.php delete mode 100644 doctrine/dbal/src/Event/SchemaColumnDefinitionEventArgs.php delete mode 100644 doctrine/dbal/src/Event/SchemaCreateTableColumnEventArgs.php delete mode 100644 doctrine/dbal/src/Event/SchemaCreateTableEventArgs.php delete mode 100644 doctrine/dbal/src/Event/SchemaDropTableEventArgs.php delete mode 100644 doctrine/dbal/src/Event/SchemaEventArgs.php delete mode 100644 doctrine/dbal/src/Event/SchemaIndexDefinitionEventArgs.php delete mode 100644 doctrine/dbal/src/Event/TransactionBeginEventArgs.php delete mode 100644 doctrine/dbal/src/Event/TransactionCommitEventArgs.php delete mode 100644 doctrine/dbal/src/Event/TransactionEventArgs.php delete mode 100644 doctrine/dbal/src/Event/TransactionRollBackEventArgs.php delete mode 100644 doctrine/dbal/src/Events.php create mode 100644 doctrine/dbal/src/Exception/CommitFailedRollbackOnly.php create mode 100644 doctrine/dbal/src/Exception/DriverRequired.php create mode 100644 doctrine/dbal/src/Exception/InvalidColumnDeclaration.php create mode 100644 doctrine/dbal/src/Exception/InvalidColumnIndex.php create mode 100644 doctrine/dbal/src/Exception/InvalidColumnType.php create mode 100644 doctrine/dbal/src/Exception/InvalidColumnType/ColumnLengthRequired.php create mode 100644 doctrine/dbal/src/Exception/InvalidColumnType/ColumnPrecisionRequired.php create mode 100644 doctrine/dbal/src/Exception/InvalidColumnType/ColumnScaleRequired.php create mode 100644 doctrine/dbal/src/Exception/InvalidColumnType/ColumnValuesRequired.php create mode 100644 doctrine/dbal/src/Exception/InvalidDriverClass.php delete mode 100644 doctrine/dbal/src/Exception/InvalidLockMode.php create mode 100644 doctrine/dbal/src/Exception/InvalidWrapperClass.php create mode 100644 doctrine/dbal/src/Exception/NoActiveTransaction.php create mode 100644 doctrine/dbal/src/Exception/ParseError.php create mode 100644 doctrine/dbal/src/Exception/SavepointsNotSupported.php create mode 100644 doctrine/dbal/src/Exception/UnknownDriver.php delete mode 100644 doctrine/dbal/src/FetchMode.php delete mode 100644 doctrine/dbal/src/Id/TableGenerator.php delete mode 100644 doctrine/dbal/src/Id/TableGeneratorSchemaVisitor.php delete mode 100644 doctrine/dbal/src/Logging/DebugStack.php delete mode 100644 doctrine/dbal/src/Logging/LoggerChain.php delete mode 100644 doctrine/dbal/src/Logging/SQLLogger.php delete mode 100644 doctrine/dbal/src/Platforms/DB2111Platform.php create mode 100644 doctrine/dbal/src/Platforms/Db2/Db2MetadataProvider.php create mode 100644 doctrine/dbal/src/Platforms/Exception/InvalidPlatformVersion.php create mode 100644 doctrine/dbal/src/Platforms/Exception/NoColumnsSpecifiedForTable.php create mode 100644 doctrine/dbal/src/Platforms/Exception/NotSupported.php create mode 100644 doctrine/dbal/src/Platforms/Exception/PlatformException.php create mode 100644 doctrine/dbal/src/Platforms/Keywords/MariaDB117Keywords.php create mode 100644 doctrine/dbal/src/Platforms/Keywords/MariaDB123Keywords.php delete mode 100644 doctrine/dbal/src/Platforms/Keywords/MariaDb102Keywords.php delete mode 100644 doctrine/dbal/src/Platforms/Keywords/MariaDb117Keywords.php delete mode 100644 doctrine/dbal/src/Platforms/Keywords/MySQL57Keywords.php delete mode 100644 doctrine/dbal/src/Platforms/Keywords/PostgreSQL100Keywords.php delete mode 100644 doctrine/dbal/src/Platforms/Keywords/PostgreSQL94Keywords.php delete mode 100644 doctrine/dbal/src/Platforms/Keywords/ReservedKeywordsValidator.php delete mode 100644 doctrine/dbal/src/Platforms/Keywords/SQLServer2012Keywords.php rename doctrine/dbal/src/Platforms/{MariaDb1010Platform.php => MariaDB1010Platform.php} (88%) rename doctrine/dbal/src/Platforms/{MariaDb1052Platform.php => MariaDB1052Platform.php} (50%) rename doctrine/dbal/src/Platforms/{MariaDb1060Platform.php => MariaDB1060Platform.php} (66%) create mode 100644 doctrine/dbal/src/Platforms/MariaDB110700Platform.php create mode 100644 doctrine/dbal/src/Platforms/MariaDB120300Platform.php delete mode 100644 doctrine/dbal/src/Platforms/MariaDb1027Platform.php delete mode 100644 doctrine/dbal/src/Platforms/MariaDb1043Platform.php delete mode 100644 doctrine/dbal/src/Platforms/MariaDb110700Platform.php create mode 100644 doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider.php create mode 100644 doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/CachingCharsetMetadataProvider.php create mode 100644 doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php create mode 100644 doctrine/dbal/src/Platforms/MySQL/DefaultTableOptions.php create mode 100644 doctrine/dbal/src/Platforms/MySQL/MySQLMetadataProvider.php delete mode 100644 doctrine/dbal/src/Platforms/MySQL57Platform.php create mode 100644 doctrine/dbal/src/Platforms/Oracle/OracleMetadataProvider.php create mode 100644 doctrine/dbal/src/Platforms/PostgreSQL/PostgreSQLMetadataProvider.php delete mode 100644 doctrine/dbal/src/Platforms/PostgreSQL100Platform.php delete mode 100644 doctrine/dbal/src/Platforms/PostgreSQL94Platform.php create mode 100644 doctrine/dbal/src/Platforms/SQLServer/SQLServerMetadataProvider.php delete mode 100644 doctrine/dbal/src/Platforms/SQLServer2012Platform.php create mode 100644 doctrine/dbal/src/Platforms/SQLite/SQLiteMetadataProvider.php create mode 100644 doctrine/dbal/src/Platforms/SQLite/SQLiteMetadataProvider/ForeignKeyConstraintDetails.php create mode 100644 doctrine/dbal/src/Platforms/SQLitePlatform.php delete mode 100644 doctrine/dbal/src/Platforms/SqlitePlatform.php create mode 100644 doctrine/dbal/src/Query/CommonTableExpression.php create mode 100644 doctrine/dbal/src/Query/Exception/NonUniqueAlias.php create mode 100644 doctrine/dbal/src/Query/Exception/UnknownAlias.php create mode 100644 doctrine/dbal/src/Query/From.php create mode 100644 doctrine/dbal/src/Query/Join.php create mode 100644 doctrine/dbal/src/Query/QueryType.php create mode 100644 doctrine/dbal/src/Query/Union.php create mode 100644 doctrine/dbal/src/Query/UnionQuery.php create mode 100644 doctrine/dbal/src/Query/UnionType.php create mode 100644 doctrine/dbal/src/SQL/Builder/DefaultUnionSQLBuilder.php create mode 100644 doctrine/dbal/src/SQL/Builder/UnionSQLBuilder.php create mode 100644 doctrine/dbal/src/SQL/Builder/WithSQLBuilder.php create mode 100644 doctrine/dbal/src/Schema/AbstractNamedObject.php create mode 100644 doctrine/dbal/src/Schema/AbstractOptionallyNamedObject.php create mode 100644 doctrine/dbal/src/Schema/Collections/Exception.php create mode 100644 doctrine/dbal/src/Schema/Collections/Exception/ObjectAlreadyExists.php create mode 100644 doctrine/dbal/src/Schema/Collections/Exception/ObjectDoesNotExist.php create mode 100644 doctrine/dbal/src/Schema/Collections/ObjectSet.php create mode 100644 doctrine/dbal/src/Schema/Collections/OptionallyUnqualifiedNamedObjectSet.php create mode 100644 doctrine/dbal/src/Schema/Collections/UnqualifiedNamedObjectSet.php create mode 100644 doctrine/dbal/src/Schema/ColumnEditor.php create mode 100644 doctrine/dbal/src/Schema/ComparatorConfig.php delete mode 100644 doctrine/dbal/src/Schema/Constraint.php create mode 100644 doctrine/dbal/src/Schema/DefaultExpression.php create mode 100644 doctrine/dbal/src/Schema/DefaultExpression/CurrentDate.php create mode 100644 doctrine/dbal/src/Schema/DefaultExpression/CurrentTime.php create mode 100644 doctrine/dbal/src/Schema/DefaultExpression/CurrentTimestamp.php create mode 100644 doctrine/dbal/src/Schema/Exception/IncomparableNames.php create mode 100644 doctrine/dbal/src/Schema/Exception/InvalidColumnDefinition.php create mode 100644 doctrine/dbal/src/Schema/Exception/InvalidForeignKeyConstraintDefinition.php create mode 100644 doctrine/dbal/src/Schema/Exception/InvalidIdentifier.php create mode 100644 doctrine/dbal/src/Schema/Exception/InvalidIndexDefinition.php create mode 100644 doctrine/dbal/src/Schema/Exception/InvalidName.php create mode 100644 doctrine/dbal/src/Schema/Exception/InvalidPrimaryKeyConstraintDefinition.php create mode 100644 doctrine/dbal/src/Schema/Exception/InvalidSequenceDefinition.php create mode 100644 doctrine/dbal/src/Schema/Exception/InvalidState.php create mode 100644 doctrine/dbal/src/Schema/Exception/InvalidTableDefinition.php create mode 100644 doctrine/dbal/src/Schema/Exception/InvalidTableModification.php create mode 100644 doctrine/dbal/src/Schema/Exception/InvalidUniqueConstraintDefinition.php create mode 100644 doctrine/dbal/src/Schema/Exception/InvalidViewDefinition.php delete mode 100644 doctrine/dbal/src/Schema/Exception/NamedForeignKeyRequired.php create mode 100644 doctrine/dbal/src/Schema/Exception/NotImplemented.php create mode 100644 doctrine/dbal/src/Schema/Exception/PrimaryKeyAlreadyExists.php create mode 100644 doctrine/dbal/src/Schema/Exception/UnsupportedName.php create mode 100644 doctrine/dbal/src/Schema/Exception/UnsupportedSchema.php create mode 100644 doctrine/dbal/src/Schema/ForeignKeyConstraint/Deferrability.php create mode 100644 doctrine/dbal/src/Schema/ForeignKeyConstraint/MatchType.php create mode 100644 doctrine/dbal/src/Schema/ForeignKeyConstraint/ReferentialAction.php create mode 100644 doctrine/dbal/src/Schema/ForeignKeyConstraintEditor.php create mode 100644 doctrine/dbal/src/Schema/Index/IndexType.php create mode 100644 doctrine/dbal/src/Schema/Index/IndexedColumn.php create mode 100644 doctrine/dbal/src/Schema/IndexEditor.php create mode 100644 doctrine/dbal/src/Schema/Introspection/IntrospectingSchemaProvider.php create mode 100644 doctrine/dbal/src/Schema/Introspection/MetadataProcessor/ForeignKeyConstraintColumnMetadataProcessor.php create mode 100644 doctrine/dbal/src/Schema/Introspection/MetadataProcessor/IndexColumnMetadataProcessor.php create mode 100644 doctrine/dbal/src/Schema/Introspection/MetadataProcessor/PrimaryKeyConstraintColumnMetadataProcessor.php create mode 100644 doctrine/dbal/src/Schema/Introspection/MetadataProcessor/SequenceMetadataProcessor.php create mode 100644 doctrine/dbal/src/Schema/Introspection/MetadataProcessor/ViewMetadataProcessor.php delete mode 100644 doctrine/dbal/src/Schema/LegacySchemaManagerFactory.php create mode 100644 doctrine/dbal/src/Schema/Metadata/DatabaseMetadataRow.php create mode 100644 doctrine/dbal/src/Schema/Metadata/ForeignKeyConstraintColumnMetadataRow.php create mode 100644 doctrine/dbal/src/Schema/Metadata/IndexColumnMetadataRow.php create mode 100644 doctrine/dbal/src/Schema/Metadata/MetadataProvider.php create mode 100644 doctrine/dbal/src/Schema/Metadata/PrimaryKeyConstraintColumnRow.php create mode 100644 doctrine/dbal/src/Schema/Metadata/SchemaMetadataRow.php create mode 100644 doctrine/dbal/src/Schema/Metadata/SequenceMetadataRow.php create mode 100644 doctrine/dbal/src/Schema/Metadata/TableColumnMetadataRow.php create mode 100644 doctrine/dbal/src/Schema/Metadata/TableMetadataRow.php create mode 100644 doctrine/dbal/src/Schema/Metadata/ViewMetadataRow.php create mode 100644 doctrine/dbal/src/Schema/Name.php create mode 100644 doctrine/dbal/src/Schema/Name/GenericName.php create mode 100644 doctrine/dbal/src/Schema/Name/Identifier.php create mode 100644 doctrine/dbal/src/Schema/Name/OptionallyQualifiedName.php create mode 100644 doctrine/dbal/src/Schema/Name/Parser.php create mode 100644 doctrine/dbal/src/Schema/Name/Parser/Exception.php create mode 100644 doctrine/dbal/src/Schema/Name/Parser/Exception/ExpectedDot.php create mode 100644 doctrine/dbal/src/Schema/Name/Parser/Exception/ExpectedNextIdentifier.php create mode 100644 doctrine/dbal/src/Schema/Name/Parser/Exception/InvalidName.php create mode 100644 doctrine/dbal/src/Schema/Name/Parser/Exception/UnableToParseIdentifier.php create mode 100644 doctrine/dbal/src/Schema/Name/Parser/GenericNameParser.php create mode 100644 doctrine/dbal/src/Schema/Name/Parser/OptionallyQualifiedNameParser.php create mode 100644 doctrine/dbal/src/Schema/Name/Parser/UnqualifiedNameParser.php create mode 100644 doctrine/dbal/src/Schema/Name/Parsers.php create mode 100644 doctrine/dbal/src/Schema/Name/UnqualifiedName.php create mode 100644 doctrine/dbal/src/Schema/Name/UnquotedIdentifierFolding.php create mode 100644 doctrine/dbal/src/Schema/NamedObject.php create mode 100644 doctrine/dbal/src/Schema/OptionallyNamedObject.php create mode 100644 doctrine/dbal/src/Schema/PrimaryKeyConstraint.php create mode 100644 doctrine/dbal/src/Schema/PrimaryKeyConstraintEditor.php create mode 100644 doctrine/dbal/src/Schema/SQLiteSchemaManager.php create mode 100644 doctrine/dbal/src/Schema/SchemaProvider.php create mode 100644 doctrine/dbal/src/Schema/SequenceEditor.php delete mode 100644 doctrine/dbal/src/Schema/SqliteSchemaManager.php create mode 100644 doctrine/dbal/src/Schema/TableConfiguration.php create mode 100644 doctrine/dbal/src/Schema/TableEditor.php create mode 100644 doctrine/dbal/src/Schema/UniqueConstraintEditor.php create mode 100644 doctrine/dbal/src/Schema/ViewEditor.php delete mode 100644 doctrine/dbal/src/Schema/Visitor/AbstractVisitor.php delete mode 100644 doctrine/dbal/src/Schema/Visitor/CreateSchemaSqlCollector.php delete mode 100644 doctrine/dbal/src/Schema/Visitor/DropSchemaSqlCollector.php delete mode 100644 doctrine/dbal/src/Schema/Visitor/Graphviz.php delete mode 100644 doctrine/dbal/src/Schema/Visitor/NamespaceVisitor.php delete mode 100644 doctrine/dbal/src/Schema/Visitor/RemoveNamespacedAssets.php delete mode 100644 doctrine/dbal/src/Schema/Visitor/Visitor.php create mode 100644 doctrine/dbal/src/ServerVersionProvider.php delete mode 100644 doctrine/dbal/src/Tools/Console/Command/CommandCompatibility.php delete mode 100644 doctrine/dbal/src/Tools/Console/Command/ReservedWordsCommand.php delete mode 100644 doctrine/dbal/src/Tools/Console/ConsoleRunner.php delete mode 100644 doctrine/dbal/src/Types/ArrayType.php create mode 100644 doctrine/dbal/src/Types/EnumType.php create mode 100644 doctrine/dbal/src/Types/Exception/InvalidFormat.php create mode 100644 doctrine/dbal/src/Types/Exception/InvalidType.php create mode 100644 doctrine/dbal/src/Types/Exception/SerializationFailed.php create mode 100644 doctrine/dbal/src/Types/Exception/TypeAlreadyRegistered.php create mode 100644 doctrine/dbal/src/Types/Exception/TypeArgumentCountError.php create mode 100644 doctrine/dbal/src/Types/Exception/TypeNotFound.php create mode 100644 doctrine/dbal/src/Types/Exception/TypeNotRegistered.php create mode 100644 doctrine/dbal/src/Types/Exception/TypesAlreadyExists.php create mode 100644 doctrine/dbal/src/Types/Exception/TypesException.php create mode 100644 doctrine/dbal/src/Types/Exception/UnknownColumnType.php create mode 100644 doctrine/dbal/src/Types/Exception/ValueNotConvertible.php create mode 100644 doctrine/dbal/src/Types/JsonObjectType.php create mode 100644 doctrine/dbal/src/Types/JsonTypeConvert.php create mode 100644 doctrine/dbal/src/Types/JsonbObjectType.php create mode 100644 doctrine/dbal/src/Types/JsonbType.php create mode 100644 doctrine/dbal/src/Types/NumberType.php delete mode 100644 doctrine/dbal/src/Types/ObjectType.php create mode 100644 doctrine/dbal/src/Types/PhpDateMappingType.php create mode 100644 doctrine/dbal/src/Types/PhpTimeMappingType.php create mode 100644 doctrine/dbal/src/Types/SmallFloatType.php delete mode 100644 doctrine/dbal/src/VersionAwarePlatformDriver.php delete mode 100644 doctrine/event-manager/LICENSE delete mode 100644 doctrine/event-manager/src/EventArgs.php delete mode 100644 doctrine/event-manager/src/EventManager.php delete mode 100644 doctrine/event-manager/src/EventSubscriber.php diff --git a/composer.json b/composer.json index 83091cded..660cccc48 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "cweagans/composer-patches": "^1.7", "deepdiver/zipstreamer": "^3.0.1", "deepdiver1975/tarstreamer": "^2.1.0", - "doctrine/dbal": "^3.10.4", + "doctrine/dbal": "^4", "egulias/email-validator": "^4.0.4", "firebase/php-jwt": "^7.0", "fusonic/opengraph": "^3.0.0", diff --git a/composer.lock b/composer.lock index 17422813e..c0dd9c6a3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d0ded56e0fa96043bd4b79e6fda73ad7", + "content-hash": "08ac9511062b3e0b0a08a596bac7d112", "packages": [ { "name": "aws/aws-crt-php", @@ -434,48 +434,40 @@ }, { "name": "doctrine/dbal", - "version": "3.10.4", + "version": "4.4.4", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868" + "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/63a46cb5aa6f60991186cc98c1d1b50c09311868", - "reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce", + "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce", "shasum": "" }, "require": { - "composer-runtime-api": "^2", - "doctrine/deprecations": "^0.5.3|^1", - "doctrine/event-manager": "^1|^2", - "php": "^7.4 || ^8.0", + "doctrine/deprecations": "^1.1.5", + "php": "^8.2", "psr/cache": "^1|^2|^3", "psr/log": "^1|^2|^3" }, - "conflict": { - "doctrine/cache": "< 1.11" - }, "require-dev": { - "doctrine/cache": "^1.11|^2.0", "doctrine/coding-standard": "14.0.0", "fig/log-test": "^1", - "jetbrains/phpstorm-stubs": "2023.1", + "jetbrains/phpstorm-stubs": "2023.2", "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-phpunit": "2.0.7", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "9.6.29", - "slevomat/coding-standard": "8.24.0", - "squizlabs/php_codesniffer": "4.0.0", - "symfony/cache": "^5.4|^6.0|^7.0|^8.0", - "symfony/console": "^4.4|^5.4|^6.0|^7.0|^8.0" + "phpunit/phpunit": "11.5.50", + "slevomat/coding-standard": "8.27.1", + "squizlabs/php_codesniffer": "4.0.1", + "symfony/cache": "^6.3.8|^7.0|^8.0", + "symfony/console": "^5.4|^6.3|^7.0|^8.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." }, - "bin": [ - "bin/doctrine-dbal" - ], "type": "library", "autoload": { "psr-4": { @@ -528,7 +520,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.10.4" + "source": "https://github.com/doctrine/dbal/tree/4.4.4" }, "funding": [ { @@ -544,7 +536,7 @@ "type": "tidelift" } ], - "time": "2025-11-29T10:46:08+00:00" + "time": "2026-07-21T14:34:40+00:00" }, { "name": "doctrine/deprecations", @@ -594,97 +586,6 @@ }, "time": "2026-02-07T07:09:04+00:00" }, - { - "name": "doctrine/event-manager", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/event-manager.git", - "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", - "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", - "shasum": "" - }, - "require": { - "php": "^8.1" - }, - "conflict": { - "doctrine/common": "<2.9" - }, - "require-dev": { - "doctrine/coding-standard": "^12", - "phpstan/phpstan": "^1.8.8", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.24" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - }, - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" - } - ], - "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", - "homepage": "https://www.doctrine-project.org/projects/event-manager.html", - "keywords": [ - "event", - "event dispatcher", - "event manager", - "event system", - "events" - ], - "support": { - "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/2.0.1" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", - "type": "tidelift" - } - ], - "time": "2024-05-22T20:47:39+00:00" - }, { "name": "doctrine/lexer", "version": "3.0.1", diff --git a/composer/InstalledVersions.php b/composer/InstalledVersions.php index 2052022fd..8d3f8acd3 100644 --- a/composer/InstalledVersions.php +++ b/composer/InstalledVersions.php @@ -277,7 +277,7 @@ public static function getRawData() if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 - if (substr(__DIR__, -8, 1) !== 'C') { + if (substr(__DIR__, -8, 1) !== 'C' && is_file(__DIR__ . '/installed.php')) { self::$installed = include __DIR__ . '/installed.php'; } else { self::$installed = array(); @@ -378,7 +378,7 @@ private static function getInstalled() if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 - if (substr(__DIR__, -8, 1) !== 'C') { + if (substr(__DIR__, -8, 1) !== 'C' && is_file(__DIR__ . '/installed.php')) { /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ $required = require __DIR__ . '/installed.php'; self::$installed = $required; diff --git a/composer/LICENSE b/composer/LICENSE index f27399a04..62ecfd8d0 100644 --- a/composer/LICENSE +++ b/composer/LICENSE @@ -1,4 +1,3 @@ - Copyright (c) Nils Adermann, Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy @@ -18,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/composer/autoload_classmap.php b/composer/autoload_classmap.php index f8880bf68..11e30c1b3 100644 --- a/composer/autoload_classmap.php +++ b/composer/autoload_classmap.php @@ -515,9 +515,6 @@ 'Cose\\Key\\RsaKey' => $vendorDir . '/web-auth/cose-lib/src/Key/RsaKey.php', 'Cose\\Key\\SymmetricKey' => $vendorDir . '/web-auth/cose-lib/src/Key/SymmetricKey.php', 'Deprecated' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/Deprecated.php', - 'Doctrine\\Common\\EventArgs' => $vendorDir . '/doctrine/event-manager/src/EventArgs.php', - 'Doctrine\\Common\\EventManager' => $vendorDir . '/doctrine/event-manager/src/EventManager.php', - 'Doctrine\\Common\\EventSubscriber' => $vendorDir . '/doctrine/event-manager/src/EventSubscriber.php', 'Doctrine\\Common\\Lexer\\AbstractLexer' => $vendorDir . '/doctrine/lexer/src/AbstractLexer.php', 'Doctrine\\Common\\Lexer\\Token' => $vendorDir . '/doctrine/lexer/src/Token.php', 'Doctrine\\DBAL\\ArrayParameterType' => $vendorDir . '/doctrine/dbal/src/ArrayParameterType.php', @@ -526,11 +523,14 @@ 'Doctrine\\DBAL\\ArrayParameters\\Exception\\MissingPositionalParameter' => $vendorDir . '/doctrine/dbal/src/ArrayParameters/Exception/MissingPositionalParameter.php', 'Doctrine\\DBAL\\Cache\\ArrayResult' => $vendorDir . '/doctrine/dbal/src/Cache/ArrayResult.php', 'Doctrine\\DBAL\\Cache\\CacheException' => $vendorDir . '/doctrine/dbal/src/Cache/CacheException.php', + 'Doctrine\\DBAL\\Cache\\Exception\\NoCacheKey' => $vendorDir . '/doctrine/dbal/src/Cache/Exception/NoCacheKey.php', + 'Doctrine\\DBAL\\Cache\\Exception\\NoResultDriverConfigured' => $vendorDir . '/doctrine/dbal/src/Cache/Exception/NoResultDriverConfigured.php', 'Doctrine\\DBAL\\Cache\\QueryCacheProfile' => $vendorDir . '/doctrine/dbal/src/Cache/QueryCacheProfile.php', 'Doctrine\\DBAL\\ColumnCase' => $vendorDir . '/doctrine/dbal/src/ColumnCase.php', 'Doctrine\\DBAL\\Configuration' => $vendorDir . '/doctrine/dbal/src/Configuration.php', 'Doctrine\\DBAL\\Connection' => $vendorDir . '/doctrine/dbal/src/Connection.php', 'Doctrine\\DBAL\\ConnectionException' => $vendorDir . '/doctrine/dbal/src/ConnectionException.php', + 'Doctrine\\DBAL\\Connection\\StaticServerVersionProvider' => $vendorDir . '/doctrine/dbal/src/Connection/StaticServerVersionProvider.php', 'Doctrine\\DBAL\\Connections\\PrimaryReadReplicaConnection' => $vendorDir . '/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php', 'Doctrine\\DBAL\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver.php', 'Doctrine\\DBAL\\DriverManager' => $vendorDir . '/doctrine/dbal/src/DriverManager.php', @@ -541,7 +541,6 @@ 'Doctrine\\DBAL\\Driver\\API\\PostgreSQL\\ExceptionConverter' => $vendorDir . '/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php', 'Doctrine\\DBAL\\Driver\\API\\SQLSrv\\ExceptionConverter' => $vendorDir . '/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php', 'Doctrine\\DBAL\\Driver\\API\\SQLite\\ExceptionConverter' => $vendorDir . '/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php', - 'Doctrine\\DBAL\\Driver\\API\\SQLite\\UserDefinedFunctions' => $vendorDir . '/doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php', 'Doctrine\\DBAL\\Driver\\AbstractDB2Driver' => $vendorDir . '/doctrine/dbal/src/Driver/AbstractDB2Driver.php', 'Doctrine\\DBAL\\Driver\\AbstractException' => $vendorDir . '/doctrine/dbal/src/Driver/AbstractException.php', 'Doctrine\\DBAL\\Driver\\AbstractMySQLDriver' => $vendorDir . '/doctrine/dbal/src/Driver/AbstractMySQLDriver.php', @@ -554,7 +553,8 @@ 'Doctrine\\DBAL\\Driver\\AbstractSQLiteDriver\\Middleware\\EnableForeignKeys' => $vendorDir . '/doctrine/dbal/src/Driver/AbstractSQLiteDriver/Middleware/EnableForeignKeys.php', 'Doctrine\\DBAL\\Driver\\Connection' => $vendorDir . '/doctrine/dbal/src/Driver/Connection.php', 'Doctrine\\DBAL\\Driver\\Exception' => $vendorDir . '/doctrine/dbal/src/Driver/Exception.php', - 'Doctrine\\DBAL\\Driver\\Exception\\UnknownParameterType' => $vendorDir . '/doctrine/dbal/src/Driver/Exception/UnknownParameterType.php', + 'Doctrine\\DBAL\\Driver\\Exception\\IdentityColumnsNotSupported' => $vendorDir . '/doctrine/dbal/src/Driver/Exception/IdentityColumnsNotSupported.php', + 'Doctrine\\DBAL\\Driver\\Exception\\NoIdentityValue' => $vendorDir . '/doctrine/dbal/src/Driver/Exception/NoIdentityValue.php', 'Doctrine\\DBAL\\Driver\\FetchUtils' => $vendorDir . '/doctrine/dbal/src/Driver/FetchUtils.php', 'Doctrine\\DBAL\\Driver\\IBMDB2\\Connection' => $vendorDir . '/doctrine/dbal/src/Driver/IBMDB2/Connection.php', 'Doctrine\\DBAL\\Driver\\IBMDB2\\DataSourceName' => $vendorDir . '/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php', @@ -596,7 +596,6 @@ 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\Error' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Exception/Error.php', 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\InvalidConfiguration' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Exception/InvalidConfiguration.php', 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\NonTerminatedStringLiteral' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Exception/NonTerminatedStringLiteral.php', - 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\SequenceDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Exception/SequenceDoesNotExist.php', 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\UnknownParameterIndex' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Exception/UnknownParameterIndex.php', 'Doctrine\\DBAL\\Driver\\OCI8\\ExecutionMode' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/ExecutionMode.php', 'Doctrine\\DBAL\\Driver\\OCI8\\Middleware\\InitializeSession' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Middleware/InitializeSession.php', @@ -604,11 +603,10 @@ 'Doctrine\\DBAL\\Driver\\OCI8\\Statement' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Statement.php', 'Doctrine\\DBAL\\Driver\\PDO\\Connection' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/Connection.php', 'Doctrine\\DBAL\\Driver\\PDO\\Exception' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/Exception.php', + 'Doctrine\\DBAL\\Driver\\PDO\\Exception\\InvalidConfiguration' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/Exception/InvalidConfiguration.php', 'Doctrine\\DBAL\\Driver\\PDO\\MySQL\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/MySQL/Driver.php', 'Doctrine\\DBAL\\Driver\\PDO\\OCI\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/OCI/Driver.php', 'Doctrine\\DBAL\\Driver\\PDO\\PDOConnect' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/PDOConnect.php', - 'Doctrine\\DBAL\\Driver\\PDO\\PDOException' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/PDOException.php', - 'Doctrine\\DBAL\\Driver\\PDO\\ParameterTypeMap' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/ParameterTypeMap.php', 'Doctrine\\DBAL\\Driver\\PDO\\PgSQL\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php', 'Doctrine\\DBAL\\Driver\\PDO\\Result' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/Result.php', 'Doctrine\\DBAL\\Driver\\PDO\\SQLSrv\\Connection' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php', @@ -635,29 +633,9 @@ 'Doctrine\\DBAL\\Driver\\SQLite3\\Exception' => $vendorDir . '/doctrine/dbal/src/Driver/SQLite3/Exception.php', 'Doctrine\\DBAL\\Driver\\SQLite3\\Result' => $vendorDir . '/doctrine/dbal/src/Driver/SQLite3/Result.php', 'Doctrine\\DBAL\\Driver\\SQLite3\\Statement' => $vendorDir . '/doctrine/dbal/src/Driver/SQLite3/Statement.php', - 'Doctrine\\DBAL\\Driver\\ServerInfoAwareConnection' => $vendorDir . '/doctrine/dbal/src/Driver/ServerInfoAwareConnection.php', 'Doctrine\\DBAL\\Driver\\Statement' => $vendorDir . '/doctrine/dbal/src/Driver/Statement.php', - 'Doctrine\\DBAL\\Event\\ConnectionEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/ConnectionEventArgs.php', - 'Doctrine\\DBAL\\Event\\Listeners\\OracleSessionInit' => $vendorDir . '/doctrine/dbal/src/Event/Listeners/OracleSessionInit.php', - 'Doctrine\\DBAL\\Event\\Listeners\\SQLSessionInit' => $vendorDir . '/doctrine/dbal/src/Event/Listeners/SQLSessionInit.php', - 'Doctrine\\DBAL\\Event\\Listeners\\SQLiteSessionInit' => $vendorDir . '/doctrine/dbal/src/Event/Listeners/SQLiteSessionInit.php', - 'Doctrine\\DBAL\\Event\\SchemaAlterTableAddColumnEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaAlterTableAddColumnEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaAlterTableChangeColumnEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaAlterTableChangeColumnEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaAlterTableEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaAlterTableEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaAlterTableRemoveColumnEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaAlterTableRemoveColumnEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaAlterTableRenameColumnEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaAlterTableRenameColumnEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaColumnDefinitionEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaColumnDefinitionEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaCreateTableColumnEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaCreateTableColumnEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaCreateTableEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaCreateTableEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaDropTableEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaDropTableEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaIndexDefinitionEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaIndexDefinitionEventArgs.php', - 'Doctrine\\DBAL\\Event\\TransactionBeginEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/TransactionBeginEventArgs.php', - 'Doctrine\\DBAL\\Event\\TransactionCommitEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/TransactionCommitEventArgs.php', - 'Doctrine\\DBAL\\Event\\TransactionEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/TransactionEventArgs.php', - 'Doctrine\\DBAL\\Event\\TransactionRollBackEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/TransactionRollBackEventArgs.php', - 'Doctrine\\DBAL\\Events' => $vendorDir . '/doctrine/dbal/src/Events.php', 'Doctrine\\DBAL\\Exception' => $vendorDir . '/doctrine/dbal/src/Exception.php', + 'Doctrine\\DBAL\\Exception\\CommitFailedRollbackOnly' => $vendorDir . '/doctrine/dbal/src/Exception/CommitFailedRollbackOnly.php', 'Doctrine\\DBAL\\Exception\\ConnectionException' => $vendorDir . '/doctrine/dbal/src/Exception/ConnectionException.php', 'Doctrine\\DBAL\\Exception\\ConnectionLost' => $vendorDir . '/doctrine/dbal/src/Exception/ConnectionLost.php', 'Doctrine\\DBAL\\Exception\\ConstraintViolationException' => $vendorDir . '/doctrine/dbal/src/Exception/ConstraintViolationException.php', @@ -667,17 +645,29 @@ 'Doctrine\\DBAL\\Exception\\DatabaseRequired' => $vendorDir . '/doctrine/dbal/src/Exception/DatabaseRequired.php', 'Doctrine\\DBAL\\Exception\\DeadlockException' => $vendorDir . '/doctrine/dbal/src/Exception/DeadlockException.php', 'Doctrine\\DBAL\\Exception\\DriverException' => $vendorDir . '/doctrine/dbal/src/Exception/DriverException.php', + 'Doctrine\\DBAL\\Exception\\DriverRequired' => $vendorDir . '/doctrine/dbal/src/Exception/DriverRequired.php', 'Doctrine\\DBAL\\Exception\\ForeignKeyConstraintViolationException' => $vendorDir . '/doctrine/dbal/src/Exception/ForeignKeyConstraintViolationException.php', 'Doctrine\\DBAL\\Exception\\InvalidArgumentException' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidArgumentException.php', + 'Doctrine\\DBAL\\Exception\\InvalidColumnDeclaration' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidColumnDeclaration.php', + 'Doctrine\\DBAL\\Exception\\InvalidColumnIndex' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidColumnIndex.php', + 'Doctrine\\DBAL\\Exception\\InvalidColumnType' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidColumnType.php', + 'Doctrine\\DBAL\\Exception\\InvalidColumnType\\ColumnLengthRequired' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidColumnType/ColumnLengthRequired.php', + 'Doctrine\\DBAL\\Exception\\InvalidColumnType\\ColumnPrecisionRequired' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidColumnType/ColumnPrecisionRequired.php', + 'Doctrine\\DBAL\\Exception\\InvalidColumnType\\ColumnScaleRequired' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidColumnType/ColumnScaleRequired.php', + 'Doctrine\\DBAL\\Exception\\InvalidColumnType\\ColumnValuesRequired' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidColumnType/ColumnValuesRequired.php', + 'Doctrine\\DBAL\\Exception\\InvalidDriverClass' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidDriverClass.php', 'Doctrine\\DBAL\\Exception\\InvalidFieldNameException' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidFieldNameException.php', - 'Doctrine\\DBAL\\Exception\\InvalidLockMode' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidLockMode.php', + 'Doctrine\\DBAL\\Exception\\InvalidWrapperClass' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidWrapperClass.php', 'Doctrine\\DBAL\\Exception\\LockWaitTimeoutException' => $vendorDir . '/doctrine/dbal/src/Exception/LockWaitTimeoutException.php', 'Doctrine\\DBAL\\Exception\\MalformedDsnException' => $vendorDir . '/doctrine/dbal/src/Exception/MalformedDsnException.php', + 'Doctrine\\DBAL\\Exception\\NoActiveTransaction' => $vendorDir . '/doctrine/dbal/src/Exception/NoActiveTransaction.php', 'Doctrine\\DBAL\\Exception\\NoKeyValue' => $vendorDir . '/doctrine/dbal/src/Exception/NoKeyValue.php', 'Doctrine\\DBAL\\Exception\\NonUniqueFieldNameException' => $vendorDir . '/doctrine/dbal/src/Exception/NonUniqueFieldNameException.php', 'Doctrine\\DBAL\\Exception\\NotNullConstraintViolationException' => $vendorDir . '/doctrine/dbal/src/Exception/NotNullConstraintViolationException.php', + 'Doctrine\\DBAL\\Exception\\ParseError' => $vendorDir . '/doctrine/dbal/src/Exception/ParseError.php', 'Doctrine\\DBAL\\Exception\\ReadOnlyException' => $vendorDir . '/doctrine/dbal/src/Exception/ReadOnlyException.php', 'Doctrine\\DBAL\\Exception\\RetryableException' => $vendorDir . '/doctrine/dbal/src/Exception/RetryableException.php', + 'Doctrine\\DBAL\\Exception\\SavepointsNotSupported' => $vendorDir . '/doctrine/dbal/src/Exception/SavepointsNotSupported.php', 'Doctrine\\DBAL\\Exception\\SchemaDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Exception/SchemaDoesNotExist.php', 'Doctrine\\DBAL\\Exception\\ServerException' => $vendorDir . '/doctrine/dbal/src/Exception/ServerException.php', 'Doctrine\\DBAL\\Exception\\SyntaxErrorException' => $vendorDir . '/doctrine/dbal/src/Exception/SyntaxErrorException.php', @@ -685,67 +675,66 @@ 'Doctrine\\DBAL\\Exception\\TableNotFoundException' => $vendorDir . '/doctrine/dbal/src/Exception/TableNotFoundException.php', 'Doctrine\\DBAL\\Exception\\TransactionRolledBack' => $vendorDir . '/doctrine/dbal/src/Exception/TransactionRolledBack.php', 'Doctrine\\DBAL\\Exception\\UniqueConstraintViolationException' => $vendorDir . '/doctrine/dbal/src/Exception/UniqueConstraintViolationException.php', + 'Doctrine\\DBAL\\Exception\\UnknownDriver' => $vendorDir . '/doctrine/dbal/src/Exception/UnknownDriver.php', 'Doctrine\\DBAL\\ExpandArrayParameters' => $vendorDir . '/doctrine/dbal/src/ExpandArrayParameters.php', - 'Doctrine\\DBAL\\FetchMode' => $vendorDir . '/doctrine/dbal/src/FetchMode.php', - 'Doctrine\\DBAL\\Id\\TableGenerator' => $vendorDir . '/doctrine/dbal/src/Id/TableGenerator.php', - 'Doctrine\\DBAL\\Id\\TableGeneratorSchemaVisitor' => $vendorDir . '/doctrine/dbal/src/Id/TableGeneratorSchemaVisitor.php', 'Doctrine\\DBAL\\LockMode' => $vendorDir . '/doctrine/dbal/src/LockMode.php', 'Doctrine\\DBAL\\Logging\\Connection' => $vendorDir . '/doctrine/dbal/src/Logging/Connection.php', - 'Doctrine\\DBAL\\Logging\\DebugStack' => $vendorDir . '/doctrine/dbal/src/Logging/DebugStack.php', 'Doctrine\\DBAL\\Logging\\Driver' => $vendorDir . '/doctrine/dbal/src/Logging/Driver.php', - 'Doctrine\\DBAL\\Logging\\LoggerChain' => $vendorDir . '/doctrine/dbal/src/Logging/LoggerChain.php', 'Doctrine\\DBAL\\Logging\\Middleware' => $vendorDir . '/doctrine/dbal/src/Logging/Middleware.php', - 'Doctrine\\DBAL\\Logging\\SQLLogger' => $vendorDir . '/doctrine/dbal/src/Logging/SQLLogger.php', 'Doctrine\\DBAL\\Logging\\Statement' => $vendorDir . '/doctrine/dbal/src/Logging/Statement.php', 'Doctrine\\DBAL\\ParameterType' => $vendorDir . '/doctrine/dbal/src/ParameterType.php', 'Doctrine\\DBAL\\Platforms\\AbstractMySQLPlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php', 'Doctrine\\DBAL\\Platforms\\AbstractPlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/AbstractPlatform.php', - 'Doctrine\\DBAL\\Platforms\\DB2111Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/DB2111Platform.php', 'Doctrine\\DBAL\\Platforms\\DB2Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/DB2Platform.php', 'Doctrine\\DBAL\\Platforms\\DateIntervalUnit' => $vendorDir . '/doctrine/dbal/src/Platforms/DateIntervalUnit.php', + 'Doctrine\\DBAL\\Platforms\\Db2\\Db2MetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/Db2/Db2MetadataProvider.php', + 'Doctrine\\DBAL\\Platforms\\Exception\\InvalidPlatformVersion' => $vendorDir . '/doctrine/dbal/src/Platforms/Exception/InvalidPlatformVersion.php', + 'Doctrine\\DBAL\\Platforms\\Exception\\NoColumnsSpecifiedForTable' => $vendorDir . '/doctrine/dbal/src/Platforms/Exception/NoColumnsSpecifiedForTable.php', + 'Doctrine\\DBAL\\Platforms\\Exception\\NotSupported' => $vendorDir . '/doctrine/dbal/src/Platforms/Exception/NotSupported.php', + 'Doctrine\\DBAL\\Platforms\\Exception\\PlatformException' => $vendorDir . '/doctrine/dbal/src/Platforms/Exception/PlatformException.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\DB2Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/DB2Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\KeywordList' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/KeywordList.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDB117Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MariaDB117Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDB123Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MariaDB123Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDBKeywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php', - 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDb102Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MariaDb102Keywords.php', - 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDb117Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MariaDb117Keywords.php', - 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL57Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MySQL57Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL80Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MySQL80Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL84Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MySQL84Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQLKeywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MySQLKeywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\OracleKeywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/OracleKeywords.php', - 'Doctrine\\DBAL\\Platforms\\Keywords\\PostgreSQL100Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/PostgreSQL100Keywords.php', - 'Doctrine\\DBAL\\Platforms\\Keywords\\PostgreSQL94Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/PostgreSQL94Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\PostgreSQLKeywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/PostgreSQLKeywords.php', - 'Doctrine\\DBAL\\Platforms\\Keywords\\ReservedKeywordsValidator' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/ReservedKeywordsValidator.php', - 'Doctrine\\DBAL\\Platforms\\Keywords\\SQLServer2012Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/SQLServer2012Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\SQLServerKeywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/SQLServerKeywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\SQLiteKeywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/SQLiteKeywords.php', + 'Doctrine\\DBAL\\Platforms\\MariaDB1010Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDB1010Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDB1052Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDB1052Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDB1060Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDB1060Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDB110700Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDB110700Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDB120300Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDB120300Platform.php', 'Doctrine\\DBAL\\Platforms\\MariaDBPlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDBPlatform.php', - 'Doctrine\\DBAL\\Platforms\\MariaDb1010Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDb1010Platform.php', - 'Doctrine\\DBAL\\Platforms\\MariaDb1027Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDb1027Platform.php', - 'Doctrine\\DBAL\\Platforms\\MariaDb1043Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDb1043Platform.php', - 'Doctrine\\DBAL\\Platforms\\MariaDb1052Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDb1052Platform.php', - 'Doctrine\\DBAL\\Platforms\\MariaDb1060Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDb1060Platform.php', - 'Doctrine\\DBAL\\Platforms\\MariaDb110700Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDb110700Platform.php', - 'Doctrine\\DBAL\\Platforms\\MySQL57Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL57Platform.php', 'Doctrine\\DBAL\\Platforms\\MySQL80Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL80Platform.php', 'Doctrine\\DBAL\\Platforms\\MySQL84Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL84Platform.php', 'Doctrine\\DBAL\\Platforms\\MySQLPlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQLPlatform.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\CharsetMetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\CharsetMetadataProvider\\CachingCharsetMetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/CachingCharsetMetadataProvider.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\CharsetMetadataProvider\\ConnectionCharsetMetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\MySQL\\CollationMetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\MySQL\\CollationMetadataProvider\\CachingCollationMetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/CachingCollationMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\MySQL\\CollationMetadataProvider\\ConnectionCollationMetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\MySQL\\Comparator' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL/Comparator.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\DefaultTableOptions' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL/DefaultTableOptions.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\MySQLMetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL/MySQLMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\OraclePlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/OraclePlatform.php', - 'Doctrine\\DBAL\\Platforms\\PostgreSQL100Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/PostgreSQL100Platform.php', + 'Doctrine\\DBAL\\Platforms\\Oracle\\OracleMetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/Oracle/OracleMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\PostgreSQL120Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/PostgreSQL120Platform.php', - 'Doctrine\\DBAL\\Platforms\\PostgreSQL94Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/PostgreSQL94Platform.php', 'Doctrine\\DBAL\\Platforms\\PostgreSQLPlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/PostgreSQLPlatform.php', - 'Doctrine\\DBAL\\Platforms\\SQLServer2012Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/SQLServer2012Platform.php', + 'Doctrine\\DBAL\\Platforms\\PostgreSQL\\PostgreSQLMetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/PostgreSQL/PostgreSQLMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\SQLServerPlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/SQLServerPlatform.php', 'Doctrine\\DBAL\\Platforms\\SQLServer\\Comparator' => $vendorDir . '/doctrine/dbal/src/Platforms/SQLServer/Comparator.php', + 'Doctrine\\DBAL\\Platforms\\SQLServer\\SQLServerMetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/SQLServer/SQLServerMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\SQLServer\\SQL\\Builder\\SQLServerSelectSQLBuilder' => $vendorDir . '/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php', + 'Doctrine\\DBAL\\Platforms\\SQLitePlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/SQLitePlatform.php', 'Doctrine\\DBAL\\Platforms\\SQLite\\Comparator' => $vendorDir . '/doctrine/dbal/src/Platforms/SQLite/Comparator.php', - 'Doctrine\\DBAL\\Platforms\\SqlitePlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/SqlitePlatform.php', + 'Doctrine\\DBAL\\Platforms\\SQLite\\SQLiteMetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/SQLite/SQLiteMetadataProvider.php', + 'Doctrine\\DBAL\\Platforms\\SQLite\\SQLiteMetadataProvider\\ForeignKeyConstraintDetails' => $vendorDir . '/doctrine/dbal/src/Platforms/SQLite/SQLiteMetadataProvider/ForeignKeyConstraintDetails.php', 'Doctrine\\DBAL\\Platforms\\TrimMode' => $vendorDir . '/doctrine/dbal/src/Platforms/TrimMode.php', 'Doctrine\\DBAL\\Portability\\Connection' => $vendorDir . '/doctrine/dbal/src/Portability/Connection.php', 'Doctrine\\DBAL\\Portability\\Converter' => $vendorDir . '/doctrine/dbal/src/Portability/Converter.php', @@ -755,83 +744,162 @@ 'Doctrine\\DBAL\\Portability\\Result' => $vendorDir . '/doctrine/dbal/src/Portability/Result.php', 'Doctrine\\DBAL\\Portability\\Statement' => $vendorDir . '/doctrine/dbal/src/Portability/Statement.php', 'Doctrine\\DBAL\\Query' => $vendorDir . '/doctrine/dbal/src/Query.php', + 'Doctrine\\DBAL\\Query\\CommonTableExpression' => $vendorDir . '/doctrine/dbal/src/Query/CommonTableExpression.php', + 'Doctrine\\DBAL\\Query\\Exception\\NonUniqueAlias' => $vendorDir . '/doctrine/dbal/src/Query/Exception/NonUniqueAlias.php', + 'Doctrine\\DBAL\\Query\\Exception\\UnknownAlias' => $vendorDir . '/doctrine/dbal/src/Query/Exception/UnknownAlias.php', 'Doctrine\\DBAL\\Query\\Expression\\CompositeExpression' => $vendorDir . '/doctrine/dbal/src/Query/Expression/CompositeExpression.php', 'Doctrine\\DBAL\\Query\\Expression\\ExpressionBuilder' => $vendorDir . '/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php', 'Doctrine\\DBAL\\Query\\ForUpdate' => $vendorDir . '/doctrine/dbal/src/Query/ForUpdate.php', 'Doctrine\\DBAL\\Query\\ForUpdate\\ConflictResolutionMode' => $vendorDir . '/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php', + 'Doctrine\\DBAL\\Query\\From' => $vendorDir . '/doctrine/dbal/src/Query/From.php', + 'Doctrine\\DBAL\\Query\\Join' => $vendorDir . '/doctrine/dbal/src/Query/Join.php', 'Doctrine\\DBAL\\Query\\Limit' => $vendorDir . '/doctrine/dbal/src/Query/Limit.php', 'Doctrine\\DBAL\\Query\\QueryBuilder' => $vendorDir . '/doctrine/dbal/src/Query/QueryBuilder.php', 'Doctrine\\DBAL\\Query\\QueryException' => $vendorDir . '/doctrine/dbal/src/Query/QueryException.php', + 'Doctrine\\DBAL\\Query\\QueryType' => $vendorDir . '/doctrine/dbal/src/Query/QueryType.php', 'Doctrine\\DBAL\\Query\\SelectQuery' => $vendorDir . '/doctrine/dbal/src/Query/SelectQuery.php', + 'Doctrine\\DBAL\\Query\\Union' => $vendorDir . '/doctrine/dbal/src/Query/Union.php', + 'Doctrine\\DBAL\\Query\\UnionQuery' => $vendorDir . '/doctrine/dbal/src/Query/UnionQuery.php', + 'Doctrine\\DBAL\\Query\\UnionType' => $vendorDir . '/doctrine/dbal/src/Query/UnionType.php', 'Doctrine\\DBAL\\Result' => $vendorDir . '/doctrine/dbal/src/Result.php', 'Doctrine\\DBAL\\SQL\\Builder\\CreateSchemaObjectsSQLBuilder' => $vendorDir . '/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php', 'Doctrine\\DBAL\\SQL\\Builder\\DefaultSelectSQLBuilder' => $vendorDir . '/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php', + 'Doctrine\\DBAL\\SQL\\Builder\\DefaultUnionSQLBuilder' => $vendorDir . '/doctrine/dbal/src/SQL/Builder/DefaultUnionSQLBuilder.php', 'Doctrine\\DBAL\\SQL\\Builder\\DropSchemaObjectsSQLBuilder' => $vendorDir . '/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php', 'Doctrine\\DBAL\\SQL\\Builder\\SelectSQLBuilder' => $vendorDir . '/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php', + 'Doctrine\\DBAL\\SQL\\Builder\\UnionSQLBuilder' => $vendorDir . '/doctrine/dbal/src/SQL/Builder/UnionSQLBuilder.php', + 'Doctrine\\DBAL\\SQL\\Builder\\WithSQLBuilder' => $vendorDir . '/doctrine/dbal/src/SQL/Builder/WithSQLBuilder.php', 'Doctrine\\DBAL\\SQL\\Parser' => $vendorDir . '/doctrine/dbal/src/SQL/Parser.php', 'Doctrine\\DBAL\\SQL\\Parser\\Exception' => $vendorDir . '/doctrine/dbal/src/SQL/Parser/Exception.php', 'Doctrine\\DBAL\\SQL\\Parser\\Exception\\RegularExpressionError' => $vendorDir . '/doctrine/dbal/src/SQL/Parser/Exception/RegularExpressionError.php', 'Doctrine\\DBAL\\SQL\\Parser\\Visitor' => $vendorDir . '/doctrine/dbal/src/SQL/Parser/Visitor.php', 'Doctrine\\DBAL\\Schema\\AbstractAsset' => $vendorDir . '/doctrine/dbal/src/Schema/AbstractAsset.php', + 'Doctrine\\DBAL\\Schema\\AbstractNamedObject' => $vendorDir . '/doctrine/dbal/src/Schema/AbstractNamedObject.php', + 'Doctrine\\DBAL\\Schema\\AbstractOptionallyNamedObject' => $vendorDir . '/doctrine/dbal/src/Schema/AbstractOptionallyNamedObject.php', 'Doctrine\\DBAL\\Schema\\AbstractSchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/AbstractSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\Collections\\Exception' => $vendorDir . '/doctrine/dbal/src/Schema/Collections/Exception.php', + 'Doctrine\\DBAL\\Schema\\Collections\\Exception\\ObjectAlreadyExists' => $vendorDir . '/doctrine/dbal/src/Schema/Collections/Exception/ObjectAlreadyExists.php', + 'Doctrine\\DBAL\\Schema\\Collections\\Exception\\ObjectDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Schema/Collections/Exception/ObjectDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Collections\\ObjectSet' => $vendorDir . '/doctrine/dbal/src/Schema/Collections/ObjectSet.php', + 'Doctrine\\DBAL\\Schema\\Collections\\OptionallyUnqualifiedNamedObjectSet' => $vendorDir . '/doctrine/dbal/src/Schema/Collections/OptionallyUnqualifiedNamedObjectSet.php', + 'Doctrine\\DBAL\\Schema\\Collections\\UnqualifiedNamedObjectSet' => $vendorDir . '/doctrine/dbal/src/Schema/Collections/UnqualifiedNamedObjectSet.php', 'Doctrine\\DBAL\\Schema\\Column' => $vendorDir . '/doctrine/dbal/src/Schema/Column.php', 'Doctrine\\DBAL\\Schema\\ColumnDiff' => $vendorDir . '/doctrine/dbal/src/Schema/ColumnDiff.php', + 'Doctrine\\DBAL\\Schema\\ColumnEditor' => $vendorDir . '/doctrine/dbal/src/Schema/ColumnEditor.php', 'Doctrine\\DBAL\\Schema\\Comparator' => $vendorDir . '/doctrine/dbal/src/Schema/Comparator.php', - 'Doctrine\\DBAL\\Schema\\Constraint' => $vendorDir . '/doctrine/dbal/src/Schema/Constraint.php', + 'Doctrine\\DBAL\\Schema\\ComparatorConfig' => $vendorDir . '/doctrine/dbal/src/Schema/ComparatorConfig.php', 'Doctrine\\DBAL\\Schema\\DB2SchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/DB2SchemaManager.php', + 'Doctrine\\DBAL\\Schema\\DefaultExpression' => $vendorDir . '/doctrine/dbal/src/Schema/DefaultExpression.php', + 'Doctrine\\DBAL\\Schema\\DefaultExpression\\CurrentDate' => $vendorDir . '/doctrine/dbal/src/Schema/DefaultExpression/CurrentDate.php', + 'Doctrine\\DBAL\\Schema\\DefaultExpression\\CurrentTime' => $vendorDir . '/doctrine/dbal/src/Schema/DefaultExpression/CurrentTime.php', + 'Doctrine\\DBAL\\Schema\\DefaultExpression\\CurrentTimestamp' => $vendorDir . '/doctrine/dbal/src/Schema/DefaultExpression/CurrentTimestamp.php', 'Doctrine\\DBAL\\Schema\\DefaultSchemaManagerFactory' => $vendorDir . '/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php', 'Doctrine\\DBAL\\Schema\\Exception\\ColumnAlreadyExists' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php', 'Doctrine\\DBAL\\Schema\\Exception\\ColumnDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/ColumnDoesNotExist.php', 'Doctrine\\DBAL\\Schema\\Exception\\ForeignKeyDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/ForeignKeyDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Exception\\IncomparableNames' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/IncomparableNames.php', 'Doctrine\\DBAL\\Schema\\Exception\\IndexAlreadyExists' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/IndexAlreadyExists.php', 'Doctrine\\DBAL\\Schema\\Exception\\IndexDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/IndexDoesNotExist.php', 'Doctrine\\DBAL\\Schema\\Exception\\IndexNameInvalid' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/IndexNameInvalid.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidColumnDefinition' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/InvalidColumnDefinition.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidForeignKeyConstraintDefinition' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/InvalidForeignKeyConstraintDefinition.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidIdentifier' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/InvalidIdentifier.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidIndexDefinition' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/InvalidIndexDefinition.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidName' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/InvalidName.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidPrimaryKeyConstraintDefinition' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/InvalidPrimaryKeyConstraintDefinition.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidSequenceDefinition' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/InvalidSequenceDefinition.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidState' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/InvalidState.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidTableDefinition' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/InvalidTableDefinition.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidTableModification' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/InvalidTableModification.php', 'Doctrine\\DBAL\\Schema\\Exception\\InvalidTableName' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/InvalidTableName.php', - 'Doctrine\\DBAL\\Schema\\Exception\\NamedForeignKeyRequired' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/NamedForeignKeyRequired.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidUniqueConstraintDefinition' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/InvalidUniqueConstraintDefinition.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidViewDefinition' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/InvalidViewDefinition.php', 'Doctrine\\DBAL\\Schema\\Exception\\NamespaceAlreadyExists' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php', + 'Doctrine\\DBAL\\Schema\\Exception\\NotImplemented' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/NotImplemented.php', + 'Doctrine\\DBAL\\Schema\\Exception\\PrimaryKeyAlreadyExists' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/PrimaryKeyAlreadyExists.php', 'Doctrine\\DBAL\\Schema\\Exception\\SequenceAlreadyExists' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/SequenceAlreadyExists.php', 'Doctrine\\DBAL\\Schema\\Exception\\SequenceDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/SequenceDoesNotExist.php', 'Doctrine\\DBAL\\Schema\\Exception\\TableAlreadyExists' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/TableAlreadyExists.php', 'Doctrine\\DBAL\\Schema\\Exception\\TableDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/TableDoesNotExist.php', 'Doctrine\\DBAL\\Schema\\Exception\\UniqueConstraintDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/UniqueConstraintDoesNotExist.php', 'Doctrine\\DBAL\\Schema\\Exception\\UnknownColumnOption' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/UnknownColumnOption.php', + 'Doctrine\\DBAL\\Schema\\Exception\\UnsupportedName' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/UnsupportedName.php', + 'Doctrine\\DBAL\\Schema\\Exception\\UnsupportedSchema' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/UnsupportedSchema.php', 'Doctrine\\DBAL\\Schema\\ForeignKeyConstraint' => $vendorDir . '/doctrine/dbal/src/Schema/ForeignKeyConstraint.php', + 'Doctrine\\DBAL\\Schema\\ForeignKeyConstraintEditor' => $vendorDir . '/doctrine/dbal/src/Schema/ForeignKeyConstraintEditor.php', + 'Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\\Deferrability' => $vendorDir . '/doctrine/dbal/src/Schema/ForeignKeyConstraint/Deferrability.php', + 'Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\\MatchType' => $vendorDir . '/doctrine/dbal/src/Schema/ForeignKeyConstraint/MatchType.php', + 'Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\\ReferentialAction' => $vendorDir . '/doctrine/dbal/src/Schema/ForeignKeyConstraint/ReferentialAction.php', 'Doctrine\\DBAL\\Schema\\Identifier' => $vendorDir . '/doctrine/dbal/src/Schema/Identifier.php', 'Doctrine\\DBAL\\Schema\\Index' => $vendorDir . '/doctrine/dbal/src/Schema/Index.php', - 'Doctrine\\DBAL\\Schema\\LegacySchemaManagerFactory' => $vendorDir . '/doctrine/dbal/src/Schema/LegacySchemaManagerFactory.php', + 'Doctrine\\DBAL\\Schema\\IndexEditor' => $vendorDir . '/doctrine/dbal/src/Schema/IndexEditor.php', + 'Doctrine\\DBAL\\Schema\\Index\\IndexType' => $vendorDir . '/doctrine/dbal/src/Schema/Index/IndexType.php', + 'Doctrine\\DBAL\\Schema\\Index\\IndexedColumn' => $vendorDir . '/doctrine/dbal/src/Schema/Index/IndexedColumn.php', + 'Doctrine\\DBAL\\Schema\\Introspection\\IntrospectingSchemaProvider' => $vendorDir . '/doctrine/dbal/src/Schema/Introspection/IntrospectingSchemaProvider.php', + 'Doctrine\\DBAL\\Schema\\Introspection\\MetadataProcessor\\ForeignKeyConstraintColumnMetadataProcessor' => $vendorDir . '/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/ForeignKeyConstraintColumnMetadataProcessor.php', + 'Doctrine\\DBAL\\Schema\\Introspection\\MetadataProcessor\\IndexColumnMetadataProcessor' => $vendorDir . '/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/IndexColumnMetadataProcessor.php', + 'Doctrine\\DBAL\\Schema\\Introspection\\MetadataProcessor\\PrimaryKeyConstraintColumnMetadataProcessor' => $vendorDir . '/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/PrimaryKeyConstraintColumnMetadataProcessor.php', + 'Doctrine\\DBAL\\Schema\\Introspection\\MetadataProcessor\\SequenceMetadataProcessor' => $vendorDir . '/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/SequenceMetadataProcessor.php', + 'Doctrine\\DBAL\\Schema\\Introspection\\MetadataProcessor\\ViewMetadataProcessor' => $vendorDir . '/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/ViewMetadataProcessor.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\DatabaseMetadataRow' => $vendorDir . '/doctrine/dbal/src/Schema/Metadata/DatabaseMetadataRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\ForeignKeyConstraintColumnMetadataRow' => $vendorDir . '/doctrine/dbal/src/Schema/Metadata/ForeignKeyConstraintColumnMetadataRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\IndexColumnMetadataRow' => $vendorDir . '/doctrine/dbal/src/Schema/Metadata/IndexColumnMetadataRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\MetadataProvider' => $vendorDir . '/doctrine/dbal/src/Schema/Metadata/MetadataProvider.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\PrimaryKeyConstraintColumnRow' => $vendorDir . '/doctrine/dbal/src/Schema/Metadata/PrimaryKeyConstraintColumnRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\SchemaMetadataRow' => $vendorDir . '/doctrine/dbal/src/Schema/Metadata/SchemaMetadataRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\SequenceMetadataRow' => $vendorDir . '/doctrine/dbal/src/Schema/Metadata/SequenceMetadataRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\TableColumnMetadataRow' => $vendorDir . '/doctrine/dbal/src/Schema/Metadata/TableColumnMetadataRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\TableMetadataRow' => $vendorDir . '/doctrine/dbal/src/Schema/Metadata/TableMetadataRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\ViewMetadataRow' => $vendorDir . '/doctrine/dbal/src/Schema/Metadata/ViewMetadataRow.php', 'Doctrine\\DBAL\\Schema\\MySQLSchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/MySQLSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\Name' => $vendorDir . '/doctrine/dbal/src/Schema/Name.php', + 'Doctrine\\DBAL\\Schema\\Name\\GenericName' => $vendorDir . '/doctrine/dbal/src/Schema/Name/GenericName.php', + 'Doctrine\\DBAL\\Schema\\Name\\Identifier' => $vendorDir . '/doctrine/dbal/src/Schema/Name/Identifier.php', + 'Doctrine\\DBAL\\Schema\\Name\\OptionallyQualifiedName' => $vendorDir . '/doctrine/dbal/src/Schema/Name/OptionallyQualifiedName.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser' => $vendorDir . '/doctrine/dbal/src/Schema/Name/Parser.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\Exception' => $vendorDir . '/doctrine/dbal/src/Schema/Name/Parser/Exception.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\Exception\\ExpectedDot' => $vendorDir . '/doctrine/dbal/src/Schema/Name/Parser/Exception/ExpectedDot.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\Exception\\ExpectedNextIdentifier' => $vendorDir . '/doctrine/dbal/src/Schema/Name/Parser/Exception/ExpectedNextIdentifier.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\Exception\\InvalidName' => $vendorDir . '/doctrine/dbal/src/Schema/Name/Parser/Exception/InvalidName.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\Exception\\UnableToParseIdentifier' => $vendorDir . '/doctrine/dbal/src/Schema/Name/Parser/Exception/UnableToParseIdentifier.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\GenericNameParser' => $vendorDir . '/doctrine/dbal/src/Schema/Name/Parser/GenericNameParser.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\OptionallyQualifiedNameParser' => $vendorDir . '/doctrine/dbal/src/Schema/Name/Parser/OptionallyQualifiedNameParser.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\UnqualifiedNameParser' => $vendorDir . '/doctrine/dbal/src/Schema/Name/Parser/UnqualifiedNameParser.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parsers' => $vendorDir . '/doctrine/dbal/src/Schema/Name/Parsers.php', + 'Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName' => $vendorDir . '/doctrine/dbal/src/Schema/Name/UnqualifiedName.php', + 'Doctrine\\DBAL\\Schema\\Name\\UnquotedIdentifierFolding' => $vendorDir . '/doctrine/dbal/src/Schema/Name/UnquotedIdentifierFolding.php', + 'Doctrine\\DBAL\\Schema\\NamedObject' => $vendorDir . '/doctrine/dbal/src/Schema/NamedObject.php', + 'Doctrine\\DBAL\\Schema\\OptionallyNamedObject' => $vendorDir . '/doctrine/dbal/src/Schema/OptionallyNamedObject.php', 'Doctrine\\DBAL\\Schema\\OracleSchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/OracleSchemaManager.php', 'Doctrine\\DBAL\\Schema\\PostgreSQLSchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\PrimaryKeyConstraint' => $vendorDir . '/doctrine/dbal/src/Schema/PrimaryKeyConstraint.php', + 'Doctrine\\DBAL\\Schema\\PrimaryKeyConstraintEditor' => $vendorDir . '/doctrine/dbal/src/Schema/PrimaryKeyConstraintEditor.php', 'Doctrine\\DBAL\\Schema\\SQLServerSchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/SQLServerSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\SQLiteSchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/SQLiteSchemaManager.php', 'Doctrine\\DBAL\\Schema\\Schema' => $vendorDir . '/doctrine/dbal/src/Schema/Schema.php', 'Doctrine\\DBAL\\Schema\\SchemaConfig' => $vendorDir . '/doctrine/dbal/src/Schema/SchemaConfig.php', 'Doctrine\\DBAL\\Schema\\SchemaDiff' => $vendorDir . '/doctrine/dbal/src/Schema/SchemaDiff.php', 'Doctrine\\DBAL\\Schema\\SchemaException' => $vendorDir . '/doctrine/dbal/src/Schema/SchemaException.php', 'Doctrine\\DBAL\\Schema\\SchemaManagerFactory' => $vendorDir . '/doctrine/dbal/src/Schema/SchemaManagerFactory.php', + 'Doctrine\\DBAL\\Schema\\SchemaProvider' => $vendorDir . '/doctrine/dbal/src/Schema/SchemaProvider.php', 'Doctrine\\DBAL\\Schema\\Sequence' => $vendorDir . '/doctrine/dbal/src/Schema/Sequence.php', - 'Doctrine\\DBAL\\Schema\\SqliteSchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/SqliteSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\SequenceEditor' => $vendorDir . '/doctrine/dbal/src/Schema/SequenceEditor.php', 'Doctrine\\DBAL\\Schema\\Table' => $vendorDir . '/doctrine/dbal/src/Schema/Table.php', + 'Doctrine\\DBAL\\Schema\\TableConfiguration' => $vendorDir . '/doctrine/dbal/src/Schema/TableConfiguration.php', 'Doctrine\\DBAL\\Schema\\TableDiff' => $vendorDir . '/doctrine/dbal/src/Schema/TableDiff.php', + 'Doctrine\\DBAL\\Schema\\TableEditor' => $vendorDir . '/doctrine/dbal/src/Schema/TableEditor.php', 'Doctrine\\DBAL\\Schema\\UniqueConstraint' => $vendorDir . '/doctrine/dbal/src/Schema/UniqueConstraint.php', + 'Doctrine\\DBAL\\Schema\\UniqueConstraintEditor' => $vendorDir . '/doctrine/dbal/src/Schema/UniqueConstraintEditor.php', 'Doctrine\\DBAL\\Schema\\View' => $vendorDir . '/doctrine/dbal/src/Schema/View.php', - 'Doctrine\\DBAL\\Schema\\Visitor\\AbstractVisitor' => $vendorDir . '/doctrine/dbal/src/Schema/Visitor/AbstractVisitor.php', - 'Doctrine\\DBAL\\Schema\\Visitor\\CreateSchemaSqlCollector' => $vendorDir . '/doctrine/dbal/src/Schema/Visitor/CreateSchemaSqlCollector.php', - 'Doctrine\\DBAL\\Schema\\Visitor\\DropSchemaSqlCollector' => $vendorDir . '/doctrine/dbal/src/Schema/Visitor/DropSchemaSqlCollector.php', - 'Doctrine\\DBAL\\Schema\\Visitor\\Graphviz' => $vendorDir . '/doctrine/dbal/src/Schema/Visitor/Graphviz.php', - 'Doctrine\\DBAL\\Schema\\Visitor\\NamespaceVisitor' => $vendorDir . '/doctrine/dbal/src/Schema/Visitor/NamespaceVisitor.php', - 'Doctrine\\DBAL\\Schema\\Visitor\\RemoveNamespacedAssets' => $vendorDir . '/doctrine/dbal/src/Schema/Visitor/RemoveNamespacedAssets.php', - 'Doctrine\\DBAL\\Schema\\Visitor\\Visitor' => $vendorDir . '/doctrine/dbal/src/Schema/Visitor/Visitor.php', + 'Doctrine\\DBAL\\Schema\\ViewEditor' => $vendorDir . '/doctrine/dbal/src/Schema/ViewEditor.php', + 'Doctrine\\DBAL\\ServerVersionProvider' => $vendorDir . '/doctrine/dbal/src/ServerVersionProvider.php', 'Doctrine\\DBAL\\Statement' => $vendorDir . '/doctrine/dbal/src/Statement.php', - 'Doctrine\\DBAL\\Tools\\Console\\Command\\CommandCompatibility' => $vendorDir . '/doctrine/dbal/src/Tools/Console/Command/CommandCompatibility.php', - 'Doctrine\\DBAL\\Tools\\Console\\Command\\ReservedWordsCommand' => $vendorDir . '/doctrine/dbal/src/Tools/Console/Command/ReservedWordsCommand.php', 'Doctrine\\DBAL\\Tools\\Console\\Command\\RunSqlCommand' => $vendorDir . '/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php', 'Doctrine\\DBAL\\Tools\\Console\\ConnectionNotFound' => $vendorDir . '/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php', 'Doctrine\\DBAL\\Tools\\Console\\ConnectionProvider' => $vendorDir . '/doctrine/dbal/src/Tools/Console/ConnectionProvider.php', 'Doctrine\\DBAL\\Tools\\Console\\ConnectionProvider\\SingleConnectionProvider' => $vendorDir . '/doctrine/dbal/src/Tools/Console/ConnectionProvider/SingleConnectionProvider.php', - 'Doctrine\\DBAL\\Tools\\Console\\ConsoleRunner' => $vendorDir . '/doctrine/dbal/src/Tools/Console/ConsoleRunner.php', 'Doctrine\\DBAL\\Tools\\DsnParser' => $vendorDir . '/doctrine/dbal/src/Tools/DsnParser.php', 'Doctrine\\DBAL\\TransactionIsolationLevel' => $vendorDir . '/doctrine/dbal/src/TransactionIsolationLevel.php', - 'Doctrine\\DBAL\\Types\\ArrayType' => $vendorDir . '/doctrine/dbal/src/Types/ArrayType.php', 'Doctrine\\DBAL\\Types\\AsciiStringType' => $vendorDir . '/doctrine/dbal/src/Types/AsciiStringType.php', 'Doctrine\\DBAL\\Types\\BigIntType' => $vendorDir . '/doctrine/dbal/src/Types/BigIntType.php', 'Doctrine\\DBAL\\Types\\BinaryType' => $vendorDir . '/doctrine/dbal/src/Types/BinaryType.php', @@ -846,14 +914,33 @@ 'Doctrine\\DBAL\\Types\\DateTimeTzType' => $vendorDir . '/doctrine/dbal/src/Types/DateTimeTzType.php', 'Doctrine\\DBAL\\Types\\DateType' => $vendorDir . '/doctrine/dbal/src/Types/DateType.php', 'Doctrine\\DBAL\\Types\\DecimalType' => $vendorDir . '/doctrine/dbal/src/Types/DecimalType.php', + 'Doctrine\\DBAL\\Types\\EnumType' => $vendorDir . '/doctrine/dbal/src/Types/EnumType.php', + 'Doctrine\\DBAL\\Types\\Exception\\InvalidFormat' => $vendorDir . '/doctrine/dbal/src/Types/Exception/InvalidFormat.php', + 'Doctrine\\DBAL\\Types\\Exception\\InvalidType' => $vendorDir . '/doctrine/dbal/src/Types/Exception/InvalidType.php', + 'Doctrine\\DBAL\\Types\\Exception\\SerializationFailed' => $vendorDir . '/doctrine/dbal/src/Types/Exception/SerializationFailed.php', + 'Doctrine\\DBAL\\Types\\Exception\\TypeAlreadyRegistered' => $vendorDir . '/doctrine/dbal/src/Types/Exception/TypeAlreadyRegistered.php', + 'Doctrine\\DBAL\\Types\\Exception\\TypeArgumentCountError' => $vendorDir . '/doctrine/dbal/src/Types/Exception/TypeArgumentCountError.php', + 'Doctrine\\DBAL\\Types\\Exception\\TypeNotFound' => $vendorDir . '/doctrine/dbal/src/Types/Exception/TypeNotFound.php', + 'Doctrine\\DBAL\\Types\\Exception\\TypeNotRegistered' => $vendorDir . '/doctrine/dbal/src/Types/Exception/TypeNotRegistered.php', + 'Doctrine\\DBAL\\Types\\Exception\\TypesAlreadyExists' => $vendorDir . '/doctrine/dbal/src/Types/Exception/TypesAlreadyExists.php', + 'Doctrine\\DBAL\\Types\\Exception\\TypesException' => $vendorDir . '/doctrine/dbal/src/Types/Exception/TypesException.php', + 'Doctrine\\DBAL\\Types\\Exception\\UnknownColumnType' => $vendorDir . '/doctrine/dbal/src/Types/Exception/UnknownColumnType.php', + 'Doctrine\\DBAL\\Types\\Exception\\ValueNotConvertible' => $vendorDir . '/doctrine/dbal/src/Types/Exception/ValueNotConvertible.php', 'Doctrine\\DBAL\\Types\\FloatType' => $vendorDir . '/doctrine/dbal/src/Types/FloatType.php', 'Doctrine\\DBAL\\Types\\GuidType' => $vendorDir . '/doctrine/dbal/src/Types/GuidType.php', 'Doctrine\\DBAL\\Types\\IntegerType' => $vendorDir . '/doctrine/dbal/src/Types/IntegerType.php', + 'Doctrine\\DBAL\\Types\\JsonObjectType' => $vendorDir . '/doctrine/dbal/src/Types/JsonObjectType.php', 'Doctrine\\DBAL\\Types\\JsonType' => $vendorDir . '/doctrine/dbal/src/Types/JsonType.php', - 'Doctrine\\DBAL\\Types\\ObjectType' => $vendorDir . '/doctrine/dbal/src/Types/ObjectType.php', + 'Doctrine\\DBAL\\Types\\JsonTypeConvert' => $vendorDir . '/doctrine/dbal/src/Types/JsonTypeConvert.php', + 'Doctrine\\DBAL\\Types\\JsonbObjectType' => $vendorDir . '/doctrine/dbal/src/Types/JsonbObjectType.php', + 'Doctrine\\DBAL\\Types\\JsonbType' => $vendorDir . '/doctrine/dbal/src/Types/JsonbType.php', + 'Doctrine\\DBAL\\Types\\NumberType' => $vendorDir . '/doctrine/dbal/src/Types/NumberType.php', + 'Doctrine\\DBAL\\Types\\PhpDateMappingType' => $vendorDir . '/doctrine/dbal/src/Types/PhpDateMappingType.php', 'Doctrine\\DBAL\\Types\\PhpDateTimeMappingType' => $vendorDir . '/doctrine/dbal/src/Types/PhpDateTimeMappingType.php', 'Doctrine\\DBAL\\Types\\PhpIntegerMappingType' => $vendorDir . '/doctrine/dbal/src/Types/PhpIntegerMappingType.php', + 'Doctrine\\DBAL\\Types\\PhpTimeMappingType' => $vendorDir . '/doctrine/dbal/src/Types/PhpTimeMappingType.php', 'Doctrine\\DBAL\\Types\\SimpleArrayType' => $vendorDir . '/doctrine/dbal/src/Types/SimpleArrayType.php', + 'Doctrine\\DBAL\\Types\\SmallFloatType' => $vendorDir . '/doctrine/dbal/src/Types/SmallFloatType.php', 'Doctrine\\DBAL\\Types\\SmallIntType' => $vendorDir . '/doctrine/dbal/src/Types/SmallIntType.php', 'Doctrine\\DBAL\\Types\\StringType' => $vendorDir . '/doctrine/dbal/src/Types/StringType.php', 'Doctrine\\DBAL\\Types\\TextType' => $vendorDir . '/doctrine/dbal/src/Types/TextType.php', @@ -864,7 +951,6 @@ 'Doctrine\\DBAL\\Types\\Types' => $vendorDir . '/doctrine/dbal/src/Types/Types.php', 'Doctrine\\DBAL\\Types\\VarDateTimeImmutableType' => $vendorDir . '/doctrine/dbal/src/Types/VarDateTimeImmutableType.php', 'Doctrine\\DBAL\\Types\\VarDateTimeType' => $vendorDir . '/doctrine/dbal/src/Types/VarDateTimeType.php', - 'Doctrine\\DBAL\\VersionAwarePlatformDriver' => $vendorDir . '/doctrine/dbal/src/VersionAwarePlatformDriver.php', 'Doctrine\\Deprecations\\Deprecation' => $vendorDir . '/doctrine/deprecations/src/Deprecation.php', 'Doctrine\\Deprecations\\PHPUnit\\VerifyDeprecations' => $vendorDir . '/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php', 'Egulias\\EmailValidator\\EmailLexer' => $vendorDir . '/egulias/email-validator/src/EmailLexer.php', @@ -3296,6 +3382,17 @@ 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface' => $vendorDir . '/symfony/http-foundation/Session/Storage/SessionStorageInterface.php', 'Symfony\\Component\\HttpFoundation\\StreamedJsonResponse' => $vendorDir . '/symfony/http-foundation/StreamedJsonResponse.php', 'Symfony\\Component\\HttpFoundation\\StreamedResponse' => $vendorDir . '/symfony/http-foundation/StreamedResponse.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\RequestAttributeValueSame' => $vendorDir . '/symfony/http-foundation/Test/Constraint/RequestAttributeValueSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseCookieValueSame' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseCookieValueSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseFormatSame' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseFormatSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHasCookie' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseHasCookie.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHasHeader' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseHasHeader.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHeaderLocationSame' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseHeaderLocationSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHeaderSame' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseHeaderSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseIsRedirected' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseIsRedirected.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseIsSuccessful' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseIsSuccessful.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseIsUnprocessable' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseIsUnprocessable.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseStatusCodeSame' => $vendorDir . '/symfony/http-foundation/Test/Constraint/ResponseStatusCodeSame.php', 'Symfony\\Component\\HttpFoundation\\UriSigner' => $vendorDir . '/symfony/http-foundation/UriSigner.php', 'Symfony\\Component\\HttpFoundation\\UrlHelper' => $vendorDir . '/symfony/http-foundation/UrlHelper.php', 'Symfony\\Component\\Mailer\\Command\\MailerTestCommand' => $vendorDir . '/symfony/mailer/Command/MailerTestCommand.php', @@ -3327,6 +3424,11 @@ 'Symfony\\Component\\Mailer\\Messenger\\MessageHandler' => $vendorDir . '/symfony/mailer/Messenger/MessageHandler.php', 'Symfony\\Component\\Mailer\\Messenger\\SendEmailMessage' => $vendorDir . '/symfony/mailer/Messenger/SendEmailMessage.php', 'Symfony\\Component\\Mailer\\SentMessage' => $vendorDir . '/symfony/mailer/SentMessage.php', + 'Symfony\\Component\\Mailer\\Test\\AbstractTransportFactoryTestCase' => $vendorDir . '/symfony/mailer/Test/AbstractTransportFactoryTestCase.php', + 'Symfony\\Component\\Mailer\\Test\\Constraint\\EmailCount' => $vendorDir . '/symfony/mailer/Test/Constraint/EmailCount.php', + 'Symfony\\Component\\Mailer\\Test\\Constraint\\EmailIsQueued' => $vendorDir . '/symfony/mailer/Test/Constraint/EmailIsQueued.php', + 'Symfony\\Component\\Mailer\\Test\\IncompleteDsnTestTrait' => $vendorDir . '/symfony/mailer/Test/IncompleteDsnTestTrait.php', + 'Symfony\\Component\\Mailer\\Test\\TransportFactoryTestCase' => $vendorDir . '/symfony/mailer/Test/TransportFactoryTestCase.php', 'Symfony\\Component\\Mailer\\Transport' => $vendorDir . '/symfony/mailer/Transport.php', 'Symfony\\Component\\Mailer\\Transport\\AbstractApiTransport' => $vendorDir . '/symfony/mailer/Transport/AbstractApiTransport.php', 'Symfony\\Component\\Mailer\\Transport\\AbstractHttpTransport' => $vendorDir . '/symfony/mailer/Transport/AbstractHttpTransport.php', diff --git a/composer/autoload_psr4.php b/composer/autoload_psr4.php index 267b93551..06d5754de 100644 --- a/composer/autoload_psr4.php +++ b/composer/autoload_psr4.php @@ -84,7 +84,6 @@ 'Doctrine\\Deprecations\\' => array($vendorDir . '/doctrine/deprecations/src'), 'Doctrine\\DBAL\\' => array($vendorDir . '/doctrine/dbal/src'), 'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/src'), - 'Doctrine\\Common\\' => array($vendorDir . '/doctrine/event-manager/src'), 'Cose\\' => array($vendorDir . '/web-auth/cose-lib/src'), 'CBOR\\' => array($vendorDir . '/spomky-labs/cbor-php/src'), 'Brick\\Math\\' => array($vendorDir . '/brick/math/src'), diff --git a/composer/autoload_static.php b/composer/autoload_static.php index 9cb86927e..f0da02e3c 100644 --- a/composer/autoload_static.php +++ b/composer/autoload_static.php @@ -177,7 +177,6 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Doctrine\\Deprecations\\' => 22, 'Doctrine\\DBAL\\' => 14, 'Doctrine\\Common\\Lexer\\' => 22, - 'Doctrine\\Common\\' => 16, ), 'C' => array ( @@ -508,10 +507,6 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 array ( 0 => __DIR__ . '/..' . '/doctrine/lexer/src', ), - 'Doctrine\\Common\\' => - array ( - 0 => __DIR__ . '/..' . '/doctrine/event-manager/src', - ), 'Cose\\' => array ( 0 => __DIR__ . '/..' . '/web-auth/cose-lib/src', @@ -1064,9 +1059,6 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Cose\\Key\\RsaKey' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Key/RsaKey.php', 'Cose\\Key\\SymmetricKey' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Key/SymmetricKey.php', 'Deprecated' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/Deprecated.php', - 'Doctrine\\Common\\EventArgs' => __DIR__ . '/..' . '/doctrine/event-manager/src/EventArgs.php', - 'Doctrine\\Common\\EventManager' => __DIR__ . '/..' . '/doctrine/event-manager/src/EventManager.php', - 'Doctrine\\Common\\EventSubscriber' => __DIR__ . '/..' . '/doctrine/event-manager/src/EventSubscriber.php', 'Doctrine\\Common\\Lexer\\AbstractLexer' => __DIR__ . '/..' . '/doctrine/lexer/src/AbstractLexer.php', 'Doctrine\\Common\\Lexer\\Token' => __DIR__ . '/..' . '/doctrine/lexer/src/Token.php', 'Doctrine\\DBAL\\ArrayParameterType' => __DIR__ . '/..' . '/doctrine/dbal/src/ArrayParameterType.php', @@ -1075,11 +1067,14 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Doctrine\\DBAL\\ArrayParameters\\Exception\\MissingPositionalParameter' => __DIR__ . '/..' . '/doctrine/dbal/src/ArrayParameters/Exception/MissingPositionalParameter.php', 'Doctrine\\DBAL\\Cache\\ArrayResult' => __DIR__ . '/..' . '/doctrine/dbal/src/Cache/ArrayResult.php', 'Doctrine\\DBAL\\Cache\\CacheException' => __DIR__ . '/..' . '/doctrine/dbal/src/Cache/CacheException.php', + 'Doctrine\\DBAL\\Cache\\Exception\\NoCacheKey' => __DIR__ . '/..' . '/doctrine/dbal/src/Cache/Exception/NoCacheKey.php', + 'Doctrine\\DBAL\\Cache\\Exception\\NoResultDriverConfigured' => __DIR__ . '/..' . '/doctrine/dbal/src/Cache/Exception/NoResultDriverConfigured.php', 'Doctrine\\DBAL\\Cache\\QueryCacheProfile' => __DIR__ . '/..' . '/doctrine/dbal/src/Cache/QueryCacheProfile.php', 'Doctrine\\DBAL\\ColumnCase' => __DIR__ . '/..' . '/doctrine/dbal/src/ColumnCase.php', 'Doctrine\\DBAL\\Configuration' => __DIR__ . '/..' . '/doctrine/dbal/src/Configuration.php', 'Doctrine\\DBAL\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Connection.php', 'Doctrine\\DBAL\\ConnectionException' => __DIR__ . '/..' . '/doctrine/dbal/src/ConnectionException.php', + 'Doctrine\\DBAL\\Connection\\StaticServerVersionProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Connection/StaticServerVersionProvider.php', 'Doctrine\\DBAL\\Connections\\PrimaryReadReplicaConnection' => __DIR__ . '/..' . '/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php', 'Doctrine\\DBAL\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver.php', 'Doctrine\\DBAL\\DriverManager' => __DIR__ . '/..' . '/doctrine/dbal/src/DriverManager.php', @@ -1090,7 +1085,6 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Doctrine\\DBAL\\Driver\\API\\PostgreSQL\\ExceptionConverter' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php', 'Doctrine\\DBAL\\Driver\\API\\SQLSrv\\ExceptionConverter' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php', 'Doctrine\\DBAL\\Driver\\API\\SQLite\\ExceptionConverter' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php', - 'Doctrine\\DBAL\\Driver\\API\\SQLite\\UserDefinedFunctions' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php', 'Doctrine\\DBAL\\Driver\\AbstractDB2Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/AbstractDB2Driver.php', 'Doctrine\\DBAL\\Driver\\AbstractException' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/AbstractException.php', 'Doctrine\\DBAL\\Driver\\AbstractMySQLDriver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/AbstractMySQLDriver.php', @@ -1103,7 +1097,8 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Doctrine\\DBAL\\Driver\\AbstractSQLiteDriver\\Middleware\\EnableForeignKeys' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/AbstractSQLiteDriver/Middleware/EnableForeignKeys.php', 'Doctrine\\DBAL\\Driver\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Connection.php', 'Doctrine\\DBAL\\Driver\\Exception' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Exception.php', - 'Doctrine\\DBAL\\Driver\\Exception\\UnknownParameterType' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Exception/UnknownParameterType.php', + 'Doctrine\\DBAL\\Driver\\Exception\\IdentityColumnsNotSupported' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Exception/IdentityColumnsNotSupported.php', + 'Doctrine\\DBAL\\Driver\\Exception\\NoIdentityValue' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Exception/NoIdentityValue.php', 'Doctrine\\DBAL\\Driver\\FetchUtils' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/FetchUtils.php', 'Doctrine\\DBAL\\Driver\\IBMDB2\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/IBMDB2/Connection.php', 'Doctrine\\DBAL\\Driver\\IBMDB2\\DataSourceName' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php', @@ -1145,7 +1140,6 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\Error' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Exception/Error.php', 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\InvalidConfiguration' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Exception/InvalidConfiguration.php', 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\NonTerminatedStringLiteral' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Exception/NonTerminatedStringLiteral.php', - 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\SequenceDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Exception/SequenceDoesNotExist.php', 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\UnknownParameterIndex' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Exception/UnknownParameterIndex.php', 'Doctrine\\DBAL\\Driver\\OCI8\\ExecutionMode' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/ExecutionMode.php', 'Doctrine\\DBAL\\Driver\\OCI8\\Middleware\\InitializeSession' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Middleware/InitializeSession.php', @@ -1153,11 +1147,10 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Doctrine\\DBAL\\Driver\\OCI8\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Statement.php', 'Doctrine\\DBAL\\Driver\\PDO\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/Connection.php', 'Doctrine\\DBAL\\Driver\\PDO\\Exception' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/Exception.php', + 'Doctrine\\DBAL\\Driver\\PDO\\Exception\\InvalidConfiguration' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/Exception/InvalidConfiguration.php', 'Doctrine\\DBAL\\Driver\\PDO\\MySQL\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/MySQL/Driver.php', 'Doctrine\\DBAL\\Driver\\PDO\\OCI\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/OCI/Driver.php', 'Doctrine\\DBAL\\Driver\\PDO\\PDOConnect' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/PDOConnect.php', - 'Doctrine\\DBAL\\Driver\\PDO\\PDOException' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/PDOException.php', - 'Doctrine\\DBAL\\Driver\\PDO\\ParameterTypeMap' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/ParameterTypeMap.php', 'Doctrine\\DBAL\\Driver\\PDO\\PgSQL\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php', 'Doctrine\\DBAL\\Driver\\PDO\\Result' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/Result.php', 'Doctrine\\DBAL\\Driver\\PDO\\SQLSrv\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php', @@ -1184,29 +1177,9 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Doctrine\\DBAL\\Driver\\SQLite3\\Exception' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/SQLite3/Exception.php', 'Doctrine\\DBAL\\Driver\\SQLite3\\Result' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/SQLite3/Result.php', 'Doctrine\\DBAL\\Driver\\SQLite3\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/SQLite3/Statement.php', - 'Doctrine\\DBAL\\Driver\\ServerInfoAwareConnection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/ServerInfoAwareConnection.php', 'Doctrine\\DBAL\\Driver\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Statement.php', - 'Doctrine\\DBAL\\Event\\ConnectionEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/ConnectionEventArgs.php', - 'Doctrine\\DBAL\\Event\\Listeners\\OracleSessionInit' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/Listeners/OracleSessionInit.php', - 'Doctrine\\DBAL\\Event\\Listeners\\SQLSessionInit' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/Listeners/SQLSessionInit.php', - 'Doctrine\\DBAL\\Event\\Listeners\\SQLiteSessionInit' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/Listeners/SQLiteSessionInit.php', - 'Doctrine\\DBAL\\Event\\SchemaAlterTableAddColumnEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaAlterTableAddColumnEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaAlterTableChangeColumnEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaAlterTableChangeColumnEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaAlterTableEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaAlterTableEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaAlterTableRemoveColumnEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaAlterTableRemoveColumnEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaAlterTableRenameColumnEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaAlterTableRenameColumnEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaColumnDefinitionEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaColumnDefinitionEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaCreateTableColumnEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaCreateTableColumnEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaCreateTableEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaCreateTableEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaDropTableEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaDropTableEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaEventArgs.php', - 'Doctrine\\DBAL\\Event\\SchemaIndexDefinitionEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaIndexDefinitionEventArgs.php', - 'Doctrine\\DBAL\\Event\\TransactionBeginEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/TransactionBeginEventArgs.php', - 'Doctrine\\DBAL\\Event\\TransactionCommitEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/TransactionCommitEventArgs.php', - 'Doctrine\\DBAL\\Event\\TransactionEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/TransactionEventArgs.php', - 'Doctrine\\DBAL\\Event\\TransactionRollBackEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/TransactionRollBackEventArgs.php', - 'Doctrine\\DBAL\\Events' => __DIR__ . '/..' . '/doctrine/dbal/src/Events.php', 'Doctrine\\DBAL\\Exception' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception.php', + 'Doctrine\\DBAL\\Exception\\CommitFailedRollbackOnly' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/CommitFailedRollbackOnly.php', 'Doctrine\\DBAL\\Exception\\ConnectionException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/ConnectionException.php', 'Doctrine\\DBAL\\Exception\\ConnectionLost' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/ConnectionLost.php', 'Doctrine\\DBAL\\Exception\\ConstraintViolationException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/ConstraintViolationException.php', @@ -1216,17 +1189,29 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Doctrine\\DBAL\\Exception\\DatabaseRequired' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/DatabaseRequired.php', 'Doctrine\\DBAL\\Exception\\DeadlockException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/DeadlockException.php', 'Doctrine\\DBAL\\Exception\\DriverException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/DriverException.php', + 'Doctrine\\DBAL\\Exception\\DriverRequired' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/DriverRequired.php', 'Doctrine\\DBAL\\Exception\\ForeignKeyConstraintViolationException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/ForeignKeyConstraintViolationException.php', 'Doctrine\\DBAL\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidArgumentException.php', + 'Doctrine\\DBAL\\Exception\\InvalidColumnDeclaration' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidColumnDeclaration.php', + 'Doctrine\\DBAL\\Exception\\InvalidColumnIndex' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidColumnIndex.php', + 'Doctrine\\DBAL\\Exception\\InvalidColumnType' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidColumnType.php', + 'Doctrine\\DBAL\\Exception\\InvalidColumnType\\ColumnLengthRequired' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidColumnType/ColumnLengthRequired.php', + 'Doctrine\\DBAL\\Exception\\InvalidColumnType\\ColumnPrecisionRequired' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidColumnType/ColumnPrecisionRequired.php', + 'Doctrine\\DBAL\\Exception\\InvalidColumnType\\ColumnScaleRequired' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidColumnType/ColumnScaleRequired.php', + 'Doctrine\\DBAL\\Exception\\InvalidColumnType\\ColumnValuesRequired' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidColumnType/ColumnValuesRequired.php', + 'Doctrine\\DBAL\\Exception\\InvalidDriverClass' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidDriverClass.php', 'Doctrine\\DBAL\\Exception\\InvalidFieldNameException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidFieldNameException.php', - 'Doctrine\\DBAL\\Exception\\InvalidLockMode' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidLockMode.php', + 'Doctrine\\DBAL\\Exception\\InvalidWrapperClass' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidWrapperClass.php', 'Doctrine\\DBAL\\Exception\\LockWaitTimeoutException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/LockWaitTimeoutException.php', 'Doctrine\\DBAL\\Exception\\MalformedDsnException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/MalformedDsnException.php', + 'Doctrine\\DBAL\\Exception\\NoActiveTransaction' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/NoActiveTransaction.php', 'Doctrine\\DBAL\\Exception\\NoKeyValue' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/NoKeyValue.php', 'Doctrine\\DBAL\\Exception\\NonUniqueFieldNameException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/NonUniqueFieldNameException.php', 'Doctrine\\DBAL\\Exception\\NotNullConstraintViolationException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/NotNullConstraintViolationException.php', + 'Doctrine\\DBAL\\Exception\\ParseError' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/ParseError.php', 'Doctrine\\DBAL\\Exception\\ReadOnlyException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/ReadOnlyException.php', 'Doctrine\\DBAL\\Exception\\RetryableException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/RetryableException.php', + 'Doctrine\\DBAL\\Exception\\SavepointsNotSupported' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/SavepointsNotSupported.php', 'Doctrine\\DBAL\\Exception\\SchemaDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/SchemaDoesNotExist.php', 'Doctrine\\DBAL\\Exception\\ServerException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/ServerException.php', 'Doctrine\\DBAL\\Exception\\SyntaxErrorException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/SyntaxErrorException.php', @@ -1234,67 +1219,66 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Doctrine\\DBAL\\Exception\\TableNotFoundException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/TableNotFoundException.php', 'Doctrine\\DBAL\\Exception\\TransactionRolledBack' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/TransactionRolledBack.php', 'Doctrine\\DBAL\\Exception\\UniqueConstraintViolationException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/UniqueConstraintViolationException.php', + 'Doctrine\\DBAL\\Exception\\UnknownDriver' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/UnknownDriver.php', 'Doctrine\\DBAL\\ExpandArrayParameters' => __DIR__ . '/..' . '/doctrine/dbal/src/ExpandArrayParameters.php', - 'Doctrine\\DBAL\\FetchMode' => __DIR__ . '/..' . '/doctrine/dbal/src/FetchMode.php', - 'Doctrine\\DBAL\\Id\\TableGenerator' => __DIR__ . '/..' . '/doctrine/dbal/src/Id/TableGenerator.php', - 'Doctrine\\DBAL\\Id\\TableGeneratorSchemaVisitor' => __DIR__ . '/..' . '/doctrine/dbal/src/Id/TableGeneratorSchemaVisitor.php', 'Doctrine\\DBAL\\LockMode' => __DIR__ . '/..' . '/doctrine/dbal/src/LockMode.php', 'Doctrine\\DBAL\\Logging\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Logging/Connection.php', - 'Doctrine\\DBAL\\Logging\\DebugStack' => __DIR__ . '/..' . '/doctrine/dbal/src/Logging/DebugStack.php', 'Doctrine\\DBAL\\Logging\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Logging/Driver.php', - 'Doctrine\\DBAL\\Logging\\LoggerChain' => __DIR__ . '/..' . '/doctrine/dbal/src/Logging/LoggerChain.php', 'Doctrine\\DBAL\\Logging\\Middleware' => __DIR__ . '/..' . '/doctrine/dbal/src/Logging/Middleware.php', - 'Doctrine\\DBAL\\Logging\\SQLLogger' => __DIR__ . '/..' . '/doctrine/dbal/src/Logging/SQLLogger.php', 'Doctrine\\DBAL\\Logging\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Logging/Statement.php', 'Doctrine\\DBAL\\ParameterType' => __DIR__ . '/..' . '/doctrine/dbal/src/ParameterType.php', 'Doctrine\\DBAL\\Platforms\\AbstractMySQLPlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php', 'Doctrine\\DBAL\\Platforms\\AbstractPlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/AbstractPlatform.php', - 'Doctrine\\DBAL\\Platforms\\DB2111Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/DB2111Platform.php', 'Doctrine\\DBAL\\Platforms\\DB2Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/DB2Platform.php', 'Doctrine\\DBAL\\Platforms\\DateIntervalUnit' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/DateIntervalUnit.php', + 'Doctrine\\DBAL\\Platforms\\Db2\\Db2MetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Db2/Db2MetadataProvider.php', + 'Doctrine\\DBAL\\Platforms\\Exception\\InvalidPlatformVersion' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Exception/InvalidPlatformVersion.php', + 'Doctrine\\DBAL\\Platforms\\Exception\\NoColumnsSpecifiedForTable' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Exception/NoColumnsSpecifiedForTable.php', + 'Doctrine\\DBAL\\Platforms\\Exception\\NotSupported' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Exception/NotSupported.php', + 'Doctrine\\DBAL\\Platforms\\Exception\\PlatformException' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Exception/PlatformException.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\DB2Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/DB2Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\KeywordList' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/KeywordList.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDB117Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MariaDB117Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDB123Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MariaDB123Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDBKeywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php', - 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDb102Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MariaDb102Keywords.php', - 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDb117Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MariaDb117Keywords.php', - 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL57Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MySQL57Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL80Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MySQL80Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL84Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MySQL84Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQLKeywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MySQLKeywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\OracleKeywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/OracleKeywords.php', - 'Doctrine\\DBAL\\Platforms\\Keywords\\PostgreSQL100Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/PostgreSQL100Keywords.php', - 'Doctrine\\DBAL\\Platforms\\Keywords\\PostgreSQL94Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/PostgreSQL94Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\PostgreSQLKeywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/PostgreSQLKeywords.php', - 'Doctrine\\DBAL\\Platforms\\Keywords\\ReservedKeywordsValidator' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/ReservedKeywordsValidator.php', - 'Doctrine\\DBAL\\Platforms\\Keywords\\SQLServer2012Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/SQLServer2012Keywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\SQLServerKeywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/SQLServerKeywords.php', 'Doctrine\\DBAL\\Platforms\\Keywords\\SQLiteKeywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/SQLiteKeywords.php', + 'Doctrine\\DBAL\\Platforms\\MariaDB1010Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDB1010Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDB1052Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDB1052Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDB1060Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDB1060Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDB110700Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDB110700Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDB120300Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDB120300Platform.php', 'Doctrine\\DBAL\\Platforms\\MariaDBPlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDBPlatform.php', - 'Doctrine\\DBAL\\Platforms\\MariaDb1010Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDb1010Platform.php', - 'Doctrine\\DBAL\\Platforms\\MariaDb1027Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDb1027Platform.php', - 'Doctrine\\DBAL\\Platforms\\MariaDb1043Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDb1043Platform.php', - 'Doctrine\\DBAL\\Platforms\\MariaDb1052Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDb1052Platform.php', - 'Doctrine\\DBAL\\Platforms\\MariaDb1060Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDb1060Platform.php', - 'Doctrine\\DBAL\\Platforms\\MariaDb110700Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDb110700Platform.php', - 'Doctrine\\DBAL\\Platforms\\MySQL57Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL57Platform.php', 'Doctrine\\DBAL\\Platforms\\MySQL80Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL80Platform.php', 'Doctrine\\DBAL\\Platforms\\MySQL84Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL84Platform.php', 'Doctrine\\DBAL\\Platforms\\MySQLPlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQLPlatform.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\CharsetMetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\CharsetMetadataProvider\\CachingCharsetMetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/CachingCharsetMetadataProvider.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\CharsetMetadataProvider\\ConnectionCharsetMetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\MySQL\\CollationMetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\MySQL\\CollationMetadataProvider\\CachingCollationMetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/CachingCollationMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\MySQL\\CollationMetadataProvider\\ConnectionCollationMetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\MySQL\\Comparator' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL/Comparator.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\DefaultTableOptions' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL/DefaultTableOptions.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\MySQLMetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL/MySQLMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\OraclePlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/OraclePlatform.php', - 'Doctrine\\DBAL\\Platforms\\PostgreSQL100Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/PostgreSQL100Platform.php', + 'Doctrine\\DBAL\\Platforms\\Oracle\\OracleMetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Oracle/OracleMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\PostgreSQL120Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/PostgreSQL120Platform.php', - 'Doctrine\\DBAL\\Platforms\\PostgreSQL94Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/PostgreSQL94Platform.php', 'Doctrine\\DBAL\\Platforms\\PostgreSQLPlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/PostgreSQLPlatform.php', - 'Doctrine\\DBAL\\Platforms\\SQLServer2012Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SQLServer2012Platform.php', + 'Doctrine\\DBAL\\Platforms\\PostgreSQL\\PostgreSQLMetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/PostgreSQL/PostgreSQLMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\SQLServerPlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SQLServerPlatform.php', 'Doctrine\\DBAL\\Platforms\\SQLServer\\Comparator' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SQLServer/Comparator.php', + 'Doctrine\\DBAL\\Platforms\\SQLServer\\SQLServerMetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SQLServer/SQLServerMetadataProvider.php', 'Doctrine\\DBAL\\Platforms\\SQLServer\\SQL\\Builder\\SQLServerSelectSQLBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php', + 'Doctrine\\DBAL\\Platforms\\SQLitePlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SQLitePlatform.php', 'Doctrine\\DBAL\\Platforms\\SQLite\\Comparator' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SQLite/Comparator.php', - 'Doctrine\\DBAL\\Platforms\\SqlitePlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SqlitePlatform.php', + 'Doctrine\\DBAL\\Platforms\\SQLite\\SQLiteMetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SQLite/SQLiteMetadataProvider.php', + 'Doctrine\\DBAL\\Platforms\\SQLite\\SQLiteMetadataProvider\\ForeignKeyConstraintDetails' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SQLite/SQLiteMetadataProvider/ForeignKeyConstraintDetails.php', 'Doctrine\\DBAL\\Platforms\\TrimMode' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/TrimMode.php', 'Doctrine\\DBAL\\Portability\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Portability/Connection.php', 'Doctrine\\DBAL\\Portability\\Converter' => __DIR__ . '/..' . '/doctrine/dbal/src/Portability/Converter.php', @@ -1304,83 +1288,162 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Doctrine\\DBAL\\Portability\\Result' => __DIR__ . '/..' . '/doctrine/dbal/src/Portability/Result.php', 'Doctrine\\DBAL\\Portability\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Portability/Statement.php', 'Doctrine\\DBAL\\Query' => __DIR__ . '/..' . '/doctrine/dbal/src/Query.php', + 'Doctrine\\DBAL\\Query\\CommonTableExpression' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/CommonTableExpression.php', + 'Doctrine\\DBAL\\Query\\Exception\\NonUniqueAlias' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/Exception/NonUniqueAlias.php', + 'Doctrine\\DBAL\\Query\\Exception\\UnknownAlias' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/Exception/UnknownAlias.php', 'Doctrine\\DBAL\\Query\\Expression\\CompositeExpression' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/Expression/CompositeExpression.php', 'Doctrine\\DBAL\\Query\\Expression\\ExpressionBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php', 'Doctrine\\DBAL\\Query\\ForUpdate' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/ForUpdate.php', 'Doctrine\\DBAL\\Query\\ForUpdate\\ConflictResolutionMode' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php', + 'Doctrine\\DBAL\\Query\\From' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/From.php', + 'Doctrine\\DBAL\\Query\\Join' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/Join.php', 'Doctrine\\DBAL\\Query\\Limit' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/Limit.php', 'Doctrine\\DBAL\\Query\\QueryBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/QueryBuilder.php', 'Doctrine\\DBAL\\Query\\QueryException' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/QueryException.php', + 'Doctrine\\DBAL\\Query\\QueryType' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/QueryType.php', 'Doctrine\\DBAL\\Query\\SelectQuery' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/SelectQuery.php', + 'Doctrine\\DBAL\\Query\\Union' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/Union.php', + 'Doctrine\\DBAL\\Query\\UnionQuery' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/UnionQuery.php', + 'Doctrine\\DBAL\\Query\\UnionType' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/UnionType.php', 'Doctrine\\DBAL\\Result' => __DIR__ . '/..' . '/doctrine/dbal/src/Result.php', 'Doctrine\\DBAL\\SQL\\Builder\\CreateSchemaObjectsSQLBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php', 'Doctrine\\DBAL\\SQL\\Builder\\DefaultSelectSQLBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php', + 'Doctrine\\DBAL\\SQL\\Builder\\DefaultUnionSQLBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Builder/DefaultUnionSQLBuilder.php', 'Doctrine\\DBAL\\SQL\\Builder\\DropSchemaObjectsSQLBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php', 'Doctrine\\DBAL\\SQL\\Builder\\SelectSQLBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php', + 'Doctrine\\DBAL\\SQL\\Builder\\UnionSQLBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Builder/UnionSQLBuilder.php', + 'Doctrine\\DBAL\\SQL\\Builder\\WithSQLBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Builder/WithSQLBuilder.php', 'Doctrine\\DBAL\\SQL\\Parser' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Parser.php', 'Doctrine\\DBAL\\SQL\\Parser\\Exception' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Parser/Exception.php', 'Doctrine\\DBAL\\SQL\\Parser\\Exception\\RegularExpressionError' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Parser/Exception/RegularExpressionError.php', 'Doctrine\\DBAL\\SQL\\Parser\\Visitor' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Parser/Visitor.php', 'Doctrine\\DBAL\\Schema\\AbstractAsset' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/AbstractAsset.php', + 'Doctrine\\DBAL\\Schema\\AbstractNamedObject' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/AbstractNamedObject.php', + 'Doctrine\\DBAL\\Schema\\AbstractOptionallyNamedObject' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/AbstractOptionallyNamedObject.php', 'Doctrine\\DBAL\\Schema\\AbstractSchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/AbstractSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\Collections\\Exception' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Collections/Exception.php', + 'Doctrine\\DBAL\\Schema\\Collections\\Exception\\ObjectAlreadyExists' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Collections/Exception/ObjectAlreadyExists.php', + 'Doctrine\\DBAL\\Schema\\Collections\\Exception\\ObjectDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Collections/Exception/ObjectDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Collections\\ObjectSet' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Collections/ObjectSet.php', + 'Doctrine\\DBAL\\Schema\\Collections\\OptionallyUnqualifiedNamedObjectSet' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Collections/OptionallyUnqualifiedNamedObjectSet.php', + 'Doctrine\\DBAL\\Schema\\Collections\\UnqualifiedNamedObjectSet' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Collections/UnqualifiedNamedObjectSet.php', 'Doctrine\\DBAL\\Schema\\Column' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Column.php', 'Doctrine\\DBAL\\Schema\\ColumnDiff' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/ColumnDiff.php', + 'Doctrine\\DBAL\\Schema\\ColumnEditor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/ColumnEditor.php', 'Doctrine\\DBAL\\Schema\\Comparator' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Comparator.php', - 'Doctrine\\DBAL\\Schema\\Constraint' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Constraint.php', + 'Doctrine\\DBAL\\Schema\\ComparatorConfig' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/ComparatorConfig.php', 'Doctrine\\DBAL\\Schema\\DB2SchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/DB2SchemaManager.php', + 'Doctrine\\DBAL\\Schema\\DefaultExpression' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/DefaultExpression.php', + 'Doctrine\\DBAL\\Schema\\DefaultExpression\\CurrentDate' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/DefaultExpression/CurrentDate.php', + 'Doctrine\\DBAL\\Schema\\DefaultExpression\\CurrentTime' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/DefaultExpression/CurrentTime.php', + 'Doctrine\\DBAL\\Schema\\DefaultExpression\\CurrentTimestamp' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/DefaultExpression/CurrentTimestamp.php', 'Doctrine\\DBAL\\Schema\\DefaultSchemaManagerFactory' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php', 'Doctrine\\DBAL\\Schema\\Exception\\ColumnAlreadyExists' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php', 'Doctrine\\DBAL\\Schema\\Exception\\ColumnDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/ColumnDoesNotExist.php', 'Doctrine\\DBAL\\Schema\\Exception\\ForeignKeyDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/ForeignKeyDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Exception\\IncomparableNames' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/IncomparableNames.php', 'Doctrine\\DBAL\\Schema\\Exception\\IndexAlreadyExists' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/IndexAlreadyExists.php', 'Doctrine\\DBAL\\Schema\\Exception\\IndexDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/IndexDoesNotExist.php', 'Doctrine\\DBAL\\Schema\\Exception\\IndexNameInvalid' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/IndexNameInvalid.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidColumnDefinition' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/InvalidColumnDefinition.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidForeignKeyConstraintDefinition' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/InvalidForeignKeyConstraintDefinition.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidIdentifier' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/InvalidIdentifier.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidIndexDefinition' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/InvalidIndexDefinition.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidName' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/InvalidName.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidPrimaryKeyConstraintDefinition' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/InvalidPrimaryKeyConstraintDefinition.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidSequenceDefinition' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/InvalidSequenceDefinition.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidState' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/InvalidState.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidTableDefinition' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/InvalidTableDefinition.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidTableModification' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/InvalidTableModification.php', 'Doctrine\\DBAL\\Schema\\Exception\\InvalidTableName' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/InvalidTableName.php', - 'Doctrine\\DBAL\\Schema\\Exception\\NamedForeignKeyRequired' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/NamedForeignKeyRequired.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidUniqueConstraintDefinition' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/InvalidUniqueConstraintDefinition.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidViewDefinition' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/InvalidViewDefinition.php', 'Doctrine\\DBAL\\Schema\\Exception\\NamespaceAlreadyExists' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php', + 'Doctrine\\DBAL\\Schema\\Exception\\NotImplemented' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/NotImplemented.php', + 'Doctrine\\DBAL\\Schema\\Exception\\PrimaryKeyAlreadyExists' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/PrimaryKeyAlreadyExists.php', 'Doctrine\\DBAL\\Schema\\Exception\\SequenceAlreadyExists' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/SequenceAlreadyExists.php', 'Doctrine\\DBAL\\Schema\\Exception\\SequenceDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/SequenceDoesNotExist.php', 'Doctrine\\DBAL\\Schema\\Exception\\TableAlreadyExists' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/TableAlreadyExists.php', 'Doctrine\\DBAL\\Schema\\Exception\\TableDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/TableDoesNotExist.php', 'Doctrine\\DBAL\\Schema\\Exception\\UniqueConstraintDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/UniqueConstraintDoesNotExist.php', 'Doctrine\\DBAL\\Schema\\Exception\\UnknownColumnOption' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/UnknownColumnOption.php', + 'Doctrine\\DBAL\\Schema\\Exception\\UnsupportedName' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/UnsupportedName.php', + 'Doctrine\\DBAL\\Schema\\Exception\\UnsupportedSchema' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/UnsupportedSchema.php', 'Doctrine\\DBAL\\Schema\\ForeignKeyConstraint' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/ForeignKeyConstraint.php', + 'Doctrine\\DBAL\\Schema\\ForeignKeyConstraintEditor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/ForeignKeyConstraintEditor.php', + 'Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\\Deferrability' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/ForeignKeyConstraint/Deferrability.php', + 'Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\\MatchType' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/ForeignKeyConstraint/MatchType.php', + 'Doctrine\\DBAL\\Schema\\ForeignKeyConstraint\\ReferentialAction' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/ForeignKeyConstraint/ReferentialAction.php', 'Doctrine\\DBAL\\Schema\\Identifier' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Identifier.php', 'Doctrine\\DBAL\\Schema\\Index' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Index.php', - 'Doctrine\\DBAL\\Schema\\LegacySchemaManagerFactory' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/LegacySchemaManagerFactory.php', + 'Doctrine\\DBAL\\Schema\\IndexEditor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/IndexEditor.php', + 'Doctrine\\DBAL\\Schema\\Index\\IndexType' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Index/IndexType.php', + 'Doctrine\\DBAL\\Schema\\Index\\IndexedColumn' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Index/IndexedColumn.php', + 'Doctrine\\DBAL\\Schema\\Introspection\\IntrospectingSchemaProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Introspection/IntrospectingSchemaProvider.php', + 'Doctrine\\DBAL\\Schema\\Introspection\\MetadataProcessor\\ForeignKeyConstraintColumnMetadataProcessor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/ForeignKeyConstraintColumnMetadataProcessor.php', + 'Doctrine\\DBAL\\Schema\\Introspection\\MetadataProcessor\\IndexColumnMetadataProcessor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/IndexColumnMetadataProcessor.php', + 'Doctrine\\DBAL\\Schema\\Introspection\\MetadataProcessor\\PrimaryKeyConstraintColumnMetadataProcessor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/PrimaryKeyConstraintColumnMetadataProcessor.php', + 'Doctrine\\DBAL\\Schema\\Introspection\\MetadataProcessor\\SequenceMetadataProcessor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/SequenceMetadataProcessor.php', + 'Doctrine\\DBAL\\Schema\\Introspection\\MetadataProcessor\\ViewMetadataProcessor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/ViewMetadataProcessor.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\DatabaseMetadataRow' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Metadata/DatabaseMetadataRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\ForeignKeyConstraintColumnMetadataRow' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Metadata/ForeignKeyConstraintColumnMetadataRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\IndexColumnMetadataRow' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Metadata/IndexColumnMetadataRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\MetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Metadata/MetadataProvider.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\PrimaryKeyConstraintColumnRow' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Metadata/PrimaryKeyConstraintColumnRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\SchemaMetadataRow' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Metadata/SchemaMetadataRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\SequenceMetadataRow' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Metadata/SequenceMetadataRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\TableColumnMetadataRow' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Metadata/TableColumnMetadataRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\TableMetadataRow' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Metadata/TableMetadataRow.php', + 'Doctrine\\DBAL\\Schema\\Metadata\\ViewMetadataRow' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Metadata/ViewMetadataRow.php', 'Doctrine\\DBAL\\Schema\\MySQLSchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/MySQLSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\Name' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name.php', + 'Doctrine\\DBAL\\Schema\\Name\\GenericName' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/GenericName.php', + 'Doctrine\\DBAL\\Schema\\Name\\Identifier' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/Identifier.php', + 'Doctrine\\DBAL\\Schema\\Name\\OptionallyQualifiedName' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/OptionallyQualifiedName.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/Parser.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\Exception' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/Parser/Exception.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\Exception\\ExpectedDot' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/Parser/Exception/ExpectedDot.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\Exception\\ExpectedNextIdentifier' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/Parser/Exception/ExpectedNextIdentifier.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\Exception\\InvalidName' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/Parser/Exception/InvalidName.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\Exception\\UnableToParseIdentifier' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/Parser/Exception/UnableToParseIdentifier.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\GenericNameParser' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/Parser/GenericNameParser.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\OptionallyQualifiedNameParser' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/Parser/OptionallyQualifiedNameParser.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parser\\UnqualifiedNameParser' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/Parser/UnqualifiedNameParser.php', + 'Doctrine\\DBAL\\Schema\\Name\\Parsers' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/Parsers.php', + 'Doctrine\\DBAL\\Schema\\Name\\UnqualifiedName' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/UnqualifiedName.php', + 'Doctrine\\DBAL\\Schema\\Name\\UnquotedIdentifierFolding' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Name/UnquotedIdentifierFolding.php', + 'Doctrine\\DBAL\\Schema\\NamedObject' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/NamedObject.php', + 'Doctrine\\DBAL\\Schema\\OptionallyNamedObject' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/OptionallyNamedObject.php', 'Doctrine\\DBAL\\Schema\\OracleSchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/OracleSchemaManager.php', 'Doctrine\\DBAL\\Schema\\PostgreSQLSchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\PrimaryKeyConstraint' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/PrimaryKeyConstraint.php', + 'Doctrine\\DBAL\\Schema\\PrimaryKeyConstraintEditor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/PrimaryKeyConstraintEditor.php', 'Doctrine\\DBAL\\Schema\\SQLServerSchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SQLServerSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\SQLiteSchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SQLiteSchemaManager.php', 'Doctrine\\DBAL\\Schema\\Schema' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Schema.php', 'Doctrine\\DBAL\\Schema\\SchemaConfig' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SchemaConfig.php', 'Doctrine\\DBAL\\Schema\\SchemaDiff' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SchemaDiff.php', 'Doctrine\\DBAL\\Schema\\SchemaException' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SchemaException.php', 'Doctrine\\DBAL\\Schema\\SchemaManagerFactory' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SchemaManagerFactory.php', + 'Doctrine\\DBAL\\Schema\\SchemaProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SchemaProvider.php', 'Doctrine\\DBAL\\Schema\\Sequence' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Sequence.php', - 'Doctrine\\DBAL\\Schema\\SqliteSchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SqliteSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\SequenceEditor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SequenceEditor.php', 'Doctrine\\DBAL\\Schema\\Table' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Table.php', + 'Doctrine\\DBAL\\Schema\\TableConfiguration' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/TableConfiguration.php', 'Doctrine\\DBAL\\Schema\\TableDiff' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/TableDiff.php', + 'Doctrine\\DBAL\\Schema\\TableEditor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/TableEditor.php', 'Doctrine\\DBAL\\Schema\\UniqueConstraint' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/UniqueConstraint.php', + 'Doctrine\\DBAL\\Schema\\UniqueConstraintEditor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/UniqueConstraintEditor.php', 'Doctrine\\DBAL\\Schema\\View' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/View.php', - 'Doctrine\\DBAL\\Schema\\Visitor\\AbstractVisitor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Visitor/AbstractVisitor.php', - 'Doctrine\\DBAL\\Schema\\Visitor\\CreateSchemaSqlCollector' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Visitor/CreateSchemaSqlCollector.php', - 'Doctrine\\DBAL\\Schema\\Visitor\\DropSchemaSqlCollector' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Visitor/DropSchemaSqlCollector.php', - 'Doctrine\\DBAL\\Schema\\Visitor\\Graphviz' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Visitor/Graphviz.php', - 'Doctrine\\DBAL\\Schema\\Visitor\\NamespaceVisitor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Visitor/NamespaceVisitor.php', - 'Doctrine\\DBAL\\Schema\\Visitor\\RemoveNamespacedAssets' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Visitor/RemoveNamespacedAssets.php', - 'Doctrine\\DBAL\\Schema\\Visitor\\Visitor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Visitor/Visitor.php', + 'Doctrine\\DBAL\\Schema\\ViewEditor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/ViewEditor.php', + 'Doctrine\\DBAL\\ServerVersionProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/ServerVersionProvider.php', 'Doctrine\\DBAL\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Statement.php', - 'Doctrine\\DBAL\\Tools\\Console\\Command\\CommandCompatibility' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/Console/Command/CommandCompatibility.php', - 'Doctrine\\DBAL\\Tools\\Console\\Command\\ReservedWordsCommand' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/Console/Command/ReservedWordsCommand.php', 'Doctrine\\DBAL\\Tools\\Console\\Command\\RunSqlCommand' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php', 'Doctrine\\DBAL\\Tools\\Console\\ConnectionNotFound' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php', 'Doctrine\\DBAL\\Tools\\Console\\ConnectionProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/Console/ConnectionProvider.php', 'Doctrine\\DBAL\\Tools\\Console\\ConnectionProvider\\SingleConnectionProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/Console/ConnectionProvider/SingleConnectionProvider.php', - 'Doctrine\\DBAL\\Tools\\Console\\ConsoleRunner' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/Console/ConsoleRunner.php', 'Doctrine\\DBAL\\Tools\\DsnParser' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/DsnParser.php', 'Doctrine\\DBAL\\TransactionIsolationLevel' => __DIR__ . '/..' . '/doctrine/dbal/src/TransactionIsolationLevel.php', - 'Doctrine\\DBAL\\Types\\ArrayType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/ArrayType.php', 'Doctrine\\DBAL\\Types\\AsciiStringType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/AsciiStringType.php', 'Doctrine\\DBAL\\Types\\BigIntType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/BigIntType.php', 'Doctrine\\DBAL\\Types\\BinaryType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/BinaryType.php', @@ -1395,14 +1458,33 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Doctrine\\DBAL\\Types\\DateTimeTzType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/DateTimeTzType.php', 'Doctrine\\DBAL\\Types\\DateType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/DateType.php', 'Doctrine\\DBAL\\Types\\DecimalType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/DecimalType.php', + 'Doctrine\\DBAL\\Types\\EnumType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/EnumType.php', + 'Doctrine\\DBAL\\Types\\Exception\\InvalidFormat' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/Exception/InvalidFormat.php', + 'Doctrine\\DBAL\\Types\\Exception\\InvalidType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/Exception/InvalidType.php', + 'Doctrine\\DBAL\\Types\\Exception\\SerializationFailed' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/Exception/SerializationFailed.php', + 'Doctrine\\DBAL\\Types\\Exception\\TypeAlreadyRegistered' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/Exception/TypeAlreadyRegistered.php', + 'Doctrine\\DBAL\\Types\\Exception\\TypeArgumentCountError' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/Exception/TypeArgumentCountError.php', + 'Doctrine\\DBAL\\Types\\Exception\\TypeNotFound' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/Exception/TypeNotFound.php', + 'Doctrine\\DBAL\\Types\\Exception\\TypeNotRegistered' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/Exception/TypeNotRegistered.php', + 'Doctrine\\DBAL\\Types\\Exception\\TypesAlreadyExists' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/Exception/TypesAlreadyExists.php', + 'Doctrine\\DBAL\\Types\\Exception\\TypesException' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/Exception/TypesException.php', + 'Doctrine\\DBAL\\Types\\Exception\\UnknownColumnType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/Exception/UnknownColumnType.php', + 'Doctrine\\DBAL\\Types\\Exception\\ValueNotConvertible' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/Exception/ValueNotConvertible.php', 'Doctrine\\DBAL\\Types\\FloatType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/FloatType.php', 'Doctrine\\DBAL\\Types\\GuidType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/GuidType.php', 'Doctrine\\DBAL\\Types\\IntegerType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/IntegerType.php', + 'Doctrine\\DBAL\\Types\\JsonObjectType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/JsonObjectType.php', 'Doctrine\\DBAL\\Types\\JsonType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/JsonType.php', - 'Doctrine\\DBAL\\Types\\ObjectType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/ObjectType.php', + 'Doctrine\\DBAL\\Types\\JsonTypeConvert' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/JsonTypeConvert.php', + 'Doctrine\\DBAL\\Types\\JsonbObjectType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/JsonbObjectType.php', + 'Doctrine\\DBAL\\Types\\JsonbType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/JsonbType.php', + 'Doctrine\\DBAL\\Types\\NumberType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/NumberType.php', + 'Doctrine\\DBAL\\Types\\PhpDateMappingType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/PhpDateMappingType.php', 'Doctrine\\DBAL\\Types\\PhpDateTimeMappingType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/PhpDateTimeMappingType.php', 'Doctrine\\DBAL\\Types\\PhpIntegerMappingType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/PhpIntegerMappingType.php', + 'Doctrine\\DBAL\\Types\\PhpTimeMappingType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/PhpTimeMappingType.php', 'Doctrine\\DBAL\\Types\\SimpleArrayType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/SimpleArrayType.php', + 'Doctrine\\DBAL\\Types\\SmallFloatType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/SmallFloatType.php', 'Doctrine\\DBAL\\Types\\SmallIntType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/SmallIntType.php', 'Doctrine\\DBAL\\Types\\StringType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/StringType.php', 'Doctrine\\DBAL\\Types\\TextType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/TextType.php', @@ -1413,7 +1495,6 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Doctrine\\DBAL\\Types\\Types' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/Types.php', 'Doctrine\\DBAL\\Types\\VarDateTimeImmutableType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/VarDateTimeImmutableType.php', 'Doctrine\\DBAL\\Types\\VarDateTimeType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/VarDateTimeType.php', - 'Doctrine\\DBAL\\VersionAwarePlatformDriver' => __DIR__ . '/..' . '/doctrine/dbal/src/VersionAwarePlatformDriver.php', 'Doctrine\\Deprecations\\Deprecation' => __DIR__ . '/..' . '/doctrine/deprecations/src/Deprecation.php', 'Doctrine\\Deprecations\\PHPUnit\\VerifyDeprecations' => __DIR__ . '/..' . '/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php', 'Egulias\\EmailValidator\\EmailLexer' => __DIR__ . '/..' . '/egulias/email-validator/src/EmailLexer.php', @@ -3845,6 +3926,17 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/SessionStorageInterface.php', 'Symfony\\Component\\HttpFoundation\\StreamedJsonResponse' => __DIR__ . '/..' . '/symfony/http-foundation/StreamedJsonResponse.php', 'Symfony\\Component\\HttpFoundation\\StreamedResponse' => __DIR__ . '/..' . '/symfony/http-foundation/StreamedResponse.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\RequestAttributeValueSame' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/RequestAttributeValueSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseCookieValueSame' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseCookieValueSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseFormatSame' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseFormatSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHasCookie' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseHasCookie.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHasHeader' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseHasHeader.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHeaderLocationSame' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseHeaderLocationSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseHeaderSame' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseHeaderSame.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseIsRedirected' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseIsRedirected.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseIsSuccessful' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseIsSuccessful.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseIsUnprocessable' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseIsUnprocessable.php', + 'Symfony\\Component\\HttpFoundation\\Test\\Constraint\\ResponseStatusCodeSame' => __DIR__ . '/..' . '/symfony/http-foundation/Test/Constraint/ResponseStatusCodeSame.php', 'Symfony\\Component\\HttpFoundation\\UriSigner' => __DIR__ . '/..' . '/symfony/http-foundation/UriSigner.php', 'Symfony\\Component\\HttpFoundation\\UrlHelper' => __DIR__ . '/..' . '/symfony/http-foundation/UrlHelper.php', 'Symfony\\Component\\Mailer\\Command\\MailerTestCommand' => __DIR__ . '/..' . '/symfony/mailer/Command/MailerTestCommand.php', @@ -3876,6 +3968,11 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Mailer\\Messenger\\MessageHandler' => __DIR__ . '/..' . '/symfony/mailer/Messenger/MessageHandler.php', 'Symfony\\Component\\Mailer\\Messenger\\SendEmailMessage' => __DIR__ . '/..' . '/symfony/mailer/Messenger/SendEmailMessage.php', 'Symfony\\Component\\Mailer\\SentMessage' => __DIR__ . '/..' . '/symfony/mailer/SentMessage.php', + 'Symfony\\Component\\Mailer\\Test\\AbstractTransportFactoryTestCase' => __DIR__ . '/..' . '/symfony/mailer/Test/AbstractTransportFactoryTestCase.php', + 'Symfony\\Component\\Mailer\\Test\\Constraint\\EmailCount' => __DIR__ . '/..' . '/symfony/mailer/Test/Constraint/EmailCount.php', + 'Symfony\\Component\\Mailer\\Test\\Constraint\\EmailIsQueued' => __DIR__ . '/..' . '/symfony/mailer/Test/Constraint/EmailIsQueued.php', + 'Symfony\\Component\\Mailer\\Test\\IncompleteDsnTestTrait' => __DIR__ . '/..' . '/symfony/mailer/Test/IncompleteDsnTestTrait.php', + 'Symfony\\Component\\Mailer\\Test\\TransportFactoryTestCase' => __DIR__ . '/..' . '/symfony/mailer/Test/TransportFactoryTestCase.php', 'Symfony\\Component\\Mailer\\Transport' => __DIR__ . '/..' . '/symfony/mailer/Transport.php', 'Symfony\\Component\\Mailer\\Transport\\AbstractApiTransport' => __DIR__ . '/..' . '/symfony/mailer/Transport/AbstractApiTransport.php', 'Symfony\\Component\\Mailer\\Transport\\AbstractHttpTransport' => __DIR__ . '/..' . '/symfony/mailer/Transport/AbstractHttpTransport.php', diff --git a/composer/installed.json b/composer/installed.json index 5bd50b80c..71ea5f3d2 100644 --- a/composer/installed.json +++ b/composer/installed.json @@ -449,50 +449,42 @@ }, { "name": "doctrine/dbal", - "version": "3.10.4", - "version_normalized": "3.10.4.0", + "version": "4.4.4", + "version_normalized": "4.4.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868" + "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/63a46cb5aa6f60991186cc98c1d1b50c09311868", - "reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce", + "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce", "shasum": "" }, "require": { - "composer-runtime-api": "^2", - "doctrine/deprecations": "^0.5.3|^1", - "doctrine/event-manager": "^1|^2", - "php": "^7.4 || ^8.0", + "doctrine/deprecations": "^1.1.5", + "php": "^8.2", "psr/cache": "^1|^2|^3", "psr/log": "^1|^2|^3" }, - "conflict": { - "doctrine/cache": "< 1.11" - }, "require-dev": { - "doctrine/cache": "^1.11|^2.0", "doctrine/coding-standard": "14.0.0", "fig/log-test": "^1", - "jetbrains/phpstorm-stubs": "2023.1", + "jetbrains/phpstorm-stubs": "2023.2", "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-phpunit": "2.0.7", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "9.6.29", - "slevomat/coding-standard": "8.24.0", - "squizlabs/php_codesniffer": "4.0.0", - "symfony/cache": "^5.4|^6.0|^7.0|^8.0", - "symfony/console": "^4.4|^5.4|^6.0|^7.0|^8.0" + "phpunit/phpunit": "11.5.50", + "slevomat/coding-standard": "8.27.1", + "squizlabs/php_codesniffer": "4.0.1", + "symfony/cache": "^6.3.8|^7.0|^8.0", + "symfony/console": "^5.4|^6.3|^7.0|^8.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." }, - "time": "2025-11-29T10:46:08+00:00", - "bin": [ - "bin/doctrine-dbal" - ], + "time": "2026-07-21T14:34:40+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -546,7 +538,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.10.4" + "source": "https://github.com/doctrine/dbal/tree/4.4.4" }, "funding": [ { @@ -615,100 +607,6 @@ }, "install-path": "../doctrine/deprecations" }, - { - "name": "doctrine/event-manager", - "version": "2.0.1", - "version_normalized": "2.0.1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/event-manager.git", - "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", - "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", - "shasum": "" - }, - "require": { - "php": "^8.1" - }, - "conflict": { - "doctrine/common": "<2.9" - }, - "require-dev": { - "doctrine/coding-standard": "^12", - "phpstan/phpstan": "^1.8.8", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.24" - }, - "time": "2024-05-22T20:47:39+00:00", - "type": "library", - "installation-source": "dist", - "autoload": { - "psr-4": { - "Doctrine\\Common\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - }, - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" - } - ], - "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", - "homepage": "https://www.doctrine-project.org/projects/event-manager.html", - "keywords": [ - "event", - "event dispatcher", - "event manager", - "event system", - "events" - ], - "support": { - "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/2.0.1" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", - "type": "tidelift" - } - ], - "install-path": "../doctrine/event-manager" - }, { "name": "doctrine/lexer", "version": "3.0.1", @@ -6769,6 +6667,6 @@ "install-path": "../web-auth/webauthn-lib" } ], - "dev": false, + "dev": true, "dev-package-names": [] } diff --git a/composer/installed.php b/composer/installed.php index 5995ff377..ed16d1658 100644 --- a/composer/installed.php +++ b/composer/installed.php @@ -7,7 +7,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), - 'dev' => false, + 'dev' => true, ), 'versions' => array( 'aws/aws-crt-php' => array( @@ -74,9 +74,9 @@ 'dev_requirement' => false, ), 'doctrine/dbal' => array( - 'pretty_version' => '3.10.4', - 'version' => '3.10.4.0', - 'reference' => '63a46cb5aa6f60991186cc98c1d1b50c09311868', + 'pretty_version' => '4.4.4', + 'version' => '4.4.4.0', + 'reference' => 'fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce', 'type' => 'library', 'install_path' => __DIR__ . '/../doctrine/dbal', 'aliases' => array(), @@ -91,15 +91,6 @@ 'aliases' => array(), 'dev_requirement' => false, ), - 'doctrine/event-manager' => array( - 'pretty_version' => '2.0.1', - 'version' => '2.0.1.0', - 'reference' => 'b680156fa328f1dfd874fd48c7026c41570b9c6e', - 'type' => 'library', - 'install_path' => __DIR__ . '/../doctrine/event-manager', - 'aliases' => array(), - 'dev_requirement' => false, - ), 'doctrine/lexer' => array( 'pretty_version' => '3.0.1', 'version' => '3.0.1.0', diff --git a/doctrine/dbal/src/ArrayParameterType.php b/doctrine/dbal/src/ArrayParameterType.php index d78cd5f16..851d47d18 100644 --- a/doctrine/dbal/src/ArrayParameterType.php +++ b/doctrine/dbal/src/ArrayParameterType.php @@ -1,42 +1,39 @@ ParameterType::INTEGER, + self::STRING => ParameterType::STRING, + self::ASCII => ParameterType::ASCII, + self::BINARY => ParameterType::BINARY, + }; } } diff --git a/doctrine/dbal/src/ArrayParameters/Exception.php b/doctrine/dbal/src/ArrayParameters/Exception.php index 7fc0f7060..e5a580b76 100644 --- a/doctrine/dbal/src/ArrayParameters/Exception.php +++ b/doctrine/dbal/src/ArrayParameters/Exception.php @@ -1,5 +1,7 @@ > */ - private array $data; + private int $num = 0; - private int $columnCount = 0; - private int $num = 0; + /** + * @param list $columnNames The names of the result columns. Must be non-empty. + * @param list> $rows The rows of the result. Each row must have the same number of columns + * as the number of column names. + */ + public function __construct( + private readonly array $columnNames, + private array $rows, + ) { + } - /** @param list> $data */ - public function __construct(array $data) + public function fetchNumeric(): array|false { - $this->data = $data; - if (count($data) === 0) { - return; - } - - $this->columnCount = count($data[0]); + return $this->fetch(); } - /** - * {@inheritDoc} - */ - public function fetchNumeric() + public function fetchAssociative(): array|false { $row = $this->fetch(); @@ -40,21 +43,10 @@ public function fetchNumeric() return false; } - return array_values($row); + return array_combine($this->columnNames, $row); } - /** - * {@inheritDoc} - */ - public function fetchAssociative() - { - return $this->fetch(); - } - - /** - * {@inheritDoc} - */ - public function fetchOne() + public function fetchOne(): mixed { $row = $this->fetch(); @@ -62,7 +54,7 @@ public function fetchOne() return false; } - return reset($row); + return $row[0]; } /** @@ -91,26 +83,54 @@ public function fetchFirstColumn(): array public function rowCount(): int { - return count($this->data); + return count($this->rows); } public function columnCount(): int { - return $this->columnCount; + return count($this->columnNames); + } + + public function getColumnName(int $index): string + { + return $this->columnNames[$index] ?? throw InvalidColumnIndex::new($index); } public function free(): void { - $this->data = []; + $this->rows = []; + } + + /** @return array{list, list>} */ + public function __serialize(): array + { + return [$this->columnNames, $this->rows]; + } + + /** @param mixed[] $data */ + public function __unserialize(array $data): void + { + // Handle objects serialized with DBAL 4.1 and earlier. + if (isset($data["\0" . self::class . "\0data"])) { + /** @var list> $legacyData */ + $legacyData = $data["\0" . self::class . "\0data"]; + + $this->columnNames = array_keys($legacyData[0] ?? []); + $this->rows = array_map(array_values(...), $legacyData); + + return; + } + + [$this->columnNames, $this->rows] = $data; } - /** @return array|false */ - private function fetch() + /** @return list|false */ + private function fetch(): array|false { - if (! isset($this->data[$this->num])) { + if (! isset($this->rows[$this->num])) { return false; } - return $this->data[$this->num++]; + return $this->rows[$this->num++]; } } diff --git a/doctrine/dbal/src/Cache/CacheException.php b/doctrine/dbal/src/Cache/CacheException.php index fd10fd5d8..780a833ab 100644 --- a/doctrine/dbal/src/Cache/CacheException.php +++ b/doctrine/dbal/src/Cache/CacheException.php @@ -1,20 +1,11 @@ lifetime = $lifetime; - $this->cacheKey = $cacheKey; - if ($resultCache instanceof CacheItemPoolInterface) { - $this->resultCache = $resultCache; - } elseif ($resultCache instanceof Cache) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4620', - 'Passing an instance of %s to %s as $resultCache is deprecated. Pass an instance of %s instead.', - Cache::class, - __METHOD__, - CacheItemPoolInterface::class, - ); - - $this->resultCache = CacheAdapter::wrap($resultCache); - } elseif ($resultCache !== null) { - throw new TypeError(sprintf( - '$resultCache: Expected either null or an instance of %s or %s, got %s.', - CacheItemPoolInterface::class, - Cache::class, - get_class($resultCache), - )); - } + public function __construct( + private readonly int $lifetime = 0, + private readonly ?string $cacheKey = null, + private readonly ?CacheItemPoolInterface $resultCache = null, + ) { } public function getResultCache(): ?CacheItemPoolInterface @@ -70,50 +34,16 @@ public function getResultCache(): ?CacheItemPoolInterface return $this->resultCache; } - /** - * @deprecated Use {@see getResultCache()} instead. - * - * @return Cache|null - */ - public function getResultCacheDriver() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4620', - '%s is deprecated, call getResultCache() instead.', - __METHOD__, - ); - - if ($this->resultCache === null) { - return null; - } - - if (! class_exists(DoctrineProvider::class)) { - throw new RuntimeException(sprintf( - 'Calling %s() is not supported if the doctrine/cache package is not installed. ' - . 'Try running "composer require doctrine/cache" or migrate cache access to PSR-6.', - __METHOD__, - )); - } - - return DoctrineProvider::wrap($this->resultCache); - } - - /** @return int */ - public function getLifetime() + public function getLifetime(): int { return $this->lifetime; } - /** - * @return string - * - * @throws CacheException - */ - public function getCacheKey() + /** @throws CacheException */ + public function getCacheKey(): string { if ($this->cacheKey === null) { - throw CacheException::noCacheKey(); + throw NoCacheKey::new(); } return $this->cacheKey; @@ -122,14 +52,13 @@ public function getCacheKey() /** * Generates the real cache key from query, params, types and connection parameters. * - * @param string $sql - * @param list|array $params - * @param array|array $types - * @param array $connectionParams + * @param list|array $params + * @param array $connectionParams + * @phpstan-param array|array $types * * @return array{string, string} */ - public function generateCacheKeys($sql, $params, $types, array $connectionParams = []) + public function generateCacheKeys(string $sql, array $params, array $types, array $connectionParams = []): array { if (isset($connectionParams['password'])) { unset($connectionParams['password']); @@ -151,39 +80,12 @@ public function setResultCache(CacheItemPoolInterface $cache): QueryCacheProfile return new QueryCacheProfile($this->lifetime, $this->cacheKey, $cache); } - /** - * @deprecated Use {@see setResultCache()} instead. - * - * @return QueryCacheProfile - */ - public function setResultCacheDriver(Cache $cache) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4620', - '%s is deprecated, call setResultCache() instead.', - __METHOD__, - ); - - return new QueryCacheProfile($this->lifetime, $this->cacheKey, CacheAdapter::wrap($cache)); - } - - /** - * @param string|null $cacheKey - * - * @return QueryCacheProfile - */ - public function setCacheKey($cacheKey) + public function setCacheKey(?string $cacheKey): self { return new QueryCacheProfile($this->lifetime, $cacheKey, $this->resultCache); } - /** - * @param int $lifetime - * - * @return QueryCacheProfile - */ - public function setLifetime($lifetime) + public function setLifetime(int $lifetime): self { return new QueryCacheProfile($lifetime, $this->cacheKey, $this->resultCache); } diff --git a/doctrine/dbal/src/ColumnCase.php b/doctrine/dbal/src/ColumnCase.php index cb0dd409b..687a04f80 100644 --- a/doctrine/dbal/src/ColumnCase.php +++ b/doctrine/dbal/src/ColumnCase.php @@ -1,28 +1,21 @@ sqlLogger = $logger; - } - - /** - * Gets the SQL logger that is used. - * - * @deprecated - */ - public function getSQLLogger(): ?SQLLogger - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4967', - '%s is deprecated.', - __METHOD__, - ); - - return $this->sqlLogger; - } - /** * Gets the cache driver implementation that is used for query result caching. */ @@ -119,89 +51,26 @@ public function getResultCache(): ?CacheItemPoolInterface return $this->resultCache; } - /** - * Gets the cache driver implementation that is used for query result caching. - * - * @deprecated Use {@see getResultCache()} instead. - */ - public function getResultCacheImpl(): ?Cache - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4620', - '%s is deprecated, call getResultCache() instead.', - __METHOD__, - ); - - if ($this->resultCache !== null && ! interface_exists(Cache::class)) { - throw new RuntimeException(sprintf( - 'Calling %s() is not supported if the doctrine/cache package is not installed. ' - . 'Try running "composer require doctrine/cache" or migrate cache access to PSR-6.', - __METHOD__, - )); - } - - return $this->resultCacheImpl; - } - /** * Sets the cache driver implementation that is used for query result caching. */ public function setResultCache(CacheItemPoolInterface $cache): void { - if (class_exists(DoctrineProvider::class)) { - $this->resultCacheImpl = DoctrineProvider::wrap($cache); - } - $this->resultCache = $cache; } - /** - * Sets the cache driver implementation that is used for query result caching. - * - * @deprecated Use {@see setResultCache()} instead. - */ - public function setResultCacheImpl(Cache $cacheImpl): void - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4620', - '%s is deprecated, call setResultCache() instead.', - __METHOD__, - ); - - $this->resultCacheImpl = $cacheImpl; - $this->resultCache = CacheAdapter::wrap($cacheImpl); - } - /** * Sets the callable to use to filter schema assets. */ - public function setSchemaAssetsFilter(?callable $callable = null): void + public function setSchemaAssetsFilter(callable $schemaAssetsFilter): void { - if (func_num_args() < 1) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5483', - 'Not passing an argument to %s is deprecated.', - __METHOD__, - ); - } elseif ($callable === null) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5483', - 'Using NULL as a schema asset filter is deprecated.' - . ' Use a callable that always returns true instead.', - ); - } - - $this->schemaAssetsFilter = $callable; + $this->schemaAssetsFilter = $schemaAssetsFilter; } /** * Returns the callable to use to filter schema assets. */ - public function getSchemaAssetsFilter(): ?callable + public function getSchemaAssetsFilter(): callable { return $this->schemaAssetsFilter; } @@ -213,7 +82,7 @@ public function getSchemaAssetsFilter(): ?callable * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either * the method commit or the method rollback. By default, new connections are in auto-commit mode. * - * @see getAutoCommit + * @see getAutoCommit * * @param bool $autoCommit True to enable auto-commit mode; false to disable it */ @@ -267,13 +136,15 @@ public function setSchemaManagerFactory(SchemaManagerFactory $schemaManagerFacto public function getDisableTypeComments(): bool { - return $this->disableTypeComments; + return true; } /** @return $this */ public function setDisableTypeComments(bool $disableTypeComments): self { - $this->disableTypeComments = $disableTypeComments; + if (! $disableTypeComments) { + throw new InvalidArgumentException('Column comments cannot be enabled anymore.'); + } return $this; } diff --git a/doctrine/dbal/src/Connection.php b/doctrine/dbal/src/Connection.php index 2902429f4..0e523708c 100644 --- a/doctrine/dbal/src/Connection.php +++ b/doctrine/dbal/src/Connection.php @@ -1,25 +1,27 @@ , + * WrapperParameterType>|array * @phpstan-consistent-constructor */ -class Connection +class Connection implements ServerVersionProvider { - /** - * Represents an array of ints to be expanded by Doctrine SQL parsing. - * - * @deprecated Use {@see ArrayParameterType::INTEGER} instead. - */ - public const PARAM_INT_ARRAY = ArrayParameterType::INTEGER; - - /** - * Represents an array of strings to be expanded by Doctrine SQL parsing. - * - * @deprecated Use {@see ArrayParameterType::STRING} instead. - */ - public const PARAM_STR_ARRAY = ArrayParameterType::STRING; - - /** - * Represents an array of ascii strings to be expanded by Doctrine SQL parsing. - * - * @deprecated Use {@see ArrayParameterType::ASCII} instead. - */ - public const PARAM_ASCII_STR_ARRAY = ArrayParameterType::ASCII; - - /** - * Offset by which PARAM_* constants are detected as arrays of the param type. - * - * @internal Should be used only within the wrapper layer. - */ - public const ARRAY_PARAM_OFFSET = 100; - /** * The wrapped driver connection. - * - * @var DriverConnection|null */ - protected $_conn; - - /** @var Configuration */ - protected $_config; + protected ?DriverConnection $_conn = null; - /** - * @deprecated - * - * @var EventManager - */ - protected $_eventManager; - - /** - * @deprecated Use {@see createExpressionBuilder()} instead. - * - * @var ExpressionBuilder - */ - protected $_expr; + protected Configuration $_config; /** * The current auto-commit mode of this connection. @@ -122,15 +81,8 @@ class Connection /** * The currently active transaction isolation level or NULL before it has been determined. - * - * @var TransactionIsolationLevel::*|null - */ - private $transactionIsolationLevel; - - /** - * If nested transactions should use savepoints. */ - private bool $nestTransactionsWithSavepoints = false; + private ?TransactionIsolationLevel $transactionIsolationLevel = null; /** * The parameters used during creation of the Connection instance. @@ -148,22 +100,6 @@ class Connection private ?ExceptionConverter $exceptionConverter = null; private ?Parser $parser = null; - /** - * The schema manager. - * - * @deprecated Use {@see createSchemaManager()} instead. - * - * @var AbstractSchemaManager|null - */ - protected $_schemaManager; - - /** - * The used DBAL driver. - * - * @var Driver - */ - protected $_driver; - /** * Flag that indicates whether the current transaction is marked for rollback only. */ @@ -176,66 +112,23 @@ class Connection * * @internal The connection can be only instantiated by the driver manager. * - * @param array $params The connection parameters. - * @param Driver $driver The driver to use. - * @param Configuration|null $config The configuration, optional. - * @param EventManager|null $eventManager The event manager, optional. + * @param array $params The connection parameters. + * @param Driver $driver The driver to use. + * @param Configuration|null $config The configuration, optional. * @phpstan-param Params $params - * - * @throws Exception */ public function __construct( #[SensitiveParameter] array $params, - Driver $driver, + protected Driver $driver, ?Configuration $config = null, - ?EventManager $eventManager = null ) { - $this->_driver = $driver; - $this->params = $params; - - // Create default config and event manager if none given - $config ??= new Configuration(); - $eventManager ??= new EventManager(); - - $this->_config = $config; - $this->_eventManager = $eventManager; - - if (isset($params['platform'])) { - if (! $params['platform'] instanceof Platforms\AbstractPlatform) { - throw Exception::invalidPlatformType($params['platform']); - } - - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5699', - 'The "platform" connection parameter is deprecated.' - . ' Use a driver middleware that would instantiate the platform instead.', - ); - - $this->platform = $params['platform']; - $this->platform->setEventManager($this->_eventManager); - $this->platform->setDisableTypeComments($config->getDisableTypeComments()); - } - - $this->_expr = $this->createExpressionBuilder(); - - $this->autoCommit = $config->getAutoCommit(); + $this->_config = $config ?? new Configuration(); + $this->params = $params; + $this->autoCommit = $this->_config->getAutoCommit(); - $schemaManagerFactory = $config->getSchemaManagerFactory(); - if ($schemaManagerFactory === null) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5812', - 'Not configuring a schema manager factory is deprecated.' - . ' Use %s which is going to be the default in DBAL 4.', - DefaultSchemaManagerFactory::class, - ); - - $schemaManagerFactory = new LegacySchemaManagerFactory(); - } - - $this->schemaManagerFactory = $schemaManagerFactory; + $this->schemaManagerFactory = $this->_config->getSchemaManagerFactory() + ?? new DefaultSchemaManagerFactory(); } /** @@ -246,7 +139,7 @@ public function __construct( * @return array * @phpstan-return Params */ - public function getParams() + public function getParams(): array { return $this->params; } @@ -254,75 +147,53 @@ public function getParams() /** * Gets the name of the currently selected database. * - * @return string|null The name of the database or NULL if a database is not selected. - * The platforms which don't support the concept of a database (e.g. embedded databases) - * must always return a string as an indicator of an implicitly selected database. + * @return ?non-empty-string The name of the database or NULL if a database is not selected. + * The platforms which don't support the concept of a database (e.g. embedded databases) + * must always return a string as an indicator of an implicitly selected database. * * @throws Exception */ - public function getDatabase() + public function getDatabase(): ?string { $platform = $this->getDatabasePlatform(); $query = $platform->getDummySelectSQL($platform->getCurrentDatabaseExpression()); - $database = $this->fetchOne($query); - assert(is_string($database) || $database === null); - - return $database; + return $this->fetchOne($query); } /** * Gets the DBAL driver instance. - * - * @return Driver */ - public function getDriver() + public function getDriver(): Driver { - return $this->_driver; + return $this->driver; } /** * Gets the Configuration used by the Connection. - * - * @return Configuration */ - public function getConfiguration() + public function getConfiguration(): Configuration { return $this->_config; } - /** - * Gets the EventManager used by the Connection. - * - * @deprecated - * - * @return EventManager - */ - public function getEventManager() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - '%s is deprecated.', - __METHOD__, - ); - - return $this->_eventManager; - } - /** * Gets the DatabasePlatform for the connection. * - * @return AbstractPlatform - * * @throws Exception */ - public function getDatabasePlatform() + public function getDatabasePlatform(): AbstractPlatform { if ($this->platform === null) { - $this->platform = $this->detectDatabasePlatform(); - $this->platform->setEventManager($this->_eventManager); - $this->platform->setDisableTypeComments($this->_config->getDisableTypeComments()); + $versionProvider = $this; + + if (isset($this->params['serverVersion'])) { + $versionProvider = new StaticServerVersionProvider($this->params['serverVersion']); + } elseif (isset($this->params['primary']['serverVersion'])) { + $versionProvider = new StaticServerVersionProvider($this->params['primary']['serverVersion']); + } + + $this->platform = $this->driver->getDatabasePlatform($versionProvider); } return $this->platform; @@ -337,50 +208,18 @@ public function createExpressionBuilder(): ExpressionBuilder } /** - * Gets the ExpressionBuilder for the connection. - * - * @deprecated Use {@see createExpressionBuilder()} instead. - * - * @return ExpressionBuilder - */ - public function getExpressionBuilder() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4515', - 'Connection::getExpressionBuilder() is deprecated,' - . ' use Connection::createExpressionBuilder() instead.', - ); - - return $this->_expr; - } - - /** - * Establishes the connection with the database. - * - * @internal This method will be made protected in DBAL 4.0. - * - * @return bool TRUE if the connection was successfully established, FALSE if - * the connection is already open. + * Establishes the connection with the database and returns the underlying connection. * * @throws Exception - * - * @phpstan-assert !null $this->_conn */ - public function connect() + protected function connect(): DriverConnection { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4966', - 'Public access to Connection::connect() is deprecated.', - ); - if ($this->_conn !== null) { - return false; + return $this->_conn; } try { - $this->_conn = $this->_driver->connect($this->params); + $connection = $this->_conn = $this->driver->connect($this->params); } catch (Driver\Exception $e) { throw $this->convertException($e); } @@ -389,144 +228,17 @@ public function connect() $this->beginTransaction(); } - if ($this->_eventManager->hasListeners(Events::postConnect)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated. Implement a middleware instead.', - Events::postConnect, - ); - - $eventArgs = new Event\ConnectionEventArgs($this); - $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs); - } - - return true; - } - - /** - * Detects and sets the database platform. - * - * Evaluates custom platform class and version in order to set the correct platform. - * - * @throws Exception If an invalid platform was specified for this connection. - */ - private function detectDatabasePlatform(): AbstractPlatform - { - $version = $this->getDatabasePlatformVersion(); - - if ($version !== null) { - assert($this->_driver instanceof VersionAwarePlatformDriver); - - return $this->_driver->createDatabasePlatformForVersion($version); - } - - return $this->_driver->getDatabasePlatform(); + return $connection; } /** - * Returns the version of the related platform if applicable. - * - * Returns null if either the driver is not capable to create version - * specific platform instances, no explicit server version was specified - * or the underlying driver connection cannot determine the platform - * version without having to query it (performance reasons). - * - * @return string|null - * - * @throws Throwable - */ - private function getDatabasePlatformVersion() - { - // Driver does not support version specific platforms. - if (! $this->_driver instanceof VersionAwarePlatformDriver) { - return null; - } - - // Explicit platform version requested (supersedes auto-detection). - if (isset($this->params['serverVersion'])) { - return $this->params['serverVersion']; - } - - if (isset($this->params['primary']) && isset($this->params['primary']['serverVersion'])) { - return $this->params['primary']['serverVersion']; - } - - // If not connected, we need to connect now to determine the platform version. - if ($this->_conn === null) { - try { - $this->connect(); - } catch (Exception $originalException) { - if (! isset($this->params['dbname'])) { - throw $originalException; - } - - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5707', - 'Relying on a fallback connection used to determine the database platform while connecting' - . ' to a non-existing database is deprecated. Either use an existing database name in' - . ' connection parameters or omit the database name if the platform' - . ' and the server configuration allow that.', - ); - - // The database to connect to might not yet exist. - // Retry detection without database name connection parameter. - $params = $this->params; - - unset($this->params['dbname']); - - try { - $this->connect(); - } catch (Exception $fallbackException) { - // Either the platform does not support database-less connections - // or something else went wrong. - throw $originalException; - } finally { - $this->params = $params; - } - - $serverVersion = $this->getServerVersion(); - - // Close "temporary" connection to allow connecting to the real database again. - $this->close(); - - return $serverVersion; - } - } - - return $this->getServerVersion(); - } - - /** - * Returns the database server version if the underlying driver supports it. - * - * @return string|null + * {@inheritDoc} * * @throws Exception */ - private function getServerVersion() + public function getServerVersion(): string { - $connection = $this->getWrappedConnection(); - - // Automatic platform version detection. - if ($connection instanceof ServerInfoAwareConnection) { - try { - return $connection->getServerVersion(); - } catch (Driver\Exception $e) { - throw $this->convertException($e); - } - } - - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4750', - 'Not implementing the ServerInfoAwareConnection interface in %s is deprecated', - get_class($connection), - ); - - // Unable to detect platform version. - return null; + return $this->connect()->getServerVersion(); } /** @@ -536,9 +248,9 @@ private function getServerVersion() * * @return bool True if auto-commit mode is currently enabled for this connection, false otherwise. */ - public function isAutoCommit() + public function isAutoCommit(): bool { - return $this->autoCommit === true; + return $this->autoCommit; } /** @@ -551,16 +263,12 @@ public function isAutoCommit() * NOTE: If this method is called during a transaction and the auto-commit mode is changed, the transaction is * committed. If this method is called and the auto-commit mode is not changed, the call is a no-op. * - * @see isAutoCommit + * @see isAutoCommit * - * @param bool $autoCommit True to enable auto-commit mode; false to disable it. - * - * @return void + * @throws Exception */ - public function setAutoCommit($autoCommit) + public function setAutoCommit(bool $autoCommit): void { - $autoCommit = (bool) $autoCommit; - // Mode not changed, no-op. if ($autoCommit === $this->autoCommit) { return; @@ -569,26 +277,23 @@ public function setAutoCommit($autoCommit) $this->autoCommit = $autoCommit; // Commit all currently active transactions if any when switching auto-commit mode. - if ($this->_conn === null || $this->transactionNestingLevel === 0) { - return; + if ($this->_conn !== null && $this->transactionNestingLevel !== 0) { + $this->commitAll(); } - - $this->commitAll(); } /** * Prepares and executes an SQL query and returns the first row of the result * as an associative array. * - * @param string $query SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @return array|false False is returned if no rows are found. * * @throws Exception */ - public function fetchAssociative(string $query, array $params = [], array $types = []) + public function fetchAssociative(string $query, array $params = [], array $types = []): array|false { return $this->executeQuery($query, $params, $types)->fetchAssociative(); } @@ -597,15 +302,14 @@ public function fetchAssociative(string $query, array $params = [], array $types * Prepares and executes an SQL query and returns the first row of the result * as a numerically indexed array. * - * @param string $query SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @return list|false False is returned if no rows are found. * * @throws Exception */ - public function fetchNumeric(string $query, array $params = [], array $types = []) + public function fetchNumeric(string $query, array $params = [], array $types = []): array|false { return $this->executeQuery($query, $params, $types)->fetchNumeric(); } @@ -614,15 +318,14 @@ public function fetchNumeric(string $query, array $params = [], array $types = [ * Prepares and executes an SQL query and returns the value of a single column * of the first row of the result. * - * @param string $query SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @return mixed|false False is returned if no rows are found. * * @throws Exception */ - public function fetchOne(string $query, array $params = [], array $types = []) + public function fetchOne(string $query, array $params = [], array $types = []): mixed { return $this->executeQuery($query, $params, $types)->fetchOne(); } @@ -630,9 +333,9 @@ public function fetchOne(string $query, array $params = [], array $types = []) /** * Whether an actual connection to the database is established. * - * @return bool + * @phpstan-assert-if-true !null $this->_conn */ - public function isConnected() + public function isConnected(): bool { return $this->_conn !== null; } @@ -642,7 +345,7 @@ public function isConnected() * * @return bool TRUE if a transaction is currently active, FALSE otherwise. */ - public function isTransactionActive() + public function isTransactionActive(): bool { return $this->transactionNestingLevel > 0; } @@ -650,24 +353,17 @@ public function isTransactionActive() /** * Adds condition based on the criteria to the query components * - * @param array $criteria Map of key columns to their values - * @param string[] $columns Column names - * @param mixed[] $values Column values - * @param string[] $conditions Key conditions + * @param array $criteria Map of key columns to their values * - * @throws Exception + * @return array{list, list, list} */ - private function addCriteriaCondition( - array $criteria, - array &$columns, - array &$values, - array &$conditions - ): void { - $platform = $this->getDatabasePlatform(); + private function getCriteriaCondition(array $criteria): array + { + $columns = $values = $conditions = []; foreach ($criteria as $columnName => $value) { if ($value === null) { - $conditions[] = $platform->getIsNullExpression($columnName); + $conditions[] = $columnName . ' IS NULL'; continue; } @@ -675,6 +371,8 @@ private function addCriteriaCondition( $values[] = $value; $conditions[] = $columnName . ' = ?'; } + + return [$columns, $values, $conditions]; } /** @@ -682,26 +380,25 @@ private function addCriteriaCondition( * * Table expression and columns are not escaped and are not safe for user-input. * - * @param string $table Table name - * @param array $criteria Deletion criteria - * @param array|array $types Parameter types + * @param array $criteria + * @param array, string|ParameterType|Type>|array $types * - * @return int|string The number of affected rows. + * @return int|numeric-string The number of affected rows. * * @throws Exception */ - public function delete($table, array $criteria, array $types = []) + public function delete(string $table, array $criteria = [], array $types = []): int|string { - if (count($criteria) === 0) { - throw InvalidArgumentException::fromEmptyCriteria(); - } + [$columns, $values, $conditions] = $this->getCriteriaCondition($criteria); - $columns = $values = $conditions = []; + $sql = 'DELETE FROM ' . $table; - $this->addCriteriaCondition($criteria, $columns, $values, $conditions); + if ($conditions !== []) { + $sql .= ' WHERE ' . implode(' AND ', $conditions); + } return $this->executeStatement( - 'DELETE FROM ' . $table . ' WHERE ' . implode(' AND ', $conditions), + $sql, $values, is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types, ); @@ -709,10 +406,8 @@ public function delete($table, array $criteria, array $types = []) /** * Closes the connection. - * - * @return void */ - public function close() + public function close(): void { $this->_conn = null; $this->transactionNestingLevel = 0; @@ -721,27 +416,25 @@ public function close() /** * Sets the transaction isolation level. * - * @param TransactionIsolationLevel::* $level The level to set. - * - * @return int|string + * @param TransactionIsolationLevel $level The level to set. * * @throws Exception */ - public function setTransactionIsolation($level) + public function setTransactionIsolation(TransactionIsolationLevel $level): void { $this->transactionIsolationLevel = $level; - return $this->executeStatement($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level)); + $this->executeStatement($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level)); } /** * Gets the currently active transaction isolation level. * - * @return TransactionIsolationLevel::* The current transaction isolation level. + * @return TransactionIsolationLevel The current transaction isolation level. * * @throws Exception */ - public function getTransactionIsolation() + public function getTransactionIsolation(): TransactionIsolationLevel { return $this->transactionIsolationLevel ??= $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel(); } @@ -751,16 +444,15 @@ public function getTransactionIsolation() * * Table expression and columns are not escaped and are not safe for user-input. * - * @param string $table Table name - * @param array $data Column-value pairs - * @param array $criteria Update criteria - * @param array|array $types Parameter types + * @param array $data + * @param array $criteria + * @param array, string|ParameterType|Type>|array $types * - * @return int|string The number of affected rows. + * @return int|numeric-string The number of affected rows. * * @throws Exception */ - public function update($table, array $data, array $criteria, array $types = []) + public function update(string $table, array $data, array $criteria = [], array $types = []): int|string { $columns = $values = $conditions = $set = []; @@ -770,14 +462,21 @@ public function update($table, array $data, array $criteria, array $types = []) $set[] = $columnName . ' = ?'; } - $this->addCriteriaCondition($criteria, $columns, $values, $conditions); + [$criteriaColumns, $criteriaValues, $criteriaConditions] = $this->getCriteriaCondition($criteria); + + $columns = array_merge($columns, $criteriaColumns); + $values = array_merge($values, $criteriaValues); + $conditions = array_merge($conditions, $criteriaConditions); if (is_string(key($types))) { $types = $this->extractTypeValues($columns, $types); } - $sql = 'UPDATE ' . $table . ' SET ' . implode(', ', $set) - . ' WHERE ' . implode(' AND ', $conditions); + $sql = 'UPDATE ' . $table . ' SET ' . implode(', ', $set); + + if ($conditions !== []) { + $sql .= ' WHERE ' . implode(' AND ', $conditions); + } return $this->executeStatement($sql, $values, $types); } @@ -787,15 +486,14 @@ public function update($table, array $data, array $criteria, array $types = []) * * Table expression and columns are not escaped and are not safe for user-input. * - * @param string $table Table name - * @param array $data Column-value pairs - * @param array|array $types Parameter types + * @param array $data + * @param array, string|ParameterType|Type>|array $types * - * @return int|string The number of affected rows. + * @return int|numeric-string The number of affected rows. * * @throws Exception */ - public function insert($table, array $data, array $types = []) + public function insert(string $table, array $data, array $types = []): int|string { if (count($data) === 0) { return $this->executeStatement('INSERT INTO ' . $table . ' () VALUES ()'); @@ -822,16 +520,16 @@ public function insert($table, array $data, array $types = []) /** * Extract ordered type list from an ordered column list and type map. * - * @param array $columnList - * @param array|array $types + * @param array $columns + * @param array|array $types * - * @return array|array + * @return array, string|ParameterType|Type> */ - private function extractTypeValues(array $columnList, array $types): array + private function extractTypeValues(array $columns, array $types): array { $typeValues = []; - foreach ($columnList as $columnName) { + foreach ($columns as $columnName) { $typeValues[] = $types[$columnName] ?? ParameterType::STRING; } @@ -848,39 +546,55 @@ private function extractTypeValues(array $columnList, array $types): array * you SHOULD use them. In general, they end up causing way more * problems than they solve. * - * @param string $str The name to be quoted. + * @deprecated Use {@link quoteSingleIdentifier()} individually for each part of a qualified name instead. + * + * @param string $identifier The identifier to be quoted. + * + * @return string The quoted identifier. + * + * @throws Exception + */ + public function quoteIdentifier(string $identifier): string + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6590', + <<<'DEPRECATION' + Method %s is deprecated and will be removed in 5.0. + Use quoteSingleIdentifier() individually for each part of a qualified name instead. + DEPRECATION, + __METHOD__, + ); + + return $this->getDatabasePlatform()->quoteIdentifier($identifier); + } + + /** + * Quotes a string so that it can be safely used as an identifier in SQL. * - * @return string The quoted name. + * @throws Exception */ - public function quoteIdentifier($str) + public function quoteSingleIdentifier(string $identifier): string { - return $this->getDatabasePlatform()->quoteIdentifier($str); + return $this->getDatabasePlatform()->quoteSingleIdentifier($identifier); } /** * The usage of this method is discouraged. Use prepared statements * or {@see AbstractPlatform::quoteStringLiteral()} instead. * - * @param mixed $value - * @param int|string|Type|null $type - * - * @return mixed + * @throws Exception */ - public function quote($value, $type = ParameterType::STRING) + public function quote(string $value): string { - $connection = $this->getWrappedConnection(); - - [$value, $bindingType] = $this->getBindingInfo($value, $type); - - return $connection->quote($value, $bindingType); + return $this->connect()->quote($value); } /** * Prepares and executes an SQL query and returns the result as an array of numeric arrays. * - * @param string $query SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @return list> * @@ -894,9 +608,8 @@ public function fetchAllNumeric(string $query, array $params = [], array $types /** * Prepares and executes an SQL query and returns the result as an array of associative arrays. * - * @param string $query SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @return list> * @@ -911,9 +624,8 @@ public function fetchAllAssociative(string $query, array $params = [], array $ty * Prepares and executes an SQL query and returns the result as an associative array with the keys * mapped to the first column and the values mapped to the second column. * - * @param string $query SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @return array * @@ -929,9 +641,8 @@ public function fetchAllKeyValue(string $query, array $params = [], array $types * to the first column and the values being an associative array representing the rest of the columns * and their values. * - * @param string $query SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @return array> * @@ -945,9 +656,8 @@ public function fetchAllAssociativeIndexed(string $query, array $params = [], ar /** * Prepares and executes an SQL query and returns the result as an array of the first column values. * - * @param string $query SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @return list * @@ -961,9 +671,8 @@ public function fetchFirstColumn(string $query, array $params = [], array $types /** * Prepares and executes an SQL query and returns the result as an iterator over rows represented as numeric arrays. * - * @param string $query SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @return Traversable> * @@ -978,9 +687,8 @@ public function iterateNumeric(string $query, array $params = [], array $types = * Prepares and executes an SQL query and returns the result as an iterator over rows represented * as associative arrays. * - * @param string $query SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @return Traversable> * @@ -995,9 +703,8 @@ public function iterateAssociative(string $query, array $params = [], array $typ * Prepares and executes an SQL query and returns the result as an iterator with the keys * mapped to the first column and the values mapped to the second column. * - * @param string $query SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @return Traversable * @@ -1013,9 +720,8 @@ public function iterateKeyValue(string $query, array $params = [], array $types * to the first column and the values being an associative array representing the rest of the columns * and their values. * - * @param string $query SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @return Traversable> * @@ -1029,9 +735,8 @@ public function iterateAssociativeIndexed(string $query, array $params = [], arr /** * Prepares and executes an SQL query and returns the result as an iterator over the first column values. * - * @param string $query SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @return Traversable * @@ -1051,7 +756,7 @@ public function iterateColumn(string $query, array $params = [], array $types = */ public function prepare(string $sql): Statement { - $connection = $this->getWrappedConnection(); + $connection = $this->connect(); try { $statement = $connection->prepare($sql); @@ -1066,36 +771,27 @@ public function prepare(string $sql): Statement * Executes an, optionally parameterized, SQL query. * * If the query is parametrized, a prepared statement is used. - * If an SQLLogger is configured, the execution is logged. * - * @param string $sql SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @throws Exception */ public function executeQuery( string $sql, array $params = [], - $types = [], - ?QueryCacheProfile $qcp = null + array $types = [], + ?QueryCacheProfile $qcp = null, ): Result { if ($qcp !== null) { return $this->executeCacheQuery($sql, $params, $types, $qcp); } - $connection = $this->getWrappedConnection(); - - $logger = $this->_config->getSQLLogger(); - if ($logger !== null) { - $logger->startQuery($sql, $params, $types); - } + $connection = $this->connect(); try { if (count($params) > 0) { - if ($this->needsArrayParameterConversion($params, $types)) { - [$sql, $params, $types] = $this->expandArrayParameters($sql, $params, $types); - } + [$sql, $params, $types] = $this->expandArrayParameters($sql, $params, $types); $stmt = $connection->prepare($sql); @@ -1109,36 +805,32 @@ public function executeQuery( return new Result($result, $this); } catch (Driver\Exception $e) { throw $this->convertExceptionDuringQuery($e, $sql, $params, $types); - } finally { - if ($logger !== null) { - $logger->stopQuery(); - } } } /** * Executes a caching query. * - * @param string $sql SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @throws CacheException * @throws Exception */ - public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp): Result + public function executeCacheQuery(string $sql, array $params, array $types, QueryCacheProfile $qcp): Result { $resultCache = $qcp->getResultCache() ?? $this->_config->getResultCache(); if ($resultCache === null) { - throw CacheException::noResultDriverConfigured(); + throw NoResultDriverConfigured::new(); } $connectionParams = $this->params; - unset($connectionParams['platform'], $connectionParams['password'], $connectionParams['url']); + unset($connectionParams['password']); [$cacheKey, $realKey] = $qcp->generateCacheKeys($sql, $params, $types, $connectionParams); + // @phpstan-ignore missingType.checkedException $item = $resultCache->getItem($cacheKey); if ($item->isHit()) { @@ -1147,16 +839,23 @@ public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp) $value = []; } - if (isset($value[$realKey])) { - return new Result(new ArrayResult($value[$realKey]), $this); + if (isset($value[$realKey]) && $value[$realKey] instanceof ArrayResult) { + return new Result(clone $value[$realKey], $this); } } else { $value = []; } - $data = $this->fetchAllAssociative($sql, $params, $types); + $result = $this->executeQuery($sql, $params, $types); + + $columnNames = []; + for ($i = 0; $i < $result->columnCount(); $i++) { + $columnNames[] = $result->getColumnName($i); + } - $value[$realKey] = $data; + $rows = $result->fetchAllNumeric(); + + $value[$realKey] = new ArrayResult($columnNames, $rows); $item->set($value); @@ -1167,7 +866,7 @@ public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp) $resultCache->save($item); - return new Result(new ArrayResult($data), $this); + return new Result(clone $value[$realKey], $this); } /** @@ -1182,28 +881,20 @@ public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp) * * This method supports PDO binding types as well as DBAL mapping types. * - * @param string $sql SQL statement - * @param list|array $params Statement parameters - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * - * @return int|string The number of affected rows. + * @return int|numeric-string * * @throws Exception */ - public function executeStatement($sql, array $params = [], array $types = []) + public function executeStatement(string $sql, array $params = [], array $types = []): int|string { - $connection = $this->getWrappedConnection(); - - $logger = $this->_config->getSQLLogger(); - if ($logger !== null) { - $logger->startQuery($sql, $params, $types); - } + $connection = $this->connect(); try { if (count($params) > 0) { - if ($this->needsArrayParameterConversion($params, $types)) { - [$sql, $params, $types] = $this->expandArrayParameters($sql, $params, $types); - } + [$sql, $params, $types] = $this->expandArrayParameters($sql, $params, $types); $stmt = $connection->prepare($sql); @@ -1216,10 +907,6 @@ public function executeStatement($sql, array $params = [], array $types = []) return $connection->exec($sql); } catch (Driver\Exception $e) { throw $this->convertExceptionDuringQuery($e, $sql, $params, $types); - } finally { - if ($logger !== null) { - $logger->stopQuery(); - } } } @@ -1228,37 +915,22 @@ public function executeStatement($sql, array $params = [], array $types = []) * * @return int The nesting level. A value of 0 means there's no active transaction. */ - public function getTransactionNestingLevel() + public function getTransactionNestingLevel(): int { return $this->transactionNestingLevel; } /** - * Returns the ID of the last inserted row, or the last value from a sequence object, - * depending on the underlying driver. + * Returns the ID of the last inserted row. * - * Note: This method may not return a meaningful or consistent result across different drivers, - * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY - * columns or sequences. - * - * @param string|null $name Name of the sequence object from which the ID should be returned. - * - * @return string|int|false A string representation of the last inserted ID. + * If the underlying driver does not support identity columns, an exception is thrown. * * @throws Exception */ - public function lastInsertId($name = null) + public function lastInsertId(): int|string { - if ($name !== null) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4687', - 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', - ); - } - try { - return $this->getWrappedConnection()->lastInsertId($name); + return $this->connect()->lastInsertId(); } catch (Driver\Exception $e) { throw $this->convertException($e); } @@ -1280,7 +952,7 @@ public function lastInsertId($name = null) * * @template T */ - public function transactional(Closure $func) + public function transactional(Closure $func): mixed { $this->beginTransaction(); @@ -1290,8 +962,11 @@ public function transactional(Closure $func) $res = $func($this); $successful = true; + } catch (ConnectionLost $connectionLost) { + // Catching here only to be able to prevent a rollback attempt + throw $connectionLost; } finally { - if (! $successful) { + if (! isset($connectionLost) && ! $successful) { $this->rollBack(); } } @@ -1302,12 +977,12 @@ public function transactional(Closure $func) $shouldRollback = false; } catch (TheDriverException $t) { - $convertedException = $this->handleDriverException($t, null); - $shouldRollback = ! ( - $convertedException instanceof TransactionRolledBack - || $convertedException instanceof UniqueConstraintViolationException - || $convertedException instanceof ForeignKeyConstraintViolationException - || $convertedException instanceof DeadlockException + $shouldRollback = ! ( + $t instanceof TransactionRolledBack + || $t instanceof UniqueConstraintViolationException + || $t instanceof ForeignKeyConstraintViolationException + || $t instanceof DeadlockException + || $t instanceof ConnectionLost ); throw $t; @@ -1323,189 +998,108 @@ public function transactional(Closure $func) /** * Sets if nested transactions should use savepoints. * - * @param bool $nestTransactionsWithSavepoints - * - * @return void + * @deprecated No replacement planned * * @throws Exception */ - public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints) + public function setNestTransactionsWithSavepoints(bool $nestTransactionsWithSavepoints): void { if (! $nestTransactionsWithSavepoints) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5383', - <<<'DEPRECATION' - Nesting transactions without enabling savepoints is deprecated. - Call %s::setNestTransactionsWithSavepoints(true) to enable savepoints. - DEPRECATION, - self::class, - ); - } - - if ($this->transactionNestingLevel > 0) { - throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction(); + throw new InvalidArgumentException(sprintf( + 'Calling %s with false to enable nesting transactions without savepoints is no longer supported.', + __METHOD__, + )); } - $this->nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints; + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5383', + '%s is deprecated and will be removed in 5.0', + __METHOD__, + ); } /** * Gets if nested transactions should use savepoints. * - * @return bool + * @deprecated No replacement planned */ - public function getNestTransactionsWithSavepoints() + public function getNestTransactionsWithSavepoints(): bool { - return $this->nestTransactionsWithSavepoints; + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5383', + '%s is deprecated and will be removed in 5.0', + __METHOD__, + ); + + return true; } /** * Returns the savepoint name to use for nested transactions. - * - * @return string */ - protected function _getNestedTransactionSavePointName() + protected function _getNestedTransactionSavePointName(): string { return 'DOCTRINE_' . $this->transactionNestingLevel; } - /** - * @return bool - * - * @throws Exception - */ - public function beginTransaction() + /** @throws Exception */ + public function beginTransaction(): void { - $connection = $this->getWrappedConnection(); + $connection = $this->connect(); ++$this->transactionNestingLevel; - $logger = $this->_config->getSQLLogger(); - if ($this->transactionNestingLevel === 1) { - if ($logger !== null) { - $logger->startQuery('"START TRANSACTION"'); - } - - $connection->beginTransaction(); - - if ($logger !== null) { - $logger->stopQuery(); - } - } elseif ($this->nestTransactionsWithSavepoints) { - if ($logger !== null) { - $logger->startQuery('"SAVEPOINT"'); - } - - $this->createSavepoint($this->_getNestedTransactionSavePointName()); - if ($logger !== null) { - $logger->stopQuery(); + try { + $connection->beginTransaction(); + } catch (Driver\Exception $e) { + throw $this->convertException($e); } } else { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5383', - <<<'DEPRECATION' - Nesting transactions without enabling savepoints is deprecated. - Call %s::setNestTransactionsWithSavepoints(true) to enable savepoints. - DEPRECATION, - self::class, - ); - } - - $eventManager = $this->getEventManager(); - - if ($eventManager->hasListeners(Events::onTransactionBegin)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated.', - Events::onTransactionBegin, - ); - - $eventManager->dispatchEvent(Events::onTransactionBegin, new TransactionBeginEventArgs($this)); + $this->createSavepoint($this->_getNestedTransactionSavePointName()); } - - return true; } - /** - * @return bool - * - * @throws Exception - */ - public function commit() + /** @throws Exception */ + public function commit(): void { if ($this->transactionNestingLevel === 0) { - throw ConnectionException::noActiveTransaction(); + throw NoActiveTransaction::new(); } if ($this->isRollbackOnly) { - throw ConnectionException::commitFailedRollbackOnly(); + throw CommitFailedRollbackOnly::new(); } - $result = true; - - $connection = $this->getWrappedConnection(); + $connection = $this->connect(); try { if ($this->transactionNestingLevel === 1) { - $result = $this->doCommit($connection); - } elseif ($this->nestTransactionsWithSavepoints) { + try { + $connection->commit(); + } catch (Driver\Exception $e) { + throw $this->convertException($e); + } + } else { $this->releaseSavepoint($this->_getNestedTransactionSavePointName()); } } finally { $this->updateTransactionStateAfterCommit(); } - - return $result; } + /** @throws Exception */ private function updateTransactionStateAfterCommit(): void { - --$this->transactionNestingLevel; - - $eventManager = $this->getEventManager(); - - if ($eventManager->hasListeners(Events::onTransactionCommit)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated.', - Events::onTransactionCommit, - ); - - $eventManager->dispatchEvent(Events::onTransactionCommit, new TransactionCommitEventArgs($this)); - } - - if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) { - return; - } - - $this->beginTransaction(); - } - - /** - * @return bool - * - * @throws DriverException - */ - private function doCommit(DriverConnection $connection) - { - $logger = $this->_config->getSQLLogger(); - - if ($logger !== null) { - $logger->startQuery('"COMMIT"'); + if ($this->transactionNestingLevel !== 0) { + --$this->transactionNestingLevel; } - $result = $connection->commit(); - - if ($logger !== null) { - $logger->stopQuery(); + if ($this->autoCommit === false && $this->transactionNestingLevel === 0) { + $this->beginTransaction(); } - - return $result; } /** @@ -1528,67 +1122,33 @@ private function commitAll(): void } } - /** - * Cancels any database changes done during the current transaction. - * - * @return bool - * - * @throws Exception - */ - public function rollBack() + /** @throws Exception */ + public function rollBack(): void { if ($this->transactionNestingLevel === 0) { - throw ConnectionException::noActiveTransaction(); + throw NoActiveTransaction::new(); } - $connection = $this->getWrappedConnection(); - - $logger = $this->_config->getSQLLogger(); + $connection = $this->connect(); if ($this->transactionNestingLevel === 1) { - if ($logger !== null) { - $logger->startQuery('"ROLLBACK"'); - } - $this->transactionNestingLevel = 0; - $connection->rollBack(); - $this->isRollbackOnly = false; - if ($logger !== null) { - $logger->stopQuery(); - } - if ($this->autoCommit === false) { - $this->beginTransaction(); - } - } elseif ($this->nestTransactionsWithSavepoints) { - if ($logger !== null) { - $logger->startQuery('"ROLLBACK TO SAVEPOINT"'); - } + try { + $connection->rollBack(); + } catch (Driver\Exception $e) { + throw $this->convertException($e); + } finally { + $this->isRollbackOnly = false; - $this->rollbackSavepoint($this->_getNestedTransactionSavePointName()); - --$this->transactionNestingLevel; - if ($logger !== null) { - $logger->stopQuery(); + if ($this->autoCommit === false) { + $this->beginTransaction(); + } } } else { - $this->isRollbackOnly = true; + $this->rollbackSavepoint($this->_getNestedTransactionSavePointName()); --$this->transactionNestingLevel; } - - $eventManager = $this->getEventManager(); - - if ($eventManager->hasListeners(Events::onTransactionRollBack)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated.', - Events::onTransactionRollBack, - ); - - $eventManager->dispatchEvent(Events::onTransactionRollBack, new TransactionRollBackEventArgs($this)); - } - - return true; } /** @@ -1596,16 +1156,14 @@ public function rollBack() * * @param string $savepoint The name of the savepoint to create. * - * @return void - * * @throws Exception */ - public function createSavepoint($savepoint) + public function createSavepoint(string $savepoint): void { $platform = $this->getDatabasePlatform(); if (! $platform->supportsSavepoints()) { - throw ConnectionException::savepointsNotSupported(); + throw SavepointsNotSupported::new(); } $this->executeStatement($platform->createSavePoint($savepoint)); @@ -1616,39 +1174,19 @@ public function createSavepoint($savepoint) * * @param string $savepoint The name of the savepoint to release. * - * @return void - * * @throws Exception */ - public function releaseSavepoint($savepoint) + public function releaseSavepoint(string $savepoint): void { - $logger = $this->_config->getSQLLogger(); - $platform = $this->getDatabasePlatform(); if (! $platform->supportsSavepoints()) { - throw ConnectionException::savepointsNotSupported(); + throw SavepointsNotSupported::new(); } - if (! $platform->supportsReleaseSavepoints()) { - if ($logger !== null) { - $logger->stopQuery(); - } - - return; + if ($platform->supportsReleaseSavepoints()) { + $this->executeStatement($platform->releaseSavePoint($savepoint)); } - - if ($logger !== null) { - $logger->startQuery('"RELEASE SAVEPOINT"'); - } - - $this->executeStatement($platform->releaseSavePoint($savepoint)); - - if ($logger === null) { - return; - } - - $logger->stopQuery(); } /** @@ -1656,57 +1194,29 @@ public function releaseSavepoint($savepoint) * * @param string $savepoint The name of the savepoint to rollback to. * - * @return void - * * @throws Exception */ - public function rollbackSavepoint($savepoint) + public function rollbackSavepoint(string $savepoint): void { $platform = $this->getDatabasePlatform(); if (! $platform->supportsSavepoints()) { - throw ConnectionException::savepointsNotSupported(); + throw SavepointsNotSupported::new(); } $this->executeStatement($platform->rollbackSavePoint($savepoint)); } /** - * Gets the wrapped driver connection. + * Provides access to the native database connection. * - * @deprecated Use {@link getNativeConnection()} to access the native connection. - * - * @return DriverConnection + * @return resource|object * * @throws Exception */ - public function getWrappedConnection() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4966', - 'Connection::getWrappedConnection() is deprecated.' - . ' Use Connection::getNativeConnection() to access the native connection.', - ); - - $this->connect(); - - return $this->_conn; - } - - /** @return resource|object */ public function getNativeConnection() { - $this->connect(); - - if (! method_exists($this->_conn, 'getNativeConnection')) { - throw new LogicException(sprintf( - 'The driver connection %s does not support accessing the native connection.', - get_class($this->_conn), - )); - } - - return $this->_conn->getNativeConnection(); + return $this->connect()->getNativeConnection(); } /** @@ -1720,39 +1230,16 @@ public function createSchemaManager(): AbstractSchemaManager return $this->schemaManagerFactory->createSchemaManager($this); } - /** - * Gets the SchemaManager that can be used to inspect or change the - * database schema through the connection. - * - * @deprecated Use {@see createSchemaManager()} instead. - * - * @return AbstractSchemaManager - * - * @throws Exception - */ - public function getSchemaManager() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4515', - 'Connection::getSchemaManager() is deprecated, use Connection::createSchemaManager() instead.', - ); - - return $this->_schemaManager ??= $this->createSchemaManager(); - } - /** * Marks the current transaction so that the only possible * outcome for the transaction to be rolled back. * - * @return void - * * @throws ConnectionException If no transaction is active. */ - public function setRollbackOnly() + public function setRollbackOnly(): void { if ($this->transactionNestingLevel === 0) { - throw ConnectionException::noActiveTransaction(); + throw NoActiveTransaction::new(); } $this->isRollbackOnly = true; @@ -1761,14 +1248,12 @@ public function setRollbackOnly() /** * Checks whether the current transaction is marked for rollback only. * - * @return bool - * * @throws ConnectionException If no transaction is active. */ - public function isRollbackOnly() + public function isRollbackOnly(): bool { if ($this->transactionNestingLevel === 0) { - throw ConnectionException::noActiveTransaction(); + throw NoActiveTransaction::new(); } return $this->isRollbackOnly; @@ -1785,7 +1270,7 @@ public function isRollbackOnly() * * @throws Exception */ - public function convertToDatabaseValue($value, $type) + public function convertToDatabaseValue(mixed $value, string $type): mixed { return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform()); } @@ -1801,7 +1286,7 @@ public function convertToDatabaseValue($value, $type) * * @throws Exception */ - public function convertToPHPValue($value, $type) + public function convertToPHPValue(mixed $value, string $type): mixed { return Type::getType($type)->convertToPHPValue($value, $this->getDatabasePlatform()); } @@ -1810,9 +1295,8 @@ public function convertToPHPValue($value, $type) * Binds a set of parameters, some or all of which are typed with a PDO binding type * or DBAL mapping type, to a given statement. * - * @param DriverStatement $stmt Prepared statement - * @param list|array $params Statement parameters - * @param array|array $types Parameter types + * @param list|array $params + * @param array|array $types * * @throws Exception */ @@ -1823,46 +1307,36 @@ private function bindParameters(DriverStatement $stmt, array $params, array $typ $bindIndex = 1; foreach ($params as $key => $value) { - if (isset($types[$key])) { + if (array_key_exists($key, $types)) { $type = $types[$key]; [$value, $bindingType] = $this->getBindingInfo($value, $type); } else { - if (array_key_exists($key, $types)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5550', - 'Using NULL as prepared statement parameter type is deprecated.' - . 'Omit or use ParameterType::STRING instead', - ); - } - $bindingType = ParameterType::STRING; } - $stmt->bindValue($bindIndex, $value, $bindingType); + try { + $stmt->bindValue($bindIndex, $value, $bindingType); + } catch (Driver\Exception $e) { + throw $this->convertException($e); + } ++$bindIndex; } } else { // Named parameters foreach ($params as $name => $value) { - if (isset($types[$name])) { + if (array_key_exists($name, $types)) { $type = $types[$name]; [$value, $bindingType] = $this->getBindingInfo($value, $type); } else { - if (array_key_exists($name, $types)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5550', - 'Using NULL as prepared statement parameter type is deprecated.' - . 'Omit or use ParameterType::STRING instead', - ); - } - $bindingType = ParameterType::STRING; } - $stmt->bindValue($name, $value, $bindingType); + try { + $stmt->bindValue($name, $value, $bindingType); + } catch (Driver\Exception $e) { + throw $this->convertException($e); + } } } } @@ -1870,14 +1344,14 @@ private function bindParameters(DriverStatement $stmt, array $params, array $typ /** * Gets the binding type of a given type. * - * @param mixed $value The value to bind. - * @param int|string|Type|null $type The type to bind (PDO or DBAL). + * @param mixed $value The value to bind. + * @param string|ParameterType|Type $type The type to bind. * - * @return array{mixed, int} [0] => the (escaped) value, [1] => the binding type. + * @return array{mixed, ParameterType} [0] => the (escaped) value, [1] => the binding type. * * @throws Exception */ - private function getBindingInfo($value, $type): array + private function getBindingInfo(mixed $value, string|ParameterType|Type $type): array { if (is_string($type)) { $type = Type::getType($type); @@ -1887,7 +1361,7 @@ private function getBindingInfo($value, $type): array $value = $type->convertToDatabaseValue($value, $this->getDatabasePlatform()); $bindingType = $type->getBindingType(); } else { - $bindingType = $type ?? ParameterType::STRING; + $bindingType = $type; } return [$value, $bindingType]; @@ -1895,10 +1369,8 @@ private function getBindingInfo($value, $type): array /** * Creates a new instance of a SQL query builder. - * - * @return QueryBuilder */ - public function createQueryBuilder() + public function createQueryBuilder(): QueryBuilder { return new Query\QueryBuilder($this); } @@ -1906,14 +1378,14 @@ public function createQueryBuilder() /** * @internal * - * @param list|array $params - * @param array|array $types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types */ final public function convertExceptionDuringQuery( Driver\Exception $e, string $sql, array $params = [], - array $types = [] + array $types = [], ): DriverException { return $this->handleDriverException($e, new Query($sql, $params, $types)); } @@ -1925,17 +1397,47 @@ final public function convertException(Driver\Exception $e): DriverException } /** - * @param array|array $params - * @param array|array $types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * - * @return array{string, list, array} + * @return array{ + * string, + * list|array, + * array, string|ParameterType|Type>|array + * } + * + * @throws Exception */ private function expandArrayParameters(string $sql, array $params, array $types): array { + $needsConversion = false; + $nonArrayTypes = []; + + if (is_string(key($params))) { + $needsConversion = true; + } else { + foreach ($types as $key => $type) { + if ($type instanceof ArrayParameterType) { + $needsConversion = true; + break; + } + + $nonArrayTypes[$key] = $type; + } + } + + if (! $needsConversion) { + return [$sql, $params, $nonArrayTypes]; + } + $this->parser ??= $this->getDatabasePlatform()->createSQLParser(); $visitor = new ExpandArrayParameters($params, $types); - $this->parser->parse($sql, $visitor); + try { + $this->parser->parse($sql, $visitor); + } catch (Parser\Exception $e) { + throw ParseError::fromParserException($e); + } return [ $visitor->getSQL(), @@ -1944,35 +1446,11 @@ private function expandArrayParameters(string $sql, array $params, array $types) ]; } - /** - * @param array|array $params - * @param array|array $types - */ - private function needsArrayParameterConversion(array $params, array $types): bool - { - if (is_string(key($params))) { - return true; - } - - foreach ($types as $type) { - if ( - $type === ArrayParameterType::INTEGER - || $type === ArrayParameterType::STRING - || $type === ArrayParameterType::ASCII - || $type === ArrayParameterType::BINARY - ) { - return true; - } - } - - return false; - } - private function handleDriverException( Driver\Exception $driverException, - ?Query $query + ?Query $query, ): DriverException { - $this->exceptionConverter ??= $this->_driver->getExceptionConverter(); + $this->exceptionConverter ??= $this->driver->getExceptionConverter(); $exception = $this->exceptionConverter->convert($driverException, $query); if ($exception instanceof ConnectionLost) { @@ -1981,58 +1459,4 @@ private function handleDriverException( return $exception; } - - /** - * BC layer for a wide-spread use-case of old DBAL APIs - * - * @deprecated Use {@see executeStatement()} instead - * - * @param array $params The query parameters - * @param array $types The parameter types - */ - public function executeUpdate(string $sql, array $params = [], array $types = []): int - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4163', - '%s is deprecated, please use executeStatement() instead.', - __METHOD__, - ); - - return $this->executeStatement($sql, $params, $types); - } - - /** - * BC layer for a wide-spread use-case of old DBAL APIs - * - * @deprecated Use {@see executeQuery()} instead - */ - public function query(string $sql): Result - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4163', - '%s is deprecated, please use executeQuery() instead.', - __METHOD__, - ); - - return $this->executeQuery($sql); - } - - /** - * BC layer for a wide-spread use-case of old DBAL APIs - * - * @deprecated please use {@see executeStatement()} instead - */ - public function exec(string $sql): int - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4163', - '%s is deprecated, please use executeStatement() instead.', - __METHOD__, - ); - - return $this->executeStatement($sql); - } } diff --git a/doctrine/dbal/src/Connection/StaticServerVersionProvider.php b/doctrine/dbal/src/Connection/StaticServerVersionProvider.php new file mode 100644 index 000000000..9e0e4e255 --- /dev/null +++ b/doctrine/dbal/src/Connection/StaticServerVersionProvider.php @@ -0,0 +1,20 @@ +version; + } +} diff --git a/doctrine/dbal/src/ConnectionException.php b/doctrine/dbal/src/ConnectionException.php index 49d9efe41..bb11a23c2 100644 --- a/doctrine/dbal/src/ConnectionException.php +++ b/doctrine/dbal/src/ConnectionException.php @@ -1,30 +1,9 @@ */ - protected $connections = ['primary' => null, 'replica' => null]; + protected array $connections = ['primary' => null, 'replica' => null]; /** * You can keep the replica connection and then switch back to it * during the request if you know what you are doing. - * - * @var bool */ - protected $keepReplica = false; + protected bool $keepReplica = false; /** * Creates Primary Replica Connection. * * @internal The connection can be only instantiated by the driver manager. * - * @param array $params + * @param array $params * @phpstan-param Params $params - * - * @throws Exception - * @throws InvalidArgumentException */ - public function __construct( - array $params, - Driver $driver, - ?Configuration $config = null, - ?EventManager $eventManager = null - ) { + public function __construct(array $params, Driver $driver, ?Configuration $config = null) + { if (! isset($params['replica'], $params['primary'])) { throw new InvalidArgumentException('primary or replica configuration missing'); } @@ -125,9 +116,9 @@ public function __construct( } } - $this->keepReplica = (bool) ($params['keepReplica'] ?? false); + $this->keepReplica = ! empty($params['keepReplica']); - parent::__construct($params, $driver, $config, $eventManager); + parent::__construct($params, $driver, $config); } /** @@ -138,12 +129,7 @@ public function isConnectedToPrimary(): bool return $this->_conn !== null && $this->_conn === $this->connections['primary']; } - /** - * @param string|null $connectionName - * - * @return bool - */ - public function connect($connectionName = null) + public function connect(?string $connectionName = null): DriverConnection { if ($connectionName !== null) { throw new InvalidArgumentException( @@ -155,10 +141,11 @@ public function connect($connectionName = null) return $this->performConnect(); } - protected function performConnect(?string $connectionName = null): bool + /** @throws Exception */ + protected function performConnect(?string $connectionName = null): DriverConnection { $requestedConnectionChange = ($connectionName !== null); - $connectionName = $connectionName ?? 'replica'; + $connectionName ??= 'replica'; if ($connectionName !== 'replica' && $connectionName !== 'primary') { throw new InvalidArgumentException('Invalid option to connect(), only primary or replica allowed.'); @@ -168,7 +155,7 @@ protected function performConnect(?string $connectionName = null): bool // change request, then abort right here, because we are already done. // This prevents writes to the replica in case of "keepReplica" option enabled. if ($this->_conn !== null && ! $requestedConnectionChange) { - return false; + return $this->_conn; } $forcePrimaryAsReplica = false; @@ -185,7 +172,7 @@ protected function performConnect(?string $connectionName = null): bool $this->connections['replica'] = $this->_conn; } - return false; + return $this->_conn; } if ($connectionName === 'primary') { @@ -199,29 +186,19 @@ protected function performConnect(?string $connectionName = null): bool $this->connections['replica'] = $this->_conn = $this->connectTo($connectionName); } - if ($this->_eventManager->hasListeners(Events::postConnect)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated. Implement a middleware instead.', - Events::postConnect, - ); - - $eventArgs = new ConnectionEventArgs($this); - $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs); - } - - return true; + return $this->_conn; } /** * Connects to the primary node of the database cluster. * * All following statements after this will be executed against the primary node. + * + * @throws Exception */ - public function ensureConnectedToPrimary(): bool + public function ensureConnectedToPrimary(): void { - return $this->performConnect('primary'); + $this->performConnect('primary'); } /** @@ -230,102 +207,92 @@ public function ensureConnectedToPrimary(): bool * All following statements after this will be executed against the replica node, * unless the keepReplica option is set to false and a primary connection * was already opened. + * + * @throws Exception */ - public function ensureConnectedToReplica(): bool + public function ensureConnectedToReplica(): void { - return $this->performConnect('replica'); + $this->performConnect('replica'); } /** * Connects to a specific connection. * - * @param string $connectionName - * - * @return DriverConnection - * * @throws Exception */ - protected function connectTo($connectionName) + protected function connectTo(string $connectionName): DriverConnection { $params = $this->getParams(); + assert(isset($params['primary'])); - $connectionParams = $this->chooseConnectionConfiguration($connectionName, $params); + if ($connectionName === 'primary') { + $connectionParams = $params['primary']; + } else { + assert(isset($params['replica'])); + $connectionParams = $this->chooseReplicaConnectionParameters($params['primary'], $params['replica']); + } try { - return $this->_driver->connect($connectionParams); + return $this->driver->connect($connectionParams); } catch (DriverException $e) { throw $this->convertException($e); } } /** - * @param string $connectionName - * @param mixed[] $params + * @param OverrideParams $primary + * @param array $replicas * - * @return mixed + * @return array + * @phpstan-return OverrideParams */ - protected function chooseConnectionConfiguration( - $connectionName, + protected function chooseReplicaConnectionParameters( #[SensitiveParameter] - $params - ) { - if ($connectionName === 'primary') { - return $params['primary']; - } - - $config = $params['replica'][array_rand($params['replica'])]; + array $primary, + #[SensitiveParameter] + array $replicas, + ): array { + $params = $replicas[array_rand($replicas)]; - if (! isset($config['charset']) && isset($params['primary']['charset'])) { - $config['charset'] = $params['primary']['charset']; + if (! isset($params['charset']) && isset($primary['charset'])) { + $params['charset'] = $primary['charset']; } - return $config; + return $params; } /** * {@inheritDoc} */ - public function executeStatement($sql, array $params = [], array $types = []) + public function executeStatement(string $sql, array $params = [], array $types = []): int|string { $this->ensureConnectedToPrimary(); return parent::executeStatement($sql, $params, $types); } - /** - * {@inheritDoc} - */ - public function beginTransaction() + public function beginTransaction(): void { $this->ensureConnectedToPrimary(); - return parent::beginTransaction(); + parent::beginTransaction(); } - /** - * {@inheritDoc} - */ - public function commit() + public function commit(): void { $this->ensureConnectedToPrimary(); - return parent::commit(); + parent::commit(); } - /** - * {@inheritDoc} - */ - public function rollBack() + public function rollBack(): void { $this->ensureConnectedToPrimary(); - return parent::rollBack(); + parent::rollBack(); } - /** - * {@inheritDoc} - */ - public function close() + public function close(): void { unset($this->connections['primary'], $this->connections['replica']); @@ -335,30 +302,21 @@ public function close() $this->connections = ['primary' => null, 'replica' => null]; } - /** - * {@inheritDoc} - */ - public function createSavepoint($savepoint) + public function createSavepoint(string $savepoint): void { $this->ensureConnectedToPrimary(); parent::createSavepoint($savepoint); } - /** - * {@inheritDoc} - */ - public function releaseSavepoint($savepoint) + public function releaseSavepoint(string $savepoint): void { $this->ensureConnectedToPrimary(); parent::releaseSavepoint($savepoint); } - /** - * {@inheritDoc} - */ - public function rollbackSavepoint($savepoint) + public function rollbackSavepoint(string $savepoint): void { $this->ensureConnectedToPrimary(); diff --git a/doctrine/dbal/src/Driver.php b/doctrine/dbal/src/Driver.php index a376f069b..0b8bd16bf 100644 --- a/doctrine/dbal/src/Driver.php +++ b/doctrine/dbal/src/Driver.php @@ -1,12 +1,14 @@ getCode()) { - case -104: - return new SyntaxErrorException($exception, $query); - - case -203: - return new NonUniqueFieldNameException($exception, $query); - - case -204: - return new TableNotFoundException($exception, $query); - - case -206: - return new InvalidFieldNameException($exception, $query); - - case -407: - return new NotNullConstraintViolationException($exception, $query); - - case -530: - case -531: - case -532: - case -20356: - return new ForeignKeyConstraintViolationException($exception, $query); - - case -601: - return new TableExistsException($exception, $query); - - case -803: - return new UniqueConstraintViolationException($exception, $query); - - case -1336: - case -30082: - return new ConnectionException($exception, $query); - } - - return new DriverException($exception, $query); + return match ($exception->getCode()) { + -104 => new SyntaxErrorException($exception, $query), + -203 => new NonUniqueFieldNameException($exception, $query), + -204 => new TableNotFoundException($exception, $query), + -206 => new InvalidFieldNameException($exception, $query), + -407 => new NotNullConstraintViolationException($exception, $query), + -530, + -531, + -532, + -20356 => new ForeignKeyConstraintViolationException($exception, $query), + -601 => new TableExistsException($exception, $query), + -803 => new UniqueConstraintViolationException($exception, $query), + -1336, + -30082 => new ConnectionException($exception, $query), + default => new DriverException($exception, $query), + }; } } diff --git a/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php b/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php index 10507afd7..56e03d486 100644 --- a/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php +++ b/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php @@ -22,7 +22,7 @@ use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\DBAL\Query; -use function strpos; +use function str_contains; /** @internal */ final class ExceptionConverter implements ExceptionConverterInterface @@ -33,99 +33,73 @@ final class ExceptionConverter implements ExceptionConverterInterface */ public function convert(Exception $exception, ?Query $query): DriverException { - switch ($exception->getCode()) { - case 1008: - return new DatabaseDoesNotExist($exception, $query); - - case 1213: - return new DeadlockException($exception, $query); - - case 1205: - return new LockWaitTimeoutException($exception, $query); - - case 1050: - return new TableExistsException($exception, $query); - - case 1051: - case 1146: - return new TableNotFoundException($exception, $query); - - case 1216: - case 1217: - case 1451: - case 1452: - case 1701: - return new ForeignKeyConstraintViolationException($exception, $query); - - case 1062: - case 1557: - case 1569: - case 1586: - return new UniqueConstraintViolationException($exception, $query); - - case 1054: - case 1166: - case 1611: - return new InvalidFieldNameException($exception, $query); - - case 1052: - case 1060: - case 1110: - return new NonUniqueFieldNameException($exception, $query); - - case 1064: - case 1149: - case 1287: - case 1341: - case 1342: - case 1343: - case 1344: - case 1382: - case 1479: - case 1541: - case 1554: - case 1626: - return new SyntaxErrorException($exception, $query); - - case 1524: - if (strpos($exception->getMessage(), 'Plugin \'mysql_native_password\' is not loaded') === false) { - break; - } - - // Workaround for MySQL 8.4 if we request an unknown user. - // https://bugs.mysql.com/bug.php?id=114876 - return new ConnectionException($exception, $query); - - case 1044: - case 1045: - case 1046: - case 1049: - case 1095: - case 1142: - case 1143: - case 1227: - case 1370: - case 1429: - case 2002: - case 2005: - case 2054: - return new ConnectionException($exception, $query); - - case 2006: - case 4031: - return new ConnectionLost($exception, $query); - - case 1048: - case 1121: - case 1138: - case 1171: - case 1252: - case 1263: - case 1364: - case 1566: - return new NotNullConstraintViolationException($exception, $query); + if ( + $exception->getCode() === 1524 + && str_contains($exception->getMessage(), 'Plugin \'mysql_native_password\' is not loaded') + ) { + // Workaround for MySQL 8.4 if we request an unknown user. + // https://bugs.mysql.com/bug.php?id=114876 + return new ConnectionException($exception, $query); } - return new DriverException($exception, $query); + return match ($exception->getCode()) { + 1008 => new DatabaseDoesNotExist($exception, $query), + 1213 => new DeadlockException($exception, $query), + 1205 => new LockWaitTimeoutException($exception, $query), + 1050 => new TableExistsException($exception, $query), + 1051, + 1146 => new TableNotFoundException($exception, $query), + 1216, + 1217, + 1451, + 1452, + 1701 => new ForeignKeyConstraintViolationException($exception, $query), + 1062, + 1557, + 1569, + 1586 => new UniqueConstraintViolationException($exception, $query), + 1054, + 1166, + 1611 => new InvalidFieldNameException($exception, $query), + 1052, + 1060, + 1110 => new NonUniqueFieldNameException($exception, $query), + 1064, + 1149, + 1287, + 1341, + 1342, + 1343, + 1344, + 1382, + 1479, + 1541, + 1554, + 1626 => new SyntaxErrorException($exception, $query), + 1044, + 1045, + 1046, + 1049, + 1095, + 1142, + 1143, + 1227, + 1370, + 1429, + 2002, + 2005, + 2054 => new ConnectionException($exception, $query), + 2006, + 4031 => new ConnectionLost($exception, $query), + 1048, + 1121, + 1138, + 1171, + 1252, + 1263, + 1364, + 1566 => new NotNullConstraintViolationException($exception, $query), + default => new DriverException($exception, $query), + }; } } diff --git a/doctrine/dbal/src/Driver/API/OCI/ExceptionConverter.php b/doctrine/dbal/src/Driver/API/OCI/ExceptionConverter.php index 62a08a799..9bde91ea9 100644 --- a/doctrine/dbal/src/Driver/API/OCI/ExceptionConverter.php +++ b/doctrine/dbal/src/Driver/API/OCI/ExceptionConverter.php @@ -7,7 +7,7 @@ use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface; use Doctrine\DBAL\Driver\Exception; use Doctrine\DBAL\Driver\OCI8\Exception\Error; -use Doctrine\DBAL\Driver\PDO\PDOException; +use Doctrine\DBAL\Driver\PDO\Exception as DriverPDOException; use Doctrine\DBAL\Exception\ConnectionException; use Doctrine\DBAL\Exception\DatabaseDoesNotExist; use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException; @@ -23,6 +23,8 @@ use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\DBAL\Query; +use function assert; +use function count; use function explode; use function str_replace; @@ -32,75 +34,47 @@ final class ExceptionConverter implements ExceptionConverterInterface /** @link http://www.dba-oracle.com/t_error_code_list.htm */ public function convert(Exception $exception, ?Query $query): DriverException { - /** @phpstan-var int|'HY000' $code */ // @phpstan-ignore varTag.type - $code = $exception->getCode(); - // @phpstan-ignore property.notFound, property.notFound - if ($code === 'HY000' && isset($exception->errorInfo[1], $exception->errorInfo[2])) { - $errorInfo = $exception->errorInfo; - $exception = new PDOException($errorInfo[2], $errorInfo[1]); - $exception->errorInfo = $errorInfo; - $code = $exception->getCode(); - } - - switch ($code) { - case 1: - case 2299: - case 38911: - return new UniqueConstraintViolationException($exception, $query); - - case 904: - return new InvalidFieldNameException($exception, $query); - - case 918: - case 960: - return new NonUniqueFieldNameException($exception, $query); - - case 923: - return new SyntaxErrorException($exception, $query); - - case 942: - return new TableNotFoundException($exception, $query); - - case 955: - return new TableExistsException($exception, $query); - - case 1017: - case 12545: - return new ConnectionException($exception, $query); - - case 1400: - return new NotNullConstraintViolationException($exception, $query); - - case 1918: - return new DatabaseDoesNotExist($exception, $query); - - case 2091: - //ORA-02091: transaction rolled back + return match ($exception->getCode()) { + 1, + 2299, + 38911 => new UniqueConstraintViolationException($exception, $query), + 904 => new InvalidFieldNameException($exception, $query), + 918, + 960 => new NonUniqueFieldNameException($exception, $query), + 923 => new SyntaxErrorException($exception, $query), + 942 => new TableNotFoundException($exception, $query), + 955 => new TableExistsException($exception, $query), + 1017, + 12545 => new ConnectionException($exception, $query), + 1400 => new NotNullConstraintViolationException($exception, $query), + 1918 => new DatabaseDoesNotExist($exception, $query), + 2091 => (function () use ($exception, $query) { + //SQLSTATE[HY000]: General error: 2091 OCITransCommit: ORA-02091: transaction rolled back //ORA-00001: unique constraint (DOCTRINE.GH3423_UNIQUE) violated - [, $causeError] = explode("\n", $exception->getMessage(), 2); + $lines = explode("\n", $exception->getMessage(), 2); + assert(count($lines) >= 2); + + [, $causeError] = $lines; [$causeCode] = explode(': ', $causeError, 2); $code = (int) str_replace('ORA-', '', $causeCode); - if ($exception instanceof PDOException) { - $why = $this->convert(new PDOException($causeError, $code, $exception), $query); + $sqlState = $exception->getSQLState(); + if ($exception instanceof DriverPDOException) { + $why = $this->convert(new DriverPDOException($causeError, $sqlState, $code, $exception), $query); } else { - $why = $this->convert(new Error($causeError, null, $code, $exception), $query); + $why = $this->convert(new Error($causeError, $sqlState, $code, $exception), $query); } return new TransactionRolledBack($why, $query); - - case 2289: - case 2443: - case 4080: - return new DatabaseObjectNotFoundException($exception, $query); - - case 2266: - case 2291: - case 2292: - return new ForeignKeyConstraintViolationException($exception, $query); - } - - return new DriverException($exception, $query); + })(), + 2289, + 2443, + 4080 => new DatabaseObjectNotFoundException($exception, $query), + 2266, + 2291, + 2292 => new ForeignKeyConstraintViolationException($exception, $query), + default => new DriverException($exception, $query), + }; } } diff --git a/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php b/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php index 2baca1ee2..dca9c9eaf 100644 --- a/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php +++ b/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php @@ -7,6 +7,7 @@ use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface; use Doctrine\DBAL\Driver\Exception; use Doctrine\DBAL\Exception\ConnectionException; +use Doctrine\DBAL\Exception\ConnectionLost; use Doctrine\DBAL\Exception\DatabaseDoesNotExist; use Doctrine\DBAL\Exception\DeadlockException; use Doctrine\DBAL\Exception\DriverException; @@ -21,7 +22,7 @@ use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\DBAL\Query; -use function strpos; +use function str_contains; /** @internal */ final class ExceptionConverter implements ExceptionConverterInterface @@ -37,7 +38,7 @@ public function convert(Exception $exception, ?Query $query): DriverException case '0A000': // Foreign key constraint violations during a TRUNCATE operation // are considered "feature not supported" in PostgreSQL. - if (strpos($exception->getMessage(), 'truncate') !== false) { + if (str_contains($exception->getMessage(), 'truncate')) { return new ForeignKeyConstraintViolationException($exception, $query); } @@ -77,11 +78,11 @@ public function convert(Exception $exception, ?Query $query): DriverException return new ConnectionException($exception, $query); } - // Prior to fixing https://bugs.php.net/bug.php?id=64705 (PHP 7.4.10), - // in some cases (mainly connection errors) the PDO exception wouldn't provide a SQLSTATE via its code. - // We have to match against the SQLSTATE in the error message in these cases. - if ($exception->getCode() === 7 && strpos($exception->getMessage(), 'SQLSTATE[08006]') !== false) { - return new ConnectionException($exception, $query); + if ( + str_contains($exception->getMessage(), 'terminating connection') + || str_contains($exception->getMessage(), 'server closed the connection') + ) { + return new ConnectionLost($exception, $query); } return new DriverException($exception, $query); diff --git a/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php b/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php index d0e8e9f46..561e58b96 100644 --- a/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php +++ b/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php @@ -28,42 +28,22 @@ final class ExceptionConverter implements ExceptionConverterInterface { public function convert(Exception $exception, ?Query $query): DriverException { - switch ($exception->getCode()) { - case 102: - return new SyntaxErrorException($exception, $query); - - case 207: - return new InvalidFieldNameException($exception, $query); - - case 208: - return new TableNotFoundException($exception, $query); - - case 209: - return new NonUniqueFieldNameException($exception, $query); - - case 515: - return new NotNullConstraintViolationException($exception, $query); - - case 547: - case 4712: - return new ForeignKeyConstraintViolationException($exception, $query); - - case 2601: - case 2627: - return new UniqueConstraintViolationException($exception, $query); - - case 2714: - return new TableExistsException($exception, $query); - - case 3701: - case 15151: - return new DatabaseObjectNotFoundException($exception, $query); - - case 11001: - case 18456: - return new ConnectionException($exception, $query); - } - - return new DriverException($exception, $query); + return match ($exception->getCode()) { + 102 => new SyntaxErrorException($exception, $query), + 207 => new InvalidFieldNameException($exception, $query), + 208 => new TableNotFoundException($exception, $query), + 209 => new NonUniqueFieldNameException($exception, $query), + 515 => new NotNullConstraintViolationException($exception, $query), + 547, + 4712 => new ForeignKeyConstraintViolationException($exception, $query), + 2601, + 2627 => new UniqueConstraintViolationException($exception, $query), + 2714 => new TableExistsException($exception, $query), + 3701, + 15151 => new DatabaseObjectNotFoundException($exception, $query), + 11001, + 18456 => new ConnectionException($exception, $query), + default => new DriverException($exception, $query), + }; } } diff --git a/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php b/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php index 9e67155ad..58851955f 100644 --- a/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php +++ b/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php @@ -20,7 +20,7 @@ use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\DBAL\Query; -use function strpos; +use function str_contains; /** @internal */ final class ExceptionConverter implements ExceptionConverterInterface @@ -28,55 +28,55 @@ final class ExceptionConverter implements ExceptionConverterInterface /** @link http://www.sqlite.org/c3ref/c_abort.html */ public function convert(Exception $exception, ?Query $query): DriverException { - if (strpos($exception->getMessage(), 'database is locked') !== false) { + if (str_contains($exception->getMessage(), 'database is locked')) { return new LockWaitTimeoutException($exception, $query); } if ( - strpos($exception->getMessage(), 'must be unique') !== false || - strpos($exception->getMessage(), 'is not unique') !== false || - strpos($exception->getMessage(), 'are not unique') !== false || - strpos($exception->getMessage(), 'UNIQUE constraint failed') !== false + str_contains($exception->getMessage(), 'must be unique') || + str_contains($exception->getMessage(), 'is not unique') || + str_contains($exception->getMessage(), 'are not unique') || + str_contains($exception->getMessage(), 'UNIQUE constraint failed') ) { return new UniqueConstraintViolationException($exception, $query); } if ( - strpos($exception->getMessage(), 'may not be NULL') !== false || - strpos($exception->getMessage(), 'NOT NULL constraint failed') !== false + str_contains($exception->getMessage(), 'may not be NULL') || + str_contains($exception->getMessage(), 'NOT NULL constraint failed') ) { return new NotNullConstraintViolationException($exception, $query); } - if (strpos($exception->getMessage(), 'no such table:') !== false) { + if (str_contains($exception->getMessage(), 'no such table:')) { return new TableNotFoundException($exception, $query); } - if (strpos($exception->getMessage(), 'already exists') !== false) { + if (str_contains($exception->getMessage(), 'already exists')) { return new TableExistsException($exception, $query); } - if (strpos($exception->getMessage(), 'has no column named') !== false) { + if (str_contains($exception->getMessage(), 'has no column named')) { return new InvalidFieldNameException($exception, $query); } - if (strpos($exception->getMessage(), 'ambiguous column name') !== false) { + if (str_contains($exception->getMessage(), 'ambiguous column name')) { return new NonUniqueFieldNameException($exception, $query); } - if (strpos($exception->getMessage(), 'syntax error') !== false) { + if (str_contains($exception->getMessage(), 'syntax error')) { return new SyntaxErrorException($exception, $query); } - if (strpos($exception->getMessage(), 'attempt to write a readonly database') !== false) { + if (str_contains($exception->getMessage(), 'attempt to write a readonly database')) { return new ReadOnlyException($exception, $query); } - if (strpos($exception->getMessage(), 'unable to open database file') !== false) { + if (str_contains($exception->getMessage(), 'unable to open database file')) { return new ConnectionException($exception, $query); } - if (strpos($exception->getMessage(), 'FOREIGN KEY constraint failed') !== false) { + if (str_contains($exception->getMessage(), 'FOREIGN KEY constraint failed')) { return new ForeignKeyConstraintViolationException($exception, $query); } diff --git a/doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php b/doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php deleted file mode 100644 index 3779c8bab..000000000 --- a/doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php +++ /dev/null @@ -1,80 +0,0 @@ - ['callback' => [SqlitePlatform::class, 'udfSqrt'], 'numArgs' => 1], - 'mod' => ['callback' => [SqlitePlatform::class, 'udfMod'], 'numArgs' => 2], - 'locate' => ['callback' => [SqlitePlatform::class, 'udfLocate'], 'numArgs' => -1], - ]; - - /** - * @param callable(string, callable, int): bool $callback - * @param array $additionalFunctions - */ - public static function register(callable $callback, array $additionalFunctions = []): void - { - $userDefinedFunctions = array_merge(self::DEFAULT_FUNCTIONS, $additionalFunctions); - - foreach ($userDefinedFunctions as $function => $data) { - $callback($function, $data['callback'], $data['numArgs']); - } - } - - /** - * User-defined function that implements MOD(). - * - * @param int $a - * @param int $b - */ - public static function mod($a, $b): int - { - return $a % $b; - } - - /** - * User-defined function that implements LOCATE(). - * - * @param string $str - * @param string $substr - * @param int $offset - */ - public static function locate($str, $substr, $offset = 0): int - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5749', - 'Relying on DBAL\'s emulated LOCATE() function is deprecated. ' - . 'Use INSTR() or %s::getLocateExpression() instead.', - AbstractPlatform::class, - ); - - // SQL's LOCATE function works on 1-based positions, while PHP's strpos works on 0-based positions. - // So we have to make them compatible if an offset is given. - if ($offset > 0) { - $offset -= 1; - } - - $pos = strpos($str, $substr, $offset); - - if ($pos !== false) { - return $pos + 1; - } - - return 0; - } -} diff --git a/doctrine/dbal/src/Driver/AbstractDB2Driver.php b/doctrine/dbal/src/Driver/AbstractDB2Driver.php index 682ed7a19..9b6b3d82d 100644 --- a/doctrine/dbal/src/Driver/AbstractDB2Driver.php +++ b/doctrine/dbal/src/Driver/AbstractDB2Driver.php @@ -1,100 +1,27 @@ getVersionNumber($version), '11.1', '>=')) { - return new DB2111Platform(); - } - - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5156', - 'Db2 < 11.1 support is deprecated and will be removed in DBAL 4.' - . ' Consider upgrading to Db2 11.1 or later.', - ); - - return $this->getDatabasePlatform(); - } - - /** - * Detects Db2 server version - * - * @param string $versionString Version string as returned by Db2 server, i.e. 'DB2/LINUXX8664 11.5.8.0' - * - * @throws DBALException - */ - private function getVersionNumber(string $versionString): string - { - if ( - preg_match( - '/^(?:[^\s]+\s)?(?P\d+)\.(?P\d+)\.(?P\d+)/i', - $versionString, - $versionParts, - ) !== 1 - ) { - throw DBALException::invalidPlatformVersionSpecified( - $versionString, - '^(?:[^\s]+\s)?..', - ); - } - - return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch']; - } } diff --git a/doctrine/dbal/src/Driver/AbstractException.php b/doctrine/dbal/src/Driver/AbstractException.php index 9b7bff40f..5dec96f05 100644 --- a/doctrine/dbal/src/Driver/AbstractException.php +++ b/doctrine/dbal/src/Driver/AbstractException.php @@ -8,34 +8,26 @@ use Throwable; /** - * Base implementation of the {@see Exception} interface. - * - * @internal + * Abstract base implementation of the {@see DriverException} interface. */ abstract class AbstractException extends BaseException implements Exception { - /** - * The SQLSTATE of the driver. - */ - private ?string $sqlState = null; - /** * @param string $message The driver error message. * @param string|null $sqlState The SQLSTATE the driver is in at the time the error occurred, if any. * @param int $code The driver specific error code if any. * @param Throwable|null $previous The previous throwable used for the exception chaining. */ - public function __construct($message, $sqlState = null, $code = 0, ?Throwable $previous = null) - { + public function __construct( + string $message, + private readonly ?string $sqlState = null, + int $code = 0, + ?Throwable $previous = null, + ) { parent::__construct($message, $code, $previous); - - $this->sqlState = $sqlState; } - /** - * {@inheritDoc} - */ - public function getSQLState() + public function getSQLState(): ?string { return $this->sqlState; } diff --git a/doctrine/dbal/src/Driver/AbstractMySQLDriver.php b/doctrine/dbal/src/Driver/AbstractMySQLDriver.php index 1bb327033..bd2e8ae2a 100644 --- a/doctrine/dbal/src/Driver/AbstractMySQLDriver.php +++ b/doctrine/dbal/src/Driver/AbstractMySQLDriver.php @@ -1,28 +1,26 @@ getServerVersion(); + if (stripos($version, 'mariadb') !== false) { $mariaDbVersion = $this->getMariaDbMysqlVersionNumber($version); + if (version_compare($mariaDbVersion, '12.3.0', '>=')) { + return new MariaDB120300Platform(); + } + if (version_compare($mariaDbVersion, '11.7.0', '>=')) { - return new MariaDb110700Platform(); + return new MariaDB110700Platform(); } if (version_compare($mariaDbVersion, '10.10.0', '>=')) { - return new MariaDb1010Platform(); + return new MariaDB1010Platform(); } if (version_compare($mariaDbVersion, '10.6.0', '>=')) { - return new MariaDb1060Platform(); - } - - if (version_compare($mariaDbVersion, '10.5.2', '>=')) { - return new MariaDb1052Platform(); - } - - if (version_compare($mariaDbVersion, '10.4.3', '>=')) { - return new MariaDb1043Platform(); + return new MariaDB1060Platform(); } Deprecation::trigger( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6110', - 'Support for MariaDB < 10.4 is deprecated and will be removed in DBAL 4.' - . ' Consider upgrading to a more recent version of MariaDB.', + 'https://github.com/doctrine/dbal/pull/6343', + 'Support for MariaDB < 10.6.0 is deprecated and will be removed in DBAL 5', ); - if (version_compare($mariaDbVersion, '10.2.7', '>=')) { - return new MariaDb1027Platform(); - } - } else { - $oracleMysqlVersion = $this->getOracleMysqlVersionNumber($version); - - if (version_compare($oracleMysqlVersion, '8.4.0', '>=')) { - if (! version_compare($version, '8.4.0', '>=')) { - Deprecation::trigger( - 'doctrine/orm', - 'https://github.com/doctrine/dbal/pull/5779', - 'Version detection logic for MySQL will change in DBAL 4. ' - . 'Please specify the version as the server reports it, e.g. "8.4.0" instead of "8.4".', - ); - } - - return new MySQL84Platform(); - } - - if (version_compare($oracleMysqlVersion, '8', '>=')) { - if (! version_compare($version, '8.0.0', '>=')) { - Deprecation::trigger( - 'doctrine/orm', - 'https://github.com/doctrine/dbal/pull/5779', - 'Version detection logic for MySQL will change in DBAL 4. ' - . 'Please specify the version as the server reports it, e.g. "8.0.31" instead of "8".', - ); - } - - return new MySQL80Platform(); - } - - if (version_compare($oracleMysqlVersion, '5.7.9', '>=')) { - if (! version_compare($version, '5.7.9', '>=')) { - Deprecation::trigger( - 'doctrine/orm', - 'https://github.com/doctrine/dbal/pull/5779', - 'Version detection logic for MySQL will change in DBAL 4. ' - . 'Please specify the version as the server reports it, e.g. "5.7.40" instead of "5.7".', - ); - } - - return new MySQL57Platform(); + if (version_compare($mariaDbVersion, '10.5.2', '>=')) { + return new MariaDB1052Platform(); } - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5072', - 'MySQL 5.6 support is deprecated and will be removed in DBAL 4.' - . ' Consider upgrading to MySQL 5.7 or later.', - ); + return new MariaDBPlatform(); } - return $this->getDatabasePlatform(); - } + if (version_compare($version, '8.4.0', '>=')) { + return new MySQL84Platform(); + } - /** - * Get a normalized 'version number' from the server string - * returned by Oracle MySQL servers. - * - * @param string $versionString Version string returned by the driver, i.e. '5.7.10' - * - * @throws Exception - */ - private function getOracleMysqlVersionNumber(string $versionString): string - { - if ( - preg_match( - '/^(?P\d+)(?:\.(?P\d+)(?:\.(?P\d+))?)?/', - $versionString, - $versionParts, - ) !== 1 - ) { - throw Exception::invalidPlatformVersionSpecified( - $versionString, - '..', - ); + if (version_compare($version, '8.0.0', '>=')) { + return new MySQL80Platform(); } - $majorVersion = $versionParts['major']; - $minorVersion = $versionParts['minor'] ?? 0; - $patchVersion = $versionParts['patch'] ?? null; + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6343', + 'Support for MySQL < 8 is deprecated and will be removed in DBAL 5', + ); - if ($majorVersion === '5' && $minorVersion === '7') { - $patchVersion ??= '9'; - } else { - $patchVersion ??= '0'; - } + return new MySQLPlatform(); + } - return $majorVersion . '.' . $minorVersion . '.' . $patchVersion; + public function getExceptionConverter(): ExceptionConverterInterface + { + return new ExceptionConverter(); } /** @@ -168,20 +97,10 @@ private function getOracleMysqlVersionNumber(string $versionString): string * * @param string $versionString Version string as returned by mariadb server, i.e. '5.5.5-Mariadb-10.0.8-xenial' * - * @throws Exception + * @throws InvalidPlatformVersion */ private function getMariaDbMysqlVersionNumber(string $versionString): string { - if (stripos($versionString, 'MariaDB') === 0) { - Deprecation::trigger( - 'doctrine/orm', - 'https://github.com/doctrine/dbal/pull/5779', - 'Version detection logic for MySQL will change in DBAL 4. ' - . 'Please specify the version as the server reports it, ' - . 'e.g. "10.9.3-MariaDB" instead of "mariadb-10.9".', - ); - } - if ( preg_match( '/^(?:5\.5\.5-)?(mariadb-)?(?P\d+)\.(?P\d+)\.(?P\d+)/i', @@ -189,7 +108,7 @@ private function getMariaDbMysqlVersionNumber(string $versionString): string $versionParts, ) !== 1 ) { - throw Exception::invalidPlatformVersionSpecified( + throw InvalidPlatformVersion::new( $versionString, '^(?:5\.5\.5-)?(mariadb-)?..', ); @@ -197,40 +116,4 @@ private function getMariaDbMysqlVersionNumber(string $versionString): string return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch']; } - - /** - * {@inheritDoc} - * - * @return AbstractMySQLPlatform - */ - public function getDatabasePlatform() - { - return new MySQLPlatform(); - } - - /** - * {@inheritDoc} - * - * @deprecated Use {@link AbstractMySQLPlatform::createSchemaManager()} instead. - * - * @return MySQLSchemaManager - */ - public function getSchemaManager(Connection $conn, AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5458', - 'AbstractMySQLDriver::getSchemaManager() is deprecated.' - . ' Use MySQLPlatform::createSchemaManager() instead.', - ); - - assert($platform instanceof AbstractMySQLPlatform); - - return new MySQLSchemaManager($conn, $platform); - } - - public function getExceptionConverter(): ExceptionConverter - { - return new MySQL\ExceptionConverter(); - } } diff --git a/doctrine/dbal/src/Driver/AbstractOracleDriver.php b/doctrine/dbal/src/Driver/AbstractOracleDriver.php index b0f92453a..cf56cfad6 100644 --- a/doctrine/dbal/src/Driver/AbstractOracleDriver.php +++ b/doctrine/dbal/src/Driver/AbstractOracleDriver.php @@ -1,64 +1,37 @@ $params The connection parameters to return the Easy Connect String for. - * - * @return string */ - protected function getEasyConnectString(array $params) + protected function getEasyConnectString(array $params): string { return (string) EasyConnectString::fromConnectionParameters($params); } diff --git a/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php b/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php index 91bc6a7e1..8d35e8f0d 100644 --- a/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php +++ b/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php @@ -4,6 +4,8 @@ namespace Doctrine\DBAL\Driver\AbstractOracleDriver; +use Doctrine\Deprecations\Deprecation; + use function implode; use function is_array; use function sprintf; @@ -15,11 +17,8 @@ */ final class EasyConnectString { - private string $string; - - private function __construct(string $string) + private function __construct(private readonly string $string) { - $this->string = $string; } public function __toString(): string @@ -54,10 +53,19 @@ public static function fromConnectionParameters(array $params): self $connectData = []; + if (isset($params['service'])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/7042', + 'Using the "service" parameter to indicate that the value of the "dbname" parameter is the' + . ' service name is deprecated. Use the "servicename" parameter instead.', + ); + } + if (isset($params['servicename']) || isset($params['dbname'])) { $serviceKey = 'SID'; - if (isset($params['service'])) { + if (isset($params['service']) || isset($params['servicename'])) { $serviceKey = 'SERVICE_NAME'; } @@ -77,7 +85,7 @@ public static function fromConnectionParameters(array $params): self return self::fromArray([ 'DESCRIPTION' => [ 'ADDRESS' => [ - 'PROTOCOL' => 'TCP', + 'PROTOCOL' => $params['driverOptions']['protocol'] ?? 'TCP', 'HOST' => $params['host'], 'PORT' => $params['port'] ?? 1521, ], @@ -94,18 +102,15 @@ private static function renderParams(array $params): string foreach ($params as $key => $value) { $string = self::renderValue($value); - if ($string === '') { - continue; + if ($string !== '') { + $chunks[] = sprintf('(%s=%s)', $key, $string); } - - $chunks[] = sprintf('(%s=%s)', $key, $string); } return implode('', $chunks); } - /** @param mixed $value */ - private static function renderValue($value): string + private static function renderValue(mixed $value): string { if (is_array($value)) { return self::renderParams($value); diff --git a/doctrine/dbal/src/Driver/AbstractPostgreSQLDriver.php b/doctrine/dbal/src/Driver/AbstractPostgreSQLDriver.php index eba309daa..7b679c614 100644 --- a/doctrine/dbal/src/Driver/AbstractPostgreSQLDriver.php +++ b/doctrine/dbal/src/Driver/AbstractPostgreSQLDriver.php @@ -1,36 +1,32 @@ getServerVersion(); + if (preg_match('/^(?P\d+)(?:\.(?P\d+)(?:\.(?P\d+))?)?/', $version, $versionParts) !== 1) { - throw Exception::invalidPlatformVersionSpecified( + throw InvalidPlatformVersion::new( $version, '..', ); @@ -45,49 +41,17 @@ public function createDatabasePlatformForVersion($version) return new PostgreSQL120Platform(); } - if (version_compare($version, '10.0', '>=')) { - return new PostgreSQL100Platform(); - } - Deprecation::trigger( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5060', - 'PostgreSQL 9 support is deprecated and will be removed in DBAL 4.' - . ' Consider upgrading to Postgres 10 or later.', - ); - - return new PostgreSQL94Platform(); - } - - /** - * {@inheritDoc} - */ - public function getDatabasePlatform() - { - return new PostgreSQL94Platform(); - } - - /** - * {@inheritDoc} - * - * @deprecated Use {@link PostgreSQLPlatform::createSchemaManager()} instead. - */ - public function getSchemaManager(Connection $conn, AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5458', - 'AbstractPostgreSQLDriver::getSchemaManager() is deprecated.' - . ' Use PostgreSQLPlatform::createSchemaManager() instead.', + 'https://github.com/doctrine/dbal/pull/6495', + 'Support for Postgres < 12 is deprecated and will be removed in DBAL 5', ); - assert($platform instanceof PostgreSQLPlatform); - - return new PostgreSQLSchemaManager($conn, $platform); + return new PostgreSQLPlatform(); } - public function getExceptionConverter(): ExceptionConverter + public function getExceptionConverter(): ExceptionConverterInterface { - return new PostgreSQL\ExceptionConverter(); + return new ExceptionConverter(); } } diff --git a/doctrine/dbal/src/Driver/AbstractSQLServerDriver.php b/doctrine/dbal/src/Driver/AbstractSQLServerDriver.php index b9a99552e..8c2d012fe 100644 --- a/doctrine/dbal/src/Driver/AbstractSQLServerDriver.php +++ b/doctrine/dbal/src/Driver/AbstractSQLServerDriver.php @@ -1,49 +1,23 @@ fetchNumeric(); diff --git a/doctrine/dbal/src/Driver/IBMDB2/Connection.php b/doctrine/dbal/src/Driver/IBMDB2/Connection.php index dfb11c236..2c8783b28 100644 --- a/doctrine/dbal/src/Driver/IBMDB2/Connection.php +++ b/doctrine/dbal/src/Driver/IBMDB2/Connection.php @@ -1,15 +1,14 @@ connection = $connection; } - /** - * {@inheritDoc} - */ - public function getServerVersion() + public function getServerVersion(): string { $serverInfo = db2_server_info($this->connection); assert($serverInfo instanceof stdClass); @@ -53,7 +45,7 @@ public function getServerVersion() return $serverInfo->DBMS_VER; } - public function prepare(string $sql): DriverStatement + public function prepare(string $sql): Statement { $stmt = @db2_prepare($this->connection, $sql); @@ -64,26 +56,17 @@ public function prepare(string $sql): DriverStatement return new Statement($stmt); } - public function query(string $sql): ResultInterface + public function query(string $sql): Result { return $this->prepare($sql)->execute(); } - /** - * {@inheritDoc} - */ - public function quote($value, $type = ParameterType::STRING) + public function quote(string $value): string { - $value = db2_escape_string($value); - - if ($type === ParameterType::INTEGER) { - return $value; - } - - return "'" . $value . "'"; + return "'" . db2_escape_string($value) . "'"; } - public function exec(string $sql): int + public function exec(string $sql): int|string { $stmt = @db2_exec($this->connection, $sql); @@ -91,46 +74,53 @@ public function exec(string $sql): int throw StatementError::new(); } - return db2_num_rows($stmt); + $numRows = db2_num_rows($stmt); + + if ($numRows === false) { + throw StatementError::new(); + } + + return $numRows; } - /** - * {@inheritDoc} - */ - public function lastInsertId($name = null) + public function lastInsertId(): string { - if ($name !== null) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4687', - 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', - ); + $lastInsertId = db2_last_insert_id($this->connection); + + if ($lastInsertId === null) { + throw NoIdentityValue::new(); } - return db2_last_insert_id($this->connection) ?? false; + return $lastInsertId; } - public function beginTransaction(): bool + public function beginTransaction(): void { - return db2_autocommit($this->connection, DB2_AUTOCOMMIT_OFF); + if (db2_autocommit($this->connection, DB2_AUTOCOMMIT_OFF) !== true) { + throw ConnectionError::new($this->connection); + } } - public function commit(): bool + public function commit(): void { if (! db2_commit($this->connection)) { throw ConnectionError::new($this->connection); } - return db2_autocommit($this->connection, DB2_AUTOCOMMIT_ON); + if (db2_autocommit($this->connection, DB2_AUTOCOMMIT_ON) !== true) { + throw ConnectionError::new($this->connection); + } } - public function rollBack(): bool + public function rollBack(): void { if (! db2_rollback($this->connection)) { throw ConnectionError::new($this->connection); } - return db2_autocommit($this->connection, DB2_AUTOCOMMIT_ON); + if (db2_autocommit($this->connection, DB2_AUTOCOMMIT_ON) !== true) { + throw ConnectionError::new($this->connection); + } } /** @return resource */ diff --git a/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php b/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php index 1840b8a3a..cc0ebd1b1 100644 --- a/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php +++ b/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php @@ -8,20 +8,17 @@ use function implode; use function sprintf; -use function strpos; +use function str_contains; /** * Db2 DSN */ final class DataSourceName { - private string $string; - private function __construct( #[SensitiveParameter] - string $string + private readonly string $string, ) { - $this->string = $string; } public function toString(): string @@ -36,7 +33,7 @@ public function toString(): string */ public static function fromArray( #[SensitiveParameter] - array $params + array $params, ): self { $chunks = []; @@ -52,11 +49,10 @@ public static function fromArray( * * @param array $params */ - public static function fromConnectionParameters( - #[SensitiveParameter] - array $params - ): self { - if (isset($params['dbname']) && strpos($params['dbname'], '=') !== false) { + public static function fromConnectionParameters(#[SensitiveParameter] + array $params,): self + { + if (isset($params['dbname']) && str_contains($params['dbname'], '=')) { return new self($params['dbname']); } @@ -72,11 +68,9 @@ public static function fromConnectionParameters( 'password' => 'PWD', ] as $dbalParam => $dsnParam ) { - if (! isset($params[$dbalParam])) { - continue; + if (isset($params[$dbalParam])) { + $dsnParams[$dsnParam] = $params[$dbalParam]; } - - $dsnParams[$dsnParam] = $params[$dbalParam]; } return self::fromArray($dsnParams); diff --git a/doctrine/dbal/src/Driver/IBMDB2/Driver.php b/doctrine/dbal/src/Driver/IBMDB2/Driver.php index 7650db5f4..f2f4ed770 100644 --- a/doctrine/dbal/src/Driver/IBMDB2/Driver.php +++ b/doctrine/dbal/src/Driver/IBMDB2/Driver.php @@ -1,5 +1,7 @@ toString(); $username = $params['user'] ?? ''; diff --git a/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php b/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php index 05099faf0..ee0aaf173 100644 --- a/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php +++ b/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php @@ -9,7 +9,7 @@ /** @internal */ final class CannotCopyStreamToStream extends AbstractException { - /** @param array{message: string}|null $error */ + /** @phpstan-param array{message: string, ...}|null $error */ public static function new(?array $error): self { $message = 'Could not copy source stream to temporary file'; diff --git a/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCreateTemporaryFile.php b/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCreateTemporaryFile.php index 70a355732..17f7d85ab 100644 --- a/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCreateTemporaryFile.php +++ b/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCreateTemporaryFile.php @@ -9,7 +9,7 @@ /** @internal */ final class CannotCreateTemporaryFile extends AbstractException { - /** @param array{message: string}|null $error */ + /** @phpstan-param array{message: string, ...}|null $error */ public static function new(?array $error): self { $message = 'Could not create temporary file'; diff --git a/doctrine/dbal/src/Driver/IBMDB2/Exception/PrepareFailed.php b/doctrine/dbal/src/Driver/IBMDB2/Exception/PrepareFailed.php index 95da1cdc3..3feb0da03 100644 --- a/doctrine/dbal/src/Driver/IBMDB2/Exception/PrepareFailed.php +++ b/doctrine/dbal/src/Driver/IBMDB2/Exception/PrepareFailed.php @@ -9,7 +9,7 @@ /** @internal */ final class PrepareFailed extends AbstractException { - /** @param array{message: string}|null $error */ + /** @phpstan-param array{message: string, ...}|null $error */ public static function new(?array $error): self { if ($error === null) { diff --git a/doctrine/dbal/src/Driver/IBMDB2/Result.php b/doctrine/dbal/src/Driver/IBMDB2/Result.php index d8e9fc599..3acf6358e 100644 --- a/doctrine/dbal/src/Driver/IBMDB2/Result.php +++ b/doctrine/dbal/src/Driver/IBMDB2/Result.php @@ -7,9 +7,11 @@ use Doctrine\DBAL\Driver\FetchUtils; use Doctrine\DBAL\Driver\IBMDB2\Exception\StatementError; use Doctrine\DBAL\Driver\Result as ResultInterface; +use Doctrine\DBAL\Exception\InvalidColumnIndex; use function db2_fetch_array; use function db2_fetch_assoc; +use function db2_field_name; use function db2_free_result; use function db2_num_fields; use function db2_num_rows; @@ -17,23 +19,16 @@ final class Result implements ResultInterface { - /** @var resource */ - private $statement; - /** * @internal The result can be only instantiated by its driver connection or statement. * * @param resource $statement */ - public function __construct($statement) + public function __construct(private readonly mixed $statement) { - $this->statement = $statement; } - /** - * {@inheritDoc} - */ - public function fetchNumeric() + public function fetchNumeric(): array|false { $row = @db2_fetch_array($this->statement); @@ -44,10 +39,7 @@ public function fetchNumeric() return $row; } - /** - * {@inheritDoc} - */ - public function fetchAssociative() + public function fetchAssociative(): array|false { $row = @db2_fetch_assoc($this->statement); @@ -58,10 +50,7 @@ public function fetchAssociative() return $row; } - /** - * {@inheritDoc} - */ - public function fetchOne() + public function fetchOne(): mixed { return FetchUtils::fetchOne($this); } @@ -92,7 +81,13 @@ public function fetchFirstColumn(): array public function rowCount(): int { - return @db2_num_rows($this->statement); + $numRows = @db2_num_rows($this->statement); + + if ($numRows === false) { + throw StatementError::new($this->statement); + } + + return $numRows; } public function columnCount(): int @@ -106,6 +101,17 @@ public function columnCount(): int return 0; } + public function getColumnName(int $index): string + { + $name = db2_field_name($this->statement, $index); + + if ($name === false) { + throw InvalidColumnIndex::new($index); + } + + return $name; + } + public function free(): void { db2_free_result($this->statement); diff --git a/doctrine/dbal/src/Driver/IBMDB2/Statement.php b/doctrine/dbal/src/Driver/IBMDB2/Statement.php index be5831fba..dd7e5399f 100644 --- a/doctrine/dbal/src/Driver/IBMDB2/Statement.php +++ b/doctrine/dbal/src/Driver/IBMDB2/Statement.php @@ -1,22 +1,21 @@ stmt = $stmt; } - /** - * {@inheritDoc} - */ - public function bindValue($param, $value, $type = ParameterType::STRING): bool + public function bindValue(int|string $param, mixed $value, ParameterType $type): void { assert(is_int($param)); - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindValue() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - - return $this->bindParam($param, $value, $type); - } - - /** - * {@inheritDoc} - * - * @deprecated Use {@see bindValue()} instead. - */ - public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5563', - '%s is deprecated. Use bindValue() instead.', - __METHOD__, - ); - - assert(is_int($param)); - - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindParam() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - switch ($type) { case ParameterType::INTEGER: - $this->bind($param, $variable, DB2_PARAM_IN, DB2_LONG); + $this->bind($param, $value, DB2_PARAM_IN, DB2_LONG); break; case ParameterType::LARGE_OBJECT: - $this->lobs[$param] = &$variable; + $this->lobs[$param] = &$value; break; default: - $this->bind($param, $variable, DB2_PARAM_IN, DB2_CHAR); + $this->bind($param, $value, DB2_PARAM_IN, DB2_CHAR); break; } - - return true; } - /** - * @param int $position Parameter position - * @param mixed $variable - * - * @throws Exception - */ - private function bind($position, &$variable, int $parameterType, int $dataType): void + /** @throws Exception */ + private function bind(int $position, mixed &$variable, int $parameterType, int $dataType): void { $this->parameters[$position] =& $variable; @@ -131,23 +79,11 @@ private function bind($position, &$variable, int $parameterType, int $dataType): } } - /** - * {@inheritDoc} - */ - public function execute($params = null): ResultInterface + public function execute(): Result { - if ($params !== null) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5556', - 'Passing $params to Statement::execute() is deprecated. Bind parameters using' - . ' Statement::bindParam() or Statement::bindValue() instead.', - ); - } - $handles = $this->bindLobs(); - $result = @db2_execute($this->stmt, $params ?? $this->parameters); + $result = @db2_execute($this->stmt, $this->parameters); foreach ($handles as $handle) { fclose($handle); diff --git a/doctrine/dbal/src/Driver/Middleware/AbstractConnectionMiddleware.php b/doctrine/dbal/src/Driver/Middleware/AbstractConnectionMiddleware.php index f2809cd0a..bdf9b2f84 100644 --- a/doctrine/dbal/src/Driver/Middleware/AbstractConnectionMiddleware.php +++ b/doctrine/dbal/src/Driver/Middleware/AbstractConnectionMiddleware.php @@ -1,26 +1,17 @@ wrappedConnection = $wrappedConnection; } public function prepare(string $sql): Statement @@ -33,81 +24,46 @@ public function query(string $sql): Result return $this->wrappedConnection->query($sql); } - /** - * {@inheritDoc} - */ - public function quote($value, $type = ParameterType::STRING) + public function quote(string $value): string { - return $this->wrappedConnection->quote($value, $type); + return $this->wrappedConnection->quote($value); } - public function exec(string $sql): int + public function exec(string $sql): int|string { return $this->wrappedConnection->exec($sql); } - /** - * {@inheritDoc} - */ - public function lastInsertId($name = null) + public function lastInsertId(): int|string { - if ($name !== null) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4687', - 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', - ); - } - - return $this->wrappedConnection->lastInsertId($name); + return $this->wrappedConnection->lastInsertId(); } - /** - * {@inheritDoc} - */ - public function beginTransaction() + public function beginTransaction(): void { - return $this->wrappedConnection->beginTransaction(); + $this->wrappedConnection->beginTransaction(); } - /** - * {@inheritDoc} - */ - public function commit() + public function commit(): void { - return $this->wrappedConnection->commit(); + $this->wrappedConnection->commit(); } - /** - * {@inheritDoc} - */ - public function rollBack() + public function rollBack(): void { - return $this->wrappedConnection->rollBack(); + $this->wrappedConnection->rollBack(); } - /** - * {@inheritDoc} - */ - public function getServerVersion() + public function getServerVersion(): string { - if (! $this->wrappedConnection instanceof ServerInfoAwareConnection) { - throw new LogicException('The underlying connection is not a ServerInfoAwareConnection'); - } - return $this->wrappedConnection->getServerVersion(); } - /** @return resource|object */ + /** + * {@inheritDoc} + */ public function getNativeConnection() { - if (! method_exists($this->wrappedConnection, 'getNativeConnection')) { - throw new LogicException(sprintf( - 'The driver connection %s does not support accessing the native connection.', - get_class($this->wrappedConnection), - )); - } - return $this->wrappedConnection->getNativeConnection(); } } diff --git a/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php b/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php index 1c9d43097..482f13458 100644 --- a/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php +++ b/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php @@ -1,22 +1,20 @@ wrappedDriver = $wrappedDriver; } /** @@ -24,50 +22,18 @@ public function __construct(Driver $wrappedDriver) */ public function connect( #[SensitiveParameter] - array $params - ) { + array $params, + ): DriverConnection { return $this->wrappedDriver->connect($params); } - /** - * {@inheritDoc} - */ - public function getDatabasePlatform() + public function getDatabasePlatform(ServerVersionProvider $versionProvider): AbstractPlatform { - return $this->wrappedDriver->getDatabasePlatform(); - } - - /** - * {@inheritDoc} - * - * @deprecated Use {@link AbstractPlatform::createSchemaManager()} instead. - */ - public function getSchemaManager(Connection $conn, AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5458', - 'AbstractDriverMiddleware::getSchemaManager() is deprecated.' - . ' Use AbstractPlatform::createSchemaManager() instead.', - ); - - return $this->wrappedDriver->getSchemaManager($conn, $platform); + return $this->wrappedDriver->getDatabasePlatform($versionProvider); } public function getExceptionConverter(): ExceptionConverter { return $this->wrappedDriver->getExceptionConverter(); } - - /** - * {@inheritDoc} - */ - public function createDatabasePlatformForVersion($version) - { - if ($this->wrappedDriver instanceof VersionAwarePlatformDriver) { - return $this->wrappedDriver->createDatabasePlatformForVersion($version); - } - - return $this->wrappedDriver->getDatabasePlatform(); - } } diff --git a/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php b/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php index 198d39b0d..f335c216e 100644 --- a/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php +++ b/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php @@ -1,38 +1,33 @@ wrappedResult = $result; } - /** - * {@inheritDoc} - */ - public function fetchNumeric() + public function fetchNumeric(): array|false { return $this->wrappedResult->fetchNumeric(); } - /** - * {@inheritDoc} - */ - public function fetchAssociative() + public function fetchAssociative(): array|false { return $this->wrappedResult->fetchAssociative(); } - /** - * {@inheritDoc} - */ - public function fetchOne() + public function fetchOne(): mixed { return $this->wrappedResult->fetchOne(); } @@ -61,7 +56,7 @@ public function fetchFirstColumn(): array return $this->wrappedResult->fetchFirstColumn(); } - public function rowCount(): int + public function rowCount(): int|string { return $this->wrappedResult->rowCount(); } @@ -71,6 +66,18 @@ public function columnCount(): int return $this->wrappedResult->columnCount(); } + public function getColumnName(int $index): string + { + if (! method_exists($this->wrappedResult, 'getColumnName')) { + throw new LogicException(sprintf( + 'The driver result %s does not support accessing the column name.', + get_debug_type($this->wrappedResult), + )); + } + + return $this->wrappedResult->getColumnName($index); + } + public function free(): void { $this->wrappedResult->free(); diff --git a/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php b/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php index 6cd2f8f08..6eaad509f 100644 --- a/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php +++ b/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php @@ -1,71 +1,26 @@ wrappedStatement = $wrappedStatement; } - /** - * {@inheritDoc} - */ - public function bindValue($param, $value, $type = ParameterType::STRING) + public function bindValue(int|string $param, mixed $value, ParameterType $type): void { - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindValue() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - - return $this->wrappedStatement->bindValue($param, $value, $type); - } - - /** - * {@inheritDoc} - * - * @deprecated Use {@see bindValue()} instead. - */ - public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5563', - '%s is deprecated. Use bindValue() instead.', - __METHOD__, - ); - - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindParam() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - - return $this->wrappedStatement->bindParam($param, $variable, $type, $length); + $this->wrappedStatement->bindValue($param, $value, $type); } - /** - * {@inheritDoc} - */ - public function execute($params = null): Result + public function execute(): Result { - return $this->wrappedStatement->execute($params); + return $this->wrappedStatement->execute(); } } diff --git a/doctrine/dbal/src/Driver/Mysqli/Connection.php b/doctrine/dbal/src/Driver/Mysqli/Connection.php index d492684cc..a7854a2f9 100644 --- a/doctrine/dbal/src/Driver/Mysqli/Connection.php +++ b/doctrine/dbal/src/Driver/Mysqli/Connection.php @@ -1,48 +1,25 @@ connection = $connection; - } - - /** - * Retrieves mysqli native resource handle. - * - * Could be used if part of your application is not using DBAL. - * - * @deprecated Call {@see getNativeConnection()} instead. - */ - public function getWrappedResourceHandle(): mysqli + public function __construct(private readonly mysqli $connection) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5037', - '%s is deprecated, call getNativeConnection() instead.', - __METHOD__, - ); - - return $this->getNativeConnection(); } public function getServerVersion(): string @@ -50,7 +27,7 @@ public function getServerVersion(): string return $this->connection->get_server_info(); } - public function prepare(string $sql): DriverStatement + public function prepare(string $sql): Statement { try { $stmt = $this->connection->prepare($sql); @@ -65,20 +42,17 @@ public function prepare(string $sql): DriverStatement return new Statement($stmt); } - public function query(string $sql): ResultInterface + public function query(string $sql): Result { return $this->prepare($sql)->execute(); } - /** - * {@inheritDoc} - */ - public function quote($value, $type = ParameterType::STRING) + public function quote(string $value): string { return "'" . $this->connection->escape_string($value) . "'"; } - public function exec(string $sql): int + public function exec(string $sql): int|string { try { $result = $this->connection->query($sql); @@ -93,44 +67,47 @@ public function exec(string $sql): int return $this->connection->affected_rows; } - /** - * {@inheritDoc} - */ - public function lastInsertId($name = null) + public function lastInsertId(): int|string { - if ($name !== null) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4687', - 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', - ); + $lastInsertId = $this->connection->insert_id; + + if ($lastInsertId === 0) { + throw Exception\NoIdentityValue::new(); } return $this->connection->insert_id; } - public function beginTransaction(): bool + public function beginTransaction(): void { - $this->connection->begin_transaction(); - - return true; + try { + if (! $this->connection->begin_transaction()) { + throw ConnectionError::new($this->connection); + } + } catch (mysqli_sql_exception $e) { + throw ConnectionError::upcast($e); + } } - public function commit(): bool + public function commit(): void { try { - return $this->connection->commit(); + if (! $this->connection->commit()) { + throw ConnectionError::new($this->connection); + } } catch (mysqli_sql_exception $e) { - return false; + throw ConnectionError::upcast($e); } } - public function rollBack(): bool + public function rollBack(): void { try { - return $this->connection->rollback(); + if (! $this->connection->rollback()) { + throw ConnectionError::new($this->connection); + } } catch (mysqli_sql_exception $e) { - return false; + throw ConnectionError::upcast($e); } } diff --git a/doctrine/dbal/src/Driver/Mysqli/Driver.php b/doctrine/dbal/src/Driver/Mysqli/Driver.php index 4f5186875..b90f035fb 100644 --- a/doctrine/dbal/src/Driver/Mysqli/Driver.php +++ b/doctrine/dbal/src/Driver/Mysqli/Driver.php @@ -1,5 +1,7 @@ real_connect( $host, - $params['user'] ?? null, - $params['password'] ?? null, - $params['dbname'] ?? null, - $params['port'] ?? null, - $params['unix_socket'] ?? null, + $params['user'] ?? '', + $params['password'] ?? '', + $params['dbname'] ?? '', + $params['port'] ?? 0, + $params['unix_socket'] ?? '', $params['driverOptions'][Connection::OPTION_FLAGS] ?? 0, ); } catch (mysqli_sql_exception $e) { @@ -72,7 +72,7 @@ public function connect( */ private function compilePreInitializers( #[SensitiveParameter] - array $params + array $params, ): Generator { unset($params['driverOptions'][Connection::OPTION_FLAGS]); @@ -106,12 +106,10 @@ private function compilePreInitializers( */ private function compilePostInitializers( #[SensitiveParameter] - array $params + array $params, ): Generator { - if (! isset($params['charset'])) { - return; + if (isset($params['charset'])) { + yield new Charset($params['charset']); } - - yield new Charset($params['charset']); } } diff --git a/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionError.php b/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionError.php index 731a6aa94..ccbc6cbbf 100644 --- a/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionError.php +++ b/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionError.php @@ -9,8 +9,6 @@ use mysqli_sql_exception; use ReflectionProperty; -use const PHP_VERSION_ID; - /** @internal */ final class ConnectionError extends AbstractException { @@ -22,10 +20,7 @@ public static function new(mysqli $connection): self public static function upcast(mysqli_sql_exception $exception): self { $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate'); - if (PHP_VERSION_ID < 80100) { - $p->setAccessible(true); - } - return new self($exception->getMessage(), $p->getValue($exception), (int) $exception->getCode(), $exception); + return new self($exception->getMessage(), $p->getValue($exception), $exception->getCode(), $exception); } } diff --git a/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionFailed.php b/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionFailed.php index 414bf3d06..d34aafbc7 100644 --- a/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionFailed.php +++ b/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionFailed.php @@ -11,8 +11,6 @@ use function assert; -use const PHP_VERSION_ID; - /** @internal */ final class ConnectionFailed extends AbstractException { @@ -27,10 +25,7 @@ public static function new(mysqli $connection): self public static function upcast(mysqli_sql_exception $exception): self { $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate'); - if (PHP_VERSION_ID < 80100) { - $p->setAccessible(true); - } - return new self($exception->getMessage(), $p->getValue($exception), (int) $exception->getCode(), $exception); + return new self($exception->getMessage(), $p->getValue($exception), $exception->getCode(), $exception); } } diff --git a/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidCharset.php b/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidCharset.php index 21c78b6fa..afd94f11f 100644 --- a/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidCharset.php +++ b/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidCharset.php @@ -11,8 +11,6 @@ use function sprintf; -use const PHP_VERSION_ID; - /** @internal */ final class InvalidCharset extends AbstractException { @@ -28,14 +26,11 @@ public static function fromCharset(mysqli $connection, string $charset): self public static function upcast(mysqli_sql_exception $exception, string $charset): self { $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate'); - if (PHP_VERSION_ID < 80100) { - $p->setAccessible(true); - } return new self( sprintf('Failed to set charset "%s": %s', $charset, $exception->getMessage()), $p->getValue($exception), - (int) $exception->getCode(), + $exception->getCode(), $exception, ); } diff --git a/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidOption.php b/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidOption.php index abbda01c0..1f1f7aaea 100644 --- a/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidOption.php +++ b/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidOption.php @@ -11,8 +11,7 @@ /** @internal */ final class InvalidOption extends AbstractException { - /** @param mixed $value */ - public static function fromOption(int $option, $value): self + public static function fromOption(int $option, mixed $value): self { return new self( sprintf('Failed to set option %d with value "%s"', $option, $value), diff --git a/doctrine/dbal/src/Driver/Mysqli/Exception/StatementError.php b/doctrine/dbal/src/Driver/Mysqli/Exception/StatementError.php index d099d254a..683db01d3 100644 --- a/doctrine/dbal/src/Driver/Mysqli/Exception/StatementError.php +++ b/doctrine/dbal/src/Driver/Mysqli/Exception/StatementError.php @@ -9,8 +9,6 @@ use mysqli_stmt; use ReflectionProperty; -use const PHP_VERSION_ID; - /** @internal */ final class StatementError extends AbstractException { @@ -22,10 +20,7 @@ public static function new(mysqli_stmt $statement): self public static function upcast(mysqli_sql_exception $exception): self { $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate'); - if (PHP_VERSION_ID < 80100) { - $p->setAccessible(true); - } - return new self($exception->getMessage(), $p->getValue($exception), (int) $exception->getCode(), $exception); + return new self($exception->getMessage(), $p->getValue($exception), $exception->getCode(), $exception); } } diff --git a/doctrine/dbal/src/Driver/Mysqli/Initializer/Charset.php b/doctrine/dbal/src/Driver/Mysqli/Initializer/Charset.php index 8143a265c..677d87043 100644 --- a/doctrine/dbal/src/Driver/Mysqli/Initializer/Charset.php +++ b/doctrine/dbal/src/Driver/Mysqli/Initializer/Charset.php @@ -11,11 +11,8 @@ final class Charset implements Initializer { - private string $charset; - - public function __construct(string $charset) + public function __construct(private readonly string $charset) { - $this->charset = $charset; } public function initialize(mysqli $connection): void @@ -26,10 +23,8 @@ public function initialize(mysqli $connection): void throw InvalidCharset::upcast($e, $this->charset); } - if ($success) { - return; + if (! $success) { + throw InvalidCharset::fromCharset($connection, $this->charset); } - - throw InvalidCharset::fromCharset($connection, $this->charset); } } diff --git a/doctrine/dbal/src/Driver/Mysqli/Initializer/Options.php b/doctrine/dbal/src/Driver/Mysqli/Initializer/Options.php index 2e66f8d69..322395135 100644 --- a/doctrine/dbal/src/Driver/Mysqli/Initializer/Options.php +++ b/doctrine/dbal/src/Driver/Mysqli/Initializer/Options.php @@ -12,13 +12,9 @@ final class Options implements Initializer { - /** @var array */ - private array $options; - /** @param array $options */ - public function __construct(array $options) + public function __construct(private readonly array $options) { - $this->options = $options; } public function initialize(mysqli $connection): void diff --git a/doctrine/dbal/src/Driver/Mysqli/Initializer/Secure.php b/doctrine/dbal/src/Driver/Mysqli/Initializer/Secure.php index a25fcfc2d..fa819b5a2 100644 --- a/doctrine/dbal/src/Driver/Mysqli/Initializer/Secure.php +++ b/doctrine/dbal/src/Driver/Mysqli/Initializer/Secure.php @@ -10,25 +10,14 @@ final class Secure implements Initializer { - private string $key; - private string $cert; - private string $ca; - private string $capath; - private string $cipher; - public function __construct( #[SensitiveParameter] - string $key, - string $cert, - string $ca, - string $capath, - string $cipher + private readonly string $key, + private readonly string $cert, + private readonly string $ca, + private readonly string $capath, + private readonly string $cipher, ) { - $this->key = $key; - $this->cert = $cert; - $this->ca = $ca; - $this->capath = $capath; - $this->cipher = $cipher; } public function initialize(mysqli $connection): void diff --git a/doctrine/dbal/src/Driver/Mysqli/Result.php b/doctrine/dbal/src/Driver/Mysqli/Result.php index bfd558b4f..89f94f368 100644 --- a/doctrine/dbal/src/Driver/Mysqli/Result.php +++ b/doctrine/dbal/src/Driver/Mysqli/Result.php @@ -8,6 +8,7 @@ use Doctrine\DBAL\Driver\FetchUtils; use Doctrine\DBAL\Driver\Mysqli\Exception\StatementError; use Doctrine\DBAL\Driver\Result as ResultInterface; +use Doctrine\DBAL\Exception\InvalidColumnIndex; use mysqli_sql_exception; use mysqli_stmt; @@ -18,22 +19,11 @@ final class Result implements ResultInterface { - private mysqli_stmt $statement; - - /** - * Maintains a reference to the Statement that generated this result. This ensures that the lifetime of the - * Statement is managed in conjunction with its associated results, so they are destroyed together - * at the appropriate time {@see Statement::__destruct()}. - * - * @phpstan-ignore property.onlyWritten - */ - private ?Statement $statementReference = null; - /** * Whether the statement result has columns. The property should be used only after the result metadata * has been fetched ({@see $metadataFetched}). Otherwise, the property value is undetermined. */ - private bool $hasColumns = false; + private readonly bool $hasColumns; /** * Mapping of statement result column indexes to their names. The property should be used only @@ -41,7 +31,7 @@ final class Result implements ResultInterface * * @var array */ - private array $columnNames = []; + private readonly array $columnNames; /** @var mixed[] */ private array $boundValues = []; @@ -49,25 +39,25 @@ final class Result implements ResultInterface /** * @internal The result can be only instantiated by its driver connection or statement. * + * @param Statement|null $statementReference Maintains a reference to the Statement that generated this result. This + * ensures that the lifetime of the Statement is managed in conjunction + * with its associated results, so they are destroyed together at the + * appropriate time, see {@see Statement::__destruct()}. + * * @throws Exception */ public function __construct( - mysqli_stmt $statement, - ?Statement $statementReference = null + private readonly mysqli_stmt $statement, + private ?Statement $statementReference = null, // @phpstan-ignore property.onlyWritten ) { - $this->statement = $statement; - $this->statementReference = $statementReference; - - $meta = $statement->result_metadata(); + $meta = $statement->result_metadata(); + $this->hasColumns = $meta !== false; + $this->columnNames = $meta !== false ? array_column($meta->fetch_fields(), 'name') : []; if ($meta === false) { return; } - $this->hasColumns = true; - - $this->columnNames = array_column($meta->fetch_fields(), 'name'); - $meta->free(); // Store result of every execution which has it. Otherwise it will be impossible @@ -96,10 +86,7 @@ public function __construct( } } - /** - * {@inheritDoc} - */ - public function fetchNumeric() + public function fetchNumeric(): array|false { try { $ret = $this->statement->fetch(); @@ -124,10 +111,7 @@ public function fetchNumeric() return $values; } - /** - * {@inheritDoc} - */ - public function fetchAssociative() + public function fetchAssociative(): array|false { $values = $this->fetchNumeric(); @@ -138,10 +122,7 @@ public function fetchAssociative() return array_combine($this->columnNames, $values); } - /** - * {@inheritDoc} - */ - public function fetchOne() + public function fetchOne(): mixed { return FetchUtils::fetchOne($this); } @@ -170,7 +151,7 @@ public function fetchFirstColumn(): array return FetchUtils::fetchFirstColumn($this); } - public function rowCount(): int + public function rowCount(): int|string { if ($this->hasColumns) { return $this->statement->num_rows; @@ -184,6 +165,11 @@ public function columnCount(): int return $this->statement->field_count; } + public function getColumnName(int $index): string + { + return $this->columnNames[$index] ?? throw InvalidColumnIndex::new($index); + } + public function free(): void { $this->statement->free_result(); diff --git a/doctrine/dbal/src/Driver/Mysqli/Statement.php b/doctrine/dbal/src/Driver/Mysqli/Statement.php index be555e3dd..9a2151786 100644 --- a/doctrine/dbal/src/Driver/Mysqli/Statement.php +++ b/doctrine/dbal/src/Driver/Mysqli/Statement.php @@ -1,16 +1,15 @@ 's', - ParameterType::STRING => 's', - ParameterType::BINARY => 's', - ParameterType::BOOLEAN => 'i', - ParameterType::NULL => 's', - ParameterType::INTEGER => 'i', - ParameterType::LARGE_OBJECT => 'b', - ]; - - private mysqli_stmt $stmt; + private const PARAMETER_TYPE_STRING = 's'; + private const PARAMETER_TYPE_INTEGER = 'i'; + private const PARAMETER_TYPE_BINARY = 'b'; /** @var mixed[] */ private array $boundValues; @@ -52,12 +42,10 @@ final class Statement implements StatementInterface private array $values = []; /** @internal The statement can be only instantiated by its driver connection. */ - public function __construct(mysqli_stmt $stmt) + public function __construct(private readonly mysqli_stmt $stmt) { - $this->stmt = $stmt; - $paramCount = $this->stmt->param_count; - $this->types = str_repeat('s', $paramCount); + $this->types = str_repeat(self::PARAMETER_TYPE_STRING, $paramCount); $this->boundValues = array_fill(1, $paramCount, null); } @@ -66,104 +54,29 @@ public function __destruct() @$this->stmt->close(); } - /** - * @deprecated Use {@see bindValue()} instead. - * - * {@inheritDoc} - * - * @phpstan-assert ParameterType::* $type - */ - public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5563', - '%s is deprecated. Use bindValue() instead.', - __METHOD__, - ); - - assert(is_int($param)); - - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindParam() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - - if (! isset(self::PARAM_TYPE_MAP[$type])) { - throw UnknownParameterType::new($type); - } - - $this->boundValues[$param] =& $variable; - $this->types[$param - 1] = self::PARAM_TYPE_MAP[$type]; - - return true; - } - - /** - * {@inheritDoc} - * - * @phpstan-assert ParameterType::* $type - */ - public function bindValue($param, $value, $type = ParameterType::STRING): bool + public function bindValue(int|string $param, mixed $value, ParameterType $type): void { assert(is_int($param)); - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindValue() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - - if (! isset(self::PARAM_TYPE_MAP[$type])) { - throw UnknownParameterType::new($type); - } - + $this->types[$param - 1] = $this->convertParameterType($type); $this->values[$param] = $value; $this->boundValues[$param] =& $this->values[$param]; - $this->types[$param - 1] = self::PARAM_TYPE_MAP[$type]; - - return true; } - /** - * {@inheritDoc} - */ - public function execute($params = null): ResultInterface + public function execute(): Result { - if ($params !== null) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5556', - 'Passing $params to Statement::execute() is deprecated. Bind parameters using' - . ' Statement::bindParam() or Statement::bindValue() instead.', - ); + if (count($this->boundValues) > 0) { + $this->bindParameters(); } - if ($params !== null && count($params) > 0) { - if (! $this->bindUntypedValues($params)) { + try { + if (! $this->stmt->execute()) { throw StatementError::new($this->stmt); } - } elseif (count($this->boundValues) > 0) { - $this->bindTypedParameters(); - } - - try { - $result = $this->stmt->execute(); } catch (mysqli_sql_exception $e) { throw StatementError::upcast($e); } - if (! $result) { - throw StatementError::new($this->stmt); - } - return new Result($this->stmt, $this); } @@ -172,19 +85,18 @@ public function execute($params = null): ResultInterface * * @throws Exception */ - private function bindTypedParameters(): void + private function bindParameters(): void { $streams = $values = []; $types = $this->types; foreach ($this->boundValues as $parameter => $value) { assert(is_int($parameter)); - if (! isset($types[$parameter - 1])) { - $types[$parameter - 1] = self::PARAM_TYPE_MAP[ParameterType::STRING]; + $types[$parameter - 1] = self::PARAMETER_TYPE_STRING; } - if ($types[$parameter - 1] === self::PARAM_TYPE_MAP[ParameterType::LARGE_OBJECT]) { + if ($types[$parameter - 1] === self::PARAMETER_TYPE_BINARY) { if (is_resource($value)) { if (get_resource_type($value) !== 'stream') { throw NonStreamResourceUsedAsLargeObject::new($parameter); @@ -195,7 +107,7 @@ private function bindTypedParameters(): void continue; } - $types[$parameter - 1] = self::PARAM_TYPE_MAP[ParameterType::STRING]; + $types[$parameter - 1] = self::PARAMETER_TYPE_STRING; } $values[$parameter] = $value; @@ -232,13 +144,16 @@ private function sendLongData(array $streams): void } } - /** - * Binds a array of values to bound parameters. - * - * @param mixed[] $values - */ - private function bindUntypedValues(array $values): bool + private function convertParameterType(ParameterType $type): string { - return $this->stmt->bind_param(str_repeat('s', count($values)), ...$values); + return match ($type) { + ParameterType::NULL, + ParameterType::STRING, + ParameterType::ASCII, + ParameterType::BINARY => self::PARAMETER_TYPE_STRING, + ParameterType::INTEGER, + ParameterType::BOOLEAN => self::PARAMETER_TYPE_INTEGER, + ParameterType::LARGE_OBJECT => self::PARAMETER_TYPE_BINARY, + }; } } diff --git a/doctrine/dbal/src/Driver/OCI8/Connection.php b/doctrine/dbal/src/Driver/OCI8/Connection.php index 155b2d1d3..64c210c49 100644 --- a/doctrine/dbal/src/Driver/OCI8/Connection.php +++ b/doctrine/dbal/src/Driver/OCI8/Connection.php @@ -1,21 +1,17 @@ connection = $connection; $this->parser = new Parser(false); $this->executionMode = new ExecutionMode(); } @@ -47,10 +39,7 @@ public function __construct($connection) public function getServerVersion(): string { $version = oci_server_version($this->connection); - - if ($version === false) { - throw Error::new($this->connection); - } + assert($version !== false); $result = preg_match('/\s+(\d+\.\d+\.\d+\.\d+\.\d+)\s+/', $version, $matches); assert($result === 1); @@ -58,15 +47,21 @@ public function getServerVersion(): string return $matches[1]; } - /** @throws Parser\Exception */ - public function prepare(string $sql): DriverStatement + /** + * @throws Parser\Exception + * @throws Error + */ + public function prepare(string $sql): Statement { $visitor = new ConvertPositionalToNamedPlaceholders(); $this->parser->parse($sql, $visitor); - $statement = oci_parse($this->connection, $visitor->getSQL()); - assert(is_resource($statement)); + $statement = @oci_parse($this->connection, $visitor->getSQL()); + + if (! is_resource($statement)) { + throw Error::new($this->connection); + } return new Statement($this->connection, $statement, $visitor->getParameterMap(), $this->executionMode); } @@ -75,91 +70,51 @@ public function prepare(string $sql): DriverStatement * @throws Exception * @throws Parser\Exception */ - public function query(string $sql): ResultInterface + public function query(string $sql): Result { return $this->prepare($sql)->execute(); } - /** - * {@inheritDoc} - */ - public function quote($value, $type = ParameterType::STRING) + public function quote(string $value): string { - if (is_int($value) || is_float($value)) { - return $value; - } - - $value = str_replace("'", "''", $value); - - return "'" . addcslashes($value, "\000\n\r\\\032") . "'"; + return "'" . addcslashes(str_replace("'", "''", $value), "\000\n\r\\\032") . "'"; } /** * @throws Exception * @throws Parser\Exception */ - public function exec(string $sql): int + public function exec(string $sql): int|string { return $this->prepare($sql)->execute()->rowCount(); } - /** - * {@inheritDoc} - * - * @param string|null $name - * - * @return int|false - * - * @throws Parser\Exception - */ - public function lastInsertId($name = null) + public function lastInsertId(): int|string { - if ($name === null) { - return false; - } - - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4687', - 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', - ); - - $result = $this->query('SELECT ' . $name . '.CURRVAL FROM DUAL')->fetchOne(); - - if ($result === false) { - throw SequenceDoesNotExist::new(); - } - - return (int) $result; + throw IdentityColumnsNotSupported::new(); } - public function beginTransaction(): bool + public function beginTransaction(): void { $this->executionMode->disableAutoCommit(); - - return true; } - public function commit(): bool + public function commit(): void { if (! @oci_commit($this->connection)) { throw Error::new($this->connection); } $this->executionMode->enableAutoCommit(); - - return true; } - public function rollBack(): bool + public function rollBack(): void { if (! oci_rollback($this->connection)) { throw Error::new($this->connection); } $this->executionMode->enableAutoCommit(); - - return true; } /** @return resource */ diff --git a/doctrine/dbal/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php b/doctrine/dbal/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php index e2a112629..5898a2c5e 100644 --- a/doctrine/dbal/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php +++ b/doctrine/dbal/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php @@ -1,5 +1,7 @@ statement = $statement; } - /** - * {@inheritDoc} - */ - public function fetchNumeric() + public function fetchNumeric(): array|false { return $this->fetch(OCI_NUM); } - /** - * {@inheritDoc} - */ - public function fetchAssociative() + public function fetchAssociative(): array|false { return $this->fetch(OCI_ASSOC); } - /** - * {@inheritDoc} - */ - public function fetchOne() + public function fetchOne(): mixed { return FetchUtils::fetchOne($this); } @@ -99,13 +88,19 @@ public function rowCount(): int public function columnCount(): int { - $count = oci_num_fields($this->statement); + return oci_num_fields($this->statement); + } - if ($count !== false) { - return $count; + public function getColumnName(int $index): string + { + // OCI expects a 1-based index while DBAL works with a O-based index. + $name = @oci_field_name($this->statement, $index + 1); + + if ($name === false) { + throw InvalidColumnIndex::new($index); } - return 0; + return $name; } public function free(): void @@ -113,12 +108,8 @@ public function free(): void oci_cancel($this->statement); } - /** - * @return mixed|false - * - * @throws Exception - */ - private function fetch(int $mode) + /** @throws Exception */ + private function fetch(int $mode): mixed { $result = oci_fetch_array($this->statement, $mode | OCI_RETURN_NULLS | OCI_RETURN_LOBS); diff --git a/doctrine/dbal/src/Driver/OCI8/Statement.php b/doctrine/dbal/src/Driver/OCI8/Statement.php index 015a14b7b..408f0dd5d 100644 --- a/doctrine/dbal/src/Driver/OCI8/Statement.php +++ b/doctrine/dbal/src/Driver/OCI8/Statement.php @@ -1,15 +1,14 @@ */ - private array $parameterMap; - - private ExecutionMode $executionMode; - /** * @internal The statement can be only instantiated by its driver connection. * @@ -43,54 +31,16 @@ final class Statement implements StatementInterface * @param resource $statement * @param array $parameterMap */ - public function __construct($connection, $statement, array $parameterMap, ExecutionMode $executionMode) - { - $this->connection = $connection; - $this->statement = $statement; - $this->parameterMap = $parameterMap; - $this->executionMode = $executionMode; + public function __construct( + private readonly mixed $connection, + private readonly mixed $statement, + private readonly array $parameterMap, + private readonly ExecutionMode $executionMode, + ) { } - /** - * {@inheritDoc} - */ - public function bindValue($param, $value, $type = ParameterType::STRING): bool - { - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindValue() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - - return $this->bindParam($param, $value, $type); - } - - /** - * {@inheritDoc} - * - * @deprecated Use {@see bindValue()} instead. - */ - public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool + public function bindValue(int|string $param, mixed $value, ParameterType $type): void { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5563', - '%s is deprecated. Use bindValue() instead.', - __METHOD__, - ); - - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindParam() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - if (is_int($param)) { if (! isset($this->parameterMap[$param])) { throw UnknownParameterIndex::new($param); @@ -100,64 +50,43 @@ public function bindParam($param, &$variable, $type = ParameterType::STRING, $le } if ($type === ParameterType::LARGE_OBJECT) { - if ($variable !== null) { + if ($value !== null) { $lob = oci_new_descriptor($this->connection, OCI_D_LOB); - $lob->writeTemporary($variable, OCI_TEMP_BLOB); + $lob->writeTemporary($value, OCI_TEMP_BLOB); - $variable =& $lob; + $value =& $lob; } else { $type = ParameterType::STRING; } } - return oci_bind_by_name( - $this->statement, - $param, - $variable, - $length ?? -1, - $this->convertParameterType($type), - ); + if ( + ! @oci_bind_by_name( + $this->statement, + $param, + $value, + -1, + $this->convertParameterType($type), + ) + ) { + throw Error::new($this->statement); + } } /** * Converts DBAL parameter type to oci8 parameter type */ - private function convertParameterType(int $type): int + private function convertParameterType(ParameterType $type): int { - switch ($type) { - case ParameterType::BINARY: - return OCI_B_BIN; - - case ParameterType::LARGE_OBJECT: - return OCI_B_BLOB; - - default: - return SQLT_CHR; - } + return match ($type) { + ParameterType::BINARY => OCI_B_BIN, + ParameterType::LARGE_OBJECT => OCI_B_BLOB, + default => SQLT_CHR, + }; } - /** - * {@inheritDoc} - */ - public function execute($params = null): ResultInterface + public function execute(): Result { - if ($params !== null) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5556', - 'Passing $params to Statement::execute() is deprecated. Bind parameters using' - . ' Statement::bindParam() or Statement::bindValue() instead.', - ); - - foreach ($params as $key => $val) { - if (is_int($key)) { - $this->bindValue($key + 1, $val, ParameterType::STRING); - } else { - $this->bindValue($key, $val, ParameterType::STRING); - } - } - } - if ($this->executionMode->isAutoCommitEnabled()) { $mode = OCI_COMMIT_ON_SUCCESS; } else { diff --git a/doctrine/dbal/src/Driver/PDO/Connection.php b/doctrine/dbal/src/Driver/PDO/Connection.php index 4320879c8..b1faca147 100644 --- a/doctrine/dbal/src/Driver/PDO/Connection.php +++ b/doctrine/dbal/src/Driver/PDO/Connection.php @@ -1,30 +1,24 @@ setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - $this->connection = $connection; } public function exec(string $sql): int @@ -40,20 +34,12 @@ public function exec(string $sql): int } } - /** - * {@inheritDoc} - */ - public function getServerVersion() + public function getServerVersion(): string { return $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION); } - /** - * {@inheritDoc} - * - * @return Statement - */ - public function prepare(string $sql): StatementInterface + public function prepare(string $sql): Statement { try { $stmt = $this->connection->prepare($sql); @@ -65,7 +51,7 @@ public function prepare(string $sql): StatementInterface } } - public function query(string $sql): ResultInterface + public function query(string $sql): Result { try { $stmt = $this->connection->query($sql); @@ -77,64 +63,66 @@ public function query(string $sql): ResultInterface } } - /** - * {@inheritDoc} - * - * @throws UnknownParameterType - * - * @phpstan-assert ParameterType::* $type - */ - public function quote($value, $type = ParameterType::STRING) + public function quote(string $value): string { - return $this->connection->quote($value, ParameterTypeMap::convertParamType($type)); + return $this->connection->quote($value); } - /** - * {@inheritDoc} - */ - public function lastInsertId($name = null) + public function lastInsertId(): int|string { try { - if ($name === null) { - return $this->connection->lastInsertId(); + $value = $this->connection->lastInsertId(); + } catch (PDOException $exception) { + assert($exception->errorInfo !== null); + [$sqlState] = $exception->errorInfo; + + // if the PDO driver does not support this capability, PDO::lastInsertId() triggers an IM001 SQLSTATE + // see https://www.php.net/manual/en/pdo.lastinsertid.php + if ($sqlState === 'IM001') { + throw IdentityColumnsNotSupported::new(); } - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4687', - 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', - ); + // PDO PGSQL throws a 'lastval is not yet defined in this session' error when no identity value is + // available, with SQLSTATE 55000 'Object Not In Prerequisite State' + if ($sqlState === '55000' && $this->connection->getAttribute(PDO::ATTR_DRIVER_NAME) === 'pgsql') { + throw NoIdentityValue::new($exception); + } - return $this->connection->lastInsertId($name); - } catch (PDOException $exception) { throw Exception::new($exception); } + + // pdo_mysql & pdo_sqlite return '0', pdo_sqlsrv returns '' or false depending on the PHP version + if ($value === '0' || $value === '' || $value === false) { + throw NoIdentityValue::new(); + } + + return $value; } - public function beginTransaction(): bool + public function beginTransaction(): void { try { - return $this->connection->beginTransaction(); + $this->connection->beginTransaction(); } catch (PDOException $exception) { - throw DriverPDOException::new($exception); + throw Exception::new($exception); } } - public function commit(): bool + public function commit(): void { try { - return $this->connection->commit(); + $this->connection->commit(); } catch (PDOException $exception) { - throw DriverPDOException::new($exception); + throw Exception::new($exception); } } - public function rollBack(): bool + public function rollBack(): void { try { - return $this->connection->rollBack(); + $this->connection->rollBack(); } catch (PDOException $exception) { - throw DriverPDOException::new($exception); + throw Exception::new($exception); } } @@ -142,17 +130,4 @@ public function getNativeConnection(): PDO { return $this->connection; } - - /** @deprecated Call {@see getNativeConnection()} instead. */ - public function getWrappedConnection(): PDO - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5037', - '%s is deprecated, call getNativeConnection() instead.', - __METHOD__, - ); - - return $this->getNativeConnection(); - } } diff --git a/doctrine/dbal/src/Driver/PDO/Exception/InvalidConfiguration.php b/doctrine/dbal/src/Driver/PDO/Exception/InvalidConfiguration.php new file mode 100644 index 000000000..e9f045293 --- /dev/null +++ b/doctrine/dbal/src/Driver/PDO/Exception/InvalidConfiguration.php @@ -0,0 +1,22 @@ +doConnect( diff --git a/doctrine/dbal/src/Driver/PDO/OCI/Driver.php b/doctrine/dbal/src/Driver/PDO/OCI/Driver.php index a66a5cde8..49882b0d6 100644 --- a/doctrine/dbal/src/Driver/PDO/OCI/Driver.php +++ b/doctrine/dbal/src/Driver/PDO/OCI/Driver.php @@ -1,36 +1,45 @@ doConnect( diff --git a/doctrine/dbal/src/Driver/PDO/PDOConnect.php b/doctrine/dbal/src/Driver/PDO/PDOConnect.php index 43bb3c93b..928e774e9 100644 --- a/doctrine/dbal/src/Driver/PDO/PDOConnect.php +++ b/doctrine/dbal/src/Driver/PDO/PDOConnect.php @@ -19,7 +19,7 @@ private function doConnect( string $username, #[SensitiveParameter] string $password, - array $options + array $options, ): PDO { if (PHP_VERSION_ID < 80400) { return new PDO($dsn, $username, $password, $options); diff --git a/doctrine/dbal/src/Driver/PDO/PDOException.php b/doctrine/dbal/src/Driver/PDO/PDOException.php deleted file mode 100644 index 4b0b177d2..000000000 --- a/doctrine/dbal/src/Driver/PDO/PDOException.php +++ /dev/null @@ -1,29 +0,0 @@ -message, 0, $previous); - - $exception->errorInfo = $previous->errorInfo; - $exception->code = $previous->code; - $exception->sqlState = $previous->errorInfo[0] ?? null; - - return $exception; - } - - public function getSQLState(): ?string - { - return $this->sqlState; - } -} diff --git a/doctrine/dbal/src/Driver/PDO/ParameterTypeMap.php b/doctrine/dbal/src/Driver/PDO/ParameterTypeMap.php deleted file mode 100644 index af705b032..000000000 --- a/doctrine/dbal/src/Driver/PDO/ParameterTypeMap.php +++ /dev/null @@ -1,49 +0,0 @@ - PDO::PARAM_NULL, - ParameterType::INTEGER => PDO::PARAM_INT, - ParameterType::STRING => PDO::PARAM_STR, - ParameterType::ASCII => PDO::PARAM_STR, - ParameterType::BINARY => PDO::PARAM_LOB, - ParameterType::LARGE_OBJECT => PDO::PARAM_LOB, - ParameterType::BOOLEAN => PDO::PARAM_BOOL, - ]; - - /** - * Converts DBAL parameter type to PDO parameter type - * - * @phpstan-return PDO::PARAM_* - * - * @throws UnknownParameterType - * - * @phpstan-assert ParameterType::* $type - */ - public static function convertParamType(int $type): int - { - if (! isset(self::PARAM_TYPE_MAP[$type])) { - throw UnknownParameterType::new($type); - } - - return self::PARAM_TYPE_MAP[$type]; - } - - private function __construct() - { - } - - private function __clone() - { - } -} diff --git a/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php b/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php index b62c62318..f7016b7ad 100644 --- a/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php +++ b/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php @@ -1,17 +1,21 @@ doConnect( @@ -88,27 +96,6 @@ private function constructPdoDsn(array $params): string if (isset($params['dbname'])) { $dsn .= 'dbname=' . $params['dbname'] . ';'; - } elseif (isset($params['default_dbname'])) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5705', - 'The "default_dbname" connection parameter is deprecated. Use "dbname" instead.', - ); - - $dsn .= 'dbname=' . $params['default_dbname'] . ';'; - } else { - if (isset($params['user']) && $params['user'] !== 'postgres') { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5705', - 'Relying on the DBAL connecting to the "postgres" database by default is deprecated.' - . ' Unless you want to have the server determine the default database for the connection,' - . ' specify the database name explicitly.', - ); - } - - // Used for temporary connections to allow operations like dropping the database currently connected to. - $dsn .= 'dbname=postgres;'; } if (isset($params['sslmode'])) { diff --git a/doctrine/dbal/src/Driver/PDO/Result.php b/doctrine/dbal/src/Driver/PDO/Result.php index 506fdd4a9..ca2ff455c 100644 --- a/doctrine/dbal/src/Driver/PDO/Result.php +++ b/doctrine/dbal/src/Driver/PDO/Result.php @@ -5,40 +5,30 @@ namespace Doctrine\DBAL\Driver\PDO; use Doctrine\DBAL\Driver\Result as ResultInterface; +use Doctrine\DBAL\Exception\InvalidColumnIndex; use PDO; use PDOException; use PDOStatement; +use ValueError; final class Result implements ResultInterface { - private PDOStatement $statement; - /** @internal The result can be only instantiated by its driver connection or statement. */ - public function __construct(PDOStatement $statement) + public function __construct(private readonly PDOStatement $statement) { - $this->statement = $statement; } - /** - * {@inheritDoc} - */ - public function fetchNumeric() + public function fetchNumeric(): array|false { return $this->fetch(PDO::FETCH_NUM); } - /** - * {@inheritDoc} - */ - public function fetchAssociative() + public function fetchAssociative(): array|false { return $this->fetch(PDO::FETCH_ASSOC); } - /** - * {@inheritDoc} - */ - public function fetchOne() + public function fetchOne(): mixed { return $this->fetch(PDO::FETCH_COLUMN); } @@ -85,6 +75,24 @@ public function columnCount(): int } } + /** @throws Exception */ + public function getColumnName(int $index): string + { + try { + $meta = $this->statement->getColumnMeta($index); + } catch (ValueError $exception) { + throw InvalidColumnIndex::new($index, $exception); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + + if ($meta === false) { + throw InvalidColumnIndex::new($index); + } + + return $meta['name']; + } + public function free(): void { $this->statement->closeCursor(); @@ -93,11 +101,9 @@ public function free(): void /** * @phpstan-param PDO::FETCH_* $mode * - * @return mixed - * * @throws Exception */ - private function fetch(int $mode) + private function fetch(int $mode): mixed { try { return $this->statement->fetch($mode); diff --git a/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php b/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php index 9015f5558..78ba7f821 100644 --- a/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php +++ b/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php @@ -1,70 +1,29 @@ connection = $connection; } - public function prepare(string $sql): StatementInterface + public function prepare(string $sql): Statement { return new Statement( $this->connection->prepare($sql), ); } - /** - * {@inheritDoc} - */ - public function lastInsertId($name = null) - { - if ($name === null) { - return parent::lastInsertId($name); - } - - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4687', - 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', - ); - - $statement = $this->prepare( - 'SELECT CONVERT(VARCHAR(MAX), current_value) FROM sys.sequences WHERE name = ?', - ); - $statement->bindValue(1, $name); - - return $statement->execute() - ->fetchOne(); - } - public function getNativeConnection(): PDO { return $this->connection->getNativeConnection(); } - - /** @deprecated Call {@see getNativeConnection()} instead. */ - public function getWrappedConnection(): PDO - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5037', - '%s is deprecated, call getNativeConnection() instead.', - __METHOD__, - ); - - return $this->connection->getWrappedConnection(); - } } diff --git a/doctrine/dbal/src/Driver/PDO/SQLSrv/Driver.php b/doctrine/dbal/src/Driver/PDO/SQLSrv/Driver.php index fe2064aab..2d7490b8a 100644 --- a/doctrine/dbal/src/Driver/PDO/SQLSrv/Driver.php +++ b/doctrine/dbal/src/Driver/PDO/SQLSrv/Driver.php @@ -1,5 +1,7 @@ doConnect( diff --git a/doctrine/dbal/src/Driver/PDO/SQLSrv/Statement.php b/doctrine/dbal/src/Driver/PDO/SQLSrv/Statement.php index a63ff79d7..44cecc97f 100644 --- a/doctrine/dbal/src/Driver/PDO/SQLSrv/Statement.php +++ b/doctrine/dbal/src/Driver/PDO/SQLSrv/Statement.php @@ -1,109 +1,46 @@ statement = $statement; } - /** - * {@inheritDoc} - * - * @deprecated Use {@see bindValue()} instead. - * - * @param string|int $param - * @param mixed $variable - * @param int $type - * @param int|null $length - * @param mixed $driverOptions The usage of the argument is deprecated. - * - * @throws UnknownParameterType - * - * @phpstan-assert ParameterType::* $type - */ - public function bindParam( - $param, - &$variable, - $type = ParameterType::STRING, - $length = null, - $driverOptions = null - ): bool { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5563', - '%s is deprecated. Use bindValue() instead.', - __METHOD__, - ); - - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindParam() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - - if (func_num_args() > 4) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4533', - 'The $driverOptions argument of Statement::bindParam() is deprecated.', - ); - } - + public function bindValue(int|string $param, mixed $value, ParameterType $type): void + { switch ($type) { case ParameterType::LARGE_OBJECT: case ParameterType::BINARY: - $driverOptions ??= PDO::SQLSRV_ENCODING_BINARY; - + $this->statement->bindParamWithDriverOptions( + $param, + $value, + $type, + PDO::SQLSRV_ENCODING_BINARY, + ); break; case ParameterType::ASCII: - $type = ParameterType::STRING; - $length = 0; - $driverOptions = PDO::SQLSRV_ENCODING_SYSTEM; + $this->statement->bindParamWithDriverOptions( + $param, + $value, + ParameterType::STRING, + PDO::SQLSRV_ENCODING_SYSTEM, + ); break; - } - return $this->statement->bindParam($param, $variable, $type, $length ?? 0, $driverOptions); - } - - /** - * @throws UnknownParameterType - * - * {@inheritDoc} - * - * @phpstan-assert ParameterType::* $type - */ - public function bindValue($param, $value, $type = ParameterType::STRING): bool - { - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindValue() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); + default: + $this->statement->bindValue($param, $value, $type); } - - return $this->bindParam($param, $value, $type); } } diff --git a/doctrine/dbal/src/Driver/PDO/SQLite/Driver.php b/doctrine/dbal/src/Driver/PDO/SQLite/Driver.php index 2fa0e700c..fbd418753 100644 --- a/doctrine/dbal/src/Driver/PDO/SQLite/Driver.php +++ b/doctrine/dbal/src/Driver/PDO/SQLite/Driver.php @@ -1,18 +1,19 @@ constructPdoDsn(array_intersect_key($params, ['path' => true, 'memory' => true])), $params['user'] ?? '', $params['password'] ?? '', - $driverOptions, + $params['driverOptions'] ?? [], ); } catch (PDOException $exception) { throw Exception::new($exception); } - UserDefinedFunctions::register( - $pdo instanceof Sqlite ? [$pdo, 'createFunction'] : [$pdo, 'sqliteCreateFunction'], - $userDefinedFunctions, - ); - return new Connection($pdo); } diff --git a/doctrine/dbal/src/Driver/PDO/Statement.php b/doctrine/dbal/src/Driver/PDO/Statement.php index e631fad3d..a2694fcb4 100644 --- a/doctrine/dbal/src/Driver/PDO/Statement.php +++ b/doctrine/dbal/src/Driver/PDO/Statement.php @@ -1,137 +1,80 @@ stmt = $stmt; } - /** - * {@inheritDoc} - * - * @throws UnknownParameterType - * - * @phpstan-assert ParameterType::* $type - */ - public function bindValue($param, $value, $type = ParameterType::STRING) + public function bindValue(int|string $param, mixed $value, ParameterType $type): void { - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindValue() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - - $pdoType = ParameterTypeMap::convertParamType($type); + $pdoType = $this->convertParamType($type); try { - return $this->stmt->bindValue($param, $value, $pdoType); + $this->stmt->bindValue($param, $value, $pdoType); } catch (PDOException $exception) { throw Exception::new($exception); } } /** - * {@inheritDoc} - * - * @deprecated Use {@see bindValue()} instead. - * - * @param mixed $param - * @param mixed $variable - * @param int $type - * @param int|null $length - * @param mixed $driverOptions The usage of the argument is deprecated. + * @internal Driver options can be only specified by a PDO-based driver. * - * @throws UnknownParameterType - * - * @phpstan-assert ParameterType::* $type + * @throws ExceptionInterface */ - public function bindParam( - $param, - &$variable, - $type = ParameterType::STRING, - $length = null, - $driverOptions = null - ): bool { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5563', - '%s is deprecated. Use bindValue() instead.', - __METHOD__, - ); - - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindParam() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - - if (func_num_args() > 4) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4533', - 'The $driverOptions argument of Statement::bindParam() is deprecated.', - ); - } - - $pdoType = ParameterTypeMap::convertParamType($type); + public function bindParamWithDriverOptions( + string|int $param, + mixed &$variable, + ParameterType $type, + mixed $driverOptions, + ): void { + $pdoType = $this->convertParamType($type); try { - return $this->stmt->bindParam( - $param, - $variable, - $pdoType, - $length ?? 0, - ...array_slice(func_get_args(), 4), - ); + $this->stmt->bindParam($param, $variable, $pdoType, 0, $driverOptions); } catch (PDOException $exception) { throw Exception::new($exception); } } - /** - * {@inheritDoc} - */ - public function execute($params = null): ResultInterface + public function execute(): Result { - if ($params !== null) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5556', - 'Passing $params to Statement::execute() is deprecated. Bind parameters using' - . ' Statement::bindParam() or Statement::bindValue() instead.', - ); - } - try { - $this->stmt->execute($params); + $this->stmt->execute(); } catch (PDOException $exception) { throw Exception::new($exception); } return new Result($this->stmt); } + + /** + * Converts DBAL parameter type to PDO parameter type + * + * @phpstan-return PDO::PARAM_* + */ + private function convertParamType(ParameterType $type): int + { + return match ($type) { + ParameterType::NULL => PDO::PARAM_NULL, + ParameterType::INTEGER => PDO::PARAM_INT, + ParameterType::STRING, + ParameterType::ASCII => PDO::PARAM_STR, + ParameterType::BINARY, + ParameterType::LARGE_OBJECT => PDO::PARAM_LOB, + ParameterType::BOOLEAN => PDO::PARAM_BOOL, + }; + } } diff --git a/doctrine/dbal/src/Driver/PgSQL/Connection.php b/doctrine/dbal/src/Driver/PgSQL/Connection.php index 378e8ed7a..043c2de4b 100644 --- a/doctrine/dbal/src/Driver/PgSQL/Connection.php +++ b/doctrine/dbal/src/Driver/PgSQL/Connection.php @@ -1,21 +1,16 @@ connection = $connection; - $this->parser = new Parser(false); + $this->parser = new Parser(false); } public function __destruct() { - if (! isset($this->connection)) { - return; + // @phpstan-ignore isset.initializedProperty + if (isset($this->connection)) { + @pg_close($this->connection); } - - @pg_close($this->connection); } public function prepare(string $sql): Statement { $visitor = new ConvertParameters(); + + /** @phpstan-ignore missingType.checkedException */ $this->parser->parse($sql, $visitor); $statementName = uniqid('dbal', true); @@ -94,13 +76,12 @@ public function query(string $sql): Result } /** {@inheritDoc} */ - public function quote($value, $type = ParameterType::STRING) + public function quote(string $value): string { - if ($type === ParameterType::BINARY || $type === ParameterType::LARGE_OBJECT) { - return sprintf("'%s'", pg_escape_bytea($this->connection, $value)); - } + $quotedValue = pg_escape_literal($this->connection, $value); + assert($quotedValue !== false); - return pg_escape_literal($this->connection, $value); + return $quotedValue; } public function exec(string $sql): int @@ -109,43 +90,32 @@ public function exec(string $sql): int } /** {@inheritDoc} */ - public function lastInsertId($name = null) + public function lastInsertId(): int|string { - if ($name !== null) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4687', - 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', - ); - - return $this->query(sprintf('SELECT CURRVAL(%s)', $this->quote($name)))->fetchOne(); + try { + return $this->query('SELECT LASTVAL()')->fetchOne(); + } catch (Exception $exception) { + if ($exception->getSQLState() === '55000') { + throw NoIdentityValue::new($exception); + } + + throw $exception; } - - return $this->query('SELECT LASTVAL()')->fetchOne(); } - /** @return true */ - public function beginTransaction(): bool + public function beginTransaction(): void { $this->exec('BEGIN'); - - return true; } - /** @return true */ - public function commit(): bool + public function commit(): void { $this->exec('COMMIT'); - - return true; } - /** @return true */ - public function rollBack(): bool + public function rollBack(): void { $this->exec('ROLLBACK'); - - return true; } public function getServerVersion(): string @@ -153,8 +123,7 @@ public function getServerVersion(): string return (string) pg_version($this->connection)['server']; } - /** @return PgSqlConnection|resource */ - public function getNativeConnection() + public function getNativeConnection(): PgSqlConnection { return $this->connection; } diff --git a/doctrine/dbal/src/Driver/PgSQL/Driver.php b/doctrine/dbal/src/Driver/PgSQL/Driver.php index 1cdaee1f2..6bdcb6099 100644 --- a/doctrine/dbal/src/Driver/PgSQL/Driver.php +++ b/doctrine/dbal/src/Driver/PgSQL/Driver.php @@ -1,5 +1,7 @@ $params['sslmode'] ?? null, 'gssencmode' => $params['gssencmode'] ?? null, ], - static fn ($value) => $value !== '' && $value !== null, + static fn (int|string|null $value) => $value !== '' && $value !== null, ); return implode(' ', array_map( - static fn ($value, string $key) => sprintf("%s='%s'", $key, addslashes($value)), + static fn (int|string $value, string $key) => sprintf("%s='%s'", $key, addslashes((string) $value)), array_values($components), array_keys($components), )); diff --git a/doctrine/dbal/src/Driver/PgSQL/Exception.php b/doctrine/dbal/src/Driver/PgSQL/Exception.php index 5e7086ba0..91f4e3de1 100644 --- a/doctrine/dbal/src/Driver/PgSQL/Exception.php +++ b/doctrine/dbal/src/Driver/PgSQL/Exception.php @@ -1,5 +1,7 @@ result = $result; } public function __destruct() { - if (! isset($this->result)) { - return; + if (isset($this->result)) { + $this->free(); } - - $this->free(); } /** {@inheritDoc} */ - public function fetchNumeric() + public function fetchNumeric(): array|false { if ($this->result === null) { return false; @@ -76,7 +62,7 @@ public function fetchNumeric() } /** {@inheritDoc} */ - public function fetchAssociative() + public function fetchAssociative(): array|false { if ($this->result === null) { return false; @@ -91,7 +77,7 @@ public function fetchAssociative() } /** {@inheritDoc} */ - public function fetchOne() + public function fetchOne(): mixed { return FetchUtils::fetchOne($this); } @@ -103,17 +89,11 @@ public function fetchAllNumeric(): array return []; } - $resultSet = pg_fetch_all($this->result, PGSQL_NUM); - // On PHP 7.4, pg_fetch_all() might return false for empty result sets. - if ($resultSet === false) { - return []; - } - $types = $this->fetchNumericColumnTypes(); return array_map( fn (array $row) => $this->mapNumericRow($row, $types), - $resultSet, + pg_fetch_all($this->result, PGSQL_NUM), ); } @@ -124,17 +104,11 @@ public function fetchAllAssociative(): array return []; } - $resultSet = pg_fetch_all($this->result, PGSQL_ASSOC); - // On PHP 7.4, pg_fetch_all() might return false for empty result sets. - if ($resultSet === false) { - return []; - } - $types = $this->fetchAssociativeColumnTypes(); return array_map( fn (array $row) => $this->mapAssociativeRow($row, $types), - $resultSet, + pg_fetch_all($this->result, PGSQL_ASSOC), ); } @@ -171,6 +145,19 @@ public function columnCount(): int return pg_num_fields($this->result); } + public function getColumnName(int $index): string + { + if ($this->result === null) { + throw InvalidColumnIndex::new($index); + } + + try { + return pg_field_name($this->result, $index); + } catch (ValueError) { + throw InvalidColumnIndex::new($index); + } + } + public function free(): void { if ($this->result === null) { @@ -244,39 +231,23 @@ private function mapAssociativeRow(array $row, array $types): array return $mappedRow; } - /** @return string|int|float|bool|null */ - private function mapType(string $postgresType, ?string $value) + private function mapType(string $postgresType, ?string $value): string|int|float|bool|null { if ($value === null) { return null; } - switch ($postgresType) { - case 'bool': - switch ($value) { - case 't': - return true; - case 'f': - return false; - } - - throw UnexpectedValue::new($value, $postgresType); - - case 'bytea': - return hex2bin(substr($value, 2)); - - case 'float4': - case 'float8': - return (float) $value; - - case 'int2': - case 'int4': - return (int) $value; - - case 'int8': - return PHP_INT_SIZE >= 8 ? (int) $value : $value; - } - - return $value; + return match ($postgresType) { + 'bool' => match ($value) { + 't' => true, + 'f' => false, + default => throw UnexpectedValue::new($value, $postgresType), + }, + 'bytea' => hex2bin(substr($value, 2)), + 'float4', 'float8' => (float) $value, + 'int2', 'int4' => (int) $value, + 'int8' => PHP_INT_SIZE >= 8 ? (int) $value : $value, + default => $value, + }; } } diff --git a/doctrine/dbal/src/Driver/PgSQL/Statement.php b/doctrine/dbal/src/Driver/PgSQL/Statement.php index 43af9f1b6..d6aa2f90f 100644 --- a/doctrine/dbal/src/Driver/PgSQL/Statement.php +++ b/doctrine/dbal/src/Driver/PgSQL/Statement.php @@ -1,20 +1,15 @@ */ - private array $parameterMap; - /** @var array */ private array $parameters = []; - /** @var array */ + /** @phpstan-var array */ private array $parameterTypes = []; - /** - * @param PgSqlConnection|resource $connection - * @param array $parameterMap - */ - public function __construct($connection, string $name, array $parameterMap) - { - if (! is_resource($connection) && ! $connection instanceof PgSqlConnection) { - throw new TypeError(sprintf( - 'Expected connection to be a resource or an instance of %s, got %s.', - PgSqlConnection::class, - is_object($connection) ? get_class($connection) : gettype($connection), - )); - } - - $this->connection = $connection; - $this->name = $name; - $this->parameterMap = $parameterMap; + /** @param array $parameterMap */ + public function __construct( + private readonly PgSqlConnection $connection, + private readonly string $name, + private readonly array $parameterMap, + ) { } public function __destruct() { + // @phpstan-ignore isset.initializedProperty if (! isset($this->connection)) { return; } @@ -75,7 +51,7 @@ public function __destruct() } /** {@inheritDoc} */ - public function bindValue($param, $value, $type = ParameterType::STRING): bool + public function bindValue(int|string $param, mixed $value, ParameterType $type = ParameterType::STRING): void { if (! isset($this->parameterMap[$param])) { throw UnknownParameter::new((string) $param); @@ -92,82 +68,21 @@ public function bindValue($param, $value, $type = ParameterType::STRING): bool $this->parameters[$this->parameterMap[$param]] = $value; $this->parameterTypes[$this->parameterMap[$param]] = $type; } - - return true; } /** {@inheritDoc} */ - public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool + public function execute(): Result { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5563', - '%s is deprecated. Use bindValue() instead.', - __METHOD__, - ); - - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindParam() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - - if (func_num_args() > 4) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4533', - 'The $driverOptions argument of Statement::bindParam() is deprecated.', - ); - } - - if (! isset($this->parameterMap[$param])) { - throw UnknownParameter::new((string) $param); - } - - $this->parameters[$this->parameterMap[$param]] = &$variable; - $this->parameterTypes[$this->parameterMap[$param]] = $type; - - return true; - } - - /** {@inheritDoc} */ - public function execute($params = null): Result - { - if ($params !== null) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5556', - 'Passing $params to Statement::execute() is deprecated. Bind parameters using' - . ' Statement::bindParam() or Statement::bindValue() instead.', - ); - - foreach ($params as $param => $value) { - if (is_int($param)) { - $this->bindValue($param + 1, $value, ParameterType::STRING); - } else { - $this->bindValue($param, $value, ParameterType::STRING); - } - } - } - ksort($this->parameters); $escapedParameters = []; foreach ($this->parameters as $parameter => $value) { - switch ($this->parameterTypes[$parameter]) { - case ParameterType::BINARY: - case ParameterType::LARGE_OBJECT: - $escapedParameters[] = $value === null ? null : pg_escape_bytea( - $this->connection, - is_resource($value) ? stream_get_contents($value) : $value, - ); - break; - default: - $escapedParameters[] = $value; - } + $escapedParameters[] = match ($this->parameterTypes[$parameter]) { + ParameterType::BINARY, ParameterType::LARGE_OBJECT => $value === null + ? null + : pg_escape_bytea($this->connection, is_resource($value) ? stream_get_contents($value) : $value), + default => $value, + }; } if (@pg_send_execute($this->connection, $this->name, $escapedParameters) !== true) { diff --git a/doctrine/dbal/src/Driver/Result.php b/doctrine/dbal/src/Driver/Result.php index 7843a9589..c26e38cb8 100644 --- a/doctrine/dbal/src/Driver/Result.php +++ b/doctrine/dbal/src/Driver/Result.php @@ -6,6 +6,8 @@ /** * Driver-level statement execution result. + * + * @method string getColumnName(int $index) */ interface Result { @@ -16,7 +18,7 @@ interface Result * * @throws Exception */ - public function fetchNumeric(); + public function fetchNumeric(): array|false; /** * Returns the next row of the result as an associative array or FALSE if there are no more rows. @@ -25,16 +27,14 @@ public function fetchNumeric(); * * @throws Exception */ - public function fetchAssociative(); + public function fetchAssociative(): array|false; /** * Returns the first value of the next row of the result or FALSE if there are no more rows. * - * @return mixed|false - * * @throws Exception */ - public function fetchOne(); + public function fetchOne(): mixed; /** * Returns an array containing all of the result rows represented as numeric arrays. @@ -70,11 +70,13 @@ public function fetchFirstColumn(): array; * some database drivers may return the number of rows returned by that query. However, this behaviour * is not guaranteed for all drivers and should not be relied on in portable applications. * - * @return int The number of rows. + * If the number of rows exceeds {@see PHP_INT_MAX}, it might be returned as string if the driver supports it. + * + * @return int|numeric-string * * @throws Exception */ - public function rowCount(): int; + public function rowCount(): int|string; /** * Returns the number of columns in the result diff --git a/doctrine/dbal/src/Driver/SQLSrv/Connection.php b/doctrine/dbal/src/Driver/SQLSrv/Connection.php index 16e45d110..71050f17a 100644 --- a/doctrine/dbal/src/Driver/SQLSrv/Connection.php +++ b/doctrine/dbal/src/Driver/SQLSrv/Connection.php @@ -1,17 +1,13 @@ connection = $connection; } - /** - * {@inheritDoc} - */ - public function getServerVersion() + public function getServerVersion(): string { $serverInfo = sqlsrv_server_info($this->connection); return $serverInfo['SQLServerVersion']; } - public function prepare(string $sql): DriverStatement + public function prepare(string $sql): Statement { return new Statement($this->connection, $sql); } - public function query(string $sql): ResultInterface + public function query(string $sql): Result { return $this->prepare($sql)->execute(); } - /** - * {@inheritDoc} - */ - public function quote($value, $type = ParameterType::STRING) + public function quote(string $value): string { - if (is_int($value)) { - return $value; - } - - if (is_float($value)) { - return sprintf('%F', $value); - } - return "'" . str_replace("'", "''", $value) . "'"; } @@ -88,52 +66,38 @@ public function exec(string $sql): int return $rowsAffected; } - /** - * {@inheritDoc} - */ - public function lastInsertId($name = null) + public function lastInsertId(): int|string { - if ($name !== null) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4687', - 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', - ); - - $result = $this->prepare('SELECT CONVERT(VARCHAR(MAX), current_value) FROM sys.sequences WHERE name = ?') - ->execute([$name]); - } else { - $result = $this->query('SELECT @@IDENTITY'); + $result = $this->query('SELECT @@IDENTITY'); + + $lastInsertId = $result->fetchOne(); + + if ($lastInsertId === null) { + throw NoIdentityValue::new(); } - return $result->fetchOne(); + return $lastInsertId; } - public function beginTransaction(): bool + public function beginTransaction(): void { if (! sqlsrv_begin_transaction($this->connection)) { throw Error::new(); } - - return true; } - public function commit(): bool + public function commit(): void { if (! sqlsrv_commit($this->connection)) { throw Error::new(); } - - return true; } - public function rollBack(): bool + public function rollBack(): void { if (! sqlsrv_rollback($this->connection)) { throw Error::new(); } - - return true; } /** @return resource */ diff --git a/doctrine/dbal/src/Driver/SQLSrv/Driver.php b/doctrine/dbal/src/Driver/SQLSrv/Driver.php index fcbdb7734..c9c2c3430 100644 --- a/doctrine/dbal/src/Driver/SQLSrv/Driver.php +++ b/doctrine/dbal/src/Driver/SQLSrv/Driver.php @@ -1,5 +1,7 @@ statement = $stmt; } - /** - * {@inheritDoc} - */ - public function fetchNumeric() + public function fetchNumeric(): array|false { return $this->fetch(SQLSRV_FETCH_NUMERIC); } - /** - * {@inheritDoc} - */ - public function fetchAssociative() + public function fetchAssociative(): array|false { return $this->fetch(SQLSRV_FETCH_ASSOC); } - /** - * {@inheritDoc} - */ - public function fetchOne() + public function fetchOne(): mixed { return FetchUtils::fetchOne($this); } @@ -100,6 +89,17 @@ public function columnCount(): int return 0; } + public function getColumnName(int $index): string + { + $meta = sqlsrv_field_metadata($this->statement); + + if ($meta === false || ! isset($meta[$index])) { + throw InvalidColumnIndex::new($index); + } + + return $meta[$index]['Name']; + } + public function free(): void { // emulate it by fetching and discarding rows, similarly to what PDO does in this case @@ -110,8 +110,7 @@ public function free(): void } } - /** @return mixed|false */ - private function fetch(int $fetchType) + private function fetch(int $fetchType): mixed { return sqlsrv_fetch_array($this->statement, $fetchType) ?? false; } diff --git a/doctrine/dbal/src/Driver/SQLSrv/Statement.php b/doctrine/dbal/src/Driver/SQLSrv/Statement.php index 227c33456..98faaf2fa 100644 --- a/doctrine/dbal/src/Driver/SQLSrv/Statement.php +++ b/doctrine/dbal/src/Driver/SQLSrv/Statement.php @@ -1,16 +1,15 @@ + * @var array */ private array $types = []; @@ -67,98 +54,26 @@ final class Statement implements StatementInterface * @internal The statement can be only instantiated by its driver connection. * * @param resource $conn - * @param string $sql */ - public function __construct($conn, $sql) - { - $this->conn = $conn; - $this->sql = $sql; - - if (stripos($sql, 'INSERT INTO ') !== 0) { - return; + public function __construct( + private readonly mixed $conn, + private string $sql, + ) { + if (stripos($sql, 'INSERT INTO ') === 0) { + $this->sql .= self::LAST_INSERT_ID_SQL; } - - $this->sql .= self::LAST_INSERT_ID_SQL; } - /** - * {@inheritDoc} - */ - public function bindValue($param, $value, $type = ParameterType::STRING): bool + public function bindValue(int|string $param, mixed $value, ParameterType $type): void { assert(is_int($param)); - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindValue() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - $this->variables[$param] = $value; $this->types[$param] = $type; - - return true; } - /** - * {@inheritDoc} - * - * @deprecated Use {@see bindValue()} instead. - */ - public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool + public function execute(): Result { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5563', - '%s is deprecated. Use bindValue() instead.', - __METHOD__, - ); - - assert(is_int($param)); - - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindParam() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - - $this->variables[$param] =& $variable; - $this->types[$param] = $type; - - // unset the statement resource if it exists as the new one will need to be bound to the new variable - $this->stmt = null; - - return true; - } - - /** - * {@inheritDoc} - */ - public function execute($params = null): ResultInterface - { - if ($params !== null) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5556', - 'Passing $params to Statement::execute() is deprecated. Bind parameters using' - . ' Statement::bindParam() or Statement::bindValue() instead.', - ); - - foreach ($params as $key => $val) { - if (is_int($key)) { - $this->bindValue($key + 1, $val, ParameterType::STRING); - } else { - $this->bindValue($key, $val, ParameterType::STRING); - } - } - } - $this->stmt ??= $this->prepare(); if (! sqlsrv_execute($this->stmt)) { diff --git a/doctrine/dbal/src/Driver/SQLite3/Connection.php b/doctrine/dbal/src/Driver/SQLite3/Connection.php index 91b9b5ffe..1e9af93c9 100644 --- a/doctrine/dbal/src/Driver/SQLite3/Connection.php +++ b/doctrine/dbal/src/Driver/SQLite3/Connection.php @@ -1,22 +1,21 @@ connection = $connection; } public function prepare(string $sql): Statement @@ -45,8 +44,7 @@ public function query(string $sql): Result return new Result($result, $this->connection->changes()); } - /** @inheritDoc */ - public function quote($value, $type = ParameterType::STRING): string + public function quote(string $value): string { return sprintf('\'%s\'', SQLite3::escapeString($value)); } @@ -62,36 +60,40 @@ public function exec(string $sql): int return $this->connection->changes(); } - /** @inheritDoc */ - public function lastInsertId($name = null): int + public function lastInsertId(): int { - return $this->connection->lastInsertRowID(); + $value = $this->connection->lastInsertRowID(); + if ($value === 0) { + throw NoIdentityValue::new(); + } + + return $value; } - public function beginTransaction(): bool + public function beginTransaction(): void { try { - return $this->connection->exec('BEGIN'); + $this->connection->exec('BEGIN'); } catch (\Exception $e) { - return false; + throw Exception::new($e); } } - public function commit(): bool + public function commit(): void { try { - return $this->connection->exec('COMMIT'); + $this->connection->exec('COMMIT'); } catch (\Exception $e) { - return false; + throw Exception::new($e); } } - public function rollBack(): bool + public function rollBack(): void { try { - return $this->connection->exec('ROLLBACK'); + $this->connection->exec('ROLLBACK'); } catch (\Exception $e) { - return false; + throw Exception::new($e); } } diff --git a/doctrine/dbal/src/Driver/SQLite3/Driver.php b/doctrine/dbal/src/Driver/SQLite3/Driver.php index fecc4819d..e6996d366 100644 --- a/doctrine/dbal/src/Driver/SQLite3/Driver.php +++ b/doctrine/dbal/src/Driver/SQLite3/Driver.php @@ -1,9 +1,10 @@ enableExceptions(true); - UserDefinedFunctions::register([$connection, 'createFunction']); - return new Connection($connection); } } diff --git a/doctrine/dbal/src/Driver/SQLite3/Exception.php b/doctrine/dbal/src/Driver/SQLite3/Exception.php index 3219fc327..be1a225bf 100644 --- a/doctrine/dbal/src/Driver/SQLite3/Exception.php +++ b/doctrine/dbal/src/Driver/SQLite3/Exception.php @@ -1,5 +1,7 @@ result = $result; - $this->changes = $changes; + $this->result = $result; } - /** @inheritDoc */ - public function fetchNumeric() + public function fetchNumeric(): array|false { if ($this->result === null) { return false; @@ -31,8 +31,7 @@ public function fetchNumeric() return $this->result->fetchArray(SQLITE3_NUM); } - /** @inheritDoc */ - public function fetchAssociative() + public function fetchAssociative(): array|false { if ($this->result === null) { return false; @@ -41,8 +40,7 @@ public function fetchAssociative() return $this->result->fetchArray(SQLITE3_ASSOC); } - /** @inheritDoc */ - public function fetchOne() + public function fetchOne(): mixed { return FetchUtils::fetchOne($this); } @@ -79,6 +77,21 @@ public function columnCount(): int return $this->result->numColumns(); } + public function getColumnName(int $index): string + { + if ($this->result === null) { + throw InvalidColumnIndex::new($index); + } + + $name = $this->result->columnName($index); + + if ($name === false) { + throw InvalidColumnIndex::new($index); + } + + return $name; + } + public function free(): void { if ($this->result === null) { diff --git a/doctrine/dbal/src/Driver/SQLite3/Statement.php b/doctrine/dbal/src/Driver/SQLite3/Statement.php index 01c3b8bbb..eb5566718 100644 --- a/doctrine/dbal/src/Driver/SQLite3/Statement.php +++ b/doctrine/dbal/src/Driver/SQLite3/Statement.php @@ -1,17 +1,15 @@ SQLITE3_NULL, - ParameterType::INTEGER => SQLITE3_INTEGER, - ParameterType::STRING => SQLITE3_TEXT, - ParameterType::ASCII => SQLITE3_TEXT, - ParameterType::BINARY => SQLITE3_BLOB, - ParameterType::LARGE_OBJECT => SQLITE3_BLOB, - ParameterType::BOOLEAN => SQLITE3_INTEGER, - ]; - - private SQLite3 $connection; - private SQLite3Stmt $statement; + private const TYPE_BLOB = SQLITE3_BLOB; + private const TYPE_INTEGER = SQLITE3_INTEGER; + private const TYPE_NULL = SQLITE3_NULL; + private const TYPE_TEXT = SQLITE3_TEXT; /** @internal The statement can be only instantiated by its driver connection. */ - public function __construct(SQLite3 $connection, SQLite3Stmt $statement) - { - $this->connection = $connection; - $this->statement = $statement; - } - - /** - * @throws UnknownParameterType - * - * {@inheritDoc} - * - * @phpstan-assert ParameterType::* $type - */ - public function bindValue($param, $value, $type = ParameterType::STRING): bool - { - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindValue() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - - return $this->statement->bindValue($param, $value, $this->convertParamType($type)); + public function __construct( + private readonly SQLite3 $connection, + private readonly SQLite3Stmt $statement, + ) { } - /** - * @throws UnknownParameterType - * - * {@inheritDoc} - * - * @phpstan-assert ParameterType::* $type - */ - public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool + public function bindValue(int|string $param, mixed $value, ParameterType $type): void { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5563', - '%s is deprecated. Use bindValue() instead.', - __METHOD__, - ); - - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindParam() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - - return $this->statement->bindParam($param, $variable, $this->convertParamType($type)); + $this->statement->bindValue($param, $value, $this->convertParamType($type)); } - /** @inheritDoc */ - public function execute($params = null): Result + public function execute(): Result { - if ($params !== null) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5556', - 'Passing $params to Statement::execute() is deprecated. Bind parameters using' - . ' Statement::bindParam() or Statement::bindValue() instead.', - ); - - foreach ($params as $param => $value) { - if (is_int($param)) { - $this->bindValue($param + 1, $value, ParameterType::STRING); - } else { - $this->bindValue($param, $value, ParameterType::STRING); - } - } - } - try { $result = $this->statement->execute(); } catch (\Exception $e) { @@ -120,17 +48,14 @@ public function execute($params = null): Result return new Result($result, $this->connection->changes()); } - /** - * @phpstan-return value-of - * - * @phpstan-assert ParameterType::* $type - */ - private function convertParamType(int $type): int + /** @phpstan-return self::TYPE_* */ + private function convertParamType(ParameterType $type): int { - if (! isset(self::PARAM_TYPE_MAP[$type])) { - throw UnknownParameterType::new($type); - } - - return self::PARAM_TYPE_MAP[$type]; + return match ($type) { + ParameterType::NULL => self::TYPE_NULL, + ParameterType::INTEGER, ParameterType::BOOLEAN => self::TYPE_INTEGER, + ParameterType::STRING, ParameterType::ASCII => self::TYPE_TEXT, + ParameterType::BINARY, ParameterType::LARGE_OBJECT => self::TYPE_BLOB, + }; } } diff --git a/doctrine/dbal/src/Driver/ServerInfoAwareConnection.php b/doctrine/dbal/src/Driver/ServerInfoAwareConnection.php deleted file mode 100644 index 5687ab0bb..000000000 --- a/doctrine/dbal/src/Driver/ServerInfoAwareConnection.php +++ /dev/null @@ -1,21 +0,0 @@ -execute() is called. - * - * As mentioned above, the named parameters are not natively supported by the mysqli driver, use executeQuery(), - * fetchAll(), fetchArray(), fetchColumn(), fetchAssoc() methods to have the named parameter emulated by doctrine. - * - * Most parameters are input parameters, that is, parameters that are - * used in a read-only fashion to build up the query. Some drivers support the invocation - * of stored procedures that return data as output parameters, and some also as input/output - * parameters that both send in data and are updated to receive it. - * - * @deprecated Use {@see bindValue()} instead. - * - * @param string|int $param Parameter identifier. For a prepared statement using named placeholders, - * this will be a parameter name of the form :name. For a prepared statement using - * question mark placeholders, this will be the 1-indexed position of the parameter. - * @param mixed $variable Name of the PHP variable to bind to the SQL statement parameter. - * @param int $type Explicit data type for the parameter using the {@see ParameterType} + * @param int|string $param Parameter identifier. For a prepared statement using named placeholders, + * this will be a parameter name of the form :name. For a prepared statement + * using question mark placeholders, this will be the 1-indexed position + * of the parameter. + * @param mixed $value The value to bind to the parameter. + * @param ParameterType $type Explicit data type for the parameter using the {@see ParameterType} * constants. - * @param int|null $length You must specify maxlength when using an OUT bind - * so that PHP allocates enough memory to hold the returned value. - * - * @return bool TRUE on success or FALSE on failure. * * @throws Exception */ - public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null); + public function bindValue(int|string $param, mixed $value, ParameterType $type): void; /** * Executes a prepared statement * - * If the prepared statement included parameter markers, you must either: - * call {@see bindParam()} to bind PHP variables to the parameter markers: - * bound variables pass their value as input and receive the output value, - * if any, of their associated parameter markers or pass an array of input-only - * parameter values. - * - * @param mixed[]|null $params A numeric array of values with as many elements as there are - * bound parameters in the SQL statement being executed. - * * @throws Exception */ - public function execute($params = null): Result; + public function execute(): Result; } diff --git a/doctrine/dbal/src/DriverManager.php b/doctrine/dbal/src/DriverManager.php index 7586bc479..34fbe236e 100644 --- a/doctrine/dbal/src/DriverManager.php +++ b/doctrine/dbal/src/DriverManager.php @@ -1,8 +1,9 @@ , * driver?: key-of, * driverClass?: class-string, * driverOptions?: array, * host?: string, + * memory?: bool, * password?: string, * path?: string, * persistent?: bool, - * platform?: Platforms\AbstractPlatform, * port?: int, * serverVersion?: string, - * url?: string, + * sessionMode?: int, * user?: string, * unix_socket?: string, + * wrapperClass?: class-string, * } * @phpstan-type Params = array{ * application_name?: string, * charset?: string, * dbname?: string, * defaultTableOptions?: array, - * default_dbname?: string, * driver?: key-of, * driverClass?: class-string, * driverOptions?: array, * host?: string, - * keepSlave?: bool, * keepReplica?: bool, - * master?: OverrideParams, * memory?: bool, * password?: string, * path?: string, * persistent?: bool, - * platform?: Platforms\AbstractPlatform, * port?: int, * primary?: OverrideParams, * replica?: array, * serverVersion?: string, - * sharding?: array, - * slaves?: array, - * url?: string, + * sessionMode?: int, * user?: string, * wrapperClass?: class-string, * unix_socket?: string, @@ -79,37 +75,17 @@ final class DriverManager * To add your own driver use the 'driverClass' parameter to {@see DriverManager::getConnection()}. */ private const DRIVER_MAP = [ - 'pdo_mysql' => PDO\MySQL\Driver::class, - 'pdo_sqlite' => PDO\SQLite\Driver::class, - 'pdo_pgsql' => PDO\PgSQL\Driver::class, - 'pdo_oci' => PDO\OCI\Driver::class, - 'oci8' => OCI8\Driver::class, - 'ibm_db2' => IBMDB2\Driver::class, - 'pdo_sqlsrv' => PDO\SQLSrv\Driver::class, - 'mysqli' => Mysqli\Driver::class, - 'pgsql' => PgSQL\Driver::class, - 'sqlsrv' => SQLSrv\Driver::class, - 'sqlite3' => SQLite3\Driver::class, - ]; - - /** - * List of URL schemes from a database URL and their mappings to driver. - * - * @deprecated Use actual driver names instead. - * - * @var array - * @phpstan-var array> - */ - private static array $driverSchemeAliases = [ - 'db2' => 'ibm_db2', - 'mssql' => 'pdo_sqlsrv', - 'mysql' => 'pdo_mysql', - 'mysql2' => 'pdo_mysql', // Amazon RDS, for some weird reason - 'postgres' => 'pdo_pgsql', - 'postgresql' => 'pdo_pgsql', - 'pgsql' => 'pdo_pgsql', - 'sqlite' => 'pdo_sqlite', - 'sqlite3' => 'pdo_sqlite', + 'pdo_mysql' => PDO\MySQL\Driver::class, + 'pdo_sqlite' => PDO\SQLite\Driver::class, + 'pdo_pgsql' => PDO\PgSQL\Driver::class, + 'pdo_oci' => PDO\OCI\Driver::class, + 'oci8' => OCI8\Driver::class, + 'ibm_db2' => IBMDB2\Driver::class, + 'pdo_sqlsrv' => PDO\SQLSrv\Driver::class, + 'mysqli' => Mysqli\Driver::class, + 'pgsql' => PgSQL\Driver::class, + 'sqlsrv' => SQLSrv\Driver::class, + 'sqlite3' => SQLite3\Driver::class, ]; /** @@ -151,50 +127,32 @@ private function __construct() * driverClass: * The driver class to use. * - * @param Configuration|null $config The configuration to use. - * @param EventManager|null $eventManager The event manager to use. + * @param Configuration|null $config The configuration to use. * @phpstan-param Params $params * * @phpstan-return ($params is array{wrapperClass: class-string} ? T : Connection) * - * @throws Exception - * * @template T of Connection */ public static function getConnection( #[SensitiveParameter] array $params, ?Configuration $config = null, - ?EventManager $eventManager = null ): Connection { - // create default config and event manager, if not set - $config ??= new Configuration(); - $eventManager ??= new EventManager(); - $params = self::parseDatabaseUrl($params); - - // URL support for PrimaryReplicaConnection - if (isset($params['primary'])) { - $params['primary'] = self::parseDatabaseUrl($params['primary']); - } - - if (isset($params['replica'])) { - foreach ($params['replica'] as $key => $replicaParams) { - $params['replica'][$key] = self::parseDatabaseUrl($replicaParams); - } - } - - $driver = self::createDriver($params['driver'] ?? null, $params['driverClass'] ?? null); + $config ??= new Configuration(); + $driver = self::createDriver($params['driver'] ?? null, $params['driverClass'] ?? null); foreach ($config->getMiddlewares() as $middleware) { $driver = $middleware->wrap($driver); } + /** @var class-string $wrapperClass */ $wrapperClass = $params['wrapperClass'] ?? Connection::class; if (! is_a($wrapperClass, Connection::class, true)) { - throw Exception::invalidWrapperClass($wrapperClass); + throw InvalidWrapperClass::new($wrapperClass); } - return new $wrapperClass($params, $driver, $config, $eventManager); + return new $wrapperClass($params, $driver, $config); } /** @@ -209,80 +167,25 @@ public static function getAvailableDrivers(): array } /** - * @throws Exception - * - * @phpstan-assert key-of|null $driver - * @phpstan-assert class-string|null $driverClass + * @param class-string|null $driverClass + * @param key-of|null $driver */ private static function createDriver(?string $driver, ?string $driverClass): Driver { if ($driverClass === null) { if ($driver === null) { - throw Exception::driverRequired(); + throw DriverRequired::new(); } if (! isset(self::DRIVER_MAP[$driver])) { - throw Exception::unknownDriver($driver, array_keys(self::DRIVER_MAP)); + throw UnknownDriver::new($driver, array_keys(self::DRIVER_MAP)); } $driverClass = self::DRIVER_MAP[$driver]; } elseif (! is_a($driverClass, Driver::class, true)) { - throw Exception::invalidDriverClass($driverClass); + throw InvalidDriverClass::new($driverClass); } return new $driverClass(); } - - /** - * Extracts parts from a database URL, if present, and returns an - * updated list of parameters. - * - * @param mixed[] $params The list of parameters. - * @phpstan-param Params $params - * - * @return mixed[] A modified list of parameters with info from a database - * URL extracted into indidivual parameter parts. - * @phpstan-return Params - * - * @throws Exception - */ - private static function parseDatabaseUrl( - #[SensitiveParameter] - array $params - ): array { - if (! isset($params['url'])) { - return $params; - } - - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5843', - 'The "url" connection parameter is deprecated. Please use %s to parse a database url before calling %s.', - DsnParser::class, - self::class, - ); - - $parser = new DsnParser(self::$driverSchemeAliases); - try { - $parsedParams = $parser->parse($params['url']); - } catch (MalformedDsnException $e) { - throw new Exception('Malformed parameter "url".', 0, $e); - } - - if (isset($parsedParams['driver'])) { - // The requested driver from the URL scheme takes precedence - // over the default custom driver from the connection parameters (if any). - unset($params['driverClass']); - } - - $params = array_merge($params, $parsedParams); - - // If a schemeless connection URL is given, we require a default driver or default custom driver - // as connection parameter. - if (! isset($params['driverClass']) && ! isset($params['driver'])) { - throw Exception::driverRequired($params['url']); - } - - return $params; - } } diff --git a/doctrine/dbal/src/Event/ConnectionEventArgs.php b/doctrine/dbal/src/Event/ConnectionEventArgs.php deleted file mode 100644 index 9a69c2541..000000000 --- a/doctrine/dbal/src/Event/ConnectionEventArgs.php +++ /dev/null @@ -1,27 +0,0 @@ -connection = $connection; - } - - /** @return Connection */ - public function getConnection() - { - return $this->connection; - } -} diff --git a/doctrine/dbal/src/Event/Listeners/OracleSessionInit.php b/doctrine/dbal/src/Event/Listeners/OracleSessionInit.php deleted file mode 100644 index 9598f43cc..000000000 --- a/doctrine/dbal/src/Event/Listeners/OracleSessionInit.php +++ /dev/null @@ -1,77 +0,0 @@ - 'HH24:MI:SS', - 'NLS_DATE_FORMAT' => 'YYYY-MM-DD HH24:MI:SS', - 'NLS_TIMESTAMP_FORMAT' => 'YYYY-MM-DD HH24:MI:SS', - 'NLS_TIMESTAMP_TZ_FORMAT' => 'YYYY-MM-DD HH24:MI:SS TZH:TZM', - 'NLS_NUMERIC_CHARACTERS' => '.,', - ]; - - /** @param string[] $oracleSessionVars */ - public function __construct(array $oracleSessionVars = []) - { - $this->_defaultSessionVars = array_merge($this->_defaultSessionVars, $oracleSessionVars); - } - - /** - * @return void - * - * @throws Exception - */ - public function postConnect(ConnectionEventArgs $args) - { - if (count($this->_defaultSessionVars) === 0) { - return; - } - - $vars = []; - foreach (array_change_key_case($this->_defaultSessionVars, CASE_UPPER) as $option => $value) { - if ($option === 'CURRENT_SCHEMA') { - $vars[] = $option . ' = ' . $value; - } else { - $vars[] = $option . " = '" . $value . "'"; - } - } - - $sql = 'ALTER SESSION SET ' . implode(' ', $vars); - $args->getConnection()->executeStatement($sql); - } - - /** - * {@inheritDoc} - */ - public function getSubscribedEvents() - { - return [Events::postConnect]; - } -} diff --git a/doctrine/dbal/src/Event/Listeners/SQLSessionInit.php b/doctrine/dbal/src/Event/Listeners/SQLSessionInit.php deleted file mode 100644 index 4ce32d628..000000000 --- a/doctrine/dbal/src/Event/Listeners/SQLSessionInit.php +++ /dev/null @@ -1,43 +0,0 @@ -sql = $sql; - } - - /** - * @return void - * - * @throws Exception - */ - public function postConnect(ConnectionEventArgs $args) - { - $args->getConnection()->executeStatement($this->sql); - } - - /** - * {@inheritDoc} - */ - public function getSubscribedEvents() - { - return [Events::postConnect]; - } -} diff --git a/doctrine/dbal/src/Event/Listeners/SQLiteSessionInit.php b/doctrine/dbal/src/Event/Listeners/SQLiteSessionInit.php deleted file mode 100644 index 950f05f48..000000000 --- a/doctrine/dbal/src/Event/Listeners/SQLiteSessionInit.php +++ /dev/null @@ -1,30 +0,0 @@ -getConnection()->executeStatement('PRAGMA foreign_keys=ON'); - } - - /** - * {@inheritDoc} - */ - public function getSubscribedEvents() - { - return [Events::postConnect]; - } -} diff --git a/doctrine/dbal/src/Event/SchemaAlterTableAddColumnEventArgs.php b/doctrine/dbal/src/Event/SchemaAlterTableAddColumnEventArgs.php deleted file mode 100644 index 9f3ff6ea7..000000000 --- a/doctrine/dbal/src/Event/SchemaAlterTableAddColumnEventArgs.php +++ /dev/null @@ -1,81 +0,0 @@ -column = $column; - $this->tableDiff = $tableDiff; - $this->platform = $platform; - } - - /** @return Column */ - public function getColumn() - { - return $this->column; - } - - /** @return TableDiff */ - public function getTableDiff() - { - return $this->tableDiff; - } - - /** @return AbstractPlatform */ - public function getPlatform() - { - return $this->platform; - } - - /** - * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead. - * - * @param string|string[] $sql - * - * @return SchemaAlterTableAddColumnEventArgs - */ - public function addSql($sql) - { - if (is_array($sql)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3580', - 'Passing multiple SQL statements as an array to SchemaAlterTableAddColumnEventaArrgs::addSql() ' . - 'is deprecated. Pass each statement as an individual argument instead.', - ); - } - - $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args()); - - return $this; - } - - /** @return string[] */ - public function getSql() - { - return $this->sql; - } -} diff --git a/doctrine/dbal/src/Event/SchemaAlterTableChangeColumnEventArgs.php b/doctrine/dbal/src/Event/SchemaAlterTableChangeColumnEventArgs.php deleted file mode 100644 index 9ba37aade..000000000 --- a/doctrine/dbal/src/Event/SchemaAlterTableChangeColumnEventArgs.php +++ /dev/null @@ -1,71 +0,0 @@ -columnDiff = $columnDiff; - $this->tableDiff = $tableDiff; - $this->platform = $platform; - } - - /** @return ColumnDiff */ - public function getColumnDiff() - { - return $this->columnDiff; - } - - /** @return TableDiff */ - public function getTableDiff() - { - return $this->tableDiff; - } - - /** @return AbstractPlatform */ - public function getPlatform() - { - return $this->platform; - } - - /** - * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead. - * - * @param string|string[] $sql - * - * @return SchemaAlterTableChangeColumnEventArgs - */ - public function addSql($sql) - { - $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args()); - - return $this; - } - - /** @return string[] */ - public function getSql() - { - return $this->sql; - } -} diff --git a/doctrine/dbal/src/Event/SchemaAlterTableEventArgs.php b/doctrine/dbal/src/Event/SchemaAlterTableEventArgs.php deleted file mode 100644 index 07c065a9a..000000000 --- a/doctrine/dbal/src/Event/SchemaAlterTableEventArgs.php +++ /dev/null @@ -1,62 +0,0 @@ -tableDiff = $tableDiff; - $this->platform = $platform; - } - - /** @return TableDiff */ - public function getTableDiff() - { - return $this->tableDiff; - } - - /** @return AbstractPlatform */ - public function getPlatform() - { - return $this->platform; - } - - /** - * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead. - * - * @param string|string[] $sql - * - * @return SchemaAlterTableEventArgs - */ - public function addSql($sql) - { - $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args()); - - return $this; - } - - /** @return string[] */ - public function getSql() - { - return $this->sql; - } -} diff --git a/doctrine/dbal/src/Event/SchemaAlterTableRemoveColumnEventArgs.php b/doctrine/dbal/src/Event/SchemaAlterTableRemoveColumnEventArgs.php deleted file mode 100644 index 4122b418c..000000000 --- a/doctrine/dbal/src/Event/SchemaAlterTableRemoveColumnEventArgs.php +++ /dev/null @@ -1,71 +0,0 @@ -column = $column; - $this->tableDiff = $tableDiff; - $this->platform = $platform; - } - - /** @return Column */ - public function getColumn() - { - return $this->column; - } - - /** @return TableDiff */ - public function getTableDiff() - { - return $this->tableDiff; - } - - /** @return AbstractPlatform */ - public function getPlatform() - { - return $this->platform; - } - - /** - * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead. - * - * @param string|string[] $sql - * - * @return SchemaAlterTableRemoveColumnEventArgs - */ - public function addSql($sql) - { - $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args()); - - return $this; - } - - /** @return string[] */ - public function getSql() - { - return $this->sql; - } -} diff --git a/doctrine/dbal/src/Event/SchemaAlterTableRenameColumnEventArgs.php b/doctrine/dbal/src/Event/SchemaAlterTableRenameColumnEventArgs.php deleted file mode 100644 index 21d3c1645..000000000 --- a/doctrine/dbal/src/Event/SchemaAlterTableRenameColumnEventArgs.php +++ /dev/null @@ -1,82 +0,0 @@ -oldColumnName = $oldColumnName; - $this->column = $column; - $this->tableDiff = $tableDiff; - $this->platform = $platform; - } - - /** @return string */ - public function getOldColumnName() - { - return $this->oldColumnName; - } - - /** @return Column */ - public function getColumn() - { - return $this->column; - } - - /** @return TableDiff */ - public function getTableDiff() - { - return $this->tableDiff; - } - - /** @return AbstractPlatform */ - public function getPlatform() - { - return $this->platform; - } - - /** - * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead. - * - * @param string|string[] $sql - * - * @return SchemaAlterTableRenameColumnEventArgs - */ - public function addSql($sql) - { - $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args()); - - return $this; - } - - /** @return string[] */ - public function getSql() - { - return $this->sql; - } -} diff --git a/doctrine/dbal/src/Event/SchemaColumnDefinitionEventArgs.php b/doctrine/dbal/src/Event/SchemaColumnDefinitionEventArgs.php deleted file mode 100644 index 04fcbde9c..000000000 --- a/doctrine/dbal/src/Event/SchemaColumnDefinitionEventArgs.php +++ /dev/null @@ -1,87 +0,0 @@ -tableColumn = $tableColumn; - $this->table = $table; - $this->database = $database; - $this->connection = $connection; - } - - /** - * Allows to clear the column which means the column will be excluded from - * tables column list. - * - * @return SchemaColumnDefinitionEventArgs - */ - public function setColumn(?Column $column = null) - { - $this->column = $column; - - return $this; - } - - /** @return Column|null */ - public function getColumn() - { - return $this->column; - } - - /** @return mixed[] */ - public function getTableColumn() - { - return $this->tableColumn; - } - - /** @return string */ - public function getTable() - { - return $this->table; - } - - /** @return string */ - public function getDatabase() - { - return $this->database; - } - - /** @return Connection */ - public function getConnection() - { - return $this->connection; - } -} diff --git a/doctrine/dbal/src/Event/SchemaCreateTableColumnEventArgs.php b/doctrine/dbal/src/Event/SchemaCreateTableColumnEventArgs.php deleted file mode 100644 index 54f134d14..000000000 --- a/doctrine/dbal/src/Event/SchemaCreateTableColumnEventArgs.php +++ /dev/null @@ -1,71 +0,0 @@ -column = $column; - $this->table = $table; - $this->platform = $platform; - } - - /** @return Column */ - public function getColumn() - { - return $this->column; - } - - /** @return Table */ - public function getTable() - { - return $this->table; - } - - /** @return AbstractPlatform */ - public function getPlatform() - { - return $this->platform; - } - - /** - * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead. - * - * @param string|string[] $sql - * - * @return SchemaCreateTableColumnEventArgs - */ - public function addSql($sql) - { - $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args()); - - return $this; - } - - /** @return string[] */ - public function getSql() - { - return $this->sql; - } -} diff --git a/doctrine/dbal/src/Event/SchemaCreateTableEventArgs.php b/doctrine/dbal/src/Event/SchemaCreateTableEventArgs.php deleted file mode 100644 index a7d548deb..000000000 --- a/doctrine/dbal/src/Event/SchemaCreateTableEventArgs.php +++ /dev/null @@ -1,87 +0,0 @@ -table = $table; - $this->columns = $columns; - $this->options = $options; - $this->platform = $platform; - } - - /** @return Table */ - public function getTable() - { - return $this->table; - } - - /** @return mixed[][] */ - public function getColumns() - { - return $this->columns; - } - - /** @return mixed[] */ - public function getOptions() - { - return $this->options; - } - - /** @return AbstractPlatform */ - public function getPlatform() - { - return $this->platform; - } - - /** - * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead. - * - * @param string|string[] $sql - * - * @return SchemaCreateTableEventArgs - */ - public function addSql($sql) - { - $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args()); - - return $this; - } - - /** @return string[] */ - public function getSql() - { - return $this->sql; - } -} diff --git a/doctrine/dbal/src/Event/SchemaDropTableEventArgs.php b/doctrine/dbal/src/Event/SchemaDropTableEventArgs.php deleted file mode 100644 index 6f279e965..000000000 --- a/doctrine/dbal/src/Event/SchemaDropTableEventArgs.php +++ /dev/null @@ -1,59 +0,0 @@ -table = $table; - $this->platform = $platform; - } - - /** @return string|Table */ - public function getTable() - { - return $this->table; - } - - /** @return AbstractPlatform */ - public function getPlatform() - { - return $this->platform; - } - - /** - * @param string $sql - * - * @return SchemaDropTableEventArgs - */ - public function setSql($sql) - { - $this->sql = $sql; - - return $this; - } - - /** @return string|null */ - public function getSql() - { - return $this->sql; - } -} diff --git a/doctrine/dbal/src/Event/SchemaEventArgs.php b/doctrine/dbal/src/Event/SchemaEventArgs.php deleted file mode 100644 index 77d1d3908..000000000 --- a/doctrine/dbal/src/Event/SchemaEventArgs.php +++ /dev/null @@ -1,29 +0,0 @@ -preventDefault = true; - - return $this; - } - - /** @return bool */ - public function isDefaultPrevented() - { - return $this->preventDefault; - } -} diff --git a/doctrine/dbal/src/Event/SchemaIndexDefinitionEventArgs.php b/doctrine/dbal/src/Event/SchemaIndexDefinitionEventArgs.php deleted file mode 100644 index dbee55a0e..000000000 --- a/doctrine/dbal/src/Event/SchemaIndexDefinitionEventArgs.php +++ /dev/null @@ -1,75 +0,0 @@ -tableIndex = $tableIndex; - $this->table = $table; - $this->connection = $connection; - } - - /** - * Allows to clear the index which means the index will be excluded from tables index list. - * - * @return SchemaIndexDefinitionEventArgs - */ - public function setIndex(?Index $index = null) - { - $this->index = $index; - - return $this; - } - - /** @return Index|null */ - public function getIndex() - { - return $this->index; - } - - /** @return mixed[] */ - public function getTableIndex() - { - return $this->tableIndex; - } - - /** @return string */ - public function getTable() - { - return $this->table; - } - - /** @return Connection */ - public function getConnection() - { - return $this->connection; - } -} diff --git a/doctrine/dbal/src/Event/TransactionBeginEventArgs.php b/doctrine/dbal/src/Event/TransactionBeginEventArgs.php deleted file mode 100644 index be4ccdf15..000000000 --- a/doctrine/dbal/src/Event/TransactionBeginEventArgs.php +++ /dev/null @@ -1,10 +0,0 @@ -connection = $connection; - } - - public function getConnection(): Connection - { - return $this->connection; - } -} diff --git a/doctrine/dbal/src/Event/TransactionRollBackEventArgs.php b/doctrine/dbal/src/Event/TransactionRollBackEventArgs.php deleted file mode 100644 index 9e6e650d8..000000000 --- a/doctrine/dbal/src/Event/TransactionRollBackEventArgs.php +++ /dev/null @@ -1,10 +0,0 @@ -getMessage(); } else { @@ -33,17 +30,12 @@ public function __construct(TheDriverException $driverException, ?Query $query) } parent::__construct($message, $driverException->getCode(), $driverException); - - $this->query = $query; } - /** - * {@inheritDoc} - */ - public function getSQLState() + public function getSQLState(): ?string { $previous = $this->getPrevious(); - assert($previous instanceof TheDriverException); + assert($previous instanceof Driver\Exception); return $previous->getSQLState(); } diff --git a/doctrine/dbal/src/Exception/DriverRequired.php b/doctrine/dbal/src/Exception/DriverRequired.php new file mode 100644 index 000000000..883f2334c --- /dev/null +++ b/doctrine/dbal/src/Exception/DriverRequired.php @@ -0,0 +1,29 @@ +|array */ - private array $originalParameters; - - /** @var array|array */ - private array $originalTypes; - private int $originalParameterIndex = 0; /** @var list */ @@ -29,28 +26,28 @@ final class ExpandArrayParameters implements Visitor /** @var list */ private array $convertedParameters = []; - /** @var array */ + /** @var array,string|ParameterType|Type> */ private array $convertedTypes = []; /** - * @param array|array $parameters - * @param array|array $types + * @param array|array $parameters + * @phpstan-param WrapperParameterTypeArray $types */ - public function __construct(array $parameters, array $types) - { - $this->originalParameters = $parameters; - $this->originalTypes = $types; + public function __construct( + private readonly array $parameters, + private readonly array $types, + ) { } public function acceptPositionalParameter(string $sql): void { $index = $this->originalParameterIndex; - if (! array_key_exists($index, $this->originalParameters)) { + if (! array_key_exists($index, $this->parameters)) { throw MissingPositionalParameter::new($index); } - $this->acceptParameter($index, $this->originalParameters[$index]); + $this->acceptParameter($index, $this->parameters[$index]); $this->originalParameterIndex++; } @@ -59,11 +56,11 @@ public function acceptNamedParameter(string $sql): void { $name = substr($sql, 1); - if (! array_key_exists($name, $this->originalParameters)) { + if (! array_key_exists($name, $this->parameters)) { throw MissingNamedParameter::new($name); } - $this->acceptParameter($name, $this->originalParameters[$name]); + $this->acceptParameter($name, $this->parameters[$name]); } public function acceptOther(string $sql): void @@ -82,27 +79,18 @@ public function getParameters(): array return $this->convertedParameters; } - /** - * @param int|string $key - * @param mixed $value - */ - private function acceptParameter($key, $value): void + private function acceptParameter(int|string $key, mixed $value): void { - if (! isset($this->originalTypes[$key])) { + if (! isset($this->types[$key])) { $this->convertedSQL[] = '?'; $this->convertedParameters[] = $value; return; } - $type = $this->originalTypes[$key]; + $type = $this->types[$key]; - if ( - $type !== ArrayParameterType::INTEGER - && $type !== ArrayParameterType::STRING - && $type !== ArrayParameterType::ASCII - && $type !== ArrayParameterType::BINARY - ) { + if (! $type instanceof ArrayParameterType) { $this->appendTypedParameter([$value], $type); return; @@ -117,17 +105,14 @@ private function acceptParameter($key, $value): void $this->appendTypedParameter($value, ArrayParameterType::toElementParameterType($type)); } - /** @return array */ + /** @return array,string|ParameterType|Type> */ public function getTypes(): array { return $this->convertedTypes; } - /** - * @param list $values - * @param Type|int|string|null $type - */ - private function appendTypedParameter(array $values, $type): void + /** @param list $values */ + private function appendTypedParameter(array $values, string|ParameterType|Type $type): void { $this->convertedSQL[] = implode(', ', array_fill(0, count($values), '?')); diff --git a/doctrine/dbal/src/FetchMode.php b/doctrine/dbal/src/FetchMode.php deleted file mode 100644 index d80719f25..000000000 --- a/doctrine/dbal/src/FetchMode.php +++ /dev/null @@ -1,20 +0,0 @@ -getDriver() instanceof Driver\PDO\SQLite\Driver) { - throw new Exception('Cannot use TableGenerator with SQLite.'); - } - - $this->conn = DriverManager::getConnection( - $conn->getParams(), - $conn->getConfiguration(), - $conn->getEventManager(), - ); - - $this->generatorTableName = $generatorTableName; - } - - /** - * Generates the next unused value for the given sequence name. - * - * @param string $sequence - * - * @return int - * - * @throws Exception - */ - public function nextValue($sequence) - { - if (isset($this->sequences[$sequence])) { - $value = $this->sequences[$sequence]['value']; - $this->sequences[$sequence]['value']++; - if ($this->sequences[$sequence]['value'] >= $this->sequences[$sequence]['max']) { - unset($this->sequences[$sequence]); - } - - return $value; - } - - $this->conn->beginTransaction(); - - try { - $row = $this->conn->createQueryBuilder() - ->select('sequence_value', 'sequence_increment_by') - ->from($this->generatorTableName) - ->where('sequence_name = ?') - ->forUpdate() - ->setParameter(1, $sequence) - ->fetchAssociative(); - - if ($row !== false) { - $row = array_change_key_case($row, CASE_LOWER); - - $value = $row['sequence_value']; - $value++; - - assert(is_int($value)); - - if ($row['sequence_increment_by'] > 1) { - $this->sequences[$sequence] = [ - 'value' => $value, - 'max' => $row['sequence_value'] + $row['sequence_increment_by'], - ]; - } - - $sql = 'UPDATE ' . $this->generatorTableName . ' ' . - 'SET sequence_value = sequence_value + sequence_increment_by ' . - 'WHERE sequence_name = ? AND sequence_value = ?'; - $rows = $this->conn->executeStatement($sql, [$sequence, $row['sequence_value']]); - - if ($rows !== 1) { - throw new Exception('Race-condition detected while updating sequence. Aborting generation'); - } - } else { - $this->conn->insert( - $this->generatorTableName, - ['sequence_name' => $sequence, 'sequence_value' => 1, 'sequence_increment_by' => 1], - ); - $value = 1; - } - - $this->conn->commit(); - } catch (Throwable $e) { - $this->conn->rollBack(); - - throw new Exception( - 'Error occurred while generating ID with TableGenerator, aborted generation: ' . $e->getMessage(), - 0, - $e, - ); - } - - return $value; - } -} diff --git a/doctrine/dbal/src/Id/TableGeneratorSchemaVisitor.php b/doctrine/dbal/src/Id/TableGeneratorSchemaVisitor.php deleted file mode 100644 index 75c9fe9cf..000000000 --- a/doctrine/dbal/src/Id/TableGeneratorSchemaVisitor.php +++ /dev/null @@ -1,77 +0,0 @@ -generatorTableName = $generatorTableName; - } - - /** - * {@inheritDoc} - */ - public function acceptSchema(Schema $schema) - { - $table = $schema->createTable($this->generatorTableName); - $table->addColumn('sequence_name', 'string'); - $table->addColumn('sequence_value', 'integer', ['default' => 1]); - $table->addColumn('sequence_increment_by', 'integer', ['default' => 1]); - } - - /** - * {@inheritDoc} - */ - public function acceptTable(Table $table) - { - } - - /** - * {@inheritDoc} - */ - public function acceptColumn(Table $table, Column $column) - { - } - - /** - * {@inheritDoc} - */ - public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) - { - } - - /** - * {@inheritDoc} - */ - public function acceptIndex(Table $table, Index $index) - { - } - - /** - * {@inheritDoc} - */ - public function acceptSequence(Sequence $sequence) - { - } -} diff --git a/doctrine/dbal/src/LockMode.php b/doctrine/dbal/src/LockMode.php index 1a7ed2397..035353e02 100644 --- a/doctrine/dbal/src/LockMode.php +++ b/doctrine/dbal/src/LockMode.php @@ -1,23 +1,16 @@ logger = $logger; } public function __destruct() @@ -43,40 +39,31 @@ public function query(string $sql): Result return parent::query($sql); } - public function exec(string $sql): int + public function exec(string $sql): int|string { $this->logger->debug('Executing statement: {sql}', ['sql' => $sql]); return parent::exec($sql); } - /** - * {@inheritDoc} - */ - public function beginTransaction() + public function beginTransaction(): void { $this->logger->debug('Beginning transaction'); - return parent::beginTransaction(); + parent::beginTransaction(); } - /** - * {@inheritDoc} - */ - public function commit() + public function commit(): void { $this->logger->debug('Committing transaction'); - return parent::commit(); + parent::commit(); } - /** - * {@inheritDoc} - */ - public function rollBack() + public function rollBack(): void { $this->logger->debug('Rolling back transaction'); - return parent::rollBack(); + parent::rollBack(); } } diff --git a/doctrine/dbal/src/Logging/DebugStack.php b/doctrine/dbal/src/Logging/DebugStack.php deleted file mode 100644 index 1a970d060..000000000 --- a/doctrine/dbal/src/Logging/DebugStack.php +++ /dev/null @@ -1,75 +0,0 @@ -> - */ - public $queries = []; - - /** - * If Debug Stack is enabled (log queries) or not. - * - * @var bool - */ - public $enabled = true; - - /** @var float|null */ - public $start = null; - - /** @var int */ - public $currentQuery = 0; - - public function __construct() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4967', - 'DebugStack is deprecated.', - ); - } - - /** - * {@inheritDoc} - */ - public function startQuery($sql, ?array $params = null, ?array $types = null) - { - if (! $this->enabled) { - return; - } - - $this->start = microtime(true); - - $this->queries[++$this->currentQuery] = [ - 'sql' => $sql, - 'params' => $params, - 'types' => $types, - 'executionMS' => 0, - ]; - } - - /** - * {@inheritDoc} - */ - public function stopQuery() - { - if (! $this->enabled) { - return; - } - - $this->queries[$this->currentQuery]['executionMS'] = microtime(true) - $this->start; - } -} diff --git a/doctrine/dbal/src/Logging/Driver.php b/doctrine/dbal/src/Logging/Driver.php index 32a5cd2c2..35acd393b 100644 --- a/doctrine/dbal/src/Logging/Driver.php +++ b/doctrine/dbal/src/Logging/Driver.php @@ -11,14 +11,10 @@ final class Driver extends AbstractDriverMiddleware { - private LoggerInterface $logger; - /** @internal This driver can be only instantiated by its middleware. */ - public function __construct(DriverInterface $driver, LoggerInterface $logger) + public function __construct(DriverInterface $driver, private readonly LoggerInterface $logger) { parent::__construct($driver); - - $this->logger = $logger; } /** @@ -26,8 +22,8 @@ public function __construct(DriverInterface $driver, LoggerInterface $logger) */ public function connect( #[SensitiveParameter] - array $params - ) { + array $params, + ): Connection { $this->logger->info('Connecting with parameters {params}', ['params' => $this->maskPassword($params)]); return new Connection( @@ -43,16 +39,12 @@ public function connect( */ private function maskPassword( #[SensitiveParameter] - array $params + array $params, ): array { if (isset($params['password'])) { $params['password'] = ''; } - if (isset($params['url'])) { - $params['url'] = ''; - } - return $params; } } diff --git a/doctrine/dbal/src/Logging/LoggerChain.php b/doctrine/dbal/src/Logging/LoggerChain.php deleted file mode 100644 index 7a4eaa49a..000000000 --- a/doctrine/dbal/src/Logging/LoggerChain.php +++ /dev/null @@ -1,48 +0,0 @@ - */ - private iterable $loggers; - - /** @param iterable $loggers */ - public function __construct(iterable $loggers = []) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4967', - 'LoggerChain is deprecated', - ); - - $this->loggers = $loggers; - } - - /** - * {@inheritDoc} - */ - public function startQuery($sql, ?array $params = null, ?array $types = null) - { - foreach ($this->loggers as $logger) { - $logger->startQuery($sql, $params, $types); - } - } - - /** - * {@inheritDoc} - */ - public function stopQuery() - { - foreach ($this->loggers as $logger) { - $logger->stopQuery(); - } - } -} diff --git a/doctrine/dbal/src/Logging/Middleware.php b/doctrine/dbal/src/Logging/Middleware.php index da0c1b90f..989e0ca26 100644 --- a/doctrine/dbal/src/Logging/Middleware.php +++ b/doctrine/dbal/src/Logging/Middleware.php @@ -10,11 +10,8 @@ final class Middleware implements MiddlewareInterface { - private LoggerInterface $logger; - - public function __construct(LoggerInterface $logger) + public function __construct(private readonly LoggerInterface $logger) { - $this->logger = $logger; } public function wrap(DriverInterface $driver): DriverInterface diff --git a/doctrine/dbal/src/Logging/SQLLogger.php b/doctrine/dbal/src/Logging/SQLLogger.php deleted file mode 100644 index dab4a3a7d..000000000 --- a/doctrine/dbal/src/Logging/SQLLogger.php +++ /dev/null @@ -1,32 +0,0 @@ -|array|null $params Statement parameters - * @param array|array|null $types Parameter types - * - * @return void - */ - public function startQuery($sql, ?array $params = null, ?array $types = null); - - /** - * Marks the last started query as stopped. This can be used for timing of queries. - * - * @return void - */ - public function stopQuery(); -} diff --git a/doctrine/dbal/src/Logging/Statement.php b/doctrine/dbal/src/Logging/Statement.php index 039b93b6a..ed1ca7f2d 100644 --- a/doctrine/dbal/src/Logging/Statement.php +++ b/doctrine/dbal/src/Logging/Statement.php @@ -8,93 +8,41 @@ use Doctrine\DBAL\Driver\Result as ResultInterface; use Doctrine\DBAL\Driver\Statement as StatementInterface; use Doctrine\DBAL\ParameterType; -use Doctrine\Deprecations\Deprecation; use Psr\Log\LoggerInterface; -use function array_slice; -use function func_get_args; -use function func_num_args; - final class Statement extends AbstractStatementMiddleware { - private LoggerInterface $logger; - private string $sql; - /** @var array|array */ private array $params = []; - /** @var array|array */ + /** @var array|array */ private array $types = []; /** @internal This statement can be only instantiated by its connection. */ - public function __construct(StatementInterface $statement, LoggerInterface $logger, string $sql) - { + public function __construct( + StatementInterface $statement, + private readonly LoggerInterface $logger, + private readonly string $sql, + ) { parent::__construct($statement); - - $this->logger = $logger; - $this->sql = $sql; - } - - /** - * {@inheritDoc} - * - * @deprecated Use {@see bindValue()} instead. - */ - public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5563', - '%s is deprecated. Use bindValue() instead.', - __METHOD__, - ); - - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindParam() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - - $this->params[$param] = &$variable; - $this->types[$param] = $type; - - return parent::bindParam($param, $variable, $type, ...array_slice(func_get_args(), 3)); } - /** - * {@inheritDoc} - */ - public function bindValue($param, $value, $type = ParameterType::STRING) + public function bindValue(int|string $param, mixed $value, ParameterType $type): void { - if (func_num_args() < 3) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5558', - 'Not passing $type to Statement::bindValue() is deprecated.' - . ' Pass the type corresponding to the parameter being bound.', - ); - } - $this->params[$param] = $value; $this->types[$param] = $type; - return parent::bindValue($param, $value, $type); + parent::bindValue($param, $value, $type); } - /** - * {@inheritDoc} - */ - public function execute($params = null): ResultInterface + public function execute(): ResultInterface { $this->logger->debug('Executing statement: {sql} (parameters: {params}, types: {types})', [ 'sql' => $this->sql, - 'params' => $params ?? $this->params, + 'params' => $this->params, 'types' => $this->types, ]); - return parent::execute($params); + return parent::execute(); } } diff --git a/doctrine/dbal/src/ParameterType.php b/doctrine/dbal/src/ParameterType.php index 77917e870..19f577ec3 100644 --- a/doctrine/dbal/src/ParameterType.php +++ b/doctrine/dbal/src/ParameterType.php @@ -1,57 +1,46 @@ value . ')'; } - /** - * {@inheritDoc} - */ - public function getDateDiffExpression($date1, $date2) + public function getDateDiffExpression(string $date1, string $date2): string { return 'DATEDIFF(' . $date1 . ', ' . $date2 . ')'; } @@ -138,125 +114,37 @@ public function getCurrentDatabaseExpression(): string return 'DATABASE()'; } - /** - * {@inheritDoc} - */ - public function getLengthExpression($column) + public function getLengthExpression(string $string): string { - return 'CHAR_LENGTH(' . $column . ')'; + return 'CHAR_LENGTH(' . $string . ')'; } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. - */ - public function getListDatabasesSQL() + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListDatabasesSQL(): string { return 'SHOW DATABASES'; } - /** - * @deprecated - * - * {@inheritDoc} - */ - public function getListTableConstraintsSQL($table) - { - return 'SHOW INDEX FROM ' . $table; - } - - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - * - * Two approaches to listing the table indexes. The information_schema is - * preferred, because it doesn't cause problems with SQL keywords such as "order" or "table". - */ - public function getListTableIndexesSQL($table, $database = null) - { - if ($database !== null) { - return 'SELECT NON_UNIQUE AS Non_Unique, INDEX_NAME AS Key_name, COLUMN_NAME AS Column_Name,' . - ' SUB_PART AS Sub_Part, INDEX_TYPE AS Index_Type' . - ' FROM information_schema.STATISTICS WHERE TABLE_NAME = ' . $this->quoteStringLiteral($table) . - ' AND TABLE_SCHEMA = ' . $this->quoteStringLiteral($database) . - ' ORDER BY SEQ_IN_INDEX ASC'; - } - - return 'SHOW INDEX FROM ' . $table; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. - */ - public function getListViewsSQL($database) + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListViewsSQL(string $database): string { return 'SELECT * FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ' . $this->quoteStringLiteral($database); } - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * @param string $table - * @param string|null $database - * - * @return string - */ - public function getListTableForeignKeysSQL($table, $database = null) - { - // The schema name is passed multiple times as a literal in the WHERE clause instead of using a JOIN condition - // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions - // caused by https://bugs.mysql.com/bug.php?id=81347 - return 'SELECT k.CONSTRAINT_NAME, k.COLUMN_NAME, k.REFERENCED_TABLE_NAME, ' . - 'k.REFERENCED_COLUMN_NAME /*!50116 , c.UPDATE_RULE, c.DELETE_RULE */ ' . - 'FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE k /*!50116 ' . - 'INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS c ON ' . - 'c.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND ' . - 'c.TABLE_NAME = k.TABLE_NAME */ ' . - 'WHERE k.TABLE_NAME = ' . $this->quoteStringLiteral($table) . ' ' . - 'AND k.TABLE_SCHEMA = ' . $this->getDatabaseNameSQL($database) . ' /*!50116 ' . - 'AND c.CONSTRAINT_SCHEMA = ' . $this->getDatabaseNameSQL($database) . ' */' . - 'ORDER BY k.ORDINAL_POSITION'; - } - /** * {@inheritDoc} */ - protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + public function getJsonTypeDeclarationSQL(array $column): string { - if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { + if (! empty($column['jsonb'])) { Deprecation::trigger( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'Relying on the default string column length on MySQL is deprecated' - . ', specify the length explicitly.', + 'https://github.com/doctrine/dbal/pull/6939', + 'The "jsonb" column platform option is deprecated. Use the "JSONB" type instead.', ); } - return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(255)') - : ($length > 0 ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'); - } - - /** - * {@inheritDoc} - */ - protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) - { - if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'Relying on the default binary column length on MySQL is deprecated' - . ', specify the length explicitly.', - ); - } - - return $fixed - ? 'BINARY(' . ($length > 0 ? $length : 255) . ')' - : 'VARBINARY(' . ($length > 0 ? $length : 255) . ')'; + return 'JSON'; } /** @@ -268,7 +156,7 @@ protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed/*, $length * * {@inheritDoc} */ - public function getClobTypeDeclarationSQL(array $column) + public function getClobTypeDeclarationSQL(array $column): string { if (! empty($column['length']) && is_numeric($column['length'])) { $length = $column['length']; @@ -292,9 +180,15 @@ public function getClobTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getDateTimeTypeDeclarationSQL(array $column) + public function getDateTimeTypeDeclarationSQL(array $column): string { if (isset($column['version']) && $column['version'] === true) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6940', + 'The "version" column platform option is deprecated.', + ); + return 'TIMESTAMP'; } @@ -304,7 +198,7 @@ public function getDateTimeTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getDateTypeDeclarationSQL(array $column) + public function getDateTypeDeclarationSQL(array $column): string { return 'DATE'; } @@ -312,7 +206,7 @@ public function getDateTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getTimeTypeDeclarationSQL(array $column) + public function getTimeTypeDeclarationSQL(array $column): string { return 'TIME'; } @@ -320,28 +214,9 @@ public function getTimeTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getBooleanTypeDeclarationSQL(array $column) + public function getBooleanTypeDeclarationSQL(array $column): string { - return 'TINYINT(1)'; - } - - /** - * {@inheritDoc} - * - * @deprecated - * - * MySQL prefers "autoincrement" identity columns since sequences can only - * be emulated with a table. - */ - public function prefersIdentityColumns() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/1519', - 'AbstractMySQLPlatform::prefersIdentityColumns() is deprecated.', - ); - - return true; + return 'TINYINT'; } /** @@ -349,194 +224,86 @@ public function prefersIdentityColumns() * * MySQL supports this through AUTO_INCREMENT columns. */ - public function supportsIdentityColumns() + public function supportsIdentityColumns(): bool { return true; } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function supportsInlineColumnComments() + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsInlineColumnComments(): bool { return true; } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function supportsColumnCollation() + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsColumnCollation(): bool { return true; } - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTablesSQL() - { - return "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"; - } - - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTableColumnsSQL($table, $database = null) - { - return 'SELECT COLUMN_NAME AS Field, COLUMN_TYPE AS Type, IS_NULLABLE AS `Null`, ' . - 'COLUMN_KEY AS `Key`, COLUMN_DEFAULT AS `Default`, EXTRA AS Extra, COLUMN_COMMENT AS Comment, ' . - 'CHARACTER_SET_NAME AS CharacterSet, COLLATION_NAME AS Collation ' . - 'FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ' . $this->getDatabaseNameSQL($database) . - ' AND TABLE_NAME = ' . $this->quoteStringLiteral($table) . - ' ORDER BY ORDINAL_POSITION ASC'; - } - - /** - * @deprecated Use {@see getColumnTypeSQLSnippet()} instead. - * - * The SQL snippets required to elucidate a column type - * - * Returns an array of the form [column type SELECT snippet, additional JOIN statement snippet] - * - * @return array{string, string} - */ - public function getColumnTypeSQLSnippets(string $tableAlias = 'c'): array - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6202', - 'AbstractMySQLPlatform::getColumnTypeSQLSnippets() is deprecated. ' - . 'Use AbstractMySQLPlatform::getColumnTypeSQLSnippet() instead.', - ); - - return [$this->getColumnTypeSQLSnippet(...func_get_args()), '']; - } - /** * The SQL snippet required to elucidate a column type * * Returns a column type SELECT snippet string + * + * @internal The method should be only used from within the {@see MySQLSchemaManager} class hierarchy. */ - public function getColumnTypeSQLSnippet(string $tableAlias = 'c', ?string $databaseName = null): string + public function getColumnTypeSQLSnippet(string $tableAlias, string $databaseName): string { - return $tableAlias . '.COLUMN_TYPE'; - } - - /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */ - public function getListTableMetadataSQL(string $table, ?string $database = null): string - { - return sprintf( - <<<'SQL' -SELECT t.ENGINE, - t.AUTO_INCREMENT, - t.TABLE_COMMENT, - t.CREATE_OPTIONS, - t.TABLE_COLLATION, - ccsa.CHARACTER_SET_NAME -FROM information_schema.TABLES t - INNER JOIN information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` ccsa - ON ccsa.COLLATION_NAME = t.TABLE_COLLATION -WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = %s AND TABLE_NAME = %s -SQL - , - $this->getDatabaseNameSQL($database), - $this->quoteStringLiteral($table), - ); + return $tableAlias . '.DATA_TYPE'; } /** * {@inheritDoc} */ - public function getCreateTablesSQL(array $tables): array + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array { - $sql = []; - - foreach ($tables as $table) { - $sql = array_merge($sql, $this->getCreateTableWithoutForeignKeysSQL($table)); - } - - foreach ($tables as $table) { - if (! $table->hasOption('engine') || $this->engineSupportsForeignKeys($table->getOption('engine'))) { - foreach ($table->getForeignKeys() as $foreignKey) { - $sql[] = $this->getCreateForeignKeySQL( - $foreignKey, - $table->getQuotedName($this), - ); - } - } elseif (count($table->getForeignKeys()) > 0) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5414', - 'Relying on the DBAL not generating DDL for foreign keys on MySQL engines' - . ' other than InnoDB is deprecated.' - . ' Define foreign key constraints only if they are necessary.', - ); - } - } - - return $sql; - } + $this->validateCreateTableOptions($options, __METHOD__); - /** - * {@inheritDoc} - */ - protected function _getCreateTableSQL($name, array $columns, array $options = []) - { $queryFields = $this->getColumnDeclarationListSQL($columns); - if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { - foreach ($options['uniqueConstraints'] as $constraintName => $definition) { - $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($constraintName, $definition); + if (! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $definition) { + $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($definition); } } // add all indexes - if (isset($options['indexes']) && ! empty($options['indexes'])) { - foreach ($options['indexes'] as $indexName => $definition) { - $queryFields .= ', ' . $this->getIndexDeclarationSQL($indexName, $definition); + if (! empty($options['indexes'])) { + foreach ($options['indexes'] as $definition) { + $queryFields .= ', ' . $this->getIndexDeclarationSQL($definition); } } // attach all primary keys - if (isset($options['primary']) && ! empty($options['primary'])) { + if (! empty($options['primary'])) { $keyColumns = array_unique(array_values($options['primary'])); - $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + $queryFields .= ', PRIMARY KEY (' . implode(', ', $keyColumns) . ')'; } - $query = 'CREATE '; + $sql = ['CREATE']; if (! empty($options['temporary'])) { - $query .= 'TEMPORARY '; + $sql[] = 'TEMPORARY'; } - $query .= 'TABLE ' . $name . ' (' . $queryFields . ') '; - $query .= $this->buildTableOptions($options); - $query .= $this->buildPartitionOptions($options); + $sql[] = 'TABLE ' . $name . ' (' . $queryFields . ')'; - $sql = [$query]; + $tableOptions = $this->buildTableOptions($options); + + if ($tableOptions !== '') { + $sql[] = $tableOptions; + } + + if (isset($options['partition_options'])) { + $sql[] = $options['partition_options']; + } + + $sql = [implode(' ', $sql)]; - // Propagate foreign key constraints only for InnoDB. if (isset($options['foreignKeys'])) { - if (! isset($options['engine']) || $this->engineSupportsForeignKeys($options['engine'])) { - foreach ($options['foreignKeys'] as $definition) { - $sql[] = $this->getCreateForeignKeySQL($definition, $name); - } - } elseif (count($options['foreignKeys']) > 0) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5414', - 'Relying on the DBAL not generating DDL for foreign keys on MySQL engines' - . ' other than InnoDB is deprecated.' - . ' Define foreign key constraints only if they are necessary.', - ); + foreach ($options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $name); } } @@ -548,21 +315,6 @@ public function createSelectSQLBuilder(): SelectSQLBuilder return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', null); } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function getDefaultValueDeclarationSQL($column) - { - // Unset the default value if the given column definition does not allow default values. - if ($column['type'] instanceof TextType || $column['type'] instanceof BlobType) { - $column['default'] = null; - } - - return parent::getDefaultValueDeclarationSQL($column); - } - /** * Build SQL for table options * @@ -576,36 +328,18 @@ private function buildTableOptions(array $options): string $tableOptions = []; - // Charset - if (! isset($options['charset'])) { - $options['charset'] = 'utf8'; - } - - $tableOptions[] = sprintf('DEFAULT CHARACTER SET %s', $options['charset']); - - if (isset($options['collate'])) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5214', - 'The "collate" option is deprecated in favor of "collation" and will be removed in 4.0.', - ); - $options['collation'] = $options['collate']; + if (isset($options['charset'])) { + $tableOptions[] = sprintf('DEFAULT CHARACTER SET %s', $options['charset']); } - // Collation - if (! isset($options['collation'])) { - $options['collation'] = $options['charset'] . '_unicode_ci'; + if (isset($options['collation'])) { + $tableOptions[] = $this->getColumnCollationDeclarationSQL($options['collation']); } - $tableOptions[] = $this->getColumnCollationDeclarationSQL($options['collation']); - - // Engine - if (! isset($options['engine'])) { - $options['engine'] = 'InnoDB'; + if (isset($options['engine'])) { + $tableOptions[] = sprintf('ENGINE = %s', $options['engine']); } - $tableOptions[] = sprintf('ENGINE = %s', $options['engine']); - // Auto increment if (isset($options['auto_increment'])) { $tableOptions[] = sprintf('AUTO_INCREMENT = %s', $options['auto_increment']); @@ -624,50 +358,16 @@ private function buildTableOptions(array $options): string return implode(' ', $tableOptions); } - /** - * Build SQL for partition options. - * - * @param mixed[] $options - */ - private function buildPartitionOptions(array $options): string - { - return isset($options['partition_options']) - ? ' ' . $options['partition_options'] - : ''; - } - - private function engineSupportsForeignKeys(string $engine): bool - { - return strcasecmp(trim($engine), 'InnoDB') === 0; - } - /** * {@inheritDoc} */ - public function getAlterTableSQL(TableDiff $diff) + public function getAlterTableSQL(TableDiff $diff): array { - $columnSql = []; $queryParts = []; - $newName = $diff->getNewName(); - - if ($newName !== false) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5663', - 'Generation of SQL that renames a table using %s is deprecated. Use getRenameTableSQL() instead.', - __METHOD__, - ); - - $queryParts[] = 'RENAME TO ' . $newName->getQuotedName($this); - } foreach ($diff->getAddedColumns() as $column) { - if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { - continue; - } - $columnProperties = array_merge($column->toArray(), [ - 'comment' => $this->getColumnComment($column), + 'comment' => $column->getComment(), ]); $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL( @@ -677,115 +377,87 @@ public function getAlterTableSQL(TableDiff $diff) } foreach ($diff->getDroppedColumns() as $column) { - if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { - continue; - } - $queryParts[] = 'DROP ' . $column->getQuotedName($this); } - foreach ($diff->getModifiedColumns() as $columnDiff) { - if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { - continue; - } - + foreach ($diff->getChangedColumns() as $columnDiff) { $newColumn = $columnDiff->getNewColumn(); $newColumnProperties = array_merge($newColumn->toArray(), [ - 'comment' => $this->getColumnComment($newColumn), + 'comment' => $newColumn->getComment(), ]); - $oldColumn = $columnDiff->getOldColumn() ?? $columnDiff->getOldColumnName(); + $oldColumn = $columnDiff->getOldColumn(); $queryParts[] = 'CHANGE ' . $oldColumn->getQuotedName($this) . ' ' . $this->getColumnDeclarationSQL($newColumn->getQuotedName($this), $newColumnProperties); } - foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { - if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { - continue; - } + $droppedIndexes = $this->indexIndexesByLowerCaseName($diff->getDroppedIndexes()); + $addedIndexes = $this->indexIndexesByLowerCaseName($diff->getAddedIndexes()); - $oldColumnName = new Identifier($oldColumnName); + $noLongerPrimaryKeyColumns = []; - $columnProperties = array_merge($column->toArray(), [ - 'comment' => $this->getColumnComment($column), - ]); + if (isset($droppedIndexes['primary'])) { + $queryParts[] = 'DROP PRIMARY KEY'; - $queryParts[] = 'CHANGE ' . $oldColumnName->getQuotedName($this) . ' ' - . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnProperties); + $noLongerPrimaryKeyColumns = $droppedIndexes['primary']->getColumns(); } - $addedIndexes = $this->indexAssetsByLowerCaseName($diff->getAddedIndexes()); - $modifiedIndexes = $this->indexAssetsByLowerCaseName($diff->getModifiedIndexes()); - $diffModified = false; - if (isset($addedIndexes['primary'])) { - $keyColumns = array_unique(array_values($addedIndexes['primary']->getColumns())); + $keyColumns = array_values(array_unique($addedIndexes['primary']->getColumns())); $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')'; - unset($addedIndexes['primary']); - $diffModified = true; - } elseif (isset($modifiedIndexes['primary'])) { - $addedColumns = $this->indexAssetsByLowerCaseName($diff->getAddedColumns()); - - // Necessary in case the new primary key includes a new auto_increment column - foreach ($modifiedIndexes['primary']->getColumns() as $columnName) { - if (isset($addedColumns[$columnName]) && $addedColumns[$columnName]->getAutoincrement()) { - $keyColumns = array_unique(array_values($modifiedIndexes['primary']->getColumns())); - $queryParts[] = 'DROP PRIMARY KEY'; - $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')'; - unset($modifiedIndexes['primary']); - $diffModified = true; - break; - } - } - } - if ($diffModified) { - $diff = new TableDiff( - $diff->name, - $diff->getAddedColumns(), - $diff->getModifiedColumns(), - $diff->getDroppedColumns(), - array_values($addedIndexes), - array_values($modifiedIndexes), - $diff->getDroppedIndexes(), - $diff->getOldTable(), - $diff->getAddedForeignKeys(), - $diff->getModifiedForeignKeys(), - $diff->getDroppedForeignKeys(), - $diff->getRenamedColumns(), - $diff->getRenamedIndexes(), + $noLongerPrimaryKeyColumns = array_diff( + $noLongerPrimaryKeyColumns, + $addedIndexes['primary']->getColumns(), ); + + $diff->unsetAddedIndex($addedIndexes['primary']); } - $sql = []; $tableSql = []; - if (! $this->onSchemaAlterTable($diff, $tableSql)) { - if (count($queryParts) > 0) { - $sql[] = 'ALTER TABLE ' . ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this) . ' ' - . implode(', ', $queryParts); + if (isset($droppedIndexes['primary'])) { + $oldTable = $diff->getOldTable(); + foreach ($noLongerPrimaryKeyColumns as $columnName) { + if (! $oldTable->hasColumn($columnName)) { + continue; + } + + $column = $oldTable->getColumn($columnName); + if ($column->getAutoincrement()) { + $tableSql = array_merge( + $tableSql, + $this->getPreAlterTableAlterPrimaryKeySQL($diff, $droppedIndexes['primary']), + ); + break; + } } - $sql = array_merge( - $this->getPreAlterTableIndexForeignKeySQL($diff), - $sql, - $this->getPostAlterTableIndexForeignKeySQL($diff), - ); + $diff->unsetDroppedIndex($droppedIndexes['primary']); + } + + if (count($queryParts) > 0) { + $tableSql[] = 'ALTER TABLE ' . $diff->getOldTable()->getQuotedName($this) . ' ' + . implode(', ', $queryParts); } - return array_merge($sql, $tableSql, $columnSql); + return array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $tableSql, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); } /** * {@inheritDoc} */ - protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff): array { $sql = []; - $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); + $tableNameSQL = $diff->getOldTable()->getQuotedName($this); foreach ($diff->getModifiedIndexes() as $changedIndex) { $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $changedIndex)); @@ -809,7 +481,7 @@ protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) $query = 'ALTER TABLE ' . $tableNameSQL . ' DROP INDEX ' . $droppedIndex->getName() . ', '; $query .= 'ADD ' . $indexClause; - $query .= ' (' . $this->getIndexFieldDeclarationListSQL($addedIndex) . ')'; + $query .= ' (' . implode(', ', $addedIndex->getQuotedColumns($this)) . ')'; $sql[] = $query; @@ -820,36 +492,15 @@ protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) } } - $engine = 'INNODB'; - - $table = $diff->getOldTable(); - - if ($table !== null && $table->hasOption('engine')) { - $engine = strtoupper(trim($table->getOption('engine'))); - } - - // Suppress foreign key constraint propagation on non-supporting engines. - if ($engine !== 'INNODB') { - $diff->addedForeignKeys = []; - $diff->changedForeignKeys = []; - $diff->removedForeignKeys = []; - } - - $sql = array_merge( + return array_merge( $sql, $this->getPreAlterTableAlterIndexForeignKeySQL($diff), parent::getPreAlterTableIndexForeignKeySQL($diff), $this->getPreAlterTableRenameIndexForeignKeySQL($diff), ); - - return $sql; } - /** - * @return string[] - * - * @throws Exception - */ + /** @return list */ private function getPreAlterTableAlterPrimaryKeySQL(TableDiff $diff, Index $index): array { if (! $index->isPrimary()) { @@ -858,13 +509,9 @@ private function getPreAlterTableAlterPrimaryKeySQL(TableDiff $diff, Index $inde $table = $diff->getOldTable(); - if ($table === null) { - return []; - } - $sql = []; - $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); + $tableNameSQL = $table->getQuotedName($this); // Dropping primary keys requires to unset autoincrement attribute on the particular column first. foreach ($index->getColumns() as $columnName) { @@ -874,10 +521,18 @@ private function getPreAlterTableAlterPrimaryKeySQL(TableDiff $diff, Index $inde $column = $table->getColumn($columnName); - if ($column->getAutoincrement() !== true) { + if (! $column->getAutoincrement()) { continue; } + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6841', + 'Relying on the auto-increment attribute of a column being automatically dropped once a column' + . ' is no longer part of the primary key constraint is deprecated. Instead, drop the auto-increment' + . ' attribute explicitly.', + ); + $column->setAutoincrement(false); $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' MODIFY ' . @@ -893,18 +548,12 @@ private function getPreAlterTableAlterPrimaryKeySQL(TableDiff $diff, Index $inde /** * @param TableDiff $diff The table diff to gather the SQL for. * - * @return string[] - * - * @throws Exception + * @return list */ private function getPreAlterTableAlterIndexForeignKeySQL(TableDiff $diff): array { $table = $diff->getOldTable(); - if ($table === null) { - return []; - } - $primaryKey = $table->getPrimaryKey(); if ($primaryKey === null) { @@ -914,11 +563,9 @@ private function getPreAlterTableAlterIndexForeignKeySQL(TableDiff $diff): array $primaryKeyColumns = []; foreach ($primaryKey->getColumns() as $columnName) { - if (! $table->hasColumn($columnName)) { - continue; + if ($table->hasColumn($columnName)) { + $primaryKeyColumns[] = $table->getColumn($columnName); } - - $primaryKeyColumns[] = $table->getColumn($columnName); } if (count($primaryKeyColumns) === 0) { @@ -960,109 +607,14 @@ private function getPreAlterTableAlterIndexForeignKeySQL(TableDiff $diff): array /** * @param TableDiff $diff The table diff to gather the SQL for. * - * @return string[] + * @return list */ - protected function getPreAlterTableRenameIndexForeignKeySQL(TableDiff $diff) + protected function getPreAlterTableRenameIndexForeignKeySQL(TableDiff $diff): array { - $sql = []; - - $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); - - foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) { - if (in_array($foreignKey, $diff->getModifiedForeignKeys(), true)) { - continue; - } - - $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableNameSQL); - } - - return $sql; + return []; } - /** - * Returns the remaining foreign key constraints that require one of the renamed indexes. - * - * "Remaining" here refers to the diff between the foreign keys currently defined in the associated - * table and the foreign keys to be removed. - * - * @param TableDiff $diff The table diff to evaluate. - * - * @return ForeignKeyConstraint[] - */ - private function getRemainingForeignKeyConstraintsRequiringRenamedIndexes(TableDiff $diff): array - { - if (count($diff->getRenamedIndexes()) === 0) { - return []; - } - - $table = $diff->getOldTable(); - - if ($table === null) { - return []; - } - - $foreignKeys = []; - /** @var ForeignKeyConstraint[] $remainingForeignKeys */ - $remainingForeignKeys = array_diff_key( - $table->getForeignKeys(), - $diff->getDroppedForeignKeys(), - ); - - foreach ($remainingForeignKeys as $foreignKey) { - foreach ($diff->getRenamedIndexes() as $index) { - if ($foreignKey->intersectsIndexColumns($index)) { - $foreignKeys[] = $foreignKey; - - break; - } - } - } - - return $foreignKeys; - } - - /** - * {@inheritDoc} - */ - protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff) - { - return array_merge( - parent::getPostAlterTableIndexForeignKeySQL($diff), - $this->getPostAlterTableRenameIndexForeignKeySQL($diff), - ); - } - - /** - * @param TableDiff $diff The table diff to gather the SQL for. - * - * @return string[] - */ - protected function getPostAlterTableRenameIndexForeignKeySQL(TableDiff $diff) - { - $sql = []; - $newName = $diff->getNewName(); - - if ($newName !== false) { - $tableNameSQL = $newName->getQuotedName($this); - } else { - $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); - } - - foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) { - if (in_array($foreignKey, $diff->getModifiedForeignKeys(), true)) { - continue; - } - - $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableNameSQL); - } - - return $sql; - } - - /** - * {@inheritDoc} - */ - protected function getCreateIndexSQLFlags(Index $index) + protected function getCreateIndexSQLFlags(Index $index): string { $type = ''; if ($index->isUnique()) { @@ -1079,7 +631,7 @@ protected function getCreateIndexSQLFlags(Index $index) /** * {@inheritDoc} */ - public function getIntegerTypeDeclarationSQL(array $column) + public function getIntegerTypeDeclarationSQL(array $column): string { return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } @@ -1087,7 +639,7 @@ public function getIntegerTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getBigIntTypeDeclarationSQL(array $column) + public function getBigIntTypeDeclarationSQL(array $column): string { return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } @@ -1095,7 +647,7 @@ public function getBigIntTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getSmallIntTypeDeclarationSQL(array $column) + public function getSmallIntTypeDeclarationSQL(array $column): string { return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } @@ -1103,7 +655,7 @@ public function getSmallIntTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getFloatDeclarationSQL(array $column) + public function getFloatDeclarationSQL(array $column): string { return 'DOUBLE PRECISION' . $this->getUnsignedDeclaration($column); } @@ -1111,11 +663,34 @@ public function getFloatDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getDecimalTypeDeclarationSQL(array $column) + public function getSmallFloatDeclarationSQL(array $column): string + { + return 'FLOAT' . $this->getUnsignedDeclaration($column); + } + + /** + * {@inheritDoc} + */ + public function getDecimalTypeDeclarationSQL(array $column): string { return parent::getDecimalTypeDeclarationSQL($column) . $this->getUnsignedDeclaration($column); } + /** + * {@inheritDoc} + */ + public function getEnumDeclarationSQL(array $column): string + { + if (! isset($column['values']) || ! is_array($column['values']) || $column['values'] === []) { + throw ColumnValuesRequired::new($this, 'ENUM'); + } + + return sprintf('ENUM(%s)', implode(', ', array_map( + $this->quoteStringLiteral(...), + $column['values'], + ))); + } + /** * Get unsigned declaration for a column. * @@ -1129,32 +704,25 @@ private function getUnsignedDeclaration(array $columnDef): string /** * {@inheritDoc} */ - protected function _getCommonIntegerTypeDeclarationSQL(array $column) + protected function _getCommonIntegerTypeDeclarationSQL(array $column): string { - $autoinc = ''; + $sql = $this->getUnsignedDeclaration($column); + if (! empty($column['autoincrement'])) { - $autoinc = ' AUTO_INCREMENT'; + $sql .= ' AUTO_INCREMENT'; } - return $this->getUnsignedDeclaration($column) . $autoinc; + return $sql; } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function getColumnCharsetDeclarationSQL($charset) + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getColumnCharsetDeclarationSQL(string $charset): string { return 'CHARACTER SET ' . $charset; } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey): string { $query = ''; if ($foreignKey->hasOption('match')) { @@ -1166,60 +734,9 @@ public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey return $query; } - /** - * {@inheritDoc} - */ - public function getDropIndexSQL($index, $table = null) + public function getDropIndexSQL(string $name, string $table): string { - if ($index instanceof Index) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $index as an Index object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $indexName = $index->getQuotedName($this); - } elseif (is_string($index)) { - $indexName = $index; - } else { - throw new InvalidArgumentException( - __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.', - ); - } - - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $table = $table->getQuotedName($this); - } elseif (! is_string($table)) { - throw new InvalidArgumentException( - __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.', - ); - } - - if ($index instanceof Index && $index->isPrimary()) { - // MySQL primary keys are always named "PRIMARY", - // so we cannot use them in statements because of them being keyword. - return $this->getDropPrimaryKeySQL($table); - } - - return 'DROP INDEX ' . $indexName . ' ON ' . $table; - } - - /** - * @param string $table - * - * @return string - */ - protected function getDropPrimaryKeySQL($table) - { - return 'ALTER TABLE ' . $table . ' DROP PRIMARY KEY'; + return 'DROP INDEX ' . $name . ' ON ' . $table; } /** @@ -1232,40 +749,12 @@ public function getDropUniqueConstraintSQL(string $name, string $tableName): str return $this->getDropIndexSQL($name, $tableName); } - /** - * {@inheritDoc} - */ - public function getSetTransactionIsolationSQL($level) + public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string { return 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); } - /** - * {@inheritDoc} - */ - public function getName() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4749', - 'AbstractMySQLPlatform::getName() is deprecated. Identify platforms by their class.', - ); - - return 'mysql'; - } - - /** - * {@inheritDoc} - */ - public function getReadLockSQL() - { - return 'LOCK IN SHARE MODE'; - } - - /** - * {@inheritDoc} - */ - protected function initializeDoctrineTypeMappings() + protected function initializeDoctrineTypeMappings(): void { $this->doctrineTypeMapping = [ 'bigint' => Types::BIGINT, @@ -1276,9 +765,11 @@ protected function initializeDoctrineTypeMappings() 'datetime' => Types::DATETIME_MUTABLE, 'decimal' => Types::DECIMAL, 'double' => Types::FLOAT, - 'float' => Types::FLOAT, + 'enum' => Types::ENUM, + 'float' => Types::SMALLFLOAT, 'int' => Types::INTEGER, 'integer' => Types::INTEGER, + 'json' => Types::JSON, 'longblob' => Types::BLOB, 'longtext' => Types::TEXT, 'mediumblob' => Types::BLOB, @@ -1301,53 +792,17 @@ protected function initializeDoctrineTypeMappings() ]; } - /** - * {@inheritDoc} - * - * @deprecated - */ - public function getVarcharMaxLength() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'AbstractMySQLPlatform::getVarcharMaxLength() is deprecated.', - ); - - return 65535; - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function getBinaryMaxLength() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'AbstractMySQLPlatform::getBinaryMaxLength() is deprecated.', - ); - - return 65535; - } - - /** - * {@inheritDoc} - * - * @deprecated Implement {@see createReservedKeywordsList()} instead. - */ - protected function getReservedKeywordsClass() + /** @deprecated */ + protected function createReservedKeywordsList(): KeywordList { Deprecation::triggerIfCalledFromOutside( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4510', - 'AbstractMySQLPlatform::getReservedKeywordsClass() is deprecated,' - . ' use AbstractMySQLPlatform::createReservedKeywordsList() instead.', + 'https://github.com/doctrine/dbal/pull/6607', + '%s is deprecated.', + __METHOD__, ); - return Keywords\MySQLKeywords::class; + return new MySQLKeywords(); } /** @@ -1356,23 +811,8 @@ protected function getReservedKeywordsClass() * MySQL commits a transaction implicitly when DROP TABLE is executed, however not * if DROP TEMPORARY TABLE is executed. */ - public function getDropTemporaryTableSQL($table) + public function getDropTemporaryTableSQL(string $table): string { - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $table = $table->getQuotedName($this); - } elseif (! is_string($table)) { - throw new InvalidArgumentException( - __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.', - ); - } - return 'DROP TEMPORARY TABLE ' . $table; } @@ -1385,7 +825,7 @@ public function getDropTemporaryTableSQL($table) * * {@inheritDoc} */ - public function getBlobTypeDeclarationSQL(array $column) + public function getBlobTypeDeclarationSQL(array $column): string { if (! empty($column['length']) && is_numeric($column['length'])) { $length = $column['length']; @@ -1406,44 +846,35 @@ public function getBlobTypeDeclarationSQL(array $column) return 'LONGBLOB'; } - /** - * {@inheritDoc} - */ - public function quoteStringLiteral($str) + public function quoteStringLiteral(string $str): string { - $str = str_replace('\\', '\\\\', $str); // MySQL requires backslashes to be escaped + // MySQL requires backslashes to be escaped as well. + $str = str_replace('\\', '\\\\', $str); return parent::quoteStringLiteral($str); } - /** - * {@inheritDoc} - */ - public function getDefaultTransactionIsolationLevel() + public function getDefaultTransactionIsolationLevel(): TransactionIsolationLevel { return TransactionIsolationLevel::REPEATABLE_READ; } + /** @deprecated */ public function supportsColumnLengthIndexes(): bool - { - return true; - } - - /** @deprecated Will be removed without replacement. */ - protected function getDatabaseNameSQL(?string $databaseName): string { Deprecation::triggerIfCalledFromOutside( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6215', - '%s is deprecated without replacement.', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated.', __METHOD__, ); - if ($databaseName !== null) { - return $this->quoteStringLiteral($databaseName); - } + return true; + } - return $this->getCurrentDatabaseExpression(); + public function createMetadataProvider(Connection $connection): MySQLMetadataProvider + { + return new MySQLMetadataProvider($connection, $this); } public function createSchemaManager(Connection $connection): MySQLSchemaManager @@ -1452,23 +883,22 @@ public function createSchemaManager(Connection $connection): MySQLSchemaManager } /** - * @param list $assets - * - * @return array + * @param array $indexes * - * @template T of AbstractAsset + * @return array */ - private function indexAssetsByLowerCaseName(array $assets): array + private function indexIndexesByLowerCaseName(array $indexes): array { $result = []; - foreach ($assets as $asset) { - $result[strtolower($asset->getName())] = $asset; + foreach ($indexes as $index) { + $result[strtolower($index->getName())] = $index; } return $result; } + /** @internal The method should be only used from within the {@see MySQLSchemaManager} class hierarchy. */ public function fetchTableOptionsByTable(bool $includeTableName): string { $sql = <<<'SQL' @@ -1494,4 +924,9 @@ public function fetchTableOptionsByTable(bool $includeTableName): string return $sql . ' WHERE ' . implode(' AND ', $conditions); } + + public function createSQLParser(): Parser + { + return new Parser(true); + } } diff --git a/doctrine/dbal/src/Platforms/AbstractPlatform.php b/doctrine/dbal/src/Platforms/AbstractPlatform.php index ea84d21f2..bad3590b6 100644 --- a/doctrine/dbal/src/Platforms/AbstractPlatform.php +++ b/doctrine/dbal/src/Platforms/AbstractPlatform.php @@ -1,43 +1,47 @@ , + * primary_index?: Index, + * indexes?: list, + * uniqueConstraints?: list, + * foreignKeys?: list, + * comment?: string, + * } */ abstract class AbstractPlatform { + /** @deprecated */ public const CREATE_INDEXES = 1; + /** @deprecated */ public const CREATE_FOREIGNKEYS = 2; /** @var string[]|null */ - protected $doctrineTypeMapping; - - /** - * Contains a list of all columns that should generate parseable column comments for type-detection - * in reverse engineering scenarios. - * - * @deprecated This property is deprecated and will be removed in Doctrine DBAL 4.0. - * - * @var string[]|null - */ - protected $doctrineTypeComments; - - /** - * @deprecated - * - * @var EventManager|null - */ - protected $_eventManager; + protected ?array $doctrineTypeMapping = null; /** * Holds the KeywordList instance for the current platform. * - * @var KeywordList|null - */ - protected $_keywords; - - private bool $disableTypeComments = false; - - /** @internal */ - final public function setDisableTypeComments(bool $value): void - { - $this->disableTypeComments = $value; - } - - /** - * Sets the EventManager used by the Platform. - * * @deprecated - * - * @return void */ - public function setEventManager(EventManager $eventManager) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - '%s is deprecated.', - __METHOD__, - ); - - $this->_eventManager = $eventManager; - } + protected ?KeywordList $_keywords = null; /** - * Gets the EventManager used by the Platform. - * - * @deprecated - * - * @return EventManager|null + * Defines how the platform folds the case of unquoted identifiers. */ - public function getEventManager() + private ?UnquotedIdentifierFolding $unquotedIdentifierFolding = null; + + public function __construct(?UnquotedIdentifierFolding $unquotedIdentifierFolding = null) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - '%s is deprecated.', - __METHOD__, - ); + if ($unquotedIdentifierFolding === null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6823', + 'Not passing $unquotedIdentifierFolding to %s() is deprecated.', + __METHOD__, + ); + } - return $this->_eventManager; + $this->unquotedIdentifierFolding = $unquotedIdentifierFolding ?? UnquotedIdentifierFolding::UPPER; } /** * Returns the SQL snippet that declares a boolean column. * - * @param mixed[] $column - * - * @return string + * @param array $column */ - abstract public function getBooleanTypeDeclarationSQL(array $column); + abstract public function getBooleanTypeDeclarationSQL(array $column): string; /** * Returns the SQL snippet that declares a 4 byte integer column. * - * @param mixed[] $column - * - * @return string + * @param array $column */ - abstract public function getIntegerTypeDeclarationSQL(array $column); + abstract public function getIntegerTypeDeclarationSQL(array $column): string; /** * Returns the SQL snippet that declares an 8 byte integer column. * - * @param mixed[] $column - * - * @return string + * @param array $column */ - abstract public function getBigIntTypeDeclarationSQL(array $column); + abstract public function getBigIntTypeDeclarationSQL(array $column): string; /** * Returns the SQL snippet that declares a 2 byte integer column. * - * @param mixed[] $column - * - * @return string + * @param array $column */ - abstract public function getSmallIntTypeDeclarationSQL(array $column); + abstract public function getSmallIntTypeDeclarationSQL(array $column): string; /** * Returns the SQL snippet that declares common properties of an integer column. * - * @param mixed[] $column - * - * @return string + * @param array $column */ - abstract protected function _getCommonIntegerTypeDeclarationSQL(array $column); + abstract protected function _getCommonIntegerTypeDeclarationSQL(array $column): string; /** * Lazy load Doctrine Type Mappings. - * - * @return void */ - abstract protected function initializeDoctrineTypeMappings(); + abstract protected function initializeDoctrineTypeMappings(): void; /** * Initializes Doctrine Type Mappings with the platform defaults * and with all additional type mappings. + * + * @throws TypesException */ private function initializeAllDoctrineTypeMappings(): void { @@ -223,7 +183,7 @@ private function initializeAllDoctrineTypeMappings(): void * Returns the SQL snippet used to declare a column that can * store characters in the ASCII character set * - * @param mixed[] $column + * @param array $column The column definition. */ public function getAsciiStringTypeDeclarationSQL(array $column): string { @@ -231,84 +191,78 @@ public function getAsciiStringTypeDeclarationSQL(array $column): string } /** - * Returns the SQL snippet used to declare a VARCHAR column type. - * - * @deprecated Use {@link getStringTypeDeclarationSQL()} instead. - * - * @param mixed[] $column + * Returns the SQL snippet used to declare a string column type. * - * @return string + * @param array $column The column definition. */ - public function getVarcharTypeDeclarationSQL(array $column) + public function getStringTypeDeclarationSQL(array $column): string { - if (isset($column['length'])) { - $lengthOmitted = false; - } else { - $column['length'] = $this->getVarcharDefaultLength(); - $lengthOmitted = true; - } + $length = $column['length'] ?? null; - $fixed = $column['fixed'] ?? false; - - $maxLength = $fixed - ? $this->getCharMaxLength() - : $this->getVarcharMaxLength(); - - if ($column['length'] > $maxLength) { - return $this->getClobTypeDeclarationSQL($column); + if (empty($column['fixed'])) { + try { + return $this->getVarcharTypeDeclarationSQLSnippet($length); + } catch (InvalidColumnType $e) { + throw InvalidColumnDeclaration::fromInvalidColumnType($column['name'], $e); + } } - return $this->getVarcharTypeDeclarationSQLSnippet($column['length'], $fixed, $lengthOmitted); + return $this->getCharTypeDeclarationSQLSnippet($length); } /** - * Returns the SQL snippet used to declare a string column type. + * Returns the SQL snippet used to declare a binary string column type. * - * @param mixed[] $column - * - * @return string + * @param array $column The column definition. */ - public function getStringTypeDeclarationSQL(array $column) + public function getBinaryTypeDeclarationSQL(array $column): string { - return $this->getVarcharTypeDeclarationSQL($column); + $length = $column['length'] ?? null; + + try { + if (empty($column['fixed'])) { + return $this->getVarbinaryTypeDeclarationSQLSnippet($length); + } + + return $this->getBinaryTypeDeclarationSQLSnippet($length); + } catch (InvalidColumnType $e) { + throw InvalidColumnDeclaration::fromInvalidColumnType($column['name'], $e); + } } /** - * Returns the SQL snippet used to declare a BINARY/VARBINARY column type. + * Returns the SQL snippet to declare an ENUM column. + * + * Enum is a non-standard type that is especially popular in MySQL and MariaDB. By default, this method map to + * a simple VARCHAR field which allows us to deploy it on any platform, e.g. SQLite. * - * @param mixed[] $column The column definition. + * @param array $column * - * @return string + * @throws ColumnValuesRequired If the column definition does not contain any values. */ - public function getBinaryTypeDeclarationSQL(array $column) + public function getEnumDeclarationSQL(array $column): string { - if (isset($column['length'])) { - $lengthOmitted = false; - } else { - $column['length'] = $this->getBinaryDefaultLength(); - $lengthOmitted = true; + if (! isset($column['values']) || ! is_array($column['values']) || $column['values'] === []) { + throw ColumnValuesRequired::new($this, 'ENUM'); } - $fixed = $column['fixed'] ?? false; + $length = count($column['values']) > 1 + ? max(...array_map(mb_strlen(...), $column['values'])) + : mb_strlen($column['values'][key($column['values'])]); - $maxLength = $this->getBinaryMaxLength(); - - if ($column['length'] > $maxLength) { - if ($maxLength > 0) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3187', - 'Binary column length %d is greater than supported by the platform (%d).' - . ' Reduce the column length or use a BLOB column instead.', + if (isset($column['length'])) { + if ($length > $column['length']) { + throw new InvalidArgumentException(sprintf( + 'Specified column length (%d) is less than the maximum length of provided values (%d).', $column['length'], - $maxLength, - ); + $length, + )); } - return $this->getBlobTypeDeclarationSQL($column); + $length = $column['length']; } - return $this->getBinaryTypeDeclarationSQLSnippet($column['length'], $fixed, $lengthOmitted); + return $this->getStringTypeDeclarationSQL(['length' => $length]); } /** @@ -317,11 +271,9 @@ public function getBinaryTypeDeclarationSQL(array $column) * By default this maps directly to a CHAR(36) and only maps to more * special datatypes when the underlying databases support this datatype. * - * @param mixed[] $column - * - * @return string + * @param array $column The column definition. */ - public function getGuidTypeDeclarationSQL(array $column) + public function getGuidTypeDeclarationSQL(array $column): string { $column['length'] = 36; $column['fixed'] = true; @@ -335,112 +287,130 @@ public function getGuidTypeDeclarationSQL(array $column) * By default this maps directly to a CLOB and only maps to more * special datatypes when the underlying databases support this datatype. * - * @param mixed[] $column - * - * @return string + * @param array $column */ - public function getJsonTypeDeclarationSQL(array $column) + public function getJsonTypeDeclarationSQL(array $column): string { + if (! empty($column['jsonb'])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6939', + 'The "jsonb" column platform option is deprecated. Use the "JSONB" type instead.', + ); + } + return $this->getClobTypeDeclarationSQL($column); } /** - * @param int|false $length - * @param bool $fixed + * Returns the SQL snippet to declare a JSONB column. * - * @return string - * - * @throws Exception If not supported on this platform. + * @param array $column */ - protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + public function getJsonbTypeDeclarationSQL(array $column): string { - throw Exception::notSupported('VARCHARs not supported by Platform.'); + return $this->getJsonTypeDeclarationSQL($column); } /** - * Returns the SQL snippet used to declare a BINARY/VARBINARY column type. - * - * @param int|false $length The length of the column. - * @param bool $fixed Whether the column length is fixed. - * - * @return string - * - * @throws Exception If not supported on this platform. + * @param int|null $length The length of the column in characters + * or NULL if the length should be omitted. */ - protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + protected function getCharTypeDeclarationSQLSnippet(?int $length): string { - throw Exception::notSupported('BINARY/VARBINARY column types are not supported by this platform.'); + $sql = 'CHAR'; + + if ($length !== null) { + $sql .= sprintf('(%d)', $length); + } + + return $sql; } /** - * Returns the SQL snippet used to declare a CLOB column type. - * - * @param mixed[] $column - * - * @return string + * @param int|null $length The length of the column in characters + * or NULL if the length should be omitted. */ - abstract public function getClobTypeDeclarationSQL(array $column); + protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string + { + if ($length === null) { + throw ColumnLengthRequired::new($this, 'VARCHAR'); + } + + return sprintf('VARCHAR(%d)', $length); + } /** - * Returns the SQL Snippet used to declare a BLOB column type. + * Returns the SQL snippet used to declare a fixed length binary column type. * - * @param mixed[] $column + * @param int|null $length The length of the column in bytes + * or NULL if the length should be omitted. + */ + protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string + { + $sql = 'BINARY'; + + if ($length !== null) { + $sql .= sprintf('(%d)', $length); + } + + return $sql; + } + + /** + * Returns the SQL snippet used to declare a variable length binary column type. * - * @return string + * @param int|null $length The length of the column in bytes + * or NULL if the length should be omitted. */ - abstract public function getBlobTypeDeclarationSQL(array $column); + protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string + { + if ($length === null) { + throw ColumnLengthRequired::new($this, 'VARBINARY'); + } + + return sprintf('VARBINARY(%d)', $length); + } /** - * Gets the name of the platform. + * Returns the SQL snippet used to declare a CLOB column type. * - * @deprecated Identify platforms by their class. + * @param array $column + */ + abstract public function getClobTypeDeclarationSQL(array $column): string; + + /** + * Returns the SQL Snippet used to declare a BLOB column type. * - * @return string + * @param array $column */ - abstract public function getName(); + abstract public function getBlobTypeDeclarationSQL(array $column): string; /** * Registers a doctrine type to be used in conjunction with a column type of this platform. * - * @param string $dbType - * @param string $doctrineType - * - * @return void - * * @throws Exception If the type is not found. */ - public function registerDoctrineTypeMapping($dbType, $doctrineType) + public function registerDoctrineTypeMapping(string $dbType, string $doctrineType): void { if ($this->doctrineTypeMapping === null) { $this->initializeAllDoctrineTypeMappings(); } if (! Types\Type::hasType($doctrineType)) { - throw Exception::typeNotFound($doctrineType); + throw TypeNotFound::new($doctrineType); } $dbType = strtolower($dbType); $this->doctrineTypeMapping[$dbType] = $doctrineType; - - $doctrineType = Type::getType($doctrineType); - - if (! $doctrineType->requiresSQLCommentHint($this)) { - return; - } - - $this->markDoctrineTypeCommented($doctrineType); } /** * Gets the Doctrine type that is mapped for the given database column type. * - * @param string $dbType - * - * @return string - * - * @throws Exception + * @throws TypesException */ - public function getDoctrineTypeMapping($dbType) + public function getDoctrineTypeMapping(string $dbType): string { if ($this->doctrineTypeMapping === null) { $this->initializeAllDoctrineTypeMappings(); @@ -449,9 +419,11 @@ public function getDoctrineTypeMapping($dbType) $dbType = strtolower($dbType); if (! isset($this->doctrineTypeMapping[$dbType])) { - throw new Exception( - 'Unknown database type ' . $dbType . ' requested, ' . static::class . ' may not support it.', - ); + throw new InvalidArgumentException(sprintf( + 'Unknown database type "%s" requested, %s may not support it.', + $dbType, + static::class, + )); } return $this->doctrineTypeMapping[$dbType]; @@ -460,11 +432,9 @@ public function getDoctrineTypeMapping($dbType) /** * Checks if a database type is currently supported by this platform. * - * @param string $dbType - * - * @return bool + * @throws TypesException */ - public function hasDoctrineTypeMappingFor($dbType) + public function hasDoctrineTypeMappingFor(string $dbType): bool { if ($this->doctrineTypeMapping === null) { $this->initializeAllDoctrineTypeMappings(); @@ -476,3849 +446,1729 @@ public function hasDoctrineTypeMappingFor($dbType) } /** - * Initializes the Doctrine Type comments instance variable for in_array() checks. - * - * @deprecated This API will be removed in Doctrine DBAL 4.0. - * - * @return void + * Returns the regular expression operator. */ - protected function initializeCommentedDoctrineTypes() + public function getRegexpExpression(): string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5058', - '%s is deprecated and will be removed in Doctrine DBAL 4.0.', - __METHOD__, - ); - - $this->doctrineTypeComments = []; - - foreach (Type::getTypesMap() as $typeName => $className) { - $type = Type::getType($typeName); - - if (! $type->requiresSQLCommentHint($this)) { - continue; - } - - $this->doctrineTypeComments[] = $typeName; - } + throw NotSupported::new(__METHOD__); } /** - * Is it necessary for the platform to add a parsable type comment to allow reverse engineering the given type? - * - * @deprecated Use {@link Type::requiresSQLCommentHint()} instead. + * Returns the SQL snippet to get the length of a text column in characters. * - * @return bool + * @param string $string SQL expression producing the string. */ - public function isCommentedDoctrineType(Type $doctrineType) + public function getLengthExpression(string $string): string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5058', - '%s is deprecated and will be removed in Doctrine DBAL 4.0. Use Type::requiresSQLCommentHint() instead.', - __METHOD__, - ); - - if ($this->doctrineTypeComments === null) { - $this->initializeCommentedDoctrineTypes(); - } - - return $doctrineType->requiresSQLCommentHint($this); + return 'LENGTH(' . $string . ')'; } /** - * Marks this type as to be commented in ALTER TABLE and CREATE TABLE statements. + * Returns the SQL snippet to get the remainder of the operation of division of dividend by divisor. * - * @param string|Type $doctrineType - * - * @return void + * @param string $dividend SQL expression producing the dividend. + * @param string $divisor SQL expression producing the divisor. */ - public function markDoctrineTypeCommented($doctrineType) + public function getModExpression(string $dividend, string $divisor): string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5058', - '%s is deprecated and will be removed in Doctrine DBAL 4.0. Use Type::requiresSQLCommentHint() instead.', - __METHOD__, - ); + return 'MOD(' . $dividend . ', ' . $divisor . ')'; + } - if ($this->doctrineTypeComments === null) { - $this->initializeCommentedDoctrineTypes(); + /** + * Returns the SQL snippet to trim a string. + * + * @param string $str The expression to apply the trim to. + * @param TrimMode $mode The position of the trim. + * @param string|null $char The char to trim, has to be quoted already. Defaults to space. + */ + public function getTrimExpression( + string $str, + TrimMode $mode = TrimMode::UNSPECIFIED, + ?string $char = null, + ): string { + $tokens = []; + + switch ($mode) { + case TrimMode::UNSPECIFIED: + break; + + case TrimMode::LEADING: + $tokens[] = 'LEADING'; + break; + + case TrimMode::TRAILING: + $tokens[] = 'TRAILING'; + break; + + case TrimMode::BOTH: + $tokens[] = 'BOTH'; + break; + } + + if ($char !== null) { + $tokens[] = $char; + } + + if (count($tokens) > 0) { + $tokens[] = 'FROM'; } - assert(is_array($this->doctrineTypeComments)); + $tokens[] = $str; - $this->doctrineTypeComments[] = $doctrineType instanceof Type ? $doctrineType->getName() : $doctrineType; + return sprintf('TRIM(%s)', implode(' ', $tokens)); } /** - * Gets the comment to append to a column comment that helps parsing this type in reverse engineering. - * - * @deprecated This method will be removed without replacement. + * Returns the SQL snippet to get the position of the first occurrence of the substring in the string. * - * @return string + * @param string $string SQL expression producing the string to locate the substring in. + * @param string $substring SQL expression producing the substring to locate. + * @param string|null $start SQL expression producing the position to start at. + * Defaults to the beginning of the string. */ - public function getDoctrineTypeComment(Type $doctrineType) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5107', - '%s is deprecated and will be removed in Doctrine DBAL 4.0.', - __METHOD__, - ); - - return '(DC2Type:' . $doctrineType->getName() . ')'; - } + abstract public function getLocateExpression(string $string, string $substring, ?string $start = null): string; /** - * Gets the comment of a passed column modified by potential doctrine type comment hints. + * Returns an SQL snippet to get a substring inside the string. * - * @deprecated This method will be removed without replacement. + * Note: Not SQL92, but common functionality. * - * @return string|null + * @param string $string SQL expression producing the string from which a substring should be extracted. + * @param string $start SQL expression producing the position to start at, + * @param string|null $length SQL expression producing the length of the substring portion to be returned. + * By default, the entire substring is returned. */ - protected function getColumnComment(Column $column) + public function getSubstringExpression(string $string, string $start, ?string $length = null): string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5107', - '%s is deprecated and will be removed in Doctrine DBAL 4.0.', - __METHOD__, - ); - - $comment = $column->getComment(); - - if (! $this->disableTypeComments && $column->getType()->requiresSQLCommentHint($this)) { - $comment .= $this->getDoctrineTypeComment($column->getType()); + if ($length === null) { + return sprintf('SUBSTRING(%s FROM %s)', $string, $start); } - return $comment; + return sprintf('SUBSTRING(%s FROM %s FOR %s)', $string, $start, $length); } /** - * Gets the character used for identifier quoting. - * - * @deprecated Use {@see quoteIdentifier()} to quote identifiers instead. - * - * @return string + * Returns a SQL snippet to concatenate the given strings. */ - public function getIdentifierQuoteCharacter() + public function getConcatExpression(string ...$string): string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5388', - 'AbstractPlatform::getIdentifierQuoteCharacter() is deprecated. Use quoteIdentifier() instead.', - ); - - return '"'; + return implode(' || ', $string); } /** - * Gets the string portion that starts an SQL comment. + * Returns the SQL to calculate the difference in days between the two passed dates. * - * @deprecated + * Computes diff = date1 - date2. + */ + abstract public function getDateDiffExpression(string $date1, string $date2): string; + + /** + * Returns the SQL to add the number of given seconds to a date. * - * @return string + * @param string $date SQL expression producing the date. + * @param string $seconds SQL expression producing the number of seconds. */ - public function getSqlCommentStartString() + public function getDateAddSecondsExpression(string $date, string $seconds): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getSqlCommentStartString() is deprecated.', - ); - - return '--'; + return $this->getDateArithmeticIntervalExpression($date, '+', $seconds, DateIntervalUnit::SECOND); } /** - * Gets the string portion that ends an SQL comment. - * - * @deprecated + * Returns the SQL to subtract the number of given seconds from a date. * - * @return string + * @param string $date SQL expression producing the date. + * @param string $seconds SQL expression producing the number of seconds. */ - public function getSqlCommentEndString() + public function getDateSubSecondsExpression(string $date, string $seconds): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getSqlCommentEndString() is deprecated.', - ); - - return "\n"; + return $this->getDateArithmeticIntervalExpression($date, '-', $seconds, DateIntervalUnit::SECOND); } /** - * Gets the maximum length of a char column. + * Returns the SQL to add the number of given minutes to a date. * - * @deprecated + * @param string $date SQL expression producing the date. + * @param string $minutes SQL expression producing the number of minutes. */ - public function getCharMaxLength(): int + public function getDateAddMinutesExpression(string $date, string $minutes): string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'AbstractPlatform::getCharMaxLength() is deprecated.', - ); - - return $this->getVarcharMaxLength(); + return $this->getDateArithmeticIntervalExpression($date, '+', $minutes, DateIntervalUnit::MINUTE); } /** - * Gets the maximum length of a varchar column. - * - * @deprecated + * Returns the SQL to subtract the number of given minutes from a date. * - * @return int + * @param string $date SQL expression producing the date. + * @param string $minutes SQL expression producing the number of minutes. */ - public function getVarcharMaxLength() + public function getDateSubMinutesExpression(string $date, string $minutes): string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'AbstractPlatform::getVarcharMaxLength() is deprecated.', - ); - - return 4000; + return $this->getDateArithmeticIntervalExpression($date, '-', $minutes, DateIntervalUnit::MINUTE); } /** - * Gets the default length of a varchar column. - * - * @deprecated + * Returns the SQL to add the number of given hours to a date. * - * @return int + * @param string $date SQL expression producing the date. + * @param string $hours SQL expression producing the number of hours. */ - public function getVarcharDefaultLength() + public function getDateAddHourExpression(string $date, string $hours): string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'Relying on the default varchar column length is deprecated, specify the length explicitly.', - ); - - return 255; + return $this->getDateArithmeticIntervalExpression($date, '+', $hours, DateIntervalUnit::HOUR); } /** - * Gets the maximum length of a binary column. - * - * @deprecated + * Returns the SQL to subtract the number of given hours to a date. * - * @return int + * @param string $date SQL expression producing the date. + * @param string $hours SQL expression producing the number of hours. */ - public function getBinaryMaxLength() + public function getDateSubHourExpression(string $date, string $hours): string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'AbstractPlatform::getBinaryMaxLength() is deprecated.', - ); - - return 4000; + return $this->getDateArithmeticIntervalExpression($date, '-', $hours, DateIntervalUnit::HOUR); } /** - * Gets the default length of a binary column. + * Returns the SQL to add the number of given days to a date. * - * @deprecated - * - * @return int + * @param string $date SQL expression producing the date. + * @param string $days SQL expression producing the number of days. */ - public function getBinaryDefaultLength() + public function getDateAddDaysExpression(string $date, string $days): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'Relying on the default binary column length is deprecated, specify the length explicitly.', - ); - - return 255; + return $this->getDateArithmeticIntervalExpression($date, '+', $days, DateIntervalUnit::DAY); } /** - * Gets all SQL wildcard characters of the platform. - * - * @deprecated Use {@see AbstractPlatform::getLikeWildcardCharacters()} instead. + * Returns the SQL to subtract the number of given days to a date. * - * @return string[] + * @param string $date SQL expression producing the date. + * @param string $days SQL expression producing the number of days. */ - public function getWildcards() + public function getDateSubDaysExpression(string $date, string $days): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getWildcards() is deprecated.' - . ' Use AbstractPlatform::getLikeWildcardCharacters() instead.', - ); - - return ['%', '_']; + return $this->getDateArithmeticIntervalExpression($date, '-', $days, DateIntervalUnit::DAY); } /** - * Returns the regular expression operator. - * - * @return string + * Returns the SQL to add the number of given weeks to a date. * - * @throws Exception If not supported on this platform. + * @param string $date SQL expression producing the date. + * @param string $weeks SQL expression producing the number of weeks. */ - public function getRegexpExpression() + public function getDateAddWeeksExpression(string $date, string $weeks): string { - throw Exception::notSupported(__METHOD__); + return $this->getDateArithmeticIntervalExpression($date, '+', $weeks, DateIntervalUnit::WEEK); } /** - * Returns the SQL snippet to get the average value of a column. - * - * @deprecated Use AVG() in SQL instead. - * - * @param string $column The column to use. + * Returns the SQL to subtract the number of given weeks from a date. * - * @return string Generated SQL including an AVG aggregate function. + * @param string $date SQL expression producing the date. + * @param string $weeks SQL expression producing the number of weeks. */ - public function getAvgExpression($column) + public function getDateSubWeeksExpression(string $date, string $weeks): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getAvgExpression() is deprecated. Use AVG() in SQL instead.', - ); - - return 'AVG(' . $column . ')'; + return $this->getDateArithmeticIntervalExpression($date, '-', $weeks, DateIntervalUnit::WEEK); } /** - * Returns the SQL snippet to get the number of rows (without a NULL value) of a column. - * - * If a '*' is used instead of a column the number of selected rows is returned. - * - * @deprecated Use COUNT() in SQL instead. - * - * @param string|int $column The column to use. + * Returns the SQL to add the number of given months to a date. * - * @return string Generated SQL including a COUNT aggregate function. + * @param string $date SQL expression producing the date. + * @param string $months SQL expression producing the number of months. */ - public function getCountExpression($column) + public function getDateAddMonthExpression(string $date, string $months): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getCountExpression() is deprecated. Use COUNT() in SQL instead.', - ); - - return 'COUNT(' . $column . ')'; + return $this->getDateArithmeticIntervalExpression($date, '+', $months, DateIntervalUnit::MONTH); } /** - * Returns the SQL snippet to get the highest value of a column. - * - * @deprecated Use MAX() in SQL instead. - * - * @param string $column The column to use. + * Returns the SQL to subtract the number of given months to a date. * - * @return string Generated SQL including a MAX aggregate function. + * @param string $date SQL expression producing the date. + * @param string $months SQL expression producing the number of months. */ - public function getMaxExpression($column) + public function getDateSubMonthExpression(string $date, string $months): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getMaxExpression() is deprecated. Use MAX() in SQL instead.', - ); - - return 'MAX(' . $column . ')'; + return $this->getDateArithmeticIntervalExpression($date, '-', $months, DateIntervalUnit::MONTH); } /** - * Returns the SQL snippet to get the lowest value of a column. - * - * @deprecated Use MIN() in SQL instead. - * - * @param string $column The column to use. + * Returns the SQL to add the number of given quarters to a date. * - * @return string Generated SQL including a MIN aggregate function. + * @param string $date SQL expression producing the date. + * @param string $quarters SQL expression producing the number of quarters. */ - public function getMinExpression($column) + public function getDateAddQuartersExpression(string $date, string $quarters): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getMinExpression() is deprecated. Use MIN() in SQL instead.', - ); - - return 'MIN(' . $column . ')'; + return $this->getDateArithmeticIntervalExpression($date, '+', $quarters, DateIntervalUnit::QUARTER); } /** - * Returns the SQL snippet to get the total sum of a column. - * - * @deprecated Use SUM() in SQL instead. - * - * @param string $column The column to use. + * Returns the SQL to subtract the number of given quarters from a date. * - * @return string Generated SQL including a SUM aggregate function. + * @param string $date SQL expression producing the date. + * @param string $quarters SQL expression producing the number of quarters. */ - public function getSumExpression($column) + public function getDateSubQuartersExpression(string $date, string $quarters): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getSumExpression() is deprecated. Use SUM() in SQL instead.', - ); - - return 'SUM(' . $column . ')'; + return $this->getDateArithmeticIntervalExpression($date, '-', $quarters, DateIntervalUnit::QUARTER); } - // scalar functions - /** - * Returns the SQL snippet to get the md5 sum of a column. - * - * Note: Not SQL92, but common functionality. - * - * @deprecated - * - * @param string $column + * Returns the SQL to add the number of given years to a date. * - * @return string + * @param string $date SQL expression producing the date. + * @param string $years SQL expression producing the number of years. */ - public function getMd5Expression($column) + public function getDateAddYearsExpression(string $date, string $years): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getMd5Expression() is deprecated.', - ); - - return 'MD5(' . $column . ')'; + return $this->getDateArithmeticIntervalExpression($date, '+', $years, DateIntervalUnit::YEAR); } /** - * Returns the SQL snippet to get the length of a text column in characters. - * - * @param string $column + * Returns the SQL to subtract the number of given years from a date. * - * @return string + * @param string $date SQL expression producing the date. + * @param string $years SQL expression producing the number of years. */ - public function getLengthExpression($column) + public function getDateSubYearsExpression(string $date, string $years): string { - return 'LENGTH(' . $column . ')'; + return $this->getDateArithmeticIntervalExpression($date, '-', $years, DateIntervalUnit::YEAR); } /** - * Returns the SQL snippet to get the squared value of a column. - * - * @deprecated Use SQRT() in SQL instead. - * - * @param string $column The column to use. + * Returns the SQL for a date arithmetic expression. * - * @return string Generated SQL including an SQRT aggregate function. + * @param string $date SQL expression representing a date to perform the arithmetic operation on. + * @param string $operator The arithmetic operator (+ or -). + * @param string $interval SQL expression representing the value of the interval that shall be calculated + * into the date. + * @param DateIntervalUnit $unit The unit of the interval that shall be calculated into the date. */ - public function getSqrtExpression($column) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getSqrtExpression() is deprecated. Use SQRT() in SQL instead.', - ); - - return 'SQRT(' . $column . ')'; - } + abstract protected function getDateArithmeticIntervalExpression( + string $date, + string $operator, + string $interval, + DateIntervalUnit $unit, + ): string; /** - * Returns the SQL snippet to round a numeric column to the number of decimals specified. - * - * @deprecated Use ROUND() in SQL instead. - * - * @param string $column - * @param string|int $decimals + * Generates the SQL expression which represents the given date interval multiplied by a number * - * @return string + * @param string $interval SQL expression describing the interval value + * @param int $multiplier Interval multiplier */ - public function getRoundExpression($column, $decimals = 0) + protected function multiplyInterval(string $interval, int $multiplier): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getRoundExpression() is deprecated. Use ROUND() in SQL instead.', - ); - - return 'ROUND(' . $column . ', ' . $decimals . ')'; + return sprintf('(%s * %d)', $interval, $multiplier); } /** - * Returns the SQL snippet to get the remainder of the division operation $expression1 / $expression2. - * - * @param string $expression1 - * @param string $expression2 + * Returns the SQL bit AND comparison expression. * - * @return string + * @param string $value1 SQL expression producing the first value. + * @param string $value2 SQL expression producing the second value. */ - public function getModExpression($expression1, $expression2) + public function getBitAndComparisonExpression(string $value1, string $value2): string { - return 'MOD(' . $expression1 . ', ' . $expression2 . ')'; + return '(' . $value1 . ' & ' . $value2 . ')'; } /** - * Returns the SQL snippet to trim a string. - * - * @param string $str The expression to apply the trim to. - * @param int $mode The position of the trim (leading/trailing/both). - * @param string|bool $char The char to trim, has to be quoted already. Defaults to space. + * Returns the SQL bit OR comparison expression. * - * @return string + * @param string $value1 SQL expression producing the first value. + * @param string $value2 SQL expression producing the second value. */ - public function getTrimExpression($str, $mode = TrimMode::UNSPECIFIED, $char = false) + public function getBitOrComparisonExpression(string $value1, string $value2): string { - $expression = ''; - - switch ($mode) { - case TrimMode::LEADING: - $expression = 'LEADING '; - break; - - case TrimMode::TRAILING: - $expression = 'TRAILING '; - break; - - case TrimMode::BOTH: - $expression = 'BOTH '; - break; - } - - if ($char !== false) { - $expression .= $char . ' '; - } - - if ($mode !== TrimMode::UNSPECIFIED || $char !== false) { - $expression .= 'FROM '; - } - - return 'TRIM(' . $expression . $str . ')'; + return '(' . $value1 . ' | ' . $value2 . ')'; } /** - * Returns the SQL snippet to trim trailing space characters from the expression. - * - * @deprecated Use RTRIM() in SQL instead. - * - * @param string $str Literal string or column name. - * - * @return string + * Returns the SQL expression which represents the currently selected database. */ - public function getRtrimExpression($str) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getRtrimExpression() is deprecated. Use RTRIM() in SQL instead.', - ); - - return 'RTRIM(' . $str . ')'; - } + abstract public function getCurrentDatabaseExpression(): string; /** - * Returns the SQL snippet to trim leading space characters from the expression. - * - * @deprecated Use LTRIM() in SQL instead. - * - * @param string $str Literal string or column name. + * Honors that some SQL vendors such as MsSql use table hints for locking instead of the + * ANSI SQL FOR UPDATE specification. * - * @return string + * @param string $fromClause The FROM clause to append the hint for the given lock mode to */ - public function getLtrimExpression($str) + public function appendLockHint(string $fromClause, LockMode $lockMode): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getLtrimExpression() is deprecated. Use LTRIM() in SQL instead.', - ); - - return 'LTRIM(' . $str . ')'; + return $fromClause; } /** - * Returns the SQL snippet to change all characters from the expression to uppercase, - * according to the current character set mapping. - * - * @deprecated Use UPPER() in SQL instead. - * - * @param string $str Literal string or column name. - * - * @return string + * Returns the SQL snippet to drop an existing table. */ - public function getUpperExpression($str) + public function getDropTableSQL(string $table): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getUpperExpression() is deprecated. Use UPPER() in SQL instead.', - ); - - return 'UPPER(' . $str . ')'; + return 'DROP TABLE ' . $table; } /** - * Returns the SQL snippet to change all characters from the expression to lowercase, - * according to the current character set mapping. - * - * @deprecated Use LOWER() in SQL instead. - * - * @param string $str Literal string or column name. - * - * @return string + * Returns the SQL to safely drop a temporary table WITHOUT implicitly committing an open transaction. */ - public function getLowerExpression($str) + public function getDropTemporaryTableSQL(string $table): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getLowerExpression() is deprecated. Use LOWER() in SQL instead.', - ); - - return 'LOWER(' . $str . ')'; + return $this->getDropTableSQL($table); } /** - * Returns the SQL snippet to get the position of the first occurrence of substring $substr in string $str. - * - * @param string $str Literal string. - * @param string $substr Literal string to find. - * @param string|int|false $startPos Position to start at, beginning of string by default. - * - * @return string - * - * @throws Exception If not supported on this platform. + * Returns the SQL to drop an index from a table. */ - public function getLocateExpression($str, $substr, $startPos = false) + public function getDropIndexSQL(string $name, string $table): string { - throw Exception::notSupported(__METHOD__); + return 'DROP INDEX ' . $name; } /** - * Returns the SQL snippet to get the current system date. - * - * @deprecated Generate dates within the application. + * Returns the SQL to drop a constraint. * - * @return string + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ - public function getNowExpression() + protected function getDropConstraintSQL(string $name, string $table): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4753', - 'AbstractPlatform::getNowExpression() is deprecated. Generate dates within the application.', - ); - - return 'NOW()'; + return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $name; } /** - * Returns a SQL snippet to get a substring inside an SQL statement. - * - * Note: Not SQL92, but common functionality. - * - * SQLite only supports the 2 parameter variant of this function. - * - * @param string $string An sql string literal or column name/alias. - * @param string|int $start Where to start the substring portion. - * @param string|int|null $length The substring portion length. - * - * @return string + * Returns the SQL to drop a foreign key. */ - public function getSubstringExpression($string, $start, $length = null) + public function getDropForeignKeySQL(string $foreignKey, string $table): string { - if ($length === null) { - return 'SUBSTRING(' . $string . ' FROM ' . $start . ')'; - } - - return 'SUBSTRING(' . $string . ' FROM ' . $start . ' FOR ' . $length . ')'; + return 'ALTER TABLE ' . $table . ' DROP FOREIGN KEY ' . $foreignKey; } /** - * Returns a SQL snippet to concatenate the given expressions. - * - * Accepts an arbitrary number of string parameters. Each parameter must contain an expression. - * - * @return string + * Returns the SQL to drop a unique constraint. */ - public function getConcatExpression() + public function getDropUniqueConstraintSQL(string $name, string $tableName): string { - return implode(' || ', func_get_args()); + return $this->getDropConstraintSQL($name, $tableName); } /** - * Returns the SQL for a logical not. - * - * Example: - * - * $q = new Doctrine_Query(); - * $e = $q->expr; - * $q->select('*')->from('table') - * ->where($e->eq('id', $e->not('null')); - * - * - * @deprecated Use NOT() in SQL instead. - * - * @param string $expression + * Returns the SQL statement(s) to create a table with the specified name, columns and constraints + * on this platform. * - * @return string The logical expression. + * @return list The list of SQL statements. */ - public function getNotExpression($expression) + public function getCreateTableSQL(Table $table): array { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getNotExpression() is deprecated. Use NOT() in SQL instead.', - ); - - return 'NOT(' . $expression . ')'; + return $this->buildCreateTableSQL($table, true); } - /** - * Returns the SQL that checks if an expression is null. - * - * @deprecated Use IS NULL in SQL instead. - * - * @param string $expression The expression that should be compared to null. - * - * @return string The logical expression. - */ - public function getIsNullExpression($expression) + public function createSelectSQLBuilder(): SelectSQLBuilder { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getIsNullExpression() is deprecated. Use IS NULL in SQL instead.', - ); - - return $expression . ' IS NULL'; + return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', 'SKIP LOCKED'); } - /** - * Returns the SQL that checks if an expression is not null. - * - * @deprecated Use IS NOT NULL in SQL instead. - * - * @param string $expression The expression that should be compared to null. - * - * @return string The logical expression. - */ - public function getIsNotNullExpression($expression) + public function createUnionSQLBuilder(): UnionSQLBuilder { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getIsNotNullExpression() is deprecated. Use IS NOT NULL in SQL instead.', - ); - - return $expression . ' IS NOT NULL'; + return new DefaultUnionSQLBuilder($this); } - /** - * Returns the SQL that checks if an expression evaluates to a value between two values. - * - * The parameter $expression is checked if it is between $value1 and $value2. - * - * Note: There is a slight difference in the way BETWEEN works on some databases. - * http://www.w3schools.com/sql/sql_between.asp. If you want complete database - * independence you should avoid using between(). - * - * @deprecated Use BETWEEN in SQL instead. - * - * @param string $expression The value to compare to. - * @param string $value1 The lower value to compare with. - * @param string $value2 The higher value to compare with. - * - * @return string The logical expression. - */ - public function getBetweenExpression($expression, $value1, $value2) + public function createWithSQLBuilder(): WithSQLBuilder { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getBetweenExpression() is deprecated. Use BETWEEN in SQL instead.', - ); - - return $expression . ' BETWEEN ' . $value1 . ' AND ' . $value2; + return new WithSQLBuilder(); } /** - * Returns the SQL to get the arccosine of a value. - * - * @deprecated Use ACOS() in SQL instead. - * - * @param string $value + * @internal * - * @return string + * @return list */ - public function getAcosExpression($value) + final protected function getCreateTableWithoutForeignKeysSQL(Table $table): array { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getAcosExpression() is deprecated. Use ACOS() in SQL instead.', - ); - - return 'ACOS(' . $value . ')'; + return $this->buildCreateTableSQL($table, false); } - /** - * Returns the SQL to get the sine of a value. - * - * @deprecated Use SIN() in SQL instead. - * - * @param string $value - * - * @return string - */ - public function getSinExpression($value) + /** @return list */ + private function buildCreateTableSQL(Table $table, bool $createForeignKeys): array { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getSinExpression() is deprecated. Use SIN() in SQL instead.', - ); + if (count($table->getColumns()) === 0) { + throw NoColumnsSpecifiedForTable::new($table->getName()); + } - return 'SIN(' . $value . ')'; - } + $tableName = $table->getQuotedName($this); + $options = $table->getOptions(); + $options['primary'] = []; + $options['indexes'] = []; + $options['uniqueConstraints'] = []; + $options['foreignKeys'] = []; - /** - * Returns the SQL to get the PI value. - * - * @deprecated Use PI() in SQL instead. - * - * @return string - */ - public function getPiExpression() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getPiExpression() is deprecated. Use PI() in SQL instead.', - ); - - return 'PI()'; - } - - /** - * Returns the SQL to get the cosine of a value. - * - * @deprecated Use COS() in SQL instead. - * - * @param string $value - * - * @return string - */ - public function getCosExpression($value) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getCosExpression() is deprecated. Use COS() in SQL instead.', - ); - - return 'COS(' . $value . ')'; - } - - /** - * Returns the SQL to calculate the difference in days between the two passed dates. - * - * Computes diff = date1 - date2. - * - * @param string $date1 - * @param string $date2 - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateDiffExpression($date1, $date2) - { - throw Exception::notSupported(__METHOD__); - } - - /** - * Returns the SQL to add the number of given seconds to a date. - * - * @param string $date - * @param int|string $seconds - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateAddSecondsExpression($date, $seconds) - { - if (is_int($seconds)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $seconds as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '+', $seconds, DateIntervalUnit::SECOND); - } - - /** - * Returns the SQL to subtract the number of given seconds from a date. - * - * @param string $date - * @param int|string $seconds - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateSubSecondsExpression($date, $seconds) - { - if (is_int($seconds)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $seconds as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '-', $seconds, DateIntervalUnit::SECOND); - } - - /** - * Returns the SQL to add the number of given minutes to a date. - * - * @param string $date - * @param int|string $minutes - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateAddMinutesExpression($date, $minutes) - { - if (is_int($minutes)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $minutes as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '+', $minutes, DateIntervalUnit::MINUTE); - } - - /** - * Returns the SQL to subtract the number of given minutes from a date. - * - * @param string $date - * @param int|string $minutes - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateSubMinutesExpression($date, $minutes) - { - if (is_int($minutes)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $minutes as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '-', $minutes, DateIntervalUnit::MINUTE); - } - - /** - * Returns the SQL to add the number of given hours to a date. - * - * @param string $date - * @param int|string $hours - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateAddHourExpression($date, $hours) - { - if (is_int($hours)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $hours as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '+', $hours, DateIntervalUnit::HOUR); - } - - /** - * Returns the SQL to subtract the number of given hours to a date. - * - * @param string $date - * @param int|string $hours - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateSubHourExpression($date, $hours) - { - if (is_int($hours)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $hours as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '-', $hours, DateIntervalUnit::HOUR); - } - - /** - * Returns the SQL to add the number of given days to a date. - * - * @param string $date - * @param int|string $days - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateAddDaysExpression($date, $days) - { - if (is_int($days)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $days as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '+', $days, DateIntervalUnit::DAY); - } - - /** - * Returns the SQL to subtract the number of given days to a date. - * - * @param string $date - * @param int|string $days - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateSubDaysExpression($date, $days) - { - if (is_int($days)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $days as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '-', $days, DateIntervalUnit::DAY); - } - - /** - * Returns the SQL to add the number of given weeks to a date. - * - * @param string $date - * @param int|string $weeks - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateAddWeeksExpression($date, $weeks) - { - if (is_int($weeks)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $weeks as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '+', $weeks, DateIntervalUnit::WEEK); - } - - /** - * Returns the SQL to subtract the number of given weeks from a date. - * - * @param string $date - * @param int|string $weeks - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateSubWeeksExpression($date, $weeks) - { - if (is_int($weeks)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $weeks as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '-', $weeks, DateIntervalUnit::WEEK); - } - - /** - * Returns the SQL to add the number of given months to a date. - * - * @param string $date - * @param int|string $months - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateAddMonthExpression($date, $months) - { - if (is_int($months)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $months as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '+', $months, DateIntervalUnit::MONTH); - } - - /** - * Returns the SQL to subtract the number of given months to a date. - * - * @param string $date - * @param int|string $months - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateSubMonthExpression($date, $months) - { - if (is_int($months)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $months as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '-', $months, DateIntervalUnit::MONTH); - } - - /** - * Returns the SQL to add the number of given quarters to a date. - * - * @param string $date - * @param int|string $quarters - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateAddQuartersExpression($date, $quarters) - { - if (is_int($quarters)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $quarters as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '+', $quarters, DateIntervalUnit::QUARTER); - } - - /** - * Returns the SQL to subtract the number of given quarters from a date. - * - * @param string $date - * @param int|string $quarters - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateSubQuartersExpression($date, $quarters) - { - if (is_int($quarters)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $quarters as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '-', $quarters, DateIntervalUnit::QUARTER); - } - - /** - * Returns the SQL to add the number of given years to a date. - * - * @param string $date - * @param int|string $years - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateAddYearsExpression($date, $years) - { - if (is_int($years)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $years as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '+', $years, DateIntervalUnit::YEAR); - } - - /** - * Returns the SQL to subtract the number of given years from a date. - * - * @param string $date - * @param int|string $years - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDateSubYearsExpression($date, $years) - { - if (is_int($years)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3498', - 'Passing $years as an integer is deprecated. Pass it as a numeric string instead.', - ); - } - - return $this->getDateArithmeticIntervalExpression($date, '-', $years, DateIntervalUnit::YEAR); - } - - /** - * Returns the SQL for a date arithmetic expression. - * - * @param string $date The column or literal representing a date - * to perform the arithmetic operation on. - * @param string $operator The arithmetic operator (+ or -). - * @param int|string $interval The interval that shall be calculated into the date. - * @param string $unit The unit of the interval that shall be calculated into the date. - * One of the {@see DateIntervalUnit} constants. - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit) - { - throw Exception::notSupported(__METHOD__); - } - - /** - * Generates the SQL expression which represents the given date interval multiplied by a number - * - * @param string $interval SQL expression describing the interval value - * @param int $multiplier Interval multiplier - */ - protected function multiplyInterval(string $interval, int $multiplier): string - { - return sprintf('(%s * %d)', $interval, $multiplier); - } - - /** - * Returns the SQL bit AND comparison expression. - * - * @param string $value1 - * @param string $value2 - * - * @return string - */ - public function getBitAndComparisonExpression($value1, $value2) - { - return '(' . $value1 . ' & ' . $value2 . ')'; - } - - /** - * Returns the SQL bit OR comparison expression. - * - * @param string $value1 - * @param string $value2 - * - * @return string - */ - public function getBitOrComparisonExpression($value1, $value2) - { - return '(' . $value1 . ' | ' . $value2 . ')'; - } - - /** - * Returns the SQL expression which represents the currently selected database. - */ - abstract public function getCurrentDatabaseExpression(): string; - - /** - * Returns the FOR UPDATE expression. - * - * @deprecated This API is not portable. Use {@link QueryBuilder::forUpdate()}` instead. - * - * @return string - */ - public function getForUpdateSQL() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6191', - '%s is deprecated as non-portable.', - __METHOD__, - ); - - return 'FOR UPDATE'; - } - - /** - * Honors that some SQL vendors such as MsSql use table hints for locking instead of the - * ANSI SQL FOR UPDATE specification. - * - * @param string $fromClause The FROM clause to append the hint for the given lock mode to - * @param int $lockMode One of the Doctrine\DBAL\LockMode::* constants - * @phpstan-param LockMode::* $lockMode - */ - public function appendLockHint(string $fromClause, int $lockMode): string - { - switch ($lockMode) { - case LockMode::NONE: - case LockMode::OPTIMISTIC: - case LockMode::PESSIMISTIC_READ: - case LockMode::PESSIMISTIC_WRITE: - return $fromClause; - - default: - throw InvalidLockMode::fromLockMode($lockMode); - } - } - - /** - * Returns the SQL snippet to append to any SELECT statement which locks rows in shared read lock. - * - * This defaults to the ANSI SQL "FOR UPDATE", which is an exclusive lock (Write). Some database - * vendors allow to lighten this constraint up to be a real read lock. - * - * @deprecated This API is not portable. - * - * @return string - */ - public function getReadLockSQL() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6191', - '%s is deprecated as non-portable.', - __METHOD__, - ); - - return $this->getForUpdateSQL(); - } - - /** - * Returns the SQL snippet to append to any SELECT statement which obtains an exclusive lock on the rows. - * - * The semantics of this lock mode should equal the SELECT .. FOR UPDATE of the ANSI SQL standard. - * - * @deprecated This API is not portable. - * - * @return string - */ - public function getWriteLockSQL() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6191', - '%s is deprecated as non-portable.', - __METHOD__, - ); - - return $this->getForUpdateSQL(); - } - - /** - * Returns the SQL snippet to drop an existing table. - * - * @param Table|string $table - * - * @return string - * - * @throws InvalidArgumentException - */ - public function getDropTableSQL($table) - { - $tableArg = $table; - - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $table = $table->getQuotedName($this); - } - - if (! is_string($table)) { - throw new InvalidArgumentException( - __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.', - ); - } - - if ($this->_eventManager !== null && $this->_eventManager->hasListeners(Events::onSchemaDropTable)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated.', - Events::onSchemaDropTable, - ); - - $eventArgs = new SchemaDropTableEventArgs($tableArg, $this); - $this->_eventManager->dispatchEvent(Events::onSchemaDropTable, $eventArgs); - - if ($eventArgs->isDefaultPrevented()) { - $sql = $eventArgs->getSql(); - - if ($sql === null) { - throw new UnexpectedValueException('Default implementation of DROP TABLE was overridden with NULL'); - } - - return $sql; - } - } - - return 'DROP TABLE ' . $table; - } - - /** - * Returns the SQL to safely drop a temporary table WITHOUT implicitly committing an open transaction. - * - * @param Table|string $table - * - * @return string - */ - public function getDropTemporaryTableSQL($table) - { - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $table = $table->getQuotedName($this); - } - - return $this->getDropTableSQL($table); - } - - /** - * Returns the SQL to drop an index from a table. - * - * @param Index|string $index - * @param Table|string|null $table - * - * @return string - * - * @throws InvalidArgumentException - */ - public function getDropIndexSQL($index, $table = null) - { - if ($index instanceof Index) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $index as an Index object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $index = $index->getQuotedName($this); - } elseif (! is_string($index)) { - throw new InvalidArgumentException( - __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.', - ); - } - - return 'DROP INDEX ' . $index; - } - - /** - * Returns the SQL to drop a constraint. - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - * - * @param Constraint|string $constraint - * @param Table|string $table - * - * @return string - */ - public function getDropConstraintSQL($constraint, $table) - { - if ($constraint instanceof Constraint) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $constraint as a Constraint object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - } else { - $constraint = new Identifier($constraint); - } - - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - } else { - $table = new Identifier($table); - } - - $constraint = $constraint->getQuotedName($this); - $table = $table->getQuotedName($this); - - return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $constraint; - } - - /** - * Returns the SQL to drop a foreign key. - * - * @param ForeignKeyConstraint|string $foreignKey - * @param Table|string $table - * - * @return string - */ - public function getDropForeignKeySQL($foreignKey, $table) - { - if ($foreignKey instanceof ForeignKeyConstraint) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $foreignKey as a ForeignKeyConstraint object to %s is deprecated.' - . ' Pass it as a quoted name instead.', - __METHOD__, - ); - } else { - $foreignKey = new Identifier($foreignKey); - } - - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - } else { - $table = new Identifier($table); - } - - $foreignKey = $foreignKey->getQuotedName($this); - $table = $table->getQuotedName($this); - - return 'ALTER TABLE ' . $table . ' DROP FOREIGN KEY ' . $foreignKey; - } - - /** - * Returns the SQL to drop a unique constraint. - */ - public function getDropUniqueConstraintSQL(string $name, string $tableName): string - { - return $this->getDropConstraintSQL($name, $tableName); - } - - /** - * Returns the SQL statement(s) to create a table with the specified name, columns and constraints - * on this platform. - * - * @param int $createFlags - * @phpstan-param int-mask-of $createFlags - * - * @return list The list of SQL statements. - * - * @throws Exception - * @throws InvalidArgumentException - */ - public function getCreateTableSQL(Table $table, $createFlags = self::CREATE_INDEXES) - { - if (! is_int($createFlags)) { - throw new InvalidArgumentException( - 'Second argument of AbstractPlatform::getCreateTableSQL() has to be integer.', - ); - } - - if (($createFlags & self::CREATE_INDEXES) === 0) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5416', - 'Unsetting the CREATE_INDEXES flag in AbstractPlatform::getCreateTableSQL() is deprecated.', - ); - } - - if (($createFlags & self::CREATE_FOREIGNKEYS) === 0) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5416', - 'Not setting the CREATE_FOREIGNKEYS flag in AbstractPlatform::getCreateTableSQL()' - . ' is deprecated. In order to build the statements that create multiple tables' - . ' referencing each other via foreign keys, use AbstractPlatform::getCreateTablesSQL().', - ); - } - - return $this->buildCreateTableSQL( - $table, - ($createFlags & self::CREATE_INDEXES) > 0, - ($createFlags & self::CREATE_FOREIGNKEYS) > 0, - ); - } - - public function createSelectSQLBuilder(): SelectSQLBuilder - { - return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', 'SKIP LOCKED'); - } - - /** - * @internal - * - * @return list - * - * @throws Exception - */ - final protected function getCreateTableWithoutForeignKeysSQL(Table $table): array - { - return $this->buildCreateTableSQL($table, true, false); - } - - /** - * @return list - * - * @throws Exception - */ - private function buildCreateTableSQL(Table $table, bool $createIndexes, bool $createForeignKeys): array - { - if (count($table->getColumns()) === 0) { - throw Exception::noColumnsSpecifiedForTable($table->getName()); - } - - $tableName = $table->getQuotedName($this); - $options = $table->getOptions(); - $options['uniqueConstraints'] = []; - $options['indexes'] = []; - $options['primary'] = []; - - if ($createIndexes) { - foreach ($table->getIndexes() as $index) { - if (! $index->isPrimary()) { - $options['indexes'][$index->getQuotedName($this)] = $index; - - continue; - } - - $options['primary'] = $index->getQuotedColumns($this); - $options['primary_index'] = $index; - } - - foreach ($table->getUniqueConstraints() as $uniqueConstraint) { - $options['uniqueConstraints'][$uniqueConstraint->getQuotedName($this)] = $uniqueConstraint; - } - } - - if ($createForeignKeys) { - $options['foreignKeys'] = []; - - foreach ($table->getForeignKeys() as $fkConstraint) { - $options['foreignKeys'][] = $fkConstraint; - } - } - - $columnSql = []; - $columns = []; - - foreach ($table->getColumns() as $column) { - if ( - $this->_eventManager !== null - && $this->_eventManager->hasListeners(Events::onSchemaCreateTableColumn) - ) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated.', - Events::onSchemaCreateTableColumn, - ); - - $eventArgs = new SchemaCreateTableColumnEventArgs($column, $table, $this); - - $this->_eventManager->dispatchEvent(Events::onSchemaCreateTableColumn, $eventArgs); - - $columnSql = array_merge($columnSql, $eventArgs->getSql()); - - if ($eventArgs->isDefaultPrevented()) { - continue; - } - } - - $columnData = $this->columnToArray($column); - - if (in_array($column->getName(), $options['primary'], true)) { - $columnData['primary'] = true; - } - - $columns[$columnData['name']] = $columnData; - } - - if ($this->_eventManager !== null && $this->_eventManager->hasListeners(Events::onSchemaCreateTable)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated.', - Events::onSchemaCreateTable, - ); - - $eventArgs = new SchemaCreateTableEventArgs($table, $columns, $options, $this); - - $this->_eventManager->dispatchEvent(Events::onSchemaCreateTable, $eventArgs); - - if ($eventArgs->isDefaultPrevented()) { - return array_merge($eventArgs->getSql(), $columnSql); - } - } - - $sql = $this->_getCreateTableSQL($tableName, $columns, $options); - - if ($this->supportsCommentOnStatement()) { - if ($table->hasOption('comment')) { - $sql[] = $this->getCommentOnTableSQL($tableName, $table->getOption('comment')); - } - - foreach ($table->getColumns() as $column) { - $comment = $this->getColumnComment($column); - - if ($comment === null || $comment === '') { - continue; - } - - $sql[] = $this->getCommentOnColumnSQL($tableName, $column->getQuotedName($this), $comment); - } - } - - return array_merge($sql, $columnSql); - } - - /** - * @param Table[] $tables - * - * @return list - * - * @throws Exception - */ - public function getCreateTablesSQL(array $tables): array - { - $sql = []; - - foreach ($tables as $table) { - $sql = array_merge($sql, $this->getCreateTableWithoutForeignKeysSQL($table)); - } - - foreach ($tables as $table) { - foreach ($table->getForeignKeys() as $foreignKey) { - $sql[] = $this->getCreateForeignKeySQL( - $foreignKey, - $table->getQuotedName($this), - ); - } - } - - return $sql; - } - - /** - * @param list $tables - * - * @return list - */ - public function getDropTablesSQL(array $tables): array - { - $sql = []; - - foreach ($tables as $table) { - foreach ($table->getForeignKeys() as $foreignKey) { - $sql[] = $this->getDropForeignKeySQL( - $foreignKey->getQuotedName($this), - $table->getQuotedName($this), - ); - } - } - - foreach ($tables as $table) { - $sql[] = $this->getDropTableSQL($table->getQuotedName($this)); - } - - return $sql; - } - - protected function getCommentOnTableSQL(string $tableName, ?string $comment): string - { - $tableName = new Identifier($tableName); - - return sprintf( - 'COMMENT ON TABLE %s IS %s', - $tableName->getQuotedName($this), - $this->quoteStringLiteral((string) $comment), - ); - } - - /** - * @param string $tableName - * @param string $columnName - * @param string|null $comment - * - * @return string - */ - public function getCommentOnColumnSQL($tableName, $columnName, $comment) - { - $tableName = new Identifier($tableName); - $columnName = new Identifier($columnName); - - return sprintf( - 'COMMENT ON COLUMN %s.%s IS %s', - $tableName->getQuotedName($this), - $columnName->getQuotedName($this), - $this->quoteStringLiteral((string) $comment), - ); - } - - /** - * Returns the SQL to create inline comment on a column. - * - * @param string $comment - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getInlineColumnCommentSQL($comment) - { - if (! $this->supportsInlineColumnComments()) { - throw Exception::notSupported(__METHOD__); - } - - return 'COMMENT ' . $this->quoteStringLiteral($comment); - } - - /** - * Returns the SQL used to create a table. - * - * @param string $name - * @param mixed[][] $columns - * @param mixed[] $options - * - * @return string[] - */ - protected function _getCreateTableSQL($name, array $columns, array $options = []) - { - $columnListSql = $this->getColumnDeclarationListSQL($columns); - - if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { - foreach ($options['uniqueConstraints'] as $index => $definition) { - $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($index, $definition); - } - } - - if (isset($options['primary']) && ! empty($options['primary'])) { - $columnListSql .= ', PRIMARY KEY(' . implode(', ', array_unique(array_values($options['primary']))) . ')'; - } - - if (isset($options['indexes']) && ! empty($options['indexes'])) { - foreach ($options['indexes'] as $index => $definition) { - $columnListSql .= ', ' . $this->getIndexDeclarationSQL($index, $definition); - } - } - - $query = 'CREATE TABLE ' . $name . ' (' . $columnListSql; - $check = $this->getCheckDeclarationSQL($columns); - - if (! empty($check)) { - $query .= ', ' . $check; - } - - $query .= ')'; - - $sql = [$query]; - - if (isset($options['foreignKeys'])) { - foreach ($options['foreignKeys'] as $definition) { - $sql[] = $this->getCreateForeignKeySQL($definition, $name); - } - } - - return $sql; - } - - /** @return string */ - public function getCreateTemporaryTableSnippetSQL() - { - return 'CREATE TEMPORARY TABLE'; - } - - /** - * Generates SQL statements that can be used to apply the diff. - * - * @return list - */ - public function getAlterSchemaSQL(SchemaDiff $diff): array - { - return $diff->toSql($this); - } - - /** - * Returns the SQL to create a sequence on this platform. - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getCreateSequenceSQL(Sequence $sequence) - { - throw Exception::notSupported(__METHOD__); - } - - /** - * Returns the SQL to change a sequence on this platform. - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getAlterSequenceSQL(Sequence $sequence) - { - throw Exception::notSupported(__METHOD__); - } - - /** - * Returns the SQL snippet to drop an existing sequence. - * - * @param Sequence|string $sequence - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getDropSequenceSQL($sequence) - { - if (! $this->supportsSequences()) { - throw Exception::notSupported(__METHOD__); - } - - if ($sequence instanceof Sequence) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $sequence as a Sequence object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $sequence = $sequence->getQuotedName($this); - } - - return 'DROP SEQUENCE ' . $sequence; - } - - /** - * Returns the SQL to create a constraint on a table on this platform. - * - * @deprecated Use {@see getCreateIndexSQL()}, {@see getCreateForeignKeySQL()} - * or {@see getCreateUniqueConstraintSQL()} instead. - * - * @param Table|string $table - * - * @return string - * - * @throws InvalidArgumentException - */ - public function getCreateConstraintSQL(Constraint $constraint, $table) - { - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $table = $table->getQuotedName($this); - } - - $query = 'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $constraint->getQuotedName($this); - - $columnList = '(' . implode(', ', $constraint->getQuotedColumns($this)) . ')'; - - $referencesClause = ''; - if ($constraint instanceof Index) { - if ($constraint->isPrimary()) { - $query .= ' PRIMARY KEY'; - } elseif ($constraint->isUnique()) { - $query .= ' UNIQUE'; - } else { - throw new InvalidArgumentException( - 'Can only create primary or unique constraints, no common indexes with getCreateConstraintSQL().', - ); - } - } elseif ($constraint instanceof UniqueConstraint) { - $query .= ' UNIQUE'; - } elseif ($constraint instanceof ForeignKeyConstraint) { - $query .= ' FOREIGN KEY'; - - $referencesClause = ' REFERENCES ' . $constraint->getQuotedForeignTableName($this) . - ' (' . implode(', ', $constraint->getQuotedForeignColumns($this)) . ')'; - } - - $query .= ' ' . $columnList . $referencesClause; - - return $query; - } - - /** - * Returns the SQL to create an index on a table on this platform. - * - * @param Table|string $table The name of the table on which the index is to be created. - * - * @return string - * - * @throws InvalidArgumentException - */ - public function getCreateIndexSQL(Index $index, $table) - { - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $table = $table->getQuotedName($this); - } - - $name = $index->getQuotedName($this); - $columns = $index->getColumns(); - - if (count($columns) === 0) { - throw new InvalidArgumentException(sprintf( - 'Incomplete or invalid index definition %s on table %s', - $name, - $table, - )); - } - - if ($index->isPrimary()) { - return $this->getCreatePrimaryKeySQL($index, $table); - } - - $query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table; - $query .= ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')' . $this->getPartialIndexSQL($index); - - return $query; - } - - /** - * Adds condition for partial index. - * - * @return string - */ - protected function getPartialIndexSQL(Index $index) - { - if ($this->supportsPartialIndexes() && $index->hasOption('where')) { - return ' WHERE ' . $index->getOption('where'); - } - - return ''; - } - - /** - * Adds additional flags for index generation. - * - * @return string - */ - protected function getCreateIndexSQLFlags(Index $index) - { - return $index->isUnique() ? 'UNIQUE ' : ''; - } - - /** - * Returns the SQL to create an unnamed primary key constraint. - * - * @param Table|string $table - * - * @return string - */ - public function getCreatePrimaryKeySQL(Index $index, $table) - { - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $table = $table->getQuotedName($this); - } - - return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY (' . $this->getIndexFieldDeclarationListSQL($index) . ')'; - } - - /** - * Returns the SQL to create a named schema. - * - * @param string $schemaName - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getCreateSchemaSQL($schemaName) - { - if (! $this->supportsSchemas()) { - throw Exception::notSupported(__METHOD__); - } - - return 'CREATE SCHEMA ' . $schemaName; - } - - /** - * Returns the SQL to create a unique constraint on a table on this platform. - */ - public function getCreateUniqueConstraintSQL(UniqueConstraint $constraint, string $tableName): string - { - return $this->getCreateConstraintSQL($constraint, $tableName); - } - - /** - * Returns the SQL snippet to drop a schema. - * - * @throws Exception If not supported on this platform. - */ - public function getDropSchemaSQL(string $schemaName): string - { - if (! $this->supportsSchemas()) { - throw Exception::notSupported(__METHOD__); - } - - return 'DROP SCHEMA ' . $schemaName; - } - - /** - * Quotes a string so that it can be safely used as a table or column name, - * even if it is a reserved word of the platform. This also detects identifier - * chains separated by dot and quotes them independently. - * - * NOTE: Just because you CAN use quoted identifiers doesn't mean - * you SHOULD use them. In general, they end up causing way more - * problems than they solve. - * - * @param string $str The identifier name to be quoted. - * - * @return string The quoted identifier string. - */ - public function quoteIdentifier($str) - { - if (strpos($str, '.') !== false) { - $parts = array_map([$this, 'quoteSingleIdentifier'], explode('.', $str)); - - return implode('.', $parts); - } - - return $this->quoteSingleIdentifier($str); - } - - /** - * Quotes a single identifier (no dot chain separation). - * - * @param string $str The identifier name to be quoted. - * - * @return string The quoted identifier string. - */ - public function quoteSingleIdentifier($str) - { - $c = $this->getIdentifierQuoteCharacter(); - - return $c . str_replace($c, $c . $c, $str) . $c; - } - - /** - * Returns the SQL to create a new foreign key. - * - * @param ForeignKeyConstraint $foreignKey The foreign key constraint. - * @param Table|string $table The name of the table on which the foreign key is to be created. - * - * @return string - */ - public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table) - { - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $table = $table->getQuotedName($this); - } - - return 'ALTER TABLE ' . $table . ' ADD ' . $this->getForeignKeyDeclarationSQL($foreignKey); - } - - /** - * Gets the SQL statements for altering an existing table. - * - * This method returns an array of SQL statements, since some platforms need several statements. - * - * @return list - * - * @throws Exception If not supported on this platform. - */ - public function getAlterTableSQL(TableDiff $diff) - { - throw Exception::notSupported(__METHOD__); - } - - /** @return list */ - public function getRenameTableSQL(string $oldName, string $newName): array - { - return [ - sprintf('ALTER TABLE %s RENAME TO %s', $oldName, $newName), - ]; - } - - /** - * @param mixed[] $columnSql - * - * @return bool - */ - protected function onSchemaAlterTableAddColumn(Column $column, TableDiff $diff, &$columnSql) - { - if ($this->_eventManager === null) { - return false; - } - - if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTableAddColumn)) { - return false; - } - - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated.', - Events::onSchemaAlterTableAddColumn, - ); - - $eventArgs = new SchemaAlterTableAddColumnEventArgs($column, $diff, $this); - $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableAddColumn, $eventArgs); - - $columnSql = array_merge($columnSql, $eventArgs->getSql()); - - return $eventArgs->isDefaultPrevented(); - } - - /** - * @param string[] $columnSql - * - * @return bool - */ - protected function onSchemaAlterTableRemoveColumn(Column $column, TableDiff $diff, &$columnSql) - { - if ($this->_eventManager === null) { - return false; - } - - if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTableRemoveColumn)) { - return false; - } - - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated.', - Events::onSchemaAlterTableRemoveColumn, - ); - - $eventArgs = new SchemaAlterTableRemoveColumnEventArgs($column, $diff, $this); - $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableRemoveColumn, $eventArgs); - - $columnSql = array_merge($columnSql, $eventArgs->getSql()); - - return $eventArgs->isDefaultPrevented(); - } - - /** - * @param string[] $columnSql - * - * @return bool - */ - protected function onSchemaAlterTableChangeColumn(ColumnDiff $columnDiff, TableDiff $diff, &$columnSql) - { - if ($this->_eventManager === null) { - return false; - } - - if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTableChangeColumn)) { - return false; - } - - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated.', - Events::onSchemaAlterTableChangeColumn, - ); - - $eventArgs = new SchemaAlterTableChangeColumnEventArgs($columnDiff, $diff, $this); - $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableChangeColumn, $eventArgs); - - $columnSql = array_merge($columnSql, $eventArgs->getSql()); - - return $eventArgs->isDefaultPrevented(); - } - - /** - * @param string $oldColumnName - * @param string[] $columnSql - * - * @return bool - */ - protected function onSchemaAlterTableRenameColumn($oldColumnName, Column $column, TableDiff $diff, &$columnSql) - { - if ($this->_eventManager === null) { - return false; - } - - if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTableRenameColumn)) { - return false; - } - - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated.', - Events::onSchemaAlterTableRenameColumn, - ); - - $eventArgs = new SchemaAlterTableRenameColumnEventArgs($oldColumnName, $column, $diff, $this); - $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableRenameColumn, $eventArgs); - - $columnSql = array_merge($columnSql, $eventArgs->getSql()); - - return $eventArgs->isDefaultPrevented(); - } - - /** - * @param string[] $sql - * - * @return bool - */ - protected function onSchemaAlterTable(TableDiff $diff, &$sql) - { - if ($this->_eventManager === null) { - return false; - } - - if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTable)) { - return false; - } - - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated.', - Events::onSchemaAlterTable, - ); - - $eventArgs = new SchemaAlterTableEventArgs($diff, $this); - $this->_eventManager->dispatchEvent(Events::onSchemaAlterTable, $eventArgs); - - $sql = array_merge($sql, $eventArgs->getSql()); - - return $eventArgs->isDefaultPrevented(); - } - - /** @return string[] */ - protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) - { - $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); - - $sql = []; - if ($this->supportsForeignKeyConstraints()) { - foreach ($diff->getDroppedForeignKeys() as $foreignKey) { - if ($foreignKey instanceof ForeignKeyConstraint) { - $foreignKey = $foreignKey->getQuotedName($this); - } - - $sql[] = $this->getDropForeignKeySQL($foreignKey, $tableNameSQL); - } + foreach ($table->getIndexes() as $index) { + if (! $index->isPrimary()) { + $options['indexes'][] = $index; - foreach ($diff->getModifiedForeignKeys() as $foreignKey) { - $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableNameSQL); + continue; } - } - - foreach ($diff->getDroppedIndexes() as $index) { - $sql[] = $this->getDropIndexSQL($index->getQuotedName($this), $tableNameSQL); - } - foreach ($diff->getModifiedIndexes() as $index) { - $sql[] = $this->getDropIndexSQL($index->getQuotedName($this), $tableNameSQL); + $options['primary'] = $index->getQuotedColumns($this); + $options['primary_index'] = $index; } - return $sql; - } - - /** @return string[] */ - protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff) - { - $sql = []; - $newName = $diff->getNewName(); - - if ($newName !== false) { - $tableNameSQL = $newName->getQuotedName($this); - } else { - $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); + foreach ($table->getUniqueConstraints() as $uniqueConstraint) { + $options['uniqueConstraints'][] = $uniqueConstraint; } - if ($this->supportsForeignKeyConstraints()) { - foreach ($diff->getAddedForeignKeys() as $foreignKey) { - $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableNameSQL); - } - - foreach ($diff->getModifiedForeignKeys() as $foreignKey) { - $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableNameSQL); + if ($createForeignKeys) { + foreach ($table->getForeignKeys() as $fkConstraint) { + $options['foreignKeys'][] = $fkConstraint; } } - foreach ($diff->getAddedIndexes() as $index) { - $sql[] = $this->getCreateIndexSQL($index, $tableNameSQL); - } - - foreach ($diff->getModifiedIndexes() as $index) { - $sql[] = $this->getCreateIndexSQL($index, $tableNameSQL); - } - - foreach ($diff->getRenamedIndexes() as $oldIndexName => $index) { - $oldIndexName = new Identifier($oldIndexName); - $sql = array_merge( - $sql, - $this->getRenameIndexSQL($oldIndexName->getQuotedName($this), $index, $tableNameSQL), - ); - } - - return $sql; - } - - /** - * Returns the SQL for renaming an index on a table. - * - * @param string $oldIndexName The name of the index to rename from. - * @param Index $index The definition of the index to rename to. - * @param string $tableName The table to rename the given index on. - * - * @return string[] The sequence of SQL statements for renaming the given index. - */ - protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) - { - return [ - $this->getDropIndexSQL($oldIndexName, $tableName), - $this->getCreateIndexSQL($index, $tableName), - ]; - } - - /** - * Gets declaration of a number of columns in bulk. - * - * @param mixed[][] $columns A multidimensional associative array. - * The first dimension determines the column name, while the second - * dimension is keyed with the name of the properties - * of the column being declared as array indexes. Currently, the types - * of supported column properties are as follows: - * - * length - * Integer value that determines the maximum length of the text - * column. If this argument is missing the column should be - * declared to have the longest length allowed by the DBMS. - * - * default - * Text value to be used as default for this column. - * - * notnull - * Boolean flag that indicates whether this column is constrained - * to not be set to null. - * charset - * Text value with the default CHARACTER SET for this column. - * collation - * Text value with the default COLLATION for this column. - * unique - * unique constraint - * - * @return string - */ - public function getColumnDeclarationListSQL(array $columns) - { - $declarations = []; + $columns = []; - foreach ($columns as $name => $column) { - $declarations[] = $this->getColumnDeclarationSQL($name, $column); + foreach ($table->getColumns() as $column) { + $columns[] = $this->columnToArray($column); } - return implode(', ', $declarations); - } - - /** - * Obtains DBMS specific SQL code portion needed to declare a generic type - * column to be used in statements like CREATE TABLE. - * - * @param string $name The name the column to be declared. - * @param mixed[] $column An associative array with the name of the properties - * of the column being declared as array indexes. Currently, the types - * of supported column properties are as follows: - * - * length - * Integer value that determines the maximum length of the text - * column. If this argument is missing the column should be - * declared to have the longest length allowed by the DBMS. - * - * default - * Text value to be used as default for this column. - * - * notnull - * Boolean flag that indicates whether this column is constrained - * to not be set to null. - * charset - * Text value with the default CHARACTER SET for this column. - * collation - * Text value with the default COLLATION for this column. - * unique - * unique constraint - * check - * column check constraint - * columnDefinition - * a string that defines the complete column - * - * @return string DBMS specific SQL code portion that should be used to declare the column. - * - * @throws Exception - */ - public function getColumnDeclarationSQL($name, array $column) - { - if (isset($column['columnDefinition'])) { - $declaration = $this->getCustomTypeDeclarationSQL($column); - } else { - $default = $this->getDefaultValueDeclarationSQL($column); - - $charset = ! empty($column['charset']) ? - ' ' . $this->getColumnCharsetDeclarationSQL($column['charset']) : ''; - - $collation = ! empty($column['collation']) ? - ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : ''; - - $notnull = ! empty($column['notnull']) ? ' NOT NULL' : ''; - - if (! empty($column['unique'])) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5656', - 'The usage of the "unique" column property is deprecated. Use unique constraints instead.', - ); - - $unique = ' ' . $this->getUniqueFieldDeclarationSQL(); - } else { - $unique = ''; - } - - if (! empty($column['check'])) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5656', - 'The usage of the "check" column property is deprecated.', - ); + $sql = $this->_getCreateTableSQL($tableName, $columns, $options); - $check = ' ' . $column['check']; - } else { - $check = ''; + if ($this->supportsCommentOnStatement()) { + if ($table->hasOption('comment')) { + $sql[] = $this->getCommentOnTableSQL($tableName, $table->getOption('comment')); } - $typeDecl = $column['type']->getSQLDeclaration($column, $this); - $declaration = $typeDecl . $charset . $default . $notnull . $unique . $check . $collation; + foreach ($table->getColumns() as $column) { + $comment = $column->getComment(); - if ($this->supportsInlineColumnComments() && isset($column['comment']) && $column['comment'] !== '') { - $declaration .= ' ' . $this->getInlineColumnCommentSQL($column['comment']); + if ($comment !== '') { + $sql[] = $this->getCommentOnColumnSQL($tableName, $column->getQuotedName($this), $comment); + } } } - return $name . ' ' . $declaration; + return $sql; } /** - * Returns the SQL snippet that declares a floating point column of arbitrary precision. + * @param array
$tables * - * @param mixed[] $column - * - * @return string + * @return list */ - public function getDecimalTypeDeclarationSQL(array $column) + public function getCreateTablesSQL(array $tables): array { - if (empty($column['precision'])) { - if (! isset($column['precision'])) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5637', - 'Relying on the default decimal column precision is deprecated' - . ', specify the precision explicitly.', - ); - } + $sql = []; - $precision = 10; - } else { - $precision = $column['precision']; + foreach ($tables as $table) { + $sql = array_merge($sql, $this->getCreateTableWithoutForeignKeysSQL($table)); } - if (empty($column['scale'])) { - if (! isset($column['scale'])) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5637', - 'Relying on the default decimal column scale is deprecated' - . ', specify the scale explicitly.', + foreach ($tables as $table) { + foreach ($table->getForeignKeys() as $foreignKey) { + $sql[] = $this->getCreateForeignKeySQL( + $foreignKey, + $table->getQuotedName($this), ); } - - $scale = 0; - } else { - $scale = $column['scale']; } - return 'NUMERIC(' . $precision . ', ' . $scale . ')'; + return $sql; } /** - * Obtains DBMS specific SQL code portion needed to set a default value - * declaration to be used in statements like CREATE TABLE. - * - * @param mixed[] $column The column definition array. + * @param array
$tables * - * @return string DBMS specific SQL code portion needed to set a default value. + * @return list */ - public function getDefaultValueDeclarationSQL($column) + public function getDropTablesSQL(array $tables): array { - if (! isset($column['default'])) { - return empty($column['notnull']) ? ' DEFAULT NULL' : ''; - } - - $default = $column['default']; + $sql = []; - if (! isset($column['type'])) { - return " DEFAULT '" . $default . "'"; + foreach ($tables as $table) { + foreach ($table->getForeignKeys() as $foreignKey) { + $sql[] = $this->getDropForeignKeySQL( + $foreignKey->getQuotedName($this), + $table->getQuotedName($this), + ); + } } - $type = $column['type']; - - if ($type instanceof Types\PhpIntegerMappingType) { - return ' DEFAULT ' . $default; + foreach ($tables as $table) { + $sql[] = $this->getDropTableSQL($table->getQuotedName($this)); } - if ($type instanceof Types\PhpDateTimeMappingType && $default === $this->getCurrentTimestampSQL()) { - return ' DEFAULT ' . $this->getCurrentTimestampSQL(); - } + return $sql; + } - if ($type instanceof Types\TimeType && $default === $this->getCurrentTimeSQL()) { - return ' DEFAULT ' . $this->getCurrentTimeSQL(); - } + protected function getCommentOnTableSQL(string $tableName, string $comment): string + { + $tableName = new Identifier($tableName); - if ($type instanceof Types\DateType && $default === $this->getCurrentDateSQL()) { - return ' DEFAULT ' . $this->getCurrentDateSQL(); - } + return sprintf( + 'COMMENT ON TABLE %s IS %s', + $tableName->getQuotedName($this), + $this->quoteStringLiteral($comment), + ); + } - if ($type instanceof Types\BooleanType) { - return ' DEFAULT ' . $this->convertBooleans($default); - } + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getCommentOnColumnSQL(string $tableName, string $columnName, string $comment): string + { + $tableName = new Identifier($tableName); + $columnName = new Identifier($columnName); - return ' DEFAULT ' . $this->quoteStringLiteral($default); + return sprintf( + 'COMMENT ON COLUMN %s.%s IS %s', + $tableName->getQuotedName($this), + $columnName->getQuotedName($this), + $this->quoteStringLiteral($comment), + ); } /** - * Obtains DBMS specific SQL code portion needed to set a CHECK constraint - * declaration to be used in statements like CREATE TABLE. - * - * @param string[]|mixed[][] $definition The check definition. + * Returns the SQL to create inline comment on a column. * - * @return string DBMS specific SQL code portion needed to set a CHECK constraint. + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ - public function getCheckDeclarationSQL(array $definition) + public function getInlineColumnCommentSQL(string $comment): string { - $constraints = []; - foreach ($definition as $column => $def) { - if (is_string($def)) { - $constraints[] = 'CHECK (' . $def . ')'; - } else { - if (isset($def['min'])) { - $constraints[] = 'CHECK (' . $column . ' >= ' . $def['min'] . ')'; - } - - if (isset($def['max'])) { - $constraints[] = 'CHECK (' . $column . ' <= ' . $def['max'] . ')'; - } - } + if (! $this->supportsInlineColumnComments()) { + throw NotSupported::new(__METHOD__); } - return implode(', ', $constraints); + return 'COMMENT ' . $this->quoteStringLiteral($comment); } /** - * Obtains DBMS specific SQL code portion needed to set a unique - * constraint declaration to be used in statements like CREATE TABLE. - * - * @param string $name The name of the unique constraint. - * @param UniqueConstraint $constraint The unique constraint definition. + * Returns the SQL used to create a table. * - * @return string DBMS specific SQL code portion needed to set a constraint. + * @param list $columns + * @param CreateTableParameters $options * - * @throws InvalidArgumentException + * @return list */ - public function getUniqueConstraintDeclarationSQL($name, UniqueConstraint $constraint) + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array { - $columns = $constraint->getQuotedColumns($this); - $name = new Identifier($name); + $this->validateCreateTableOptions($options, __METHOD__); - if (count($columns) === 0) { - throw new InvalidArgumentException("Incomplete definition. 'columns' required."); + $columnListSql = $this->getColumnDeclarationListSQL($columns); + + if (! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $definition) { + $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($definition); + } } - $constraintFlags = array_merge(['UNIQUE'], array_map('strtoupper', $constraint->getFlags())); - $constraintName = $name->getQuotedName($this); - $columnListNames = $this->getColumnsFieldDeclarationListSQL($columns); + if (! empty($options['primary'])) { + $columnListSql .= ', PRIMARY KEY (' . implode(', ', array_unique(array_values($options['primary']))) . ')'; + } - return sprintf('CONSTRAINT %s %s (%s)', $constraintName, implode(' ', $constraintFlags), $columnListNames); - } + if (! empty($options['indexes'])) { + foreach ($options['indexes'] as $definition) { + $columnListSql .= ', ' . $this->getIndexDeclarationSQL($definition); + } + } - /** - * Obtains DBMS specific SQL code portion needed to set an index - * declaration to be used in statements like CREATE TABLE. - * - * @param string $name The name of the index. - * @param Index $index The index definition. - * - * @return string DBMS specific SQL code portion needed to set an index. - * - * @throws InvalidArgumentException - */ - public function getIndexDeclarationSQL($name, Index $index) - { - $columns = $index->getColumns(); - $name = new Identifier($name); + $query = 'CREATE TABLE ' . $name . ' (' . $columnListSql; + $check = $this->getCheckDeclarationSQL($columns); - if (count($columns) === 0) { - throw new InvalidArgumentException("Incomplete definition. 'columns' required."); + if (! empty($check)) { + $query .= ', ' . $check; } - return $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name->getQuotedName($this) - . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')' . $this->getPartialIndexSQL($index); - } + $query .= ')'; - /** - * Obtains SQL code portion needed to create a custom column, - * e.g. when a column has the "columnDefinition" keyword. - * Only "AUTOINCREMENT" and "PRIMARY KEY" are added if appropriate. - * - * @deprecated - * - * @param mixed[] $column - * - * @return string - */ - public function getCustomTypeDeclarationSQL(array $column) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5527', - '%s is deprecated.', - __METHOD__, - ); + $sql = [$query]; + + if (isset($options['foreignKeys'])) { + foreach ($options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $name); + } + } - return $column['columnDefinition']; + return $sql; } /** - * Obtains DBMS specific SQL code portion needed to set an index - * declaration to be used in statements like CREATE TABLE. + * @internal * - * @deprecated + * @param CreateTableParameters $options */ - public function getIndexFieldDeclarationListSQL(Index $index): string + final protected function validateCreateTableOptions(array $options, string $methodName): void { - Deprecation::triggerIfCalledFromOutside( + if ( + isset( + $options['primary'], + $options['indexes'], + $options['uniqueConstraints'], + $options['foreignKeys'], + ) + ) { + return; + } + + Deprecation::trigger( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5527', - '%s is deprecated.', - __METHOD__, + 'https://github.com/doctrine/dbal/pull/6805', + 'Not passing $options or any of its following keys to %s() is deprecated:' + . ' "primary", "indexes", "uniqueConstraints", "foreignKeys".', + $methodName, ); + } - return implode(', ', $index->getQuotedColumns($this)); + public function getCreateTemporaryTableSnippetSQL(): string + { + return 'CREATE TEMPORARY TABLE'; } /** - * Obtains DBMS specific SQL code portion needed to set an index - * declaration to be used in statements like CREATE TABLE. - * - * @deprecated + * Generates SQL statements that can be used to apply the diff. * - * @param mixed[] $columns + * @return list */ - public function getColumnsFieldDeclarationListSQL(array $columns): string + public function getAlterSchemaSQL(SchemaDiff $diff): array { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5527', - '%s is deprecated.', - __METHOD__, - ); + $sql = []; + + if ($this->supportsSchemas()) { + foreach ($diff->getCreatedSchemas() as $schema) { + $sql[] = $this->getCreateSchemaSQL($schema); + } + } - $ret = []; + if ($this->supportsSequences()) { + foreach ($diff->getAlteredSequences() as $sequence) { + $sql[] = $this->getAlterSequenceSQL($sequence); + } - foreach ($columns as $column => $definition) { - if (is_array($definition)) { - $ret[] = $column; - } else { - $ret[] = $definition; + foreach ($diff->getDroppedSequences() as $sequence) { + $sql[] = $this->getDropSequenceSQL($sequence->getQuotedName($this)); + } + + foreach ($diff->getCreatedSequences() as $sequence) { + $sql[] = $this->getCreateSequenceSQL($sequence); } } - return implode(', ', $ret); + $sql = array_merge( + $sql, + $this->getCreateTablesSQL( + $diff->getCreatedTables(), + ), + $this->getDropTablesSQL( + $diff->getDroppedTables(), + ), + ); + + foreach ($diff->getAlteredTables() as $tableDiff) { + $sql = array_merge($sql, $this->getAlterTableSQL($tableDiff)); + } + + return $sql; } /** - * Returns the required SQL string that fits between CREATE ... TABLE - * to create the table as a temporary table. - * - * Should be overridden in driver classes to return the correct string for the - * specific database type. - * - * The default is to return the string "TEMPORARY" - this will result in a - * SQL error for any database that does not support temporary tables, or that - * requires a different SQL command from "CREATE TEMPORARY TABLE". - * - * @deprecated - * - * @return string The string required to be placed between "CREATE" and "TABLE" - * to generate a temporary table, if possible. + * Returns the SQL to create a sequence on this platform. */ - public function getTemporaryTableSQL() + public function getCreateSequenceSQL(Sequence $sequence): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getTemporaryTableSQL() is deprecated.', - ); - - return 'TEMPORARY'; + throw NotSupported::new(__METHOD__); } /** - * Some vendors require temporary table names to be qualified specially. - * - * @param string $tableName - * - * @return string + * Returns the SQL to change a sequence on this platform. */ - public function getTemporaryTableName($tableName) + public function getAlterSequenceSQL(Sequence $sequence): string { - return $tableName; + throw NotSupported::new(__METHOD__); } /** - * Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint - * of a column declaration to be used in statements like CREATE TABLE. - * - * @return string DBMS specific SQL code portion needed to set the FOREIGN KEY constraint - * of a column declaration. + * Returns the SQL snippet to drop an existing sequence. */ - public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey) + public function getDropSequenceSQL(string $name): string { - $sql = $this->getForeignKeyBaseDeclarationSQL($foreignKey); - $sql .= $this->getAdvancedForeignKeyOptionsSQL($foreignKey); + if (! $this->supportsSequences()) { + throw NotSupported::new(__METHOD__); + } - return $sql; + return 'DROP SEQUENCE ' . $name; } /** - * Returns the FOREIGN KEY query section dealing with non-standard options - * as MATCH, INITIALLY DEFERRED, ON UPDATE, ... - * - * @param ForeignKeyConstraint $foreignKey The foreign key definition. - * - * @return string + * Returns the SQL to create an index on a table on this platform. */ - public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) + public function getCreateIndexSQL(Index $index, string $table): string { - $query = ''; - if ($foreignKey->hasOption('onUpdate')) { - $query .= ' ON UPDATE ' . $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onUpdate')); + $name = $index->getQuotedName($this); + $columns = $index->getColumns(); + + if (count($columns) === 0) { + throw new InvalidArgumentException(sprintf( + 'Incomplete or invalid index definition %s on table %s', + $name, + $table, + )); } - if ($foreignKey->hasOption('onDelete')) { - $query .= ' ON DELETE ' . $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onDelete')); + if ($index->isPrimary()) { + return $this->getCreatePrimaryKeySQL($index, $table); } + $query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table; + $query .= ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index); + return $query; } /** - * Returns the given referential action in uppercase if valid, otherwise throws an exception. - * - * @param string $action The foreign key referential action. - * - * @return string - * - * @throws InvalidArgumentException If unknown referential action given. + * Adds condition for partial index. */ - public function getForeignKeyReferentialActionSQL($action) + protected function getPartialIndexSQL(Index $index): string { - $upper = strtoupper($action); - switch ($upper) { - case 'CASCADE': - case 'SET NULL': - case 'NO ACTION': - case 'RESTRICT': - case 'SET DEFAULT': - return $upper; - - default: - throw new InvalidArgumentException('Invalid foreign key action: ' . $upper); + if ($this->supportsPartialIndexes() && $index->hasOption('where')) { + return ' WHERE ' . $index->getOption('where'); } + + return ''; } /** - * Obtains DBMS specific SQL code portion needed to set the FOREIGN KEY constraint - * of a column declaration to be used in statements like CREATE TABLE. - * - * @return string - * - * @throws InvalidArgumentException + * Adds additional flags for index generation. */ - public function getForeignKeyBaseDeclarationSQL(ForeignKeyConstraint $foreignKey) + protected function getCreateIndexSQLFlags(Index $index): string { - $sql = ''; - if (strlen($foreignKey->getName()) > 0) { - $sql .= 'CONSTRAINT ' . $foreignKey->getQuotedName($this) . ' '; - } - - $sql .= 'FOREIGN KEY ('; - - if (count($foreignKey->getLocalColumns()) === 0) { - throw new InvalidArgumentException("Incomplete definition. 'local' required."); - } - - if (count($foreignKey->getForeignColumns()) === 0) { - throw new InvalidArgumentException("Incomplete definition. 'foreign' required."); - } - - if (strlen($foreignKey->getForeignTableName()) === 0) { - throw new InvalidArgumentException("Incomplete definition. 'foreignTable' required."); - } - - return $sql . implode(', ', $foreignKey->getQuotedLocalColumns($this)) - . ') REFERENCES ' - . $foreignKey->getQuotedForeignTableName($this) . ' (' - . implode(', ', $foreignKey->getQuotedForeignColumns($this)) . ')'; + return $index->isUnique() ? 'UNIQUE ' : ''; } /** - * Obtains DBMS specific SQL code portion needed to set the UNIQUE constraint - * of a column declaration to be used in statements like CREATE TABLE. - * - * @deprecated Use UNIQUE in SQL instead. + * Returns the SQL to create an unnamed primary key constraint. * - * @return string DBMS specific SQL code portion needed to set the UNIQUE constraint - * of a column declaration. + * @deprecated */ - public function getUniqueFieldDeclarationSQL() + public function getCreatePrimaryKeySQL(Index $index, string $table): string { - Deprecation::trigger( + Deprecation::triggerIfCalledFromOutside( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getUniqueFieldDeclarationSQL() is deprecated. Use UNIQUE in SQL instead.', + 'https://github.com/doctrine/dbal/pull/6867', + '%s() is deprecated.', + __METHOD__, ); - return 'UNIQUE'; + return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY (' . implode(', ', $index->getQuotedColumns($this)) . ')'; } /** - * Obtains DBMS specific SQL code portion needed to set the CHARACTER SET - * of a column declaration to be used in statements like CREATE TABLE. - * - * @param string $charset The name of the charset. - * - * @return string DBMS specific SQL code portion needed to set the CHARACTER SET - * of a column declaration. + * Returns the SQL to create a named schema. */ - public function getColumnCharsetDeclarationSQL($charset) + public function getCreateSchemaSQL(string $schemaName): string { - return ''; + if (! $this->supportsSchemas()) { + throw NotSupported::new(__METHOD__); + } + + return 'CREATE SCHEMA ' . $schemaName; } /** - * Obtains DBMS specific SQL code portion needed to set the COLLATION - * of a column declaration to be used in statements like CREATE TABLE. - * - * @param string $collation The name of the collation. - * - * @return string DBMS specific SQL code portion needed to set the COLLATION - * of a column declaration. + * Returns the SQL to create a unique constraint on a table on this platform. */ - public function getColumnCollationDeclarationSQL($collation) + public function getCreateUniqueConstraintSQL(UniqueConstraint $constraint, string $tableName): string { - return $this->supportsColumnCollation() ? 'COLLATE ' . $this->quoteSingleIdentifier($collation) : ''; + return 'ALTER TABLE ' . $tableName . ' ADD ' . $this->getUniqueConstraintDeclarationSQL($constraint); } /** - * Whether the platform prefers identity columns (eg. autoincrement) for ID generation. - * Subclasses should override this method to return TRUE if they prefer identity columns. - * - * @deprecated - * - * @return bool + * Returns the SQL snippet to drop a schema. */ - public function prefersIdentityColumns() + public function getDropSchemaSQL(string $schemaName): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/1519', - 'AbstractPlatform::prefersIdentityColumns() is deprecated.', - ); + if (! $this->supportsSchemas()) { + throw NotSupported::new(__METHOD__); + } - return false; + return 'DROP SCHEMA ' . $schemaName; } /** - * Some platforms need the boolean values to be converted. - * - * The default conversion in this implementation converts to integers (false => 0, true => 1). + * Quotes a string so that it can be safely used as a table or column name, + * even if it is a reserved word of the platform. This also detects identifier + * chains separated by dot and quotes them independently. * - * Note: if the input is not a boolean the original input might be returned. + * NOTE: Just because you CAN use quoted identifiers doesn't mean + * you SHOULD use them. In general, they end up causing way more + * problems than they solve. * - * There are two contexts when converting booleans: Literals and Prepared Statements. - * This method should handle the literal case + * @deprecated Use {@link quoteSingleIdentifier()} individually for each part of a qualified name instead. * - * @param mixed $item A boolean or an array of them. + * @param string $identifier The identifier name to be quoted. * - * @return mixed A boolean database value or an array of them. + * @return string The quoted identifier string. */ - public function convertBooleans($item) + public function quoteIdentifier(string $identifier): string { - if (is_array($item)) { - foreach ($item as $k => $value) { - if (! is_bool($value)) { - continue; - } + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6590', + <<<'DEPRECATION' + Method %s is deprecated and will be removed in 5.0. + Use quoteSingleIdentifier() individually for each part of a qualified name instead. + DEPRECATION, + __METHOD__, + ); - $item[$k] = (int) $value; - } - } elseif (is_bool($item)) { - $item = (int) $item; + if (str_contains($identifier, '.')) { + $parts = array_map($this->quoteSingleIdentifier(...), explode('.', $identifier)); + + return implode('.', $parts); } - return $item; + return $this->quoteSingleIdentifier($identifier); } /** - * Some platforms have boolean literals that needs to be correctly converted - * - * The default conversion tries to convert value into bool "(bool)$item" - * - * @param T $item + * Quotes a single identifier (no dot chain separation). * - * @return (T is null ? null : bool) + * @param string $str The identifier name to be quoted. * - * @template T + * @return string The quoted identifier string. */ - public function convertFromBoolean($item) + public function quoteSingleIdentifier(string $str): string { - return $item === null ? null : (bool) $item; + return '"' . str_replace('"', '""', $str) . '"'; } /** - * This method should handle the prepared statements case. When there is no - * distinction, it's OK to use the same method. + * Returns the SQL to create a new foreign key. * - * Note: if the input is not a boolean the original input might be returned. + * @param ForeignKeyConstraint $foreignKey The foreign key constraint. + * @param string $table The name of the table on which the foreign key is to be created. + */ + public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, string $table): string + { + return 'ALTER TABLE ' . $table . ' ADD ' . $this->getForeignKeyDeclarationSQL($foreignKey); + } + + /** + * Gets the SQL statements for altering an existing table. * - * @param mixed $item A boolean or an array of them. + * This method returns an array of SQL statements, since some platforms need several statements. * - * @return mixed A boolean database value or an array of them. + * @return list */ - public function convertBooleansToDatabaseValue($item) + abstract public function getAlterTableSQL(TableDiff $diff): array; + + public function getRenameTableSQL(string $oldName, string $newName): string { - return $this->convertBooleans($item); + return sprintf('ALTER TABLE %s RENAME TO %s', $oldName, $newName); + } + + /** @return list */ + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff): array + { + $tableNameSQL = $diff->getOldTable()->getQuotedName($this); + + $sql = []; + + foreach ($diff->getDroppedForeignKeys() as $foreignKey) { + $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableNameSQL); + } + + foreach ($diff->getModifiedForeignKeys() as $foreignKey) { + $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableNameSQL); + } + + foreach ($diff->getDroppedIndexes() as $index) { + $sql[] = $this->getDropIndexSQL($index->getQuotedName($this), $tableNameSQL); + } + + foreach ($diff->getModifiedIndexes() as $index) { + $sql[] = $this->getDropIndexSQL($index->getQuotedName($this), $tableNameSQL); + } + + return $sql; + } + + /** @return list */ + protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff): array + { + $sql = []; + + $tableNameSQL = $diff->getOldTable()->getQuotedName($this); + + foreach ($diff->getAddedForeignKeys() as $foreignKey) { + $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableNameSQL); + } + + foreach ($diff->getModifiedForeignKeys() as $foreignKey) { + $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableNameSQL); + } + + foreach ($diff->getAddedIndexes() as $index) { + $sql[] = $this->getCreateIndexSQL($index, $tableNameSQL); + } + + foreach ($diff->getModifiedIndexes() as $index) { + $sql[] = $this->getCreateIndexSQL($index, $tableNameSQL); + } + + foreach ($diff->getRenamedIndexes() as $oldIndexName => $index) { + $oldIndexName = new Identifier($oldIndexName); + $sql = array_merge( + $sql, + $this->getRenameIndexSQL($oldIndexName->getQuotedName($this), $index, $tableNameSQL), + ); + } + + return $sql; } /** - * Returns the SQL specific for the platform to get the current date. + * Returns the SQL for renaming an index on a table. + * + * @param string $oldIndexName The name of the index to rename from. + * @param Index $index The definition of the index to rename to. + * @param string $tableName The table to rename the given index on. * - * @return string + * @return list The sequence of SQL statements for renaming the given index. */ - public function getCurrentDateSQL() + protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array { - return 'CURRENT_DATE'; + return [ + $this->getDropIndexSQL($oldIndexName, $tableName), + $this->getCreateIndexSQL($index, $tableName), + ]; } /** - * Returns the SQL specific for the platform to get the current time. + * Returns the SQL for renaming a column + * + * @param string $tableName The table to rename the column on. + * @param string $oldColumnName The name of the column we want to rename. + * @param string $newColumnName The name we should rename it to. * - * @return string + * @return list The sequence of SQL statements for renaming the given column. */ - public function getCurrentTimeSQL() + protected function getRenameColumnSQL(string $tableName, string $oldColumnName, string $newColumnName): array { - return 'CURRENT_TIME'; + return [sprintf('ALTER TABLE %s RENAME COLUMN %s TO %s', $tableName, $oldColumnName, $newColumnName)]; } /** - * Returns the SQL specific for the platform to get the current timestamp + * Gets declaration of a number of columns in bulk. * - * @return string + * @param list $columns The properties of the columns to be declared. */ - public function getCurrentTimestampSQL() + public function getColumnDeclarationListSQL(array $columns): string { - return 'CURRENT_TIMESTAMP'; + $declarations = []; + + foreach ($columns as $column) { + $declarations[] = $this->getColumnDeclarationSQL($column['name'], $column); + } + + return implode(', ', $declarations); } /** - * Returns the SQL for a given transaction isolation level Connection constant. + * Obtains DBMS specific SQL code portion needed to declare a generic type + * column to be used in statements like CREATE TABLE. * - * @param int $level + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * - * @return string + * @param string $name The name of the column to be declared. + * @param ColumnProperties $column Column properties. * - * @throws InvalidArgumentException + * @return string DBMS specific SQL code portion that should be used to declare the column. */ - protected function _getTransactionIsolationLevelSQL($level) + public function getColumnDeclarationSQL(string $name, array $column): string { - switch ($level) { - case TransactionIsolationLevel::READ_UNCOMMITTED: - return 'READ UNCOMMITTED'; + if (isset($column['columnDefinition'])) { + $declaration = $column['columnDefinition']; + } else { + $default = $this->getDefaultValueDeclarationSQL($column); + + $charset = ! empty($column['charset']) ? + ' ' . $this->getColumnCharsetDeclarationSQL($column['charset']) : ''; - case TransactionIsolationLevel::READ_COMMITTED: - return 'READ COMMITTED'; + $collation = ! empty($column['collation']) ? + ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : ''; - case TransactionIsolationLevel::REPEATABLE_READ: - return 'REPEATABLE READ'; + $notnull = ! empty($column['notnull']) ? ' NOT NULL' : ''; - case TransactionIsolationLevel::SERIALIZABLE: - return 'SERIALIZABLE'; + $typeDecl = $column['type']->getSQLDeclaration($column, $this); + $declaration = $typeDecl . $charset . $default . $notnull . $collation; - default: - throw new InvalidArgumentException('Invalid isolation level:' . $level); + if ($this->supportsInlineColumnComments() && isset($column['comment']) && $column['comment'] !== '') { + $declaration .= ' ' . $this->getInlineColumnCommentSQL($column['comment']); + } } - } - /** - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getListDatabasesSQL() - { - throw Exception::notSupported(__METHOD__); + return $name . ' ' . $declaration; } /** - * Returns the SQL statement for retrieving the namespaces defined in the database. - * - * @deprecated Use {@see AbstractSchemaManager::listSchemaNames()} instead. - * - * @return string + * Returns the SQL snippet that declares a floating point column of arbitrary precision. * - * @throws Exception If not supported on this platform. + * @param array $column */ - public function getListNamespacesSQL() + public function getDecimalTypeDeclarationSQL(array $column): string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4503', - 'AbstractPlatform::getListNamespacesSQL() is deprecated,' - . ' use AbstractSchemaManager::listSchemaNames() instead.', - ); + if (! isset($column['precision'])) { + throw InvalidColumnDeclaration::fromInvalidColumnType($column['name'], ColumnPrecisionRequired::new()); + } + + if (! isset($column['scale'])) { + throw InvalidColumnDeclaration::fromInvalidColumnType($column['name'], ColumnScaleRequired::new()); + } - throw Exception::notSupported(__METHOD__); + return 'NUMERIC(' . $column['precision'] . ', ' . $column['scale'] . ')'; } /** - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + * Obtains DBMS specific SQL code portion needed to set a default value + * declaration to be used in statements like CREATE TABLE. * - * @param string $database + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * - * @return string + * @param array $column The column definition array. * - * @throws Exception If not supported on this platform. + * @return string DBMS specific SQL code portion needed to set a default value. */ - public function getListSequencesSQL($database) + public function getDefaultValueDeclarationSQL(array $column): string { - throw Exception::notSupported(__METHOD__); + if (! isset($column['default'])) { + return empty($column['notnull']) ? ' DEFAULT NULL' : ''; + } + + $default = $column['default']; + + if ($default instanceof DefaultExpression) { + return ' DEFAULT ' . $default->toSQL($this); + } + + if (! isset($column['type'])) { + return " DEFAULT '" . $default . "'"; + } + + $type = $column['type']; + + if ($type instanceof Types\PhpIntegerMappingType) { + return ' DEFAULT ' . $default; + } + + if ($type instanceof Types\PhpDateTimeMappingType && $default === $this->getCurrentTimestampSQL()) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/7195', + 'Using "%s" as a column default value is deprecated. Use a CurrentTimestamp instance instead.', + $default, + ); + + return ' DEFAULT ' . $this->getCurrentTimestampSQL(); + } + + if ($type instanceof Types\PhpTimeMappingType && $default === $this->getCurrentTimeSQL()) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/7195', + 'Using "%s" as a column default value is deprecated. Use a CurrentTime instance instead.', + $default, + ); + + return ' DEFAULT ' . $this->getCurrentTimeSQL(); + } + + if ($type instanceof Types\PhpDateMappingType && $default === $this->getCurrentDateSQL()) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/7195', + 'Using "%s" as a column default value is deprecated. Use a CurrentDate instance instead.', + $default, + ); + + return ' DEFAULT ' . $this->getCurrentDateSQL(); + } + + if ($type instanceof Types\BooleanType) { + return ' DEFAULT ' . $this->convertBooleans($default); + } + + if (is_int($default) || is_float($default)) { + return ' DEFAULT ' . $default; + } + + return ' DEFAULT ' . $this->quoteStringLiteral($default); } /** - * @deprecated + * Obtains DBMS specific SQL code portion needed to set a CHECK constraint + * declaration to be used in statements like CREATE TABLE. * - * @param string $table + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * - * @return string + * @param list $definition The check definition. * - * @throws Exception If not supported on this platform. + * @return string DBMS specific SQL code portion needed to set a CHECK constraint. */ - public function getListTableConstraintsSQL($table) + public function getCheckDeclarationSQL(array $definition): string { - throw Exception::notSupported(__METHOD__); + $constraints = []; + foreach ($definition as $def) { + if (is_string($def)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6805', + 'Passing column definition to %s() as string is deprecated. Pass the definition as array' + . ' instead.', + __METHOD__, + ); + + $constraints[] = 'CHECK (' . $def . ')'; + } else { + if (isset($def['min'])) { + $constraints[] = 'CHECK (' . $def['name'] . ' >= ' . $def['min'] . ')'; + } + + if (isset($def['max'])) { + $constraints[] = 'CHECK (' . $def['name'] . ' <= ' . $def['max'] . ')'; + } + } + } + + return implode(', ', $constraints); } /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * Obtains DBMS specific DDL fragment that defines a unique constraint to be used in statements like CREATE + * TABLE or ALTER TABLE. * - * @param string $table - * @param string $database + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * - * @return string + * @param UniqueConstraint $constraint The unique constraint definition. * - * @throws Exception If not supported on this platform. + * @return string DBMS specific DDL fragment that defines the constraint. */ - public function getListTableColumnsSQL($table, $database = null) + public function getUniqueConstraintDeclarationSQL(UniqueConstraint $constraint): string { - throw Exception::notSupported(__METHOD__); - } + $columns = $constraint->getQuotedColumns($this); - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getListTablesSQL() - { - throw Exception::notSupported(__METHOD__); - } + if (count($columns) === 0) { + throw new InvalidArgumentException('Incomplete definition. "columns" required.'); + } - /** - * @deprecated - * - * @return string - * - * @throws Exception If not supported on this platform. - */ - public function getListUsersSQL() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::getListUsersSQL() is deprecated.', - ); + $chunks = []; + + if ($constraint->getName() !== '') { + $chunks[] = 'CONSTRAINT'; + $chunks[] = $constraint->getQuotedName($this); + } + + $chunks[] = 'UNIQUE'; + + if ($constraint->hasFlag('clustered')) { + $chunks[] = 'CLUSTERED'; + } + + $chunks[] = sprintf('(%s)', implode(', ', $columns)); - throw Exception::notSupported(__METHOD__); + return implode(' ', $chunks); } /** - * Returns the SQL to list all views of a database or user. - * - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + * Obtains DBMS specific SQL code portion needed to set an index + * declaration to be used in statements like CREATE TABLE. * - * @param string $database + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * - * @return string + * @param Index $index The index definition. * - * @throws Exception If not supported on this platform. + * @return string DBMS specific SQL code portion needed to set an index. */ - public function getListViewsSQL($database) + public function getIndexDeclarationSQL(Index $index): string { - throw Exception::notSupported(__METHOD__); + $columns = $index->getColumns(); + + if (count($columns) === 0) { + throw new InvalidArgumentException('Incomplete definition. "columns" required.'); + } + + return $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $index->getQuotedName($this) + . ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index); } /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * Returns the list of indexes for the current database. - * - * The current database parameter is optional but will always be passed - * when using the SchemaManager API and is the database the given table is in. - * - * Attention: Some platforms only support currentDatabase when they - * are connected with that database. Cross-database information schema - * requests may be impossible. - * - * @param string $table - * @param string $database - * - * @return string - * - * @throws Exception If not supported on this platform. + * Some vendors require temporary table names to be qualified specially. */ - public function getListTableIndexesSQL($table, $database = null) + public function getTemporaryTableName(string $tableName): string { - throw Exception::notSupported(__METHOD__); + return $tableName; } /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * @param string $table + * Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a column declaration to be used in statements like CREATE TABLE. * - * @return string + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * - * @throws Exception If not supported on this platform. + * @return string DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a column declaration. */ - public function getListTableForeignKeysSQL($table) + public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey): string { - throw Exception::notSupported(__METHOD__); + $sql = $this->getForeignKeyBaseDeclarationSQL($foreignKey); + $sql .= $this->getAdvancedForeignKeyOptionsSQL($foreignKey); + + return $sql; } /** - * @param string $name - * @param string $sql + * Returns the FOREIGN KEY query section dealing with non-standard options + * as MATCH, INITIALLY DEFERRED, ON UPDATE, ... + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * - * @return string + * @param ForeignKeyConstraint $foreignKey The foreign key definition. */ - public function getCreateViewSQL($name, $sql) + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey): string { - return 'CREATE VIEW ' . $name . ' AS ' . $sql; + $query = ''; + if ($foreignKey->hasOption('onUpdate')) { + $query .= ' ON UPDATE ' . $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onUpdate')); + } + + if ($foreignKey->hasOption('onDelete')) { + $query .= ' ON DELETE ' . $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onDelete')); + } + + return $query; } /** - * @param string $name - * - * @return string + * Returns the SQL fragment representing the deferrability of a constraint. */ - public function getDropViewSQL($name) + protected function getConstraintDeferrabilitySQL(ForeignKeyConstraint $foreignKey): string { - return 'DROP VIEW ' . $name; + $sql = ''; + + if ($foreignKey->hasOption('deferrable')) { + if ($foreignKey->getOption('deferrable') !== false) { + $sql .= ' DEFERRABLE'; + } else { + $sql .= ' NOT DEFERRABLE'; + } + } + + if ($foreignKey->hasOption('deferred')) { + if ($foreignKey->getOption('deferred') !== false) { + $sql .= ' INITIALLY DEFERRED'; + } else { + $sql .= ' INITIALLY IMMEDIATE'; + } + } + + return $sql; } /** - * @param string $sequence + * Returns the given referential action in uppercase if valid, otherwise throws an exception. * - * @return string + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * - * @throws Exception If not supported on this platform. + * @param string $action The foreign key referential action. */ - public function getSequenceNextValSQL($sequence) + public function getForeignKeyReferentialActionSQL(string $action): string { - throw Exception::notSupported(__METHOD__); + $upper = strtoupper($action); + + return match ($upper) { + 'CASCADE', + 'SET NULL', + 'NO ACTION', + 'RESTRICT', + 'SET DEFAULT' => $upper, + default => throw new InvalidArgumentException(sprintf('Invalid foreign key action "%s".', $upper)), + }; } /** - * Returns the SQL to create a new database. - * - * @param string $name The name of the database that should be created. - * - * @return string + * Obtains DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a column declaration to be used in statements like CREATE TABLE. * - * @throws Exception If not supported on this platform. + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ - public function getCreateDatabaseSQL($name) + public function getForeignKeyBaseDeclarationSQL(ForeignKeyConstraint $foreignKey): string { - if (! $this->supportsCreateDropDatabase()) { - throw Exception::notSupported(__METHOD__); + $sql = ''; + if ($foreignKey->getName() !== '') { + $sql .= 'CONSTRAINT ' . $foreignKey->getQuotedName($this) . ' '; } - return 'CREATE DATABASE ' . $name; - } + $sql .= 'FOREIGN KEY ('; - /** - * Returns the SQL snippet to drop an existing database. - * - * @param string $name The name of the database that should be dropped. - * - * @return string - */ - public function getDropDatabaseSQL($name) - { - if (! $this->supportsCreateDropDatabase()) { - throw Exception::notSupported(__METHOD__); + if (count($foreignKey->getLocalColumns()) === 0) { + throw new InvalidArgumentException('Incomplete definition. "local" required.'); } - return 'DROP DATABASE ' . $name; + if (count($foreignKey->getForeignColumns()) === 0) { + throw new InvalidArgumentException('Incomplete definition. "foreign" required.'); + } + + if (strlen($foreignKey->getForeignTableName()) === 0) { + throw new InvalidArgumentException('Incomplete definition. "foreignTable" required.'); + } + + return $sql . implode(', ', $foreignKey->getQuotedLocalColumns($this)) + . ') REFERENCES ' + . $foreignKey->getQuotedForeignTableName($this) . ' (' + . implode(', ', $foreignKey->getQuotedForeignColumns($this)) . ')'; } /** - * Returns the SQL to set the transaction isolation level. + * Obtains DBMS specific SQL code portion needed to set the CHARACTER SET + * of a column declaration to be used in statements like CREATE TABLE. * - * @param int $level + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * - * @return string + * @param string $charset The name of the charset. * - * @throws Exception If not supported on this platform. + * @return string DBMS specific SQL code portion needed to set the CHARACTER SET + * of a column declaration. */ - public function getSetTransactionIsolationSQL($level) + public function getColumnCharsetDeclarationSQL(string $charset): string { - throw Exception::notSupported(__METHOD__); + return ''; } /** - * Obtains DBMS specific SQL to be used to create datetime columns in - * statements like CREATE TABLE. + * Obtains DBMS specific SQL code portion needed to set the COLLATION + * of a column declaration to be used in statements like CREATE TABLE. * - * @param mixed[] $column + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * - * @return string + * @param string $collation The name of the collation. * - * @throws Exception If not supported on this platform. + * @return string DBMS specific SQL code portion needed to set the COLLATION + * of a column declaration. */ - public function getDateTimeTypeDeclarationSQL(array $column) + public function getColumnCollationDeclarationSQL(string $collation): string { - throw Exception::notSupported(__METHOD__); + return $this->supportsColumnCollation() ? 'COLLATE ' . $this->quoteSingleIdentifier($collation) : ''; } /** - * Obtains DBMS specific SQL to be used to create datetime with timezone offset columns. + * Some platforms need the boolean values to be converted. + * + * The default conversion in this implementation converts to integers (false => 0, true => 1). + * + * Note: if the input is not a boolean the original input might be returned. * - * @param mixed[] $column + * There are two contexts when converting booleans: Literals and Prepared Statements. + * This method should handle the literal case + * + * @param mixed $item A boolean or an array of them. * - * @return string + * @return mixed A boolean database value or an array of them. */ - public function getDateTimeTzTypeDeclarationSQL(array $column) + public function convertBooleans(mixed $item): mixed { - return $this->getDateTimeTypeDeclarationSQL($column); + if (is_array($item)) { + foreach ($item as $k => $value) { + if (is_bool($value)) { + $item[$k] = (int) $value; + } + } + } elseif (is_bool($item)) { + $item = (int) $item; + } + + return $item; } /** - * Obtains DBMS specific SQL to be used to create date columns in statements - * like CREATE TABLE. + * Some platforms have boolean literals that needs to be correctly converted + * + * The default conversion tries to convert value into bool "(bool)$item" * - * @param mixed[] $column + * @param T $item * - * @return string + * @return (T is null ? null : bool) * - * @throws Exception If not supported on this platform. + * @template T */ - public function getDateTypeDeclarationSQL(array $column) + public function convertFromBoolean(mixed $item): ?bool { - throw Exception::notSupported(__METHOD__); + if ($item === null) { + return null; + } + + return (bool) $item; } /** - * Obtains DBMS specific SQL to be used to create time columns in statements - * like CREATE TABLE. + * This method should handle the prepared statements case. When there is no + * distinction, it's OK to use the same method. * - * @param mixed[] $column + * Note: if the input is not a boolean the original input might be returned. * - * @return string + * @param mixed $item A boolean or an array of them. * - * @throws Exception If not supported on this platform. + * @return mixed A boolean database value or an array of them. */ - public function getTimeTypeDeclarationSQL(array $column) + public function convertBooleansToDatabaseValue(mixed $item): mixed { - throw Exception::notSupported(__METHOD__); + return $this->convertBooleans($item); } /** - * @param mixed[] $column - * - * @return string + * Returns the SQL specific for the platform to get the current date. */ - public function getFloatDeclarationSQL(array $column) + public function getCurrentDateSQL(): string { - return 'DOUBLE PRECISION'; + return 'CURRENT_DATE'; } /** - * Gets the default transaction isolation level of the platform. - * - * @see TransactionIsolationLevel - * - * @return TransactionIsolationLevel::* The default isolation level. + * Returns the SQL specific for the platform to get the current time. */ - public function getDefaultTransactionIsolationLevel() + public function getCurrentTimeSQL(): string { - return TransactionIsolationLevel::READ_COMMITTED; + return 'CURRENT_TIME'; } - /* supports*() methods */ - /** - * Whether the platform supports sequences. - * - * @return bool + * Returns the SQL specific for the platform to get the current timestamp */ - public function supportsSequences() + public function getCurrentTimestampSQL(): string { - return false; + return 'CURRENT_TIMESTAMP'; } /** - * Whether the platform supports identity columns. - * - * Identity columns are columns that receive an auto-generated value from the - * database on insert of a row. - * - * @return bool + * Returns the SQL for a given transaction isolation level Connection constant. */ - public function supportsIdentityColumns() + protected function _getTransactionIsolationLevelSQL(TransactionIsolationLevel $level): string { - return false; + return match ($level) { + TransactionIsolationLevel::READ_UNCOMMITTED => 'READ UNCOMMITTED', + TransactionIsolationLevel::READ_COMMITTED => 'READ COMMITTED', + TransactionIsolationLevel::REPEATABLE_READ => 'REPEATABLE READ', + TransactionIsolationLevel::SERIALIZABLE => 'SERIALIZABLE', + }; } - /** - * Whether the platform emulates identity columns through sequences. - * - * Some platforms that do not support identity columns natively - * but support sequences can emulate identity columns by using - * sequences. - * - * @deprecated - * - * @return bool - */ - public function usesSequenceEmulatedIdentityColumns() + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListDatabasesSQL(): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5513', - '%s is deprecated.', - __METHOD__, - ); + throw NotSupported::new(__METHOD__); + } - return false; + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListSequencesSQL(string $database): string + { + throw NotSupported::new(__METHOD__); } /** - * Returns the name of the sequence for a particular identity column in a particular table. - * - * @deprecated - * - * @see usesSequenceEmulatedIdentityColumns - * - * @param string $tableName The name of the table to return the sequence name for. - * @param string $columnName The name of the identity column in the table to return the sequence name for. - * - * @return string + * Returns the SQL to list all views of a database or user. * - * @throws Exception If not supported on this platform. + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ - public function getIdentitySequenceName($tableName, $columnName) + abstract public function getListViewsSQL(string $database): string; + + public function getCreateViewSQL(string $name, string $sql): string { - throw Exception::notSupported(__METHOD__); + return 'CREATE VIEW ' . $name . ' AS ' . $sql; } - /** - * Whether the platform supports indexes. - * - * @deprecated - * - * @return bool - */ - public function supportsIndexes() + public function getDropViewSQL(string $name): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::supportsIndexes() is deprecated.', - ); + return 'DROP VIEW ' . $name; + } - return true; + public function getSequenceNextValSQL(string $sequence): string + { + throw NotSupported::new(__METHOD__); } /** - * Whether the platform supports partial indexes. + * Returns the SQL to create a new database. * - * @return bool + * @param string $name The name of the database that should be created. */ - public function supportsPartialIndexes() + public function getCreateDatabaseSQL(string $name): string { - return false; + return 'CREATE DATABASE ' . $name; } /** - * Whether the platform supports indexes with column length definitions. + * Returns the SQL snippet to drop an existing database. + * + * @param string $name The name of the database that should be dropped. */ - public function supportsColumnLengthIndexes(): bool + public function getDropDatabaseSQL(string $name): string { - return false; + return 'DROP DATABASE ' . $name; } /** - * Whether the platform supports altering tables. - * - * @deprecated All platforms must implement altering tables. - * - * @return bool + * Returns the SQL to set the transaction isolation level. */ - public function supportsAlterTable() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::supportsAlterTable() is deprecated. All platforms must implement altering tables.', - ); - - return true; - } + abstract public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string; /** - * Whether the platform supports transactions. - * - * @deprecated + * Obtains DBMS specific SQL to be used to create datetime columns in + * statements like CREATE TABLE. * - * @return bool + * @param array $column */ - public function supportsTransactions() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::supportsTransactions() is deprecated.', - ); - - return true; - } + abstract public function getDateTimeTypeDeclarationSQL(array $column): string; /** - * Whether the platform supports savepoints. + * Obtains DBMS specific SQL to be used to create datetime with timezone offset columns. * - * @return bool + * @param array $column */ - public function supportsSavepoints() + public function getDateTimeTzTypeDeclarationSQL(array $column): string { - return true; + return $this->getDateTimeTypeDeclarationSQL($column); } /** - * Whether the platform supports releasing savepoints. + * Obtains DBMS specific SQL to be used to create date columns in statements + * like CREATE TABLE. * - * @return bool + * @param array $column */ - public function supportsReleaseSavepoints() - { - return $this->supportsSavepoints(); - } + abstract public function getDateTypeDeclarationSQL(array $column): string; /** - * Whether the platform supports primary key constraints. - * - * @deprecated + * Obtains DBMS specific SQL to be used to create time columns in statements + * like CREATE TABLE. * - * @return bool + * @param array $column */ - public function supportsPrimaryConstraints() + abstract public function getTimeTypeDeclarationSQL(array $column): string; + + /** @param array $column */ + public function getFloatDeclarationSQL(array $column): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::supportsPrimaryConstraints() is deprecated.', - ); + return 'DOUBLE PRECISION'; + } - return true; + /** @param array $column */ + public function getSmallFloatDeclarationSQL(array $column): string + { + return 'REAL'; } /** - * Whether the platform supports foreign key constraints. - * - * @deprecated All platforms should support foreign key constraints. + * Gets the default transaction isolation level of the platform. * - * @return bool + * @return TransactionIsolationLevel The default isolation level. */ - public function supportsForeignKeyConstraints() + public function getDefaultTransactionIsolationLevel(): TransactionIsolationLevel { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5409', - 'AbstractPlatform::supportsForeignKeyConstraints() is deprecated.', - ); - - return true; + return TransactionIsolationLevel::READ_COMMITTED; } + /* supports*() methods */ + /** - * Whether the platform supports database schemas. - * - * @return bool + * Whether the platform supports sequences. */ - public function supportsSchemas() + public function supportsSequences(): bool { return false; } /** - * Whether this platform can emulate schemas. - * - * @deprecated - * - * Platforms that either support or emulate schemas don't automatically - * filter a schema for the namespaced elements in {@see AbstractManager::introspectSchema()}. + * Whether the platform supports identity columns. * - * @return bool + * Identity columns are columns that receive an auto-generated value from the + * database on insert of a row. */ - public function canEmulateSchemas() + public function supportsIdentityColumns(): bool { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4805', - 'AbstractPlatform::canEmulateSchemas() is deprecated.', - ); - return false; } /** - * Returns the default schema name. - * - * @deprecated - * - * @return string + * Whether the platform supports partial indexes. * - * @throws Exception If not supported on this platform. + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ - public function getDefaultSchemaName() + public function supportsPartialIndexes(): bool { - throw Exception::notSupported(__METHOD__); + return false; } /** - * Whether this platform supports create database. - * - * Some databases don't allow to create and drop databases at all or only with certain tools. + * Whether the platform supports indexes with column length definitions. * * @deprecated - * - * @return bool */ - public function supportsCreateDropDatabase() + public function supportsColumnLengthIndexes(): bool { Deprecation::triggerIfCalledFromOutside( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5513', + 'https://github.com/doctrine/dbal/pull/6886', '%s is deprecated.', __METHOD__, ); - return true; + return false; } /** - * Whether the platform supports getting the affected rows of a recent update/delete type query. - * - * @deprecated - * - * @return bool + * Whether the platform supports savepoints. */ - public function supportsGettingAffectedRows() + public function supportsSavepoints(): bool { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::supportsGettingAffectedRows() is deprecated.', - ); - return true; } /** - * Whether this platform support to add inline column comments as postfix. - * - * @return bool + * Whether the platform supports releasing savepoints. */ - public function supportsInlineColumnComments() + public function supportsReleaseSavepoints(): bool { - return false; + return $this->supportsSavepoints(); } /** - * Whether this platform support the proprietary syntax "COMMENT ON asset". - * - * @return bool + * Whether the platform supports database schemas. */ - public function supportsCommentOnStatement() + public function supportsSchemas(): bool { return false; } /** - * Does this platform have native guid type. - * - * @deprecated + * Whether this platform support to add inline column comments as postfix. * - * @return bool + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ - public function hasNativeGuidType() + public function supportsInlineColumnComments(): bool { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - return false; } /** - * Does this platform have native JSON type. - * - * @deprecated + * Whether this platform support the proprietary syntax "COMMENT ON asset". * - * @return bool + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ - public function hasNativeJsonType() + public function supportsCommentOnStatement(): bool { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - return false; } - /** - * Whether this platform supports views. - * - * @deprecated All platforms must implement support for views. - * - * @return bool - */ - public function supportsViews() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::supportsViews() is deprecated. All platforms must implement support for views.', - ); - - return true; - } - /** * Does this platform support column collation? * - * @return bool + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ - public function supportsColumnCollation() + public function supportsColumnCollation(): bool { return false; } @@ -4329,7 +2179,7 @@ public function supportsColumnCollation() * * @return string The format string. */ - public function getDateTimeFormatString() + public function getDateTimeFormatString(): string { return 'Y-m-d H:i:s'; } @@ -4340,7 +2190,7 @@ public function getDateTimeFormatString() * * @return string The format string. */ - public function getDateTimeTzFormatString() + public function getDateTimeTzFormatString(): string { return 'Y-m-d H:i:s'; } @@ -4351,7 +2201,7 @@ public function getDateTimeTzFormatString() * * @return string The format string. */ - public function getDateFormatString() + public function getDateFormatString(): string { return 'Y-m-d'; } @@ -4362,53 +2212,30 @@ public function getDateFormatString() * * @return string The format string. */ - public function getTimeFormatString() + public function getTimeFormatString(): string { return 'H:i:s'; } /** * Adds an driver-specific LIMIT clause to the query. - * - * @param string $query - * @param int|null $limit - * @param int $offset - * - * @throws Exception */ - final public function modifyLimitQuery($query, $limit, $offset = 0): string + final public function modifyLimitQuery(string $query, ?int $limit, int $offset = 0): string { if ($offset < 0) { - throw new Exception(sprintf( - 'Offset must be a positive integer or zero, %d given', + throw new InvalidArgumentException(sprintf( + 'Offset must be a positive integer or zero, %d given.', $offset, )); } - if ($offset > 0 && ! $this->supportsLimitOffset()) { - throw new Exception(sprintf( - 'Platform %s does not support offset values in limit queries.', - $this->getName(), - )); - } - - if ($limit !== null) { - $limit = (int) $limit; - } - - return $this->doModifyLimitQuery($query, $limit, (int) $offset); + return $this->doModifyLimitQuery($query, $limit, $offset); } /** * Adds an platform-specific LIMIT clause to the query. - * - * @param string $query - * @param int|null $limit - * @param int $offset - * - * @return string */ - protected function doModifyLimitQuery($query, $limit, $offset) + protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string { if ($limit !== null) { $query .= sprintf(' LIMIT %d', $limit); @@ -4421,44 +2248,20 @@ protected function doModifyLimitQuery($query, $limit, $offset) return $query; } - /** - * Whether the database platform support offsets in modify limit clauses. - * - * @deprecated All platforms must implement support for offsets in modify limit clauses. - * - * @return bool - */ - public function supportsLimitOffset() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4724', - 'AbstractPlatform::supportsViews() is deprecated.' - . ' All platforms must implement support for offsets in modify limit clauses.', - ); - - return true; - } - /** * Maximum length of any given database identifier, like tables or column names. * - * @return int + * @return positive-int */ - public function getMaxIdentifierLength() + public function getMaxIdentifierLength(): int { return 63; } /** * Returns the insert SQL for an empty insert statement. - * - * @param string $quotedTableName - * @param string $quotedIdentifierColumnName - * - * @return string */ - public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName) + public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string { return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (null)'; } @@ -4468,13 +2271,8 @@ public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierCol * * Cascade is not supported on many platforms but would optionally cascade the truncate by * following the foreign keys. - * - * @param string $tableName - * @param bool $cascade - * - * @return string */ - public function getTruncateTableSQL($tableName, $cascade = false) + public function getTruncateTableSQL(string $tableName, bool $cascade = false): string { $tableIdentifier = new Identifier($tableName); @@ -4483,48 +2281,32 @@ public function getTruncateTableSQL($tableName, $cascade = false) /** * This is for test reasons, many vendors have special requirements for dummy statements. - * - * @return string */ - public function getDummySelectSQL() + public function getDummySelectSQL(string $expression = '1'): string { - $expression = func_num_args() > 0 ? func_get_arg(0) : '1'; - return sprintf('SELECT %s', $expression); } /** * Returns the SQL to create a new savepoint. - * - * @param string $savepoint - * - * @return string */ - public function createSavePoint($savepoint) + public function createSavePoint(string $savepoint): string { return 'SAVEPOINT ' . $savepoint; } /** * Returns the SQL to release a savepoint. - * - * @param string $savepoint - * - * @return string */ - public function releaseSavePoint($savepoint) + public function releaseSavePoint(string $savepoint): string { return 'RELEASE SAVEPOINT ' . $savepoint; } /** * Returns the SQL to rollback a savepoint. - * - * @param string $savepoint - * - * @return string */ - public function rollbackSavePoint($savepoint) + public function rollbackSavePoint(string $savepoint): string { return 'ROLLBACK TO SAVEPOINT ' . $savepoint; } @@ -4532,10 +2314,17 @@ public function rollbackSavePoint($savepoint) /** * Returns the keyword list instance of this platform. * - * @throws Exception If no keyword list is specified. + * @deprecated */ final public function getReservedKeywordsList(): KeywordList { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6607', + '%s is deprecated.', + __METHOD__, + ); + // Store the instance so it doesn't need to be generated on every request. return $this->_keywords ??= $this->createReservedKeywordsList(); } @@ -4543,42 +2332,9 @@ final public function getReservedKeywordsList(): KeywordList /** * Creates an instance of the reserved keyword list of this platform. * - * This method will become @abstract in DBAL 4.0.0. - * - * @throws Exception - */ - protected function createReservedKeywordsList(): KeywordList - { - $class = $this->getReservedKeywordsClass(); - $keywords = new $class(); - if (! $keywords instanceof KeywordList) { - throw Exception::notSupported(__METHOD__); - } - - return $keywords; - } - - /** - * Returns the class name of the reserved keywords list. - * - * @deprecated Implement {@see createReservedKeywordsList()} instead. - * - * @return string - * @phpstan-return class-string - * - * @throws Exception If not supported on this platform. + * @deprecated */ - protected function getReservedKeywordsClass() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4510', - 'AbstractPlatform::getReservedKeywordsClass() is deprecated,' - . ' use AbstractPlatform::createReservedKeywordsList() instead.', - ); - - throw Exception::notSupported(__METHOD__); - } + abstract protected function createReservedKeywordsList(): KeywordList; /** * Quotes a literal string. @@ -4590,30 +2346,9 @@ protected function getReservedKeywordsClass() * * @return string The quoted literal string. */ - public function quoteStringLiteral($str) - { - $c = $this->getStringLiteralQuoteCharacter(); - - return $c . str_replace($c, $c . $c, $str) . $c; - } - - /** - * Gets the character used for string literal quoting. - * - * @deprecated Use {@see quoteStringLiteral()} to quote string literals instead. - * - * @return string - */ - public function getStringLiteralQuoteCharacter() + public function quoteStringLiteral(string $str): string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5388', - 'AbstractPlatform::getStringLiteralQuoteCharacter() is deprecated.' - . ' Use quoteStringLiteral() instead.', - ); - - return "'"; + return "'" . str_replace("'", "''", $str) . "'"; } /** @@ -4626,25 +2361,27 @@ public function getStringLiteralQuoteCharacter() */ final public function escapeStringForLike(string $inputString, string $escapeChar): string { - return preg_replace( + $sql = preg_replace( '~([' . preg_quote($this->getLikeWildcardCharacters() . $escapeChar, '~') . '])~u', addcslashes($escapeChar, '\\') . '$1', $inputString, ); + + assert(is_string($sql)); + + return $sql; } /** - * @return array An associative array with the name of the properties - * of the column being declared as array indexes. + * @return ColumnProperties An associative array with the name of the properties of the column being declared as + * array keys. */ private function columnToArray(Column $column): array { - $name = $column->getQuotedName($this); - return array_merge($column->toArray(), [ - 'name' => $name, + 'name' => $column->getQuotedName($this), 'version' => $column->hasPlatformOption('version') ? $column->getPlatformOption('version') : false, - 'comment' => $this->getColumnComment($column), + 'comment' => $column->getComment(), ]); } @@ -4661,8 +2398,6 @@ protected function getLikeWildcardCharacters(): string /** * Compares the definitions of the given columns in the context of this platform. - * - * @throws Exception */ public function columnsEqual(Column $column1, Column $column2): bool { @@ -4670,8 +2405,8 @@ public function columnsEqual(Column $column1, Column $column2): bool $column2Array = $this->columnToArray($column2); // ignore explicit columnDefinition since it's not set on the Column generated by the SchemaManager - unset($column1Array['columnDefinition']); - unset($column2Array['columnDefinition']); + $column1Array['columnDefinition'] = null; + $column2Array['columnDefinition'] = null; if ( $this->getColumnDeclarationSQL('', $column1Array) @@ -4680,48 +2415,67 @@ public function columnsEqual(Column $column1, Column $column2): bool return false; } - if (! $this->columnDeclarationsMatch($column1, $column2)) { - return false; - } - // If the platform supports inline comments, all comparison is already done above if ($this->supportsInlineColumnComments()) { return true; } - if ($column1->getComment() !== $column2->getComment()) { - return false; - } + return $column1->getComment() === $column2->getComment(); + } - // If disableTypeComments is true, we do not need to check types, all comparison is already done above - if ($this->disableTypeComments) { - return true; - } + /** + * Returns the union select query part surrounded by parenthesis if possible for platform. + */ + public function getUnionSelectPartSQL(string $subQuery): string + { + return sprintf('(%s)', $subQuery); + } - return $column1->getType() === $column2->getType(); + /** + * Returns the `UNION ALL` keyword. + */ + public function getUnionAllSQL(): string + { + return 'UNION ALL'; } /** - * Whether the database data type matches that expected for the doctrine type for the given colunms. + * Returns the compatible `UNION DISTINCT` keyword. */ - private function columnDeclarationsMatch(Column $column1, Column $column2): bool + public function getUnionDistinctSQL(): string { - return ! ( - $column1->hasPlatformOption('declarationMismatch') || - $column2->hasPlatformOption('declarationMismatch') - ); + return 'UNION'; + } + + public function getUnquotedIdentifierFolding(): UnquotedIdentifierFolding + { + if ($this->unquotedIdentifierFolding === null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6823', + 'Not calling the %s constructor from child class constructors is deprecated.', + self::class, + ); + + $this->unquotedIdentifierFolding = UnquotedIdentifierFolding::UPPER; + } + + return $this->unquotedIdentifierFolding; } /** - * Creates the schema manager that can be used to inspect and change the underlying - * database schema according to the dialect of the platform. + * Creates a metadata provider that can be used access the metadata of the underlying database schema. * * @throws Exception - * - * @abstract */ - public function createSchemaManager(Connection $connection): AbstractSchemaManager + public function createMetadataProvider(Connection $connection): MetadataProvider { - throw Exception::notSupported(__METHOD__); + throw NotSupported::new(__METHOD__); } + + /** + * Creates the schema manager that can be used to inspect and change the underlying + * database schema according to the dialect of the platform. + */ + abstract public function createSchemaManager(Connection $connection): AbstractSchemaManager; } diff --git a/doctrine/dbal/src/Platforms/DB2111Platform.php b/doctrine/dbal/src/Platforms/DB2111Platform.php deleted file mode 100644 index c1087151f..000000000 --- a/doctrine/dbal/src/Platforms/DB2111Platform.php +++ /dev/null @@ -1,40 +0,0 @@ - 0) { - $query .= sprintf(' OFFSET %u ROWS', $offset); - } - - if ($limit !== null) { - if ($limit < 0) { - throw new Exception(sprintf('Limit must be a positive integer or zero, %d given', $limit)); - } - - $query .= sprintf(' FETCH %s %u ROWS ONLY', $offset === 0 ? 'FIRST' : 'NEXT', $limit); - } - - return $query; - } -} diff --git a/doctrine/dbal/src/Platforms/DB2Platform.php b/doctrine/dbal/src/Platforms/DB2Platform.php index 18d19bb26..f9caa5368 100644 --- a/doctrine/dbal/src/Platforms/DB2Platform.php +++ b/doctrine/dbal/src/Platforms/DB2Platform.php @@ -1,17 +1,24 @@ getCharMaxLength(); - } - - return parent::getVarcharTypeDeclarationSQL($column); + parent::__construct(UnquotedIdentifierFolding::UPPER); } /** * {@inheritDoc} */ - public function getBlobTypeDeclarationSQL(array $column) + public function getBlobTypeDeclarationSQL(array $column): string { // todo blob(n) with $column['length']; return 'BLOB(1M)'; } - /** - * {@inheritDoc} - */ - protected function initializeDoctrineTypeMappings() + protected function initializeDoctrineTypeMappings(): void { $this->doctrineTypeMapping = [ 'bigint' => Types::BIGINT, @@ -136,7 +61,7 @@ protected function initializeDoctrineTypeMappings() 'decimal' => Types::DECIMAL, 'double' => Types::FLOAT, 'integer' => Types::INTEGER, - 'real' => Types::FLOAT, + 'real' => Types::SMALLFLOAT, 'smallint' => Types::SMALLINT, 'time' => Types::TIME_MUTABLE, 'timestamp' => Types::DATETIME_MUTABLE, @@ -145,66 +70,20 @@ protected function initializeDoctrineTypeMappings() ]; } - /** - * {@inheritDoc} - */ - public function isCommentedDoctrineType(Type $doctrineType) + protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5058', - '%s() is deprecated and will be removed in Doctrine DBAL 4.0. Use Type::requiresSQLCommentHint() instead.', - __METHOD__, - ); - - if ($doctrineType->getName() === Types::BOOLEAN) { - // We require a commented boolean type in order to distinguish between boolean and smallint - // as both (have to) map to the same native type. - return true; - } - - return parent::isCommentedDoctrineType($doctrineType); + return $this->getCharTypeDeclarationSQLSnippet($length) . ' FOR BIT DATA'; } - /** - * {@inheritDoc} - */ - protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string { - if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'Relying on the default string column length on Db2 is deprecated' - . ', specify the length explicitly.', - ); - } - - return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(254)') - : ($length > 0 ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'); + return $this->getVarcharTypeDeclarationSQLSnippet($length) . ' FOR BIT DATA'; } /** * {@inheritDoc} */ - protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) - { - if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'Relying on the default binary column length on Db2 is deprecated' - . ', specify the length explicitly.', - ); - } - - return $this->getVarcharTypeDeclarationSQLSnippet($length, $fixed) . ' FOR BIT DATA'; - } - - /** - * {@inheritDoc} - */ - public function getClobTypeDeclarationSQL(array $column) + public function getClobTypeDeclarationSQL(array $column): string { // todo clob(n) with $column['length']; return 'CLOB(1M)'; @@ -213,22 +92,7 @@ public function getClobTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getName() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4749', - '%s() is deprecated. Identify platforms by their class.', - __METHOD__, - ); - - return 'db2'; - } - - /** - * {@inheritDoc} - */ - public function getBooleanTypeDeclarationSQL(array $column) + public function getBooleanTypeDeclarationSQL(array $column): string { return 'SMALLINT'; } @@ -236,7 +100,7 @@ public function getBooleanTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getIntegerTypeDeclarationSQL(array $column) + public function getIntegerTypeDeclarationSQL(array $column): string { return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($column); } @@ -244,7 +108,7 @@ public function getIntegerTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getBigIntTypeDeclarationSQL(array $column) + public function getBigIntTypeDeclarationSQL(array $column): string { return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } @@ -252,7 +116,7 @@ public function getBigIntTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getSmallIntTypeDeclarationSQL(array $column) + public function getSmallIntTypeDeclarationSQL(array $column): string { return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } @@ -260,56 +124,47 @@ public function getSmallIntTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - protected function _getCommonIntegerTypeDeclarationSQL(array $column) + protected function _getCommonIntegerTypeDeclarationSQL(array $column): string { - $autoinc = ''; if (! empty($column['autoincrement'])) { - $autoinc = ' GENERATED BY DEFAULT AS IDENTITY'; + return ' GENERATED BY DEFAULT AS IDENTITY'; } - return $autoinc; + return ''; } - /** - * {@inheritDoc} - */ - public function getBitAndComparisonExpression($value1, $value2) + public function getBitAndComparisonExpression(string $value1, string $value2): string { return 'BITAND(' . $value1 . ', ' . $value2 . ')'; } - /** - * {@inheritDoc} - */ - public function getBitOrComparisonExpression($value1, $value2) + public function getBitOrComparisonExpression(string $value1, string $value2): string { return 'BITOR(' . $value1 . ', ' . $value2 . ')'; } - /** - * {@inheritDoc} - */ - protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit) - { + protected function getDateArithmeticIntervalExpression( + string $date, + string $operator, + string $interval, + DateIntervalUnit $unit, + ): string { switch ($unit) { case DateIntervalUnit::WEEK: - $interval = $this->multiplyInterval((string) $interval, 7); + $interval = $this->multiplyInterval($interval, 7); $unit = DateIntervalUnit::DAY; break; case DateIntervalUnit::QUARTER: - $interval = $this->multiplyInterval((string) $interval, 3); + $interval = $this->multiplyInterval($interval, 3); $unit = DateIntervalUnit::MONTH; break; } - return $date . ' ' . $operator . ' ' . $interval . ' ' . $unit; + return $date . ' ' . $operator . ' ' . $interval . ' ' . $unit->value; } - /** - * {@inheritDoc} - */ - public function getDateDiffExpression($date1, $date2) + public function getDateDiffExpression(string $date1, string $date2): string { return 'DAYS(' . $date1 . ') - DAYS(' . $date2 . ')'; } @@ -317,9 +172,15 @@ public function getDateDiffExpression($date1, $date2) /** * {@inheritDoc} */ - public function getDateTimeTypeDeclarationSQL(array $column) + public function getDateTimeTypeDeclarationSQL(array $column): string { if (isset($column['version']) && $column['version'] === true) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6940', + 'The "version" column platform option is deprecated.', + ); + return 'TIMESTAMP(0) WITH DEFAULT'; } @@ -329,7 +190,7 @@ public function getDateTimeTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getDateTypeDeclarationSQL(array $column) + public function getDateTypeDeclarationSQL(array $column): string { return 'DATE'; } @@ -337,232 +198,64 @@ public function getDateTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getTimeTypeDeclarationSQL(array $column) + public function getTimeTypeDeclarationSQL(array $column): string { return 'TIME'; } - /** - * {@inheritDoc} - */ - public function getTruncateTableSQL($tableName, $cascade = false) + public function getTruncateTableSQL(string $tableName, bool $cascade = false): string { $tableIdentifier = new Identifier($tableName); return 'TRUNCATE ' . $tableIdentifier->getQuotedName($this) . ' IMMEDIATE'; } - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * This code fragment is originally from the Zend_Db_Adapter_Db2 class, but has been edited. - * - * @param string $table - * @param string $database - * - * @return string - */ - public function getListTableColumnsSQL($table, $database = null) - { - $table = $this->quoteStringLiteral($table); - - // We do the funky subquery and join syscat.columns.default this crazy way because - // as of db2 v10, the column is CLOB(64k) and the distinct operator won't allow a CLOB, - // it wants shorter stuff like a varchar. - return " - SELECT - cols.default, - subq.* - FROM ( - SELECT DISTINCT - c.tabschema, - c.tabname, - c.colname, - c.colno, - c.typename, - c.codepage, - c.nulls, - c.length, - c.scale, - c.identity, - tc.type AS tabconsttype, - c.remarks AS comment, - k.colseq, - CASE - WHEN c.generated = '" . self::SYSCAT_COLUMNS_GENERATED_DEFAULT . "' THEN 1 - ELSE 0 - END AS autoincrement - FROM syscat.columns c - LEFT JOIN (syscat.keycoluse k JOIN syscat.tabconst tc - ON (k.tabschema = tc.tabschema - AND k.tabname = tc.tabname - AND tc.type = '" . self::SYSCAT_TABCONST_TYPE_PRIMARY_KEY . "')) - ON (c.tabschema = k.tabschema - AND c.tabname = k.tabname - AND c.colname = k.colname) - WHERE UPPER(c.tabname) = UPPER(" . $table . ') - ORDER BY c.colno - ) subq - JOIN syscat.columns cols - ON subq.tabschema = cols.tabschema - AND subq.tabname = cols.tabname - AND subq.colno = cols.colno - ORDER BY subq.colno - '; - } - - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTablesSQL() + public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string { - return "SELECT NAME FROM SYSIBM.SYSTABLES WHERE TYPE = '" . self::SYSIBM_SYSTABLES_TYPE_TABLE . "'" - . ' AND CREATOR = CURRENT_USER'; + throw NotSupported::new(__METHOD__); } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. - */ - public function getListViewsSQL($database) + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListViewsSQL(string $database): string { return 'SELECT NAME, TEXT FROM SYSIBM.SYSVIEWS'; } - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTableIndexesSQL($table, $database = null) - { - $table = $this->quoteStringLiteral($table); - - return "SELECT idx.INDNAME AS key_name, - idxcol.COLNAME AS column_name, - CASE - WHEN idx.UNIQUERULE = '" . self::SYSCAT_INDEXES_UNIQUERULE_IMPLEMENTS_PRIMARY_KEY . "' - THEN 1 - ELSE 0 - END AS primary, - CASE - WHEN idx.UNIQUERULE = '" . self::SYSCAT_INDEXES_UNIQUERULE_PERMITS_DUPLICATES . "' - THEN 1 - ELSE 0 - END AS non_unique - FROM SYSCAT.INDEXES AS idx - JOIN SYSCAT.INDEXCOLUSE AS idxcol - ON idx.INDSCHEMA = idxcol.INDSCHEMA AND idx.INDNAME = idxcol.INDNAME - WHERE idx.TABNAME = UPPER(" . $table . ') - ORDER BY idxcol.COLSEQ ASC'; - } - - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTableForeignKeysSQL($table) - { - $table = $this->quoteStringLiteral($table); - - return "SELECT fkcol.COLNAME AS local_column, - fk.REFTABNAME AS foreign_table, - pkcol.COLNAME AS foreign_column, - fk.CONSTNAME AS index_name, - CASE - WHEN fk.UPDATERULE = '" . self::SYSCAT_REFERENCES_UPDATERULE_RESTRICT . "' THEN 'RESTRICT' - ELSE NULL - END AS on_update, - CASE - WHEN fk.DELETERULE = '" . self::SYSCAT_REFERENCES_DELETERULE_CASCADE . "' THEN 'CASCADE' - WHEN fk.DELETERULE = '" . self::SYSCAT_REFERENCES_DELETERULE_SET_NULL . "' THEN 'SET NULL' - WHEN fk.DELETERULE = '" . self::SYSCAT_REFERENCES_DELETERULE_RESTRICT . "' THEN 'RESTRICT' - ELSE NULL - END AS on_delete - FROM SYSCAT.REFERENCES AS fk - JOIN SYSCAT.KEYCOLUSE AS fkcol - ON fk.CONSTNAME = fkcol.CONSTNAME - AND fk.TABSCHEMA = fkcol.TABSCHEMA - AND fk.TABNAME = fkcol.TABNAME - JOIN SYSCAT.KEYCOLUSE AS pkcol - ON fk.REFKEYNAME = pkcol.CONSTNAME - AND fk.REFTABSCHEMA = pkcol.TABSCHEMA - AND fk.REFTABNAME = pkcol.TABNAME - WHERE fk.TABNAME = UPPER(" . $table . ') - ORDER BY fkcol.COLSEQ ASC'; - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function supportsCreateDropDatabase() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5513', - '%s() is deprecated.', - __METHOD__, - ); - - return false; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function supportsCommentOnStatement() + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsCommentOnStatement(): bool { return true; } - /** - * {@inheritDoc} - */ - public function getCurrentDateSQL() + public function getCurrentDateSQL(): string { return 'CURRENT DATE'; } - /** - * {@inheritDoc} - */ - public function getCurrentTimeSQL() + public function getCurrentTimeSQL(): string { return 'CURRENT TIME'; } - /** - * {@inheritDoc} - */ - public function getCurrentTimestampSQL() + public function getCurrentTimestampSQL(): string { return 'CURRENT TIMESTAMP'; } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function getIndexDeclarationSQL($name, Index $index) + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getIndexDeclarationSQL(Index $index): string { // Index declaration in statements like CREATE TABLE is not supported. - throw Exception::notSupported(__METHOD__); + throw NotSupported::new(__METHOD__); } /** * {@inheritDoc} */ - protected function _getCreateTableSQL($name, array $columns, array $options = []) + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array { + $this->validateCreateTableOptions($options, __METHOD__); + $indexes = []; if (isset($options['indexes'])) { $indexes = $options['indexes']; @@ -582,20 +275,15 @@ protected function _getCreateTableSQL($name, array $columns, array $options = [] /** * {@inheritDoc} */ - public function getAlterTableSQL(TableDiff $diff) + public function getAlterTableSQL(TableDiff $diff): array { $sql = []; - $columnSql = []; $commentsSQL = []; - $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); + $tableNameSQL = $diff->getOldTable()->getQuotedName($this); $queryParts = []; foreach ($diff->getAddedColumns() as $column) { - if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { - continue; - } - $columnDef = $column->toArray(); $queryPart = 'ADD COLUMN ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef); @@ -610,9 +298,9 @@ public function getAlterTableSQL(TableDiff $diff) $queryParts[] = $queryPart; - $comment = $this->getColumnComment($column); + $comment = $column->getComment(); - if ($comment === null || $comment === '') { + if ($comment === '') { continue; } @@ -623,24 +311,19 @@ public function getAlterTableSQL(TableDiff $diff) ); } + $needsReorg = false; foreach ($diff->getDroppedColumns() as $column) { - if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { - continue; - } - $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); + $needsReorg = true; } - foreach ($diff->getModifiedColumns() as $columnDiff) { - if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { - continue; - } - + foreach ($diff->getChangedColumns() as $columnDiff) { if ($columnDiff->hasCommentChanged()) { + $newColumn = $columnDiff->getNewColumn(); $commentsSQL[] = $this->getCommentOnColumnSQL( $tableNameSQL, - $columnDiff->getNewColumn()->getQuotedName($this), - $this->getColumnComment($columnDiff->getNewColumn()), + $newColumn->getQuotedName($this), + $newColumn->getComment(), ); } @@ -649,88 +332,50 @@ public function getAlterTableSQL(TableDiff $diff) $columnDiff, $sql, $queryParts, + $needsReorg, ); } - foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { - if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { - continue; - } - - $oldColumnName = new Identifier($oldColumnName); - - $queryParts[] = 'RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . - ' TO ' . $column->getQuotedName($this); + if (count($queryParts) > 0) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . implode(' ', $queryParts); } - $tableSql = []; - - if (! $this->onSchemaAlterTable($diff, $tableSql)) { - if (count($queryParts) > 0) { - $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . implode(' ', $queryParts); - } - - // Some table alteration operations require a table reorganization. - if (count($diff->getDroppedColumns()) > 0 || count($diff->getModifiedColumns()) > 0) { - $sql[] = "CALL SYSPROC.ADMIN_CMD ('REORG TABLE " . $tableNameSQL . "')"; - } - - $sql = array_merge($sql, $commentsSQL); - - $newName = $diff->getNewName(); - - if ($newName !== false) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5663', - 'Generation of "rename table" SQL using %s() is deprecated. Use getRenameTableSQL() instead.', - __METHOD__, - ); - - $sql[] = sprintf( - 'RENAME TABLE %s TO %s', - $tableNameSQL, - $newName->getQuotedName($this), - ); - } - - $sql = array_merge( - $this->getPreAlterTableIndexForeignKeySQL($diff), - $sql, - $this->getPostAlterTableIndexForeignKeySQL($diff), - ); + // Some table alteration operations require a table reorganization. + if ($needsReorg) { + $sql[] = "CALL SYSPROC.ADMIN_CMD ('REORG TABLE " . $tableNameSQL . "')"; } - return array_merge($sql, $tableSql, $columnSql); + return array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $commentsSQL, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); } - /** - * {@inheritDoc} - */ - public function getRenameTableSQL(string $oldName, string $newName): array + public function getRenameTableSQL(string $oldName, string $newName): string { - return [ - sprintf('RENAME TABLE %s TO %s', $oldName, $newName), - ]; + return sprintf('RENAME TABLE %s TO %s', $oldName, $newName); } /** * Gathers the table alteration SQL for a given column diff. * - * @param string $table The table to gather the SQL for. - * @param ColumnDiff $columnDiff The column diff to evaluate. - * @param string[] $sql The sequence of table alteration statements to fill. - * @param mixed[] $queryParts The sequence of column alteration clauses to fill. + * @param string $table The table to gather the SQL for. + * @param ColumnDiff $columnDiff The column diff to evaluate. + * @param list $sql The sequence of table alteration statements to fill. + * @param list $queryParts The sequence of column alteration clauses to fill. */ private function gatherAlterColumnSQL( string $table, ColumnDiff $columnDiff, array &$sql, - array &$queryParts + array &$queryParts, + bool &$needsReorg, ): void { - $alterColumnClauses = $this->getAlterColumnClausesSQL($columnDiff); + $alterColumnClauses = $this->getAlterColumnClausesSQL($columnDiff, $needsReorg); - if (empty($alterColumnClauses)) { + if (count($alterColumnClauses) < 1) { return; } @@ -754,18 +399,28 @@ private function gatherAlterColumnSQL( * * @return string[] */ - private function getAlterColumnClausesSQL(ColumnDiff $columnDiff): array + private function getAlterColumnClausesSQL(ColumnDiff $columnDiff, bool &$needsReorg): array { - $newColumn = $columnDiff->getNewColumn()->toArray(); + $newColumn = $columnDiff->getNewColumn(); + $columnArray = $newColumn->toArray(); - $alterClause = 'ALTER COLUMN ' . $columnDiff->getNewColumn()->getQuotedName($this); + $newName = $columnDiff->getNewColumn()->getQuotedName($this); + $oldName = $columnDiff->getOldColumn()->getQuotedName($this); - if ($newColumn['columnDefinition'] !== null) { - return [$alterClause . ' ' . $newColumn['columnDefinition']]; + $alterClause = 'ALTER COLUMN ' . $newName; + + if ($newColumn->getColumnDefinition() !== null) { + $needsReorg = true; + + return [$alterClause . ' ' . $newColumn->getColumnDefinition()]; } $clauses = []; + if ($columnDiff->hasNameChanged()) { + $clauses[] = 'RENAME COLUMN ' . $oldName . ' TO ' . $newName; + } + if ( $columnDiff->hasTypeChanged() || $columnDiff->hasLengthChanged() || @@ -773,22 +428,27 @@ private function getAlterColumnClausesSQL(ColumnDiff $columnDiff): array $columnDiff->hasScaleChanged() || $columnDiff->hasFixedChanged() ) { - $clauses[] = $alterClause . ' SET DATA TYPE ' . $newColumn['type']->getSQLDeclaration($newColumn, $this); + $needsReorg = true; + $clauses[] = $alterClause . ' SET DATA TYPE ' . $newColumn->getType() + ->getSQLDeclaration($columnArray, $this); } if ($columnDiff->hasNotNullChanged()) { - $clauses[] = $newColumn['notnull'] ? $alterClause . ' SET NOT NULL' : $alterClause . ' DROP NOT NULL'; + $needsReorg = true; + $clauses[] = $newColumn->getNotnull() ? $alterClause . ' SET NOT NULL' : $alterClause . ' DROP NOT NULL'; } if ($columnDiff->hasDefaultChanged()) { - if (isset($newColumn['default'])) { - $defaultClause = $this->getDefaultValueDeclarationSQL($newColumn); + if ($newColumn->getDefault() !== null) { + $defaultClause = $this->getDefaultValueDeclarationSQL($columnArray); if ($defaultClause !== '') { - $clauses[] = $alterClause . ' SET' . $defaultClause; + $needsReorg = true; + $clauses[] = $alterClause . ' SET' . $defaultClause; } } else { - $clauses[] = $alterClause . ' DROP DEFAULT'; + $needsReorg = true; + $clauses[] = $alterClause . ' DROP DEFAULT'; } } @@ -798,44 +458,9 @@ private function getAlterColumnClausesSQL(ColumnDiff $columnDiff): array /** * {@inheritDoc} */ - protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) + protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array { - $sql = []; - - $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); - - foreach ($diff->getDroppedIndexes() as $droppedIndex) { - foreach ($diff->getAddedIndexes() as $addedIndex) { - if ($droppedIndex->getColumns() !== $addedIndex->getColumns()) { - continue; - } - - if ($droppedIndex->isPrimary()) { - $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' DROP PRIMARY KEY'; - } elseif ($droppedIndex->isUnique()) { - $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' DROP UNIQUE ' . $droppedIndex->getQuotedName($this); - } else { - $sql[] = $this->getDropIndexSQL($droppedIndex, $tableNameSQL); - } - - $sql[] = $this->getCreateIndexSQL($addedIndex, $tableNameSQL); - - $diff->unsetAddedIndex($addedIndex); - $diff->unsetDroppedIndex($droppedIndex); - - break; - } - } - - return array_merge($sql, parent::getPreAlterTableIndexForeignKeySQL($diff)); - } - - /** - * {@inheritDoc} - */ - protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) - { - if (strpos($tableName, '.') !== false) { + if (str_contains($tableName, '.')) { [$schema] = explode('.', $tableName); $oldIndexName = $schema . '.' . $oldIndexName; } @@ -848,14 +473,20 @@ protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ - public function getDefaultValueDeclarationSQL($column) + public function getDefaultValueDeclarationSQL(array $column): string { - if (! empty($column['autoincrement'])) { + if (isset($column['autoincrement']) && $column['autoincrement'] === true) { return ''; } - if (! empty($column['version'])) { - if ((string) $column['type'] !== 'DateTime') { + if (isset($column['version']) && $column['version'] === true) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6940', + 'The "version" column platform option is deprecated.', + ); + + if ($column['type'] instanceof DateTimeType) { $column['default'] = '1'; } } @@ -863,87 +494,55 @@ public function getDefaultValueDeclarationSQL($column) return parent::getDefaultValueDeclarationSQL($column); } - /** - * {@inheritDoc} - */ - public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName) + public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string { return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)'; } - /** - * {@inheritDoc} - */ - public function getCreateTemporaryTableSnippetSQL() + public function getCreateTemporaryTableSnippetSQL(): string { return 'DECLARE GLOBAL TEMPORARY TABLE'; } - /** - * {@inheritDoc} - */ - public function getTemporaryTableName($tableName) + public function getTemporaryTableName(string $tableName): string { return 'SESSION.' . $tableName; } - /** - * {@inheritDoc} - */ - protected function doModifyLimitQuery($query, $limit, $offset) + protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string { - $where = []; - if ($offset > 0) { - $where[] = sprintf('db22.DC_ROWNUM >= %d', $offset + 1); + $query .= sprintf(' OFFSET %d ROWS', $offset); } if ($limit !== null) { - $where[] = sprintf('db22.DC_ROWNUM <= %d', $offset + $limit); - } - - if (empty($where)) { - return $query; + $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit); } - // Todo OVER() needs ORDER BY data! - return sprintf( - 'SELECT db22.* FROM (SELECT db21.*, ROW_NUMBER() OVER() AS DC_ROWNUM FROM (%s) db21) db22 WHERE %s', - $query, - implode(' AND ', $where), - ); + return $query; } - /** - * {@inheritDoc} - */ - public function getLocateExpression($str, $substr, $startPos = false) + public function getLocateExpression(string $string, string $substring, ?string $start = null): string { - if ($startPos === false) { - return 'LOCATE(' . $substr . ', ' . $str . ')'; + if ($start === null) { + return sprintf('LOCATE(%s, %s)', $substring, $string); } - return 'LOCATE(' . $substr . ', ' . $str . ', ' . $startPos . ')'; + return sprintf('LOCATE(%s, %s, %s)', $substring, $string, $start); } - /** - * {@inheritDoc} - */ - public function getSubstringExpression($string, $start, $length = null) + public function getSubstringExpression(string $string, string $start, ?string $length = null): string { if ($length === null) { - return 'SUBSTR(' . $string . ', ' . $start . ')'; + return sprintf('SUBSTR(%s, %s)', $string, $start); } - return 'SUBSTR(' . $string . ', ' . $start . ', ' . $length . ')'; + return sprintf('SUBSTR(%s, %s, %s)', $string, $start, $length); } - /** - * {@inheritDoc} - */ - public function getLengthExpression($column) + public function getLengthExpression(string $string): string { - return 'LENGTH(' . $column . ', CODEUNITS32)'; + return 'LENGTH(' . $string . ', CODEUNITS32)'; } public function getCurrentDatabaseExpression(): string @@ -951,53 +550,18 @@ public function getCurrentDatabaseExpression(): string return 'CURRENT_USER'; } - /** - * {@inheritDoc} - */ - public function supportsIdentityColumns() + public function supportsIdentityColumns(): bool { return true; } - /** - * {@inheritDoc} - * - * @deprecated - */ - public function prefersIdentityColumns() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/1519', - '%s() is deprecated.', - __METHOD__, - ); - - return true; - } - public function createSelectSQLBuilder(): SelectSQLBuilder { return new DefaultSelectSQLBuilder($this, 'WITH RR USE AND KEEP UPDATE LOCKS', null); } - /** - * {@inheritDoc} - * - * @deprecated This API is not portable. - */ - public function getForUpdateSQL() + public function getDummySelectSQL(string $expression = '1'): string { - return ' WITH RR USE AND KEEP UPDATE LOCKS'; - } - - /** - * {@inheritDoc} - */ - public function getDummySelectSQL() - { - $expression = func_num_args() > 0 ? func_get_arg(0) : '1'; - return sprintf('SELECT %s FROM sysibm.sysdummy1', $expression); } @@ -1008,42 +572,27 @@ public function getDummySelectSQL() * * TODO: We have to investigate how to get Db2 up and running with savepoints. */ - public function supportsSavepoints() + public function supportsSavepoints(): bool { return false; } - /** - * {@inheritDoc} - * - * @deprecated Implement {@see createReservedKeywordsList()} instead. - */ - protected function getReservedKeywordsClass() + /** @deprecated */ + protected function createReservedKeywordsList(): KeywordList { Deprecation::triggerIfCalledFromOutside( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4510', - '%s() is deprecated,' - . ' use %s::createReservedKeywordsList() instead.', + 'https://github.com/doctrine/dbal/pull/6607', + '%s is deprecated.', __METHOD__, - static::class, ); - return Keywords\DB2Keywords::class; + return new DB2Keywords(); } - /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */ - public function getListTableCommentsSQL(string $table): string + public function createMetadataProvider(Connection $connection): Db2MetadataProvider { - return sprintf( - <<<'SQL' -SELECT REMARKS - FROM SYSIBM.SYSTABLES - WHERE NAME = UPPER( %s ) -SQL - , - $this->quoteStringLiteral($table), - ); + return new Db2MetadataProvider($connection, $this); } public function createSchemaManager(Connection $connection): DB2SchemaManager diff --git a/doctrine/dbal/src/Platforms/DateIntervalUnit.php b/doctrine/dbal/src/Platforms/DateIntervalUnit.php index a95c4e28b..ba783f372 100644 --- a/doctrine/dbal/src/Platforms/DateIntervalUnit.php +++ b/doctrine/dbal/src/Platforms/DateIntervalUnit.php @@ -4,26 +4,14 @@ namespace Doctrine\DBAL\Platforms; -final class DateIntervalUnit +enum DateIntervalUnit: string { - public const SECOND = 'SECOND'; - - public const MINUTE = 'MINUTE'; - - public const HOUR = 'HOUR'; - - public const DAY = 'DAY'; - - public const WEEK = 'WEEK'; - - public const MONTH = 'MONTH'; - - public const QUARTER = 'QUARTER'; - - public const YEAR = 'YEAR'; - - /** @codeCoverageIgnore */ - private function __construct() - { - } + case SECOND = 'SECOND'; + case MINUTE = 'MINUTE'; + case HOUR = 'HOUR'; + case DAY = 'DAY'; + case WEEK = 'WEEK'; + case MONTH = 'MONTH'; + case QUARTER = 'QUARTER'; + case YEAR = 'YEAR'; } diff --git a/doctrine/dbal/src/Platforms/Db2/Db2MetadataProvider.php b/doctrine/dbal/src/Platforms/Db2/Db2MetadataProvider.php new file mode 100644 index 000000000..39a3779c1 --- /dev/null +++ b/doctrine/dbal/src/Platforms/Db2/Db2MetadataProvider.php @@ -0,0 +1,518 @@ + ReferentialAction::NO_ACTION, + 'C' => ReferentialAction::CASCADE, + '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 DB2Platform $platform) + { + } + + /** {@inheritDoc} */ + public function getAllDatabaseNames(): iterable + { + throw NotSupported::new(__METHOD__); + } + + /** {@inheritDoc} */ + public function getAllSchemaNames(): iterable + { + throw NotSupported::new(__METHOD__); + } + + /** + * {@inheritDoc} + * + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=views-syscattables + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=registers-current-user + */ + public function getAllTableNames(): iterable + { + $sql = <<<'SQL' + SELECT TABNAME + FROM SYSCAT.TABLES + WHERE TABSCHEMA = CURRENT USER + AND TYPE = 'T' + ORDER BY TABNAME + SQL; + + foreach ($this->connection->iterateNumeric($sql) as $row) { + yield new TableMetadataRow(null, $row[0], []); + } + } + + /** {@inheritDoc} */ + public function getTableColumnsForAllTables(): iterable + { + return $this->getTableColumns(null); + } + + /** {@inheritDoc} */ + public function getTableColumnsForTable(?string $schemaName, string $tableName): iterable + { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getTableColumns($tableName); + } + + /** + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=views-syscatcolumns + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=views-syscattables + * + * @return iterable + * + * @throws Exception + */ + private function getTableColumns(?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT C.TABNAME, + C.COLNAME, + C.TYPENAME, + C.CODEPAGE, + C.NULLS, + C.LENGTH, + C.SCALE, + C.REMARKS, + C.GENERATED, + C.DEFAULT + FROM SYSCAT.COLUMNS C + JOIN SYSCAT.TABLES AS T + ON T.TABSCHEMA = C.TABSCHEMA + AND T.TABNAME = C.TABNAME + WHERE %s + AND T.TYPE = 'T' + ORDER BY C.TABNAME, + C.COLNO + SQL, + $this->buildTableQueryPredicate('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 + { + [ + $tableName, + $columnName, + $typeName, + $codePage, + $nulls, + $length, + $scale, + $remarks, + $generated, + $default, + ] = $row; + + $editor = Column::editor() + ->setQuotedName($columnName); + + $type = $this->platform->getDoctrineTypeMapping($typeName); + + switch (strtolower($typeName)) { + case 'varchar': + if ($codePage === 0) { + $type = Types::BINARY; + } + + $editor->setLength($length); + break; + + case 'character': + if ($codePage === 0) { + $type = Types::BINARY; + } + + $editor + ->setLength($length) + ->setFixed(true); + break; + + case 'clob': + $editor->setLength($length); + break; + + case 'decimal': + case 'double': + case 'real': + $editor + ->setPrecision($length) + ->setScale($scale); + break; + } + + $editor + ->setTypeName($type) + ->setNotNull($nulls === 'N') + ->setDefaultValue($this->parseDefaultExpression($default)) + ->setAutoincrement($generated === 'D'); + + if ($remarks !== null) { + $editor->setComment($remarks); + } + + return new TableColumnMetadataRow(null, $tableName, $editor->create()); + } + + private function parseDefaultExpression(?string $expression): ?string + { + if ($expression === null || $expression === 'NULL') { + return null; + } + + if (preg_match('/^\'(.*)\'$/s', $expression, $matches) === 1) { + return str_replace("''", "'", $matches[1]); + } + + return $expression; + } + + /** {@inheritDoc} */ + public function getIndexColumnsForAllTables(): iterable + { + return $this->getIndexColumns(null); + } + + /** {@inheritDoc} */ + public function getIndexColumnsForTable(?string $schemaName, string $tableName): iterable + { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getIndexColumns($tableName); + } + + /** + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=views-syscatindexcoluse + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=views-syscatindexes + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=views-syscattables + * + * @return iterable + * + * @throws Exception + */ + private function getIndexColumns(?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT I.TABNAME, + I.INDNAME, + I.UNIQUERULE, + ICU.COLNAME + FROM SYSCAT.INDEXES AS I + JOIN SYSCAT.TABLES AS T + ON I.TABSCHEMA = T.TABSCHEMA + AND I.TABNAME = T.TABNAME + JOIN SYSCAT.INDEXCOLUSE AS ICU + ON I.INDSCHEMA = ICU.INDSCHEMA + AND I.INDNAME = ICU.INDNAME + WHERE %s + AND T.TYPE = 'T' + AND I.UNIQUERULE != 'P' + ORDER BY I.TABNAME, + I.INDNAME, + ICU.COLSEQ + SQL, + $this->buildTableQueryPredicate('I', $tableName, $params), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new IndexColumnMetadataRow( + schemaName: null, + tableName: $row[0], + indexName: $row[1], + type: $row[2] === 'U' ? IndexType::UNIQUE : IndexType::REGULAR, + isClustered: false, + predicate: null, + columnName: $row[3], + columnLength: null, + ); + } + } + + /** {@inheritDoc} */ + public function getPrimaryKeyConstraintColumnsForAllTables(): iterable + { + return $this->getPrimaryKeyConstraintColumns(null); + } + + /** {@inheritDoc} */ + public function getPrimaryKeyConstraintColumnsForTable(?string $schemaName, string $tableName): iterable + { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getPrimaryKeyConstraintColumns($tableName); + } + + /** + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=views-syscatcoluse + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=views-syscattabconst + * + * @return iterable + * + * @throws Exception + */ + private function getPrimaryKeyConstraintColumns(?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT TC.TABNAME, + TC.CONSTNAME, + KCU.COLNAME + FROM SYSCAT.TABCONST TC + JOIN SYSCAT.KEYCOLUSE KCU + ON KCU.TABSCHEMA = TC.TABSCHEMA + AND KCU.TABNAME = TC.TABNAME + AND KCU.CONSTNAME = TC.CONSTNAME + WHERE %s + AND TC.TYPE = 'P' + ORDER BY TC.TABNAME, + TC.CONSTNAME, + KCU.COLSEQ + SQL, + $this->buildTableQueryPredicate('TC', $tableName, $params), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new PrimaryKeyConstraintColumnRow( + schemaName: null, + tableName: $row[0], + constraintName: $row[1], + isClustered: true, + columnName: $row[2], + ); + } + } + + /** {@inheritDoc} */ + public function getForeignKeyConstraintColumnsForAllTables(): iterable + { + return $this->getForeignKeyConstraintColumns(null); + } + + /** {@inheritDoc} */ + public function getForeignKeyConstraintColumnsForTable(?string $schemaName, string $tableName): iterable + { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getForeignKeyConstraintColumns($tableName); + } + + /** + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=views-syscatkeycoluse + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=views-syscatreferences + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=views-syscattables + * + * @return iterable + * + * @throws Exception + */ + private function getForeignKeyConstraintColumns(?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT R.TABNAME, + R.CONSTNAME, + R.REFTABNAME, + R.UPDATERULE, + R.DELETERULE, + PKCU.COLNAME, + FKCU.COLNAME + FROM SYSCAT.REFERENCES AS R + JOIN SYSCAT.TABLES AS T + ON T.TABSCHEMA = R.TABSCHEMA + AND T.TABNAME = R.TABNAME + JOIN SYSCAT.KEYCOLUSE AS PKCU + ON PKCU.CONSTNAME = R.CONSTNAME + AND PKCU.TABSCHEMA = R.TABSCHEMA + AND PKCU.TABNAME = R.TABNAME + JOIN SYSCAT.KEYCOLUSE AS FKCU + ON FKCU.CONSTNAME = R.REFKEYNAME + AND FKCU.TABSCHEMA = R.REFTABSCHEMA + AND FKCU.TABNAME = R.REFTABNAME + AND FKCU.COLSEQ = PKCU.COLSEQ + WHERE %s + AND T.TYPE = 'T' + ORDER BY R.TABNAME, + R.CONSTNAME, + PKCU.COLSEQ + SQL, + $this->buildTableQueryPredicate('R', $tableName, $params), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new ForeignKeyConstraintColumnMetadataRow( + referencingSchemaName: null, + referencingTableName: $row[0], + id: null, + name: $row[1], + referencedSchemaName: null, + referencedTableName: $row[2], + matchType: MatchType::SIMPLE, + onUpdateAction: self::REFERENTIAL_ACTIONS[$row[3]], + onDeleteAction: self::REFERENTIAL_ACTIONS[$row[4]], + isDeferrable: false, + isDeferred: false, + referencingColumnName: $row[5], + referencedColumnName: $row[6], + ); + } + } + + /** {@inheritDoc} */ + public function getTableOptionsForAllTables(): iterable + { + return $this->getTableOptions(null); + } + + /** {@inheritDoc} */ + public function getTableOptionsForTable(?string $schemaName, string $tableName): iterable + { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getTableOptions($tableName); + } + + /** + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=views-syscattables + * + * @return iterable + * + * @throws Exception + */ + private function getTableOptions(?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT TABNAME, + REMARKS + FROM SYSCAT.TABLES T + WHERE %s + AND TYPE = 'T' + ORDER BY TABNAME + SQL, + $this->buildTableQueryPredicate(null, $tableName, $params), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new TableMetadataRow(null, $row[0], [ + 'comment' => $row[1], + ]); + } + } + + /** + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=registers-current-user + * + * @param ?non-empty-string $relation + * @param list $params + * + * @return non-empty-string + */ + private function buildTableQueryPredicate(?string $relation, ?string $tableName, array &$params): string + { + $qualifier = $relation !== null ? $relation . '.' : ''; + + $conditions = [$qualifier . 'TABSCHEMA = CURRENT USER']; + + if ($tableName !== null) { + $conditions[] = $qualifier . 'TABNAME = ?'; + $params[] = $tableName; + } + + return implode(' AND ', $conditions); + } + + /** + * {@inheritDoc} + * + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=views-syscatviews + * @link https://www.ibm.com/docs/en/db2/12.1.0?topic=registers-current-user + */ + public function getAllViews(): iterable + { + $sql = <<<'SQL' + SELECT VIEWNAME, + TEXT + FROM SYSCAT.VIEWS + WHERE VIEWSCHEMA = CURRENT USER + ORDER BY VIEWNAME + SQL; + + foreach ($this->connection->iterateNumeric($sql) as $row) { + yield new ViewMetadataRow(null, ...$row); + } + } + + /** {@inheritDoc} */ + public function getAllSequences(): iterable + { + throw NotSupported::new(__METHOD__); + } +} diff --git a/doctrine/dbal/src/Platforms/Exception/InvalidPlatformVersion.php b/doctrine/dbal/src/Platforms/Exception/InvalidPlatformVersion.php new file mode 100644 index 000000000..ac7c43f4d --- /dev/null +++ b/doctrine/dbal/src/Platforms/Exception/InvalidPlatformVersion.php @@ -0,0 +1,27 @@ +keywords === null) { $this->initializeKeywords(); @@ -32,8 +42,7 @@ public function isKeyword($word) return isset($this->keywords[strtoupper($word)]); } - /** @return void */ - protected function initializeKeywords() + protected function initializeKeywords(): void { $this->keywords = array_flip(array_map('strtoupper', $this->getKeywords())); } @@ -43,14 +52,5 @@ protected function initializeKeywords() * * @return string[] */ - abstract protected function getKeywords(); - - /** - * Returns the name of this keyword list. - * - * @deprecated - * - * @return string - */ - abstract public function getName(); + abstract protected function getKeywords(): array; } diff --git a/doctrine/dbal/src/Platforms/Keywords/MariaDB117Keywords.php b/doctrine/dbal/src/Platforms/Keywords/MariaDB117Keywords.php new file mode 100644 index 000000000..c714a341d --- /dev/null +++ b/doctrine/dbal/src/Platforms/Keywords/MariaDB117Keywords.php @@ -0,0 +1,26 @@ +keywordLists = $keywordLists; - } - - /** @return string[] */ - public function getViolations() - { - return $this->violations; - } - - /** - * @param string $word - * - * @return string[] - */ - private function isReservedWord($word): array - { - if ($word[0] === '`') { - $word = str_replace('`', '', $word); - } - - $keywordLists = []; - foreach ($this->keywordLists as $keywordList) { - if (! $keywordList->isKeyword($word)) { - continue; - } - - $keywordLists[] = $keywordList->getName(); - } - - return $keywordLists; - } - - /** - * @param string $asset - * @param string[] $violatedPlatforms - */ - private function addViolation($asset, $violatedPlatforms): void - { - if (count($violatedPlatforms) === 0) { - return; - } - - $this->violations[] = $asset . ' keyword violations: ' . implode(', ', $violatedPlatforms); - } - - /** - * {@inheritDoc} - */ - public function acceptColumn(Table $table, Column $column) - { - $this->addViolation( - 'Table ' . $table->getName() . ' column ' . $column->getName(), - $this->isReservedWord($column->getName()), - ); - } - - /** - * {@inheritDoc} - */ - public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) - { - } - - /** - * {@inheritDoc} - */ - public function acceptIndex(Table $table, Index $index) - { - } - - /** - * {@inheritDoc} - */ - public function acceptSchema(Schema $schema) - { - } - - /** - * {@inheritDoc} - */ - public function acceptSequence(Sequence $sequence) - { - } - - /** - * {@inheritDoc} - */ - public function acceptTable(Table $table) - { - $this->addViolation( - 'Table ' . $table->getName(), - $this->isReservedWord($table->getName()), - ); - } -} diff --git a/doctrine/dbal/src/Platforms/Keywords/SQLServer2012Keywords.php b/doctrine/dbal/src/Platforms/Keywords/SQLServer2012Keywords.php deleted file mode 100644 index ebc45c402..000000000 --- a/doctrine/dbal/src/Platforms/Keywords/SQLServer2012Keywords.php +++ /dev/null @@ -1,12 +0,0 @@ -getQuotedName($this)]; } diff --git a/doctrine/dbal/src/Platforms/MariaDb1060Platform.php b/doctrine/dbal/src/Platforms/MariaDB1060Platform.php similarity index 66% rename from doctrine/dbal/src/Platforms/MariaDb1060Platform.php rename to doctrine/dbal/src/Platforms/MariaDB1060Platform.php index b3ce4eb97..15eb80380 100644 --- a/doctrine/dbal/src/Platforms/MariaDb1060Platform.php +++ b/doctrine/dbal/src/Platforms/MariaDB1060Platform.php @@ -1,13 +1,17 @@ quoteStringLiteral($databaseName); + + // The check for `CONSTRAINT_SCHEMA = $databaseName` is mandatory here to prevent performance issues + return <<getOldTable()->getQuotedName($this); + + $modifiedForeignKeys = $diff->getModifiedForeignKeys(); + + foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) { + if (! in_array($foreignKey, $modifiedForeignKeys, true)) { + $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableName); + } + } + + return $sql; } - /** @deprecated Implement {@see createReservedKeywordsList()} instead. */ - protected function getReservedKeywordsClass(): string + /** + * {@inheritDoc} + */ + protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff): array { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4510', - 'MariaDb1027Platform::getReservedKeywordsClass() is deprecated,' - . ' use MariaDb1027Platform::createReservedKeywordsList() instead.', + return array_merge( + parent::getPostAlterTableIndexForeignKeySQL($diff), + $this->getPostAlterTableRenameIndexForeignKeySQL($diff), ); + } - return Keywords\MariaDb102Keywords::class; + /** @return list */ + private function getPostAlterTableRenameIndexForeignKeySQL(TableDiff $diff): array + { + $sql = []; + + $tableName = $diff->getOldTable()->getQuotedName($this); + + $modifiedForeignKeys = $diff->getModifiedForeignKeys(); + + foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) { + if (! in_array($foreignKey, $modifiedForeignKeys, true)) { + $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName); + } + } + + return $sql; } - protected function initializeDoctrineTypeMappings(): void + /** + * Returns the remaining foreign key constraints that require one of the renamed indexes. + * + * "Remaining" here refers to the diff between the foreign keys currently defined in the associated + * table and the foreign keys to be removed. + * + * @param TableDiff $diff The table diff to evaluate. + * + * @return ForeignKeyConstraint[] + */ + private function getRemainingForeignKeyConstraintsRequiringRenamedIndexes(TableDiff $diff): array { - parent::initializeDoctrineTypeMappings(); + $renamedIndexes = $diff->getRenamedIndexes(); + + if (count($renamedIndexes) === 0) { + return []; + } + + $foreignKeys = []; + + $remainingForeignKeys = array_diff_key( + $diff->getOldTable()->getForeignKeys(), + $diff->getDroppedForeignKeys(), + ); + + foreach ($remainingForeignKeys as $foreignKey) { + foreach ($renamedIndexes as $index) { + if ($foreignKey->intersectsIndexColumns($index)) { + $foreignKeys[] = $foreignKey; + + break; + } + } + } + + return $foreignKeys; + } + + /** {@inheritDoc} */ + public function getColumnDeclarationSQL(string $name, array $column): string + { + // MariaDb forces column collation to utf8mb4_bin where the column was declared as JSON so ignore + // collation and character set for json columns as attempting to set them can cause an error. + if ($this->getJsonTypeDeclarationSQL([]) === 'JSON' && ($column['type'] ?? null) instanceof JsonType) { + unset($column['collation']); + unset($column['charset']); + } + + return parent::getColumnDeclarationSQL($name, $column); + } + + /** @deprecated */ + protected function createReservedKeywordsList(): KeywordList + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6607', + '%s is deprecated.', + __METHOD__, + ); - $this->doctrineTypeMapping['json'] = Types::JSON; + return new MariaDBKeywords(); } } diff --git a/doctrine/dbal/src/Platforms/MariaDb1027Platform.php b/doctrine/dbal/src/Platforms/MariaDb1027Platform.php deleted file mode 100644 index 8e555c83b..000000000 --- a/doctrine/dbal/src/Platforms/MariaDb1027Platform.php +++ /dev/null @@ -1,13 +0,0 @@ -getColumnTypeSQLSnippets('c', $database); - - return sprintf( - <<getDatabaseNameSQL($database), - $this->quoteStringLiteral($table), - ); - } - - /** - * Generate SQL snippets to reverse the aliasing of JSON to LONGTEXT. - * - * MariaDb aliases columns specified as JSON to LONGTEXT and sets a CHECK constraint to ensure the column - * is valid json. This function generates the SQL snippets which reverse this aliasing i.e. report a column - * as JSON where it was originally specified as such instead of LONGTEXT. - * - * The CHECK constraints are stored in information_schema.CHECK_CONSTRAINTS so query that table. - */ - public function getColumnTypeSQLSnippet(string $tableAlias = 'c', ?string $databaseName = null): string - { - if ($this->getJsonTypeDeclarationSQL([]) !== 'JSON') { - return parent::getColumnTypeSQLSnippet($tableAlias, $databaseName); - } - - if ($databaseName === null) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6215', - 'Not passing a database name to methods "getColumnTypeSQLSnippet()", ' - . '"getColumnTypeSQLSnippets()", and "getListTableColumnsSQL()" of "%s" is deprecated.', - self::class, - ); - } - - $subQueryAlias = 'i_' . $tableAlias; - - $databaseName = $this->getDatabaseNameSQL($databaseName); - - // The check for `CONSTRAINT_SCHEMA = $databaseName` is mandatory here to prevent performance issues - return <<getJsonTypeDeclarationSQL([]) === 'JSON' && ($column['type'] ?? null) instanceof JsonType) { - unset($column['collation']); - unset($column['charset']); - } - - return parent::getColumnDeclarationSQL($name, $column); - } -} diff --git a/doctrine/dbal/src/Platforms/MariaDb110700Platform.php b/doctrine/dbal/src/Platforms/MariaDb110700Platform.php deleted file mode 100644 index 6afd00b71..000000000 --- a/doctrine/dbal/src/Platforms/MariaDb110700Platform.php +++ /dev/null @@ -1,26 +0,0 @@ - */ + private array $cache = []; + + public function __construct(private readonly CharsetMetadataProvider $charsetMetadataProvider) + { + } + + public function getDefaultCharsetCollation(string $charset): ?string + { + if (array_key_exists($charset, $this->cache)) { + return $this->cache[$charset]; + } + + return $this->cache[$charset] = $this->charsetMetadataProvider->getDefaultCharsetCollation($charset); + } +} diff --git a/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php b/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php new file mode 100644 index 000000000..65b63df8f --- /dev/null +++ b/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php @@ -0,0 +1,37 @@ +connection->fetchOne( + <<<'SQL' + SELECT DEFAULT_COLLATE_NAME + FROM information_schema.CHARACTER_SETS + WHERE CHARACTER_SET_NAME = ?; + SQL + , + [$charset], + ); + + if ($collation !== false) { + return $collation; + } + + return null; + } +} diff --git a/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php b/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php index d52ca74a2..028edf9db 100644 --- a/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php +++ b/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php @@ -7,5 +7,10 @@ /** @internal */ interface CollationMetadataProvider { + /** + * @param non-empty-string $collation + * + * @return ?non-empty-string + */ public function getCollationCharset(string $collation): ?string; } diff --git a/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/CachingCollationMetadataProvider.php b/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/CachingCollationMetadataProvider.php index 513c08b6e..4b23894e3 100644 --- a/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/CachingCollationMetadataProvider.php +++ b/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/CachingCollationMetadataProvider.php @@ -11,15 +11,11 @@ /** @internal */ final class CachingCollationMetadataProvider implements CollationMetadataProvider { - /** @var CollationMetadataProvider */ - private $collationMetadataProvider; + /** @var array */ + private array $cache = []; - /** @var array */ - private $cache = []; - - public function __construct(CollationMetadataProvider $collationMetadataProvider) + public function __construct(private readonly CollationMetadataProvider $collationMetadataProvider) { - $this->collationMetadataProvider = $collationMetadataProvider; } public function getCollationCharset(string $collation): ?string diff --git a/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php b/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php index 8dc2421a0..fcd999562 100644 --- a/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php +++ b/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php @@ -11,12 +11,8 @@ /** @internal */ final class ConnectionCollationMetadataProvider implements CollationMetadataProvider { - /** @var Connection */ - private $connection; - - public function __construct(Connection $connection) + public function __construct(private readonly Connection $connection) { - $this->connection = $connection; } /** @throws Exception */ diff --git a/doctrine/dbal/src/Platforms/MySQL/Comparator.php b/doctrine/dbal/src/Platforms/MySQL/Comparator.php index c27fef744..30956ec4d 100644 --- a/doctrine/dbal/src/Platforms/MySQL/Comparator.php +++ b/doctrine/dbal/src/Platforms/MySQL/Comparator.php @@ -1,14 +1,17 @@ collationMetadataProvider = $collationMetadataProvider; + public function __construct( + AbstractMySQLPlatform $platform, + private readonly CharsetMetadataProvider $charsetMetadataProvider, + private readonly CollationMetadataProvider $collationMetadataProvider, + private readonly DefaultTableOptions $defaultTableOptions, + ComparatorConfig $config = new ComparatorConfig(), + ) { + parent::__construct($platform, $config); } - public function compareTables(Table $fromTable, Table $toTable): TableDiff + public function compareTables(Table $oldTable, Table $newTable): TableDiff { return parent::compareTables( - $this->normalizeColumns($fromTable), - $this->normalizeColumns($toTable), + $this->normalizeTable($oldTable), + $this->normalizeTable($newTable), ); } - /** - * {@inheritDoc} - */ - public function diffTable(Table $fromTable, Table $toTable) + private function normalizeTable(Table $table): Table { - return parent::diffTable( - $this->normalizeColumns($fromTable), - $this->normalizeColumns($toTable), - ); - } + $charset = $table->getOption('charset'); + $collation = $table->getOption('collation'); + + if ($charset === null && $collation !== null) { + $charset = $this->collationMetadataProvider->getCollationCharset($collation); + } elseif ($charset !== null && $collation === null) { + $collation = $this->charsetMetadataProvider->getDefaultCharsetCollation($charset); + } elseif ($charset === null && $collation === null) { + $charset = $this->defaultTableOptions->getCharset(); + $collation = $this->defaultTableOptions->getCollation(); + } - private function normalizeColumns(Table $table): Table - { - $tableOptions = array_intersect_key($table->getOptions(), [ - 'charset' => null, - 'collation' => null, - ]); + $tableOptions = [ + 'charset' => $charset, + 'collation' => $collation, + ]; $table = clone $table; @@ -68,6 +74,7 @@ private function normalizeColumns(Table $table): Table continue; } + /** @phpstan-ignore argument.type */ $column->setPlatformOptions($overrideOptions); } @@ -75,18 +82,16 @@ private function normalizeColumns(Table $table): Table } /** - * @param array $options + * @param PlatformOptions $options * - * @return array + * @return PlatformOptions */ private function normalizeOptions(array $options): array { - if (isset($options['collation']) && ! isset($options['charset'])) { - $charset = $this->collationMetadataProvider->getCollationCharset($options['collation']); - - if ($charset !== null) { - $options['charset'] = $charset; - } + if (isset($options['charset']) && ! isset($options['collation'])) { + $options['collation'] = $this->charsetMetadataProvider->getDefaultCharsetCollation($options['charset']); + } elseif (isset($options['collation']) && ! isset($options['charset'])) { + $options['charset'] = $this->collationMetadataProvider->getCollationCharset($options['collation']); } return $options; diff --git a/doctrine/dbal/src/Platforms/MySQL/DefaultTableOptions.php b/doctrine/dbal/src/Platforms/MySQL/DefaultTableOptions.php new file mode 100644 index 000000000..efa399d76 --- /dev/null +++ b/doctrine/dbal/src/Platforms/MySQL/DefaultTableOptions.php @@ -0,0 +1,23 @@ +charset; + } + + public function getCollation(): string + { + return $this->collation; + } +} diff --git a/doctrine/dbal/src/Platforms/MySQL/MySQLMetadataProvider.php b/doctrine/dbal/src/Platforms/MySQL/MySQLMetadataProvider.php new file mode 100644 index 000000000..697cf41a4 --- /dev/null +++ b/doctrine/dbal/src/Platforms/MySQL/MySQLMetadataProvider.php @@ -0,0 +1,686 @@ + "\0", + "\\'" => "'", + '\\"' => '"', + '\\b' => "\b", + '\\n' => "\n", + '\\r' => "\r", + '\\t' => "\t", + '\\Z' => "\x1a", + '\\\\' => '\\', + '\\%' => '%', + '\\_' => '_', + + // Internally, MariaDB escapes single quotes using the standard syntax + "''" => "'", + ]; + + /** @var non-empty-string */ + private string $databaseName; + + /** + * @internal This class can be instantiated only by a database platform. + * + * @throws Exception + */ + public function __construct(private Connection $connection, private AbstractMySQLPlatform $platform) + { + $databaseName = $connection->fetchOne('SELECT DATABASE()'); + + if ($databaseName === null) { + throw DatabaseRequired::new(__METHOD__); + } + + $this->databaseName = $databaseName; + } + + /** + * {@inheritDoc} + * + * @link https://dev.mysql.com/doc/refman/8.4/en/information-schema-schemata-table.html + */ + public function getAllDatabaseNames(): iterable + { + $sql = <<<'SQL' + SELECT SCHEMA_NAME + FROM information_schema.SCHEMATA + ORDER BY SCHEMA_NAME + SQL; + + foreach ($this->connection->iterateColumn($sql) as $databaseName) { + yield new DatabaseMetadataRow($databaseName); + } + } + + /** {@inheritDoc} */ + public function getAllSchemaNames(): iterable + { + throw NotSupported::new(__METHOD__); + } + + /** + * {@inheritDoc} + * + * @link https://dev.mysql.com/doc/refman/8.4/en/information-schema-tables-table.html + */ + public function getAllTableNames(): iterable + { + $sql = <<<'SQL' + SELECT TABLE_NAME + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = ? + AND TABLE_TYPE = 'BASE TABLE' + ORDER BY TABLE_NAME + SQL; + + foreach ($this->connection->iterateNumeric($sql, [$this->databaseName]) as $row) { + yield new TableMetadataRow(null, $row[0], []); + } + } + + /** {@inheritDoc} */ + public function getTableColumnsForAllTables(): iterable + { + return $this->getTableColumns(null); + } + + /** {@inheritDoc} */ + public function getTableColumnsForTable(?string $schemaName, string $tableName): iterable + { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getTableColumns($tableName); + } + + /** + * @link https://dev.mysql.com/doc/refman/8.4/en/information-schema-columns-table.html + * @link https://dev.mysql.com/doc/refman/8.4/en/information-schema-tables-table.html + * + * @return iterable + * + * @throws Exception + */ + private function getTableColumns(?string $tableName): iterable + { + // The schema name is passed multiple times in the WHERE clause instead of using a JOIN condition to avoid + // performance issues on MySQL older than 8.0 and the corresponding MariaDB versions caused by + // https://bugs.mysql.com/bug.php?id=81347 + $conditions = ['c.TABLE_SCHEMA = ?', 't.TABLE_SCHEMA = ?']; + $params = [$this->databaseName, $this->databaseName]; + + if ($tableName !== null) { + $conditions[] = 't.TABLE_NAME = ?'; + $params[] = $tableName; + } + + // On MySQL < 8.0 and MariaDB, information_schema compares object names case-insensitively and so + // conflates table names that differ only in case. The binary comparison prevents that. + $sql = sprintf( + <<<'SQL' + SELECT c.TABLE_NAME, + c.COLUMN_NAME, + %s, + c.COLUMN_TYPE, + c.CHARACTER_MAXIMUM_LENGTH, + c.CHARACTER_OCTET_LENGTH, + c.NUMERIC_PRECISION, + c.NUMERIC_SCALE, + c.IS_NULLABLE, + c.COLUMN_DEFAULT, + c.EXTRA, + c.COLUMN_COMMENT, + c.CHARACTER_SET_NAME, + c.COLLATION_NAME + FROM information_schema.COLUMNS c + INNER JOIN information_schema.TABLES t + ON t.TABLE_NAME = c.TABLE_NAME + AND CONVERT(t.TABLE_NAME USING binary) = CONVERT(c.TABLE_NAME USING binary) + WHERE %s + AND t.TABLE_TYPE = 'BASE TABLE' + ORDER BY c.TABLE_NAME, + c.ORDINAL_POSITION + SQL, + $this->platform->getColumnTypeSQLSnippet('c', $this->databaseName), + implode(' AND ', $conditions), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield $this->createTableColumn($row); + } + } + + /** + * @param list $row + * + * @throws TypesException + */ + private function createTableColumn(array $row): TableColumnMetadataRow + { + [ + $tableName, + $columnName, + $dbType, + $columnType, + $characterMaximumLength, + $characterOctetLength, + $numericPrecision, + $numericScale, + $isNullable, + $columnDefault, + $extra, + $columnComment, + $characterSetName, + $collationName, + ] = $row; + + $editor = Column::editor() + ->setQuotedName($columnName) + ->setTypeName( + $this->platform->getDoctrineTypeMapping($dbType), + ); + + if (str_contains($columnType, 'unsigned')) { + $editor->setUnsigned(true); + } + + switch ($dbType) { + case 'char': + case 'varchar': + $editor->setLength((int) $characterMaximumLength); + break; + + case 'binary': + case 'varbinary': + $editor->setLength((int) $characterOctetLength); + break; + + case 'tinytext': + $editor->setLength(AbstractMySQLPlatform::LENGTH_LIMIT_TINYTEXT); + break; + + case 'text': + $editor->setLength(AbstractMySQLPlatform::LENGTH_LIMIT_TEXT); + break; + + case 'mediumtext': + $editor->setLength(AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMTEXT); + break; + + case 'tinyblob': + $editor->setLength(AbstractMySQLPlatform::LENGTH_LIMIT_TINYBLOB); + break; + + case 'blob': + $editor->setLength(AbstractMySQLPlatform::LENGTH_LIMIT_BLOB); + break; + + case 'mediumblob': + $editor->setLength(AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMBLOB); + break; + + case 'float': + case 'double': + case 'real': + case 'numeric': + case 'decimal': + $editor->setPrecision((int) $numericPrecision); + + if ($numericScale !== null) { + $editor->setScale((int) $numericScale); + } + + break; + } + + switch ($dbType) { + case 'char': + case 'binary': + $editor->setFixed(true); + break; + + case 'enum': + $editor->setValues($this->parseEnumExpression($columnType)); + break; + } + + if ($this->platform instanceof MariaDBPlatform) { + $default = $this->parseMariaDBColumnDefault($this->platform, $columnDefault); + } else { + $default = $columnDefault; + } + + $editor + ->setDefaultValue($default) + ->setNotNull($isNullable !== 'YES') + ->setComment($columnComment) + ->setCharset($characterSetName) + ->setCollation($collationName); + + if (str_contains($extra, 'auto_increment')) { + $editor->setAutoincrement(true); + } + + return new TableColumnMetadataRow(null, $tableName, $editor->create()); + } + + /** @return list */ + private function parseEnumExpression(string $expression): array + { + $result = preg_match_all("/'([^']*(?:''[^']*)*)'/", $expression, $matches); + assert($result !== false); + + return array_map( + static fn (string $match): string => strtr($match, ["''" => "'"]), + $matches[1], + ); + } + + /** + * Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers. + * + * - Since MariaDB 10.2.7 column defaults stored in information_schema are quoted to distinguish them from + * expressions. + * - The CURRENT_TIMESTAMP, CURRENT_TIME and CURRENT_DATE expressions + * are represented as "current_timestamp()", "curdate()" and "curtime()" respectively. + * - Quoted 'NULL' is not enforced. It is technically possible to have "null" in some circumstances. + * - Single quotes are always escaped by doubling, even if the original DDL used backslash escaping. + * + * @link https://mariadb.com/kb/en/library/information-schema-columns-table/ + * @link https://jira.mariadb.org/browse/MDEV-10134 + * @link https://jira.mariadb.org/browse/MDEV-13132 + * @link https://jira.mariadb.org/browse/MDEV-14053 + * + * @param string|null $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7 + */ + private function parseMariaDBColumnDefault(MariaDBPlatform $platform, ?string $columnDefault): ?string + { + if ($columnDefault === 'NULL' || $columnDefault === null) { + return null; + } + + if (preg_match('/^\'(.*)\'$/', $columnDefault, $matches) === 1) { + return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES); + } + + return match ($columnDefault) { + 'current_timestamp()' => $platform->getCurrentTimestampSQL(), + 'curdate()' => $platform->getCurrentDateSQL(), + 'curtime()' => $platform->getCurrentTimeSQL(), + default => $columnDefault, + }; + } + + /** {@inheritDoc} */ + public function getIndexColumnsForAllTables(): iterable + { + return $this->getIndexColumns(null); + } + + /** {@inheritDoc} */ + public function getIndexColumnsForTable(?string $schemaName, string $tableName): iterable + { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getIndexColumns($tableName); + } + + /** + * @link https://dev.mysql.com/doc/refman/8.4/en/information-schema-statistics-table.html + * + * @return iterable + * + * @throws Exception + */ + private function getIndexColumns(?string $tableName): iterable + { + $conditions = ['TABLE_SCHEMA = ?']; + $params = [$this->databaseName]; + + if ($tableName !== null) { + $conditions[] = 'TABLE_NAME = ?'; + $params[] = $tableName; + } + + $sql = sprintf( + <<<'SQL' + SELECT TABLE_NAME, + INDEX_NAME, + INDEX_TYPE, + NON_UNIQUE, + COLUMN_NAME, + SUB_PART + FROM information_schema.STATISTICS + WHERE %s + AND INDEX_NAME != 'PRIMARY' + ORDER BY TABLE_NAME, + SEQ_IN_INDEX + SQL, + implode(' AND ', $conditions), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + if ($row[5] !== null) { + $length = (int) $row[5]; + assert($length > 0); + } else { + $length = null; + } + + if ($row[2] === 'FULLTEXT') { + $type = IndexType::FULLTEXT; + } elseif ($row[2] === 'SPATIAL') { + $type = IndexType::SPATIAL; + + // the SUB_PART column may contain a non-null value for spatial indexes, + // but this is not the prefix length + $length = null; + } elseif ($row[3]) { + $type = IndexType::REGULAR; + } else { + $type = IndexType::UNIQUE; + } + + yield new IndexColumnMetadataRow( + schemaName: null, + tableName: $row[0], + indexName: $row[1], + type: $type, + isClustered: false, + predicate: null, + columnName: $row[4], + columnLength: $length, + ); + } + } + + /** {@inheritDoc} */ + public function getPrimaryKeyConstraintColumnsForAllTables(): iterable + { + return $this->getPrimaryKeyConstraintColumns(null); + } + + /** {@inheritDoc} */ + public function getPrimaryKeyConstraintColumnsForTable( + ?string $schemaName, + string $tableName, + ): iterable { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getPrimaryKeyConstraintColumns($tableName); + } + + /** + * @link https://dev.mysql.com/doc/refman/8.4/en/information-schema-table-constraints-table.html + * @link https://dev.mysql.com/doc/refman/8.4/en/information-schema-key-column-usage-table.html + * + * @return iterable + * + * @throws Exception + */ + private function getPrimaryKeyConstraintColumns(?string $tableName): iterable + { + // The schema name is passed multiple times in the WHERE clause instead of using a JOIN condition to avoid + // performance issues on MySQL older than 8.0 and the corresponding MariaDB versions caused by + // https://bugs.mysql.com/bug.php?id=81347 + $conditions = ['tc.TABLE_SCHEMA = ?', 'kcu.TABLE_SCHEMA = ?']; + $params = [$this->databaseName, $this->databaseName]; + + if ($tableName !== null) { + $conditions[] = 'tc.TABLE_NAME = ?'; + $params[] = $tableName; + } + + // On MySQL < 8.0 and MariaDB, information_schema compares object names case-insensitively and so + // conflates table names that differ only in case. The binary comparison prevents that. + $sql = sprintf( + <<<'SQL' + SELECT tc.TABLE_NAME, + tc.CONSTRAINT_NAME, + kcu.COLUMN_NAME + FROM information_schema.TABLE_CONSTRAINTS tc + INNER JOIN information_schema.KEY_COLUMN_USAGE kcu + ON kcu.TABLE_NAME = tc.TABLE_NAME + AND CONVERT(kcu.TABLE_NAME USING binary) = CONVERT(tc.TABLE_NAME USING binary) + AND kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME + WHERE %s + AND tc.CONSTRAINT_TYPE = 'PRIMARY KEY' + ORDER BY TABLE_NAME, + kcu.ORDINAL_POSITION + SQL, + implode(' AND ', $conditions), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new PrimaryKeyConstraintColumnRow( + schemaName: null, + tableName: $row[0], + constraintName: $row[1], + isClustered: true, + columnName: $row[2], + ); + } + } + + /** {@inheritDoc} */ + public function getForeignKeyConstraintColumnsForAllTables(): iterable + { + return $this->getForeignKeyConstraintColumns(null); + } + + /** {@inheritDoc} */ + public function getForeignKeyConstraintColumnsForTable( + ?string $schemaName, + string $tableName, + ): iterable { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getForeignKeyConstraintColumns($tableName); + } + + /** + * @link https://dev.mysql.com/doc/refman/8.4/en/information-schema-key-column-usage-table.html + * @link https://dev.mysql.com/doc/refman/8.4/en/information-schema-referential-constraints-table.html + * + * @return iterable + * + * @throws Exception + */ + private function getForeignKeyConstraintColumns(?string $tableName): iterable + { + // The schema name is passed multiple times in the WHERE clause instead of using a JOIN condition + // to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions caused by + // https://bugs.mysql.com/bug.php?id=81347 + $conditions = ['k.TABLE_SCHEMA = ?', 'c.CONSTRAINT_SCHEMA = ?']; + $params = [$this->databaseName, $this->databaseName]; + + if ($tableName !== null) { + $conditions[] = 'k.TABLE_NAME = ?'; + $params[] = $tableName; + } + + // On MySQL < 8.0 and MariaDB, information_schema compares object names case-insensitively and so + // conflates table names that differ only in case. The binary comparison prevents that. + $sql = sprintf( + <<<'SQL' + SELECT k.TABLE_NAME, + k.CONSTRAINT_NAME, + k.REFERENCED_TABLE_NAME, + c.UPDATE_RULE, + c.DELETE_RULE, + k.COLUMN_NAME, + k.REFERENCED_COLUMN_NAME + FROM information_schema.KEY_COLUMN_USAGE k + INNER JOIN information_schema.REFERENTIAL_CONSTRAINTS c + ON c.CONSTRAINT_NAME = k.CONSTRAINT_NAME + AND c.TABLE_NAME = k.TABLE_NAME + AND CONVERT(c.TABLE_NAME USING binary) = CONVERT(k.TABLE_NAME USING binary) + WHERE %s + AND k.REFERENCED_COLUMN_NAME IS NOT NULL + ORDER BY k.TABLE_NAME, + k.CONSTRAINT_NAME, + k.ORDINAL_POSITION + SQL, + implode(' AND ', $conditions), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new ForeignKeyConstraintColumnMetadataRow( + referencingSchemaName: null, + referencingTableName: $row[0], + id: null, + name: $row[1], + referencedSchemaName: null, + referencedTableName: $row[2], + matchType: MatchType::SIMPLE, + onUpdateAction: $this->createReferentialAction($row[3]), + onDeleteAction: $this->createReferentialAction($row[4]), + isDeferrable: false, + isDeferred: false, + referencingColumnName: $row[5], + referencedColumnName: $row[6], + ); + } + } + + private function createReferentialAction(string $value): ReferentialAction + { + $action = ReferentialAction::tryFrom($value); + assert($action !== null); + + return $action; + } + + /** {@inheritDoc} */ + public function getTableOptionsForAllTables(): iterable + { + return $this->getTableOptions(null); + } + + /** {@inheritDoc} */ + public function getTableOptionsForTable(?string $schemaName, string $tableName): iterable + { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getTableOptions($tableName); + } + + /** + * @return iterable + * + * @throws Exception + */ + private function getTableOptions(?string $tableName): iterable + { + $sql = $this->platform->fetchTableOptionsByTable($tableName !== null); + + $params = [$this->databaseName]; + if ($tableName !== null) { + $params[] = $tableName; + } + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new TableMetadataRow(null, $row[0], [ + 'engine' => $row[1], + 'autoincrement' => $row[2], + 'comment' => $row[3], + 'create_options' => $this->parseCreateOptions($row[4]), + 'collation' => $row[5], + 'charset' => $row[6], + ]); + } + } + + /** @return array|array */ + private function parseCreateOptions(?string $string): array + { + $options = []; + + if ($string === null || $string === '') { + return $options; + } + + foreach (explode(' ', $string) as $pair) { + $parts = explode('=', $pair, 2); + + $options[$parts[0]] = $parts[1] ?? true; + } + + return $options; + } + + /** + * {@inheritDoc} + * + * @link https://dev.mysql.com/doc/refman/8.4/en/information-schema-views-table.html + */ + public function getAllViews(): iterable + { + $sql = <<<'SQL' + SELECT TABLE_NAME, + VIEW_DEFINITION + FROM information_schema.VIEWS + WHERE TABLE_SCHEMA = ? + ORDER BY TABLE_NAME + SQL; + + foreach ($this->connection->iterateNumeric($sql, [$this->databaseName]) as $row) { + yield new ViewMetadataRow(null, ...$row); + } + } + + /** {@inheritDoc} */ + public function getAllSequences(): iterable + { + throw NotSupported::new(__METHOD__); + } +} diff --git a/doctrine/dbal/src/Platforms/MySQL57Platform.php b/doctrine/dbal/src/Platforms/MySQL57Platform.php deleted file mode 100644 index d5169bddd..000000000 --- a/doctrine/dbal/src/Platforms/MySQL57Platform.php +++ /dev/null @@ -1,99 +0,0 @@ -getQuotedName($this)]; - } - - /** - * {@inheritDoc} - * - * @deprecated Implement {@see createReservedKeywordsList()} instead. - */ - protected function getReservedKeywordsClass() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4510', - 'MySQL57Platform::getReservedKeywordsClass() is deprecated,' - . ' use MySQL57Platform::createReservedKeywordsList() instead.', - ); - - return Keywords\MySQL57Keywords::class; - } - - /** - * {@inheritDoc} - */ - protected function initializeDoctrineTypeMappings() - { - parent::initializeDoctrineTypeMappings(); - - $this->doctrineTypeMapping['json'] = Types::JSON; - } -} diff --git a/doctrine/dbal/src/Platforms/MySQL80Platform.php b/doctrine/dbal/src/Platforms/MySQL80Platform.php index d9324282b..01dcfb401 100644 --- a/doctrine/dbal/src/Platforms/MySQL80Platform.php +++ b/doctrine/dbal/src/Platforms/MySQL80Platform.php @@ -1,34 +1,41 @@ getQuotedName($this)]; + } + + /** @deprecated */ + protected function createReservedKeywordsList(): KeywordList + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6607', + '%s is deprecated.', + __METHOD__, + ); + + return new MySQLKeywords(); + } } diff --git a/doctrine/dbal/src/Platforms/Oracle/OracleMetadataProvider.php b/doctrine/dbal/src/Platforms/Oracle/OracleMetadataProvider.php new file mode 100644 index 000000000..eea158123 --- /dev/null +++ b/doctrine/dbal/src/Platforms/Oracle/OracleMetadataProvider.php @@ -0,0 +1,581 @@ +connection->iterateColumn($sql) as $databaseName) { + yield new DatabaseMetadataRow($databaseName); + } + } + + /** {@inheritDoc} */ + public function getAllSchemaNames(): iterable + { + throw NotSupported::new(__METHOD__); + } + + /** + * {@inheritDoc} + * + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/USER_TABLES.html + */ + public function getAllTableNames(): iterable + { + $sql = <<<'SQL' + SELECT TABLE_NAME + FROM USER_TABLES + ORDER BY TABLE_NAME + SQL; + + foreach ($this->connection->iterateNumeric($sql) as $row) { + yield new TableMetadataRow(null, $row[0], []); + } + } + + /** {@inheritDoc} */ + public function getTableColumnsForAllTables(): iterable + { + return $this->getTableColumns(null); + } + + /** {@inheritDoc} */ + public function getTableColumnsForTable(?string $schemaName, string $tableName): iterable + { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getTableColumns($tableName); + } + + /** + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/USER_TAB_COLUMNS.html + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/USER_TABLES.html + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/USER_COL_COMMENTS.html + * + * @return iterable + * + * @throws Exception + */ + private function getTableColumns(?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT C.TABLE_NAME, + C.COLUMN_NAME, + C.DATA_TYPE, + C.DATA_DEFAULT, + C.DATA_PRECISION, + C.DATA_SCALE, + C.CHAR_LENGTH, + C.DATA_LENGTH, + C.NULLABLE, + D.COMMENTS + FROM USER_TAB_COLUMNS C + INNER JOIN USER_TABLES T + ON T.TABLE_NAME = C.TABLE_NAME + LEFT JOIN USER_COL_COMMENTS D + ON D.TABLE_NAME = C.TABLE_NAME + AND D.COLUMN_NAME = C.COLUMN_NAME + WHERE %s + ORDER BY C.TABLE_NAME, + C.COLUMN_ID + SQL, + $this->buildTableQueryPredicate('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 + { + [ + $tableName, + $columnName, + $dataType, + $dataDefault, + $dataPrecision, + $dataScale, + $characterLength, + $dataLength, + $nullable, + $comments, + ] = $row; + + $dbType = strtolower($dataType); + if (str_starts_with($dbType, 'timestamp(')) { + if (str_contains($dbType, 'with time zone')) { + $dbType = 'timestamptz'; + } else { + $dbType = 'timestamp'; + } + } + + $editor = Column::editor() + ->setQuotedName($columnName); + + $precision = null; + $scale = 0; + + if ($dataPrecision !== null) { + $precision = (int) $dataPrecision; + } + + if ($dataScale !== null) { + $scale = (int) $dataScale; + } + + $type = $this->platform->getDoctrineTypeMapping($dbType); + + switch ($dbType) { + case 'number': + if ($precision === 20 && $scale === 0) { + $type = 'bigint'; + } elseif ($precision === 5 && $scale === 0) { + $type = 'smallint'; + } elseif ($precision === 1 && $scale === 0) { + $type = 'boolean'; + } elseif ($scale > 0) { + $type = 'decimal'; + } + + break; + + case 'float': + if ($precision === 63) { + $type = 'smallfloat'; + } + + break; + + case 'varchar': + case 'varchar2': + case 'nvarchar2': + $editor->setLength((int) $characterLength); + break; + + case 'raw': + $editor + ->setLength((int) $dataLength) + ->setFixed(true); + break; + + case 'char': + case 'nchar': + $editor + ->setLength((int) $characterLength) + ->setFixed(true); + break; + } + + $editor + ->setTypeName($type) + ->setPrecision($precision) + ->setScale($scale) + ->setNotNull($nullable === 'N') + ->setDefaultValue($this->parseDefaultExpression($dataDefault)); + + if ($comments !== null) { + $editor->setComment($comments); + } + + return new TableColumnMetadataRow(null, $tableName, $editor->create()); + } + + private function parseDefaultExpression(?string $expression): ?string + { + // Default values returned from the database sometimes have trailing spaces. + if (is_string($expression)) { + $expression = trim($expression); + } + + if ($expression === null || $expression === 'NULL') { + return null; + } + + // Default values returned from the database are represented as literal expressions + if (preg_match('/^\'(.*)\'$/s', $expression, $matches) === 1) { + return str_replace("''", "'", $matches[1]); + } + + return $expression; + } + + /** {@inheritDoc} */ + public function getIndexColumnsForAllTables(): iterable + { + return $this->getIndexColumns(null); + } + + /** {@inheritDoc} */ + public function getIndexColumnsForTable(?string $schemaName, string $tableName): iterable + { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getIndexColumns($tableName); + } + + /** + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/USER_INDEXES.html + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/USER_CONSTRAINTS.html + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/USER_IND_COLUMNS.html + * + * @return iterable + * + * @throws Exception + */ + private function getIndexColumns(?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT I.TABLE_NAME, + I.INDEX_NAME, + I.UNIQUENESS, + IC.COLUMN_NAME + FROM USER_INDEXES I + LEFT JOIN USER_CONSTRAINTS C + ON C.INDEX_NAME = I.INDEX_NAME + JOIN USER_IND_COLUMNS IC + ON IC.INDEX_NAME = I.INDEX_NAME + WHERE %s + AND (C.CONSTRAINT_TYPE IS NULL OR C.CONSTRAINT_TYPE != 'P') + ORDER BY I.TABLE_NAME, + I.INDEX_NAME, + IC.COLUMN_POSITION + SQL, + $this->buildTableQueryPredicate('I', $tableName, $params), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new IndexColumnMetadataRow( + schemaName: null, + tableName: $row[0], + indexName: $row[1], + type: $row[2] === 'UNIQUE' ? IndexType::UNIQUE : IndexType::REGULAR, + isClustered: false, + predicate: null, + columnName: $row[3], + columnLength: null, + ); + } + } + + /** {@inheritDoc} */ + public function getPrimaryKeyConstraintColumnsForAllTables(): iterable + { + return $this->getPrimaryKeyConstraintColumns(null); + } + + /** {@inheritDoc} */ + public function getPrimaryKeyConstraintColumnsForTable(?string $schemaName, string $tableName): iterable + { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getPrimaryKeyConstraintColumns($tableName); + } + + /** + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/USER_CONSTRAINTS.html + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/USER_CONS_COLUMNS.html + * + * @return iterable + * + * @throws Exception + */ + private function getPrimaryKeyConstraintColumns(?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT C.TABLE_NAME, + C.CONSTRAINT_NAME, + CC.COLUMN_NAME + FROM USER_CONSTRAINTS C + JOIN USER_CONS_COLUMNS CC + ON CC.TABLE_NAME = C.TABLE_NAME + AND CC.CONSTRAINT_NAME = C.CONSTRAINT_NAME + WHERE %s + AND C.CONSTRAINT_TYPE = 'P' + ORDER BY C.TABLE_NAME, + C.CONSTRAINT_NAME, + CC.POSITION + SQL, + $this->buildTableQueryPredicate('C', $tableName, $params), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new PrimaryKeyConstraintColumnRow( + schemaName: null, + tableName: $row[0], + constraintName: $row[1], + isClustered: true, + columnName: $row[2], + ); + } + } + + /** {@inheritDoc} */ + public function getForeignKeyConstraintColumnsForAllTables(): iterable + { + return $this->getForeignKeyConstraintColumns(null); + } + + /** {@inheritDoc} */ + public function getForeignKeyConstraintColumnsForTable( + ?string $schemaName, + string $tableName, + ): iterable { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getForeignKeyConstraintColumns($tableName); + } + + /** + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/USER_CONSTRAINTS.html + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/USER_CONS_COLUMNS.html + * + * @return iterable + * + * @throws Exception + */ + private function getForeignKeyConstraintColumns(?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT C.TABLE_NAME, + C.CONSTRAINT_NAME, + FKC.TABLE_NAME, + C.DELETE_RULE, + C.DEFERRABLE, + C.DEFERRED, + PKC.COLUMN_NAME, + FKC.COLUMN_NAME + FROM USER_CONSTRAINTS C + JOIN USER_CONS_COLUMNS PKC + ON PKC.CONSTRAINT_NAME = C.CONSTRAINT_NAME + JOIN USER_CONS_COLUMNS FKC + ON FKC.CONSTRAINT_NAME = C.R_CONSTRAINT_NAME + AND FKC.POSITION = PKC.POSITION + WHERE %s + AND C.CONSTRAINT_TYPE = 'R' + ORDER BY PKC.TABLE_NAME, + PKC.CONSTRAINT_NAME, + PKC.POSITION +SQL, + $this->buildTableQueryPredicate('C', $tableName, $params), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new ForeignKeyConstraintColumnMetadataRow( + referencingSchemaName: null, + referencingTableName: $row[0], + id: null, + name: $row[1], + referencedSchemaName: null, + referencedTableName: $row[2], + matchType: MatchType::SIMPLE, + onUpdateAction: ReferentialAction::NO_ACTION, + onDeleteAction: $this->createReferentialAction($row[3]), + isDeferrable: $row[4] === 'DEFERRABLE', + isDeferred: $row[5] === 'DEFERRED', + referencingColumnName: $row[6], + referencedColumnName: $row[7], + ); + } + } + + private function createReferentialAction(string $value): ReferentialAction + { + $action = ReferentialAction::tryFrom($value); + assert($action !== null); + + return $action; + } + + /** {@inheritDoc} */ + public function getTableOptionsForAllTables(): iterable + { + return $this->getTableOptions(null); + } + + /** {@inheritDoc} */ + public function getTableOptionsForTable(?string $schemaName, string $tableName): iterable + { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getTableOptions($tableName); + } + + /** + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/USER_TAB_COMMENTS.html + * + * @return iterable + * + * @throws Exception + */ + private function getTableOptions(?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT TABLE_NAME, + COMMENTS + FROM ALL_TAB_COMMENTS + WHERE %s + ORDER BY TABLE_NAME + SQL, + $this->buildTableQueryPredicate(null, $tableName, $params), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new TableMetadataRow(null, $row[0], [ + 'comment' => $row[1], + ]); + } + } + + /** + * @param ?non-empty-string $relation + * @param array $params + * + * @return non-empty-string + */ + private function buildTableQueryPredicate(?string $relation, ?string $tableName, array &$params): string + { + $conditions = []; + + if ($tableName !== null) { + $qualifier = $relation !== null ? $relation . '.' : ''; + $conditions[] = $qualifier . 'TABLE_NAME = :TABLE_NAME'; + + $params['TABLE_NAME'] = $tableName; + } else { + $conditions[] = '1 = 1'; + } + + return implode(' AND ', $conditions); + } + + /** + * {@inheritDoc} + * + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/USER_VIEWS.html + */ + public function getAllViews(): iterable + { + $sql = <<<'SQL' + SELECT VIEW_NAME, + TEXT + FROM USER_VIEWS + ORDER BY VIEW_NAME + SQL; + + foreach ($this->connection->iterateNumeric($sql) as $row) { + yield new ViewMetadataRow(null, ...$row); + } + } + + /** + * {@inheritDoc} + * + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/USER_SEQUENCES.html + */ + public function getAllSequences(): iterable + { + $sql = <<<'SQL' + SELECT SEQUENCE_NAME, + INCREMENT_BY, + MIN_VALUE, + CACHE_SIZE + FROM USER_SEQUENCES + ORDER BY SEQUENCE_NAME + SQL; + + foreach ($this->connection->iterateNumeric($sql) as $row) { + $cacheSize = (int) $row[3]; + assert($cacheSize > 0); + + yield new SequenceMetadataRow( + schemaName: null, + sequenceName: $row[0], + allocationSize: (int) $row[1], + initialValue: (int) $row[2], + cacheSize: $cacheSize, + ); + } + } +} diff --git a/doctrine/dbal/src/Platforms/OraclePlatform.php b/doctrine/dbal/src/Platforms/OraclePlatform.php index 1b68fd5fb..a2b555e6d 100644 --- a/doctrine/dbal/src/Platforms/OraclePlatform.php +++ b/doctrine/dbal/src/Platforms/OraclePlatform.php @@ -1,15 +1,20 @@ multiplyInterval((string) $interval, 3); + $interval = $this->multiplyInterval($interval, 3); break; case DateIntervalUnit::YEAR: - $interval = $this->multiplyInterval((string) $interval, 12); + $interval = $this->multiplyInterval($interval, 12); break; } @@ -149,18 +107,12 @@ protected function getDateArithmeticIntervalExpression($date, $operator, $interv } } - /** - * {@inheritDoc} - */ - public function getDateDiffExpression($date1, $date2) + public function getDateDiffExpression(string $date1, string $date2): string { return sprintf('TRUNC(%s) - TRUNC(%s)', $date1, $date2); } - /** - * {@inheritDoc} - */ - public function getBitAndComparisonExpression($value1, $value2) + public function getBitAndComparisonExpression(string $value1, string $value2): string { return 'BITAND(' . $value1 . ', ' . $value2 . ')'; } @@ -170,34 +122,25 @@ public function getCurrentDatabaseExpression(): string return "SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')"; } - /** - * {@inheritDoc} - */ - public function getBitOrComparisonExpression($value1, $value2) + public function getBitOrComparisonExpression(string $value1, string $value2): string { return '(' . $value1 . '-' . $this->getBitAndComparisonExpression($value1, $value2) . '+' . $value2 . ')'; } - /** - * {@inheritDoc} - */ - public function getCreatePrimaryKeySQL(Index $index, $table): string + /** @deprecated */ + public function getCreatePrimaryKeySQL(Index $index, string $table): string { - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $table = $table->getQuotedName($this); - } + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6867', + '%s() is deprecated.', + __METHOD__, + ); return 'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $index->getQuotedName($this) - . ' PRIMARY KEY (' . $this->getIndexFieldDeclarationListSQL($index) . ')'; + . ' PRIMARY KEY (' . implode(', ', $index->getQuotedColumns($this)) . ')'; } /** @@ -207,86 +150,62 @@ public function getCreatePrimaryKeySQL(Index $index, $table): string * Therefore we can use MINVALUE to be able to get a hint what START WITH was for later introspection * in {@see listSequences()} */ - public function getCreateSequenceSQL(Sequence $sequence) + public function getCreateSequenceSQL(Sequence $sequence): string { return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . ' START WITH ' . $sequence->getInitialValue() . ' MINVALUE ' . $sequence->getInitialValue() . ' INCREMENT BY ' . $sequence->getAllocationSize() . - $this->getSequenceCacheSQL($sequence); + $this->getSequenceCacheSQL($sequence->getCacheSize()); } - /** - * {@inheritDoc} - */ - public function getAlterSequenceSQL(Sequence $sequence) + public function getAlterSequenceSQL(Sequence $sequence): string { return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . ' INCREMENT BY ' . $sequence->getAllocationSize() - . $this->getSequenceCacheSQL($sequence); + . $this->getSequenceCacheSQL($sequence->getCacheSize()); } /** * Cache definition for sequences */ - private function getSequenceCacheSQL(Sequence $sequence): string + private function getSequenceCacheSQL(?int $cacheSize): string { - if ($sequence->getCache() === 0) { - return ' NOCACHE'; - } - - if ($sequence->getCache() === 1) { + if ($cacheSize === 0 || $cacheSize === 1) { return ' NOCACHE'; } - if ($sequence->getCache() > 1) { - return ' CACHE ' . $sequence->getCache(); + if ($cacheSize > 1) { + return ' CACHE ' . $cacheSize; } return ''; } - /** - * {@inheritDoc} - */ - public function getSequenceNextValSQL($sequence) + public function getSequenceNextValSQL(string $sequence): string { return 'SELECT ' . $sequence . '.nextval FROM DUAL'; } - /** - * {@inheritDoc} - */ - public function getSetTransactionIsolationSQL($level) + public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string { return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); } - /** - * {@inheritDoc} - */ - protected function _getTransactionIsolationLevelSQL($level) + protected function _getTransactionIsolationLevelSQL(TransactionIsolationLevel $level): string { - switch ($level) { - case TransactionIsolationLevel::READ_UNCOMMITTED: - return 'READ UNCOMMITTED'; - - case TransactionIsolationLevel::READ_COMMITTED: - return 'READ COMMITTED'; - - case TransactionIsolationLevel::REPEATABLE_READ: - case TransactionIsolationLevel::SERIALIZABLE: - return 'SERIALIZABLE'; - - default: - return parent::_getTransactionIsolationLevelSQL($level); - } + return match ($level) { + TransactionIsolationLevel::READ_UNCOMMITTED => 'READ UNCOMMITTED', + TransactionIsolationLevel::READ_COMMITTED => 'READ COMMITTED', + TransactionIsolationLevel::REPEATABLE_READ, + TransactionIsolationLevel::SERIALIZABLE => 'SERIALIZABLE', + }; } /** * {@inheritDoc} */ - public function getBooleanTypeDeclarationSQL(array $column) + public function getBooleanTypeDeclarationSQL(array $column): string { return 'NUMBER(1)'; } @@ -294,7 +213,7 @@ public function getBooleanTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getIntegerTypeDeclarationSQL(array $column) + public function getIntegerTypeDeclarationSQL(array $column): string { return 'NUMBER(10)'; } @@ -302,7 +221,7 @@ public function getIntegerTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getBigIntTypeDeclarationSQL(array $column) + public function getBigIntTypeDeclarationSQL(array $column): string { return 'NUMBER(20)'; } @@ -310,7 +229,7 @@ public function getBigIntTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getSmallIntTypeDeclarationSQL(array $column) + public function getSmallIntTypeDeclarationSQL(array $column): string { return 'NUMBER(5)'; } @@ -318,7 +237,7 @@ public function getSmallIntTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getDateTimeTypeDeclarationSQL(array $column) + public function getDateTimeTypeDeclarationSQL(array $column): string { return 'TIMESTAMP(0)'; } @@ -326,7 +245,7 @@ public function getDateTimeTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getDateTimeTzTypeDeclarationSQL(array $column) + public function getDateTimeTzTypeDeclarationSQL(array $column): string { return 'TIMESTAMP(0) WITH TIME ZONE'; } @@ -334,7 +253,7 @@ public function getDateTimeTzTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getDateTypeDeclarationSQL(array $column) + public function getDateTypeDeclarationSQL(array $column): string { return 'DATE'; } @@ -342,7 +261,7 @@ public function getDateTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getTimeTypeDeclarationSQL(array $column) + public function getTimeTypeDeclarationSQL(array $column): string { return 'DATE'; } @@ -350,116 +269,80 @@ public function getTimeTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - protected function _getCommonIntegerTypeDeclarationSQL(array $column) + protected function _getCommonIntegerTypeDeclarationSQL(array $column): string { return ''; } - /** - * {@inheritDoc} - */ - protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string { - if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'Relying on the default string column length on Oracle is deprecated' - . ', specify the length explicitly.', - ); + if ($length === null) { + throw ColumnLengthRequired::new($this, 'VARCHAR2'); } - return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(2000)') - : ($length > 0 ? 'VARCHAR2(' . $length . ')' : 'VARCHAR2(4000)'); + return sprintf('VARCHAR2(%d)', $length); } - /** - * {@inheritDoc} - */ - protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string { - if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'Relying on the default binary column length on Oracle is deprecated' - . ', specify the length explicitly.', - ); + if ($length === null) { + throw ColumnLengthRequired::new($this, 'RAW'); } - return 'RAW(' . ($length > 0 ? $length : $this->getBinaryMaxLength()) . ')'; + return sprintf('RAW(%d)', $length); } - /** - * {@inheritDoc} - * - * @deprecated - */ - public function getBinaryMaxLength() + protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'OraclePlatform::getBinaryMaxLength() is deprecated.', - ); - - return 2000; + return $this->getBinaryTypeDeclarationSQLSnippet($length); } /** * {@inheritDoc} */ - public function getClobTypeDeclarationSQL(array $column) + public function getClobTypeDeclarationSQL(array $column): string { return 'CLOB'; } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. - */ - public function getListDatabasesSQL() + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListDatabasesSQL(): string { return 'SELECT username FROM all_users'; } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. - */ - public function getListSequencesSQL($database) + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListSequencesSQL(string $database): string { - $database = $this->normalizeIdentifier($database); - $database = $this->quoteStringLiteral($database->getName()); - - return 'SELECT sequence_name, min_value, increment_by FROM sys.all_sequences ' . - 'WHERE SEQUENCE_OWNER = ' . $database; + return 'SELECT SEQUENCE_NAME, MIN_VALUE, INCREMENT_BY FROM SYS.ALL_SEQUENCES WHERE SEQUENCE_OWNER = ' + . $this->quoteStringLiteral( + $this->normalizeIdentifier($database)->getName(), + ); } /** * {@inheritDoc} */ - protected function _getCreateTableSQL($name, array $columns, array $options = []) + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array { + $this->validateCreateTableOptions($options, __METHOD__); + $indexes = $options['indexes'] ?? []; $options['indexes'] = []; $sql = parent::_getCreateTableSQL($name, $columns, $options); - foreach ($columns as $columnName => $column) { + foreach ($columns as $column) { if (isset($column['sequence'])) { $sql[] = $this->getCreateSequenceSQL($column['sequence']); } if ( - ! isset($column['autoincrement']) || ! $column['autoincrement'] && - (! isset($column['autoinc']) || ! $column['autoinc']) + ! isset($column['autoincrement']) || $column['autoincrement'] === false ) { continue; } - $sql = array_merge($sql, $this->getCreateAutoincrementSql($columnName, $name)); + $sql = array_merge($sql, $this->getCreateAutoincrementSql($column['name'], $name)); } foreach ($indexes as $index) { @@ -469,76 +352,18 @@ protected function _getCreateTableSQL($name, array $columns, array $options = [] return $sql; } - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTableIndexesSQL($table, $database = null) - { - $table = $this->normalizeIdentifier($table); - $table = $this->quoteStringLiteral($table->getName()); - - return "SELECT uind_col.index_name AS name, - ( - SELECT uind.index_type - FROM user_indexes uind - WHERE uind.index_name = uind_col.index_name - ) AS type, - decode( - ( - SELECT uind.uniqueness - FROM user_indexes uind - WHERE uind.index_name = uind_col.index_name - ), - 'NONUNIQUE', - 0, - 'UNIQUE', - 1 - ) AS is_unique, - uind_col.column_name AS column_name, - uind_col.column_position AS column_pos, - ( - SELECT ucon.constraint_type - FROM user_constraints ucon - WHERE ucon.index_name = uind_col.index_name - AND ucon.table_name = uind_col.table_name - ) AS is_primary - FROM user_ind_columns uind_col - WHERE uind_col.table_name = " . $table . ' - ORDER BY uind_col.column_position ASC'; - } - - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTablesSQL() - { - return 'SELECT * FROM sys.user_tables'; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. - */ - public function getListViewsSQL($database) + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListViewsSQL(string $database): string { return 'SELECT view_name, text FROM sys.user_views'; } /** - * @internal The method should be only used from within the OraclePlatform class hierarchy. - * - * @param string $name - * @param string $table - * @param int $start + * @internal The method should be only used by the {@see OraclePlatform} class. * - * @return string[] + * @return array */ - public function getCreateAutoincrementSql($name, $table, $start = 1) + protected function getCreateAutoincrementSql(string $name, string $table, int $start = 1): array { $tableIdentifier = $this->normalizeIdentifier($table); $quotedTableName = $tableIdentifier->getQuotedName($this); @@ -546,7 +371,6 @@ public function getCreateAutoincrementSql($name, $table, $start = 1) $nameIdentifier = $this->normalizeIdentifier($name); $quotedName = $nameIdentifier->getQuotedName($this); - $unquotedName = $nameIdentifier->getName(); $sql = []; @@ -554,46 +378,59 @@ public function getCreateAutoincrementSql($name, $table, $start = 1) $idx = new Index($autoincrementIdentifierName, [$quotedName], true, true); - $sql[] = "DECLARE - constraints_Count NUMBER; + $sql[] = sprintf( + <<<'SQL' +DECLARE + CONSTRAINTS_COUNT NUMBER; BEGIN - SELECT COUNT(CONSTRAINT_NAME) INTO constraints_Count + SELECT COUNT(CONSTRAINT_NAME) INTO CONSTRAINTS_COUNT FROM USER_CONSTRAINTS - WHERE TABLE_NAME = '" . $unquotedTableName . "' + WHERE TABLE_NAME = %s AND CONSTRAINT_TYPE = 'P'; - IF constraints_Count = 0 OR constraints_Count = '' THEN - EXECUTE IMMEDIATE '" . $this->getCreateConstraintSQL($idx, $quotedTableName) . "'; + IF CONSTRAINTS_COUNT = 0 THEN + EXECUTE IMMEDIATE %s; END IF; -END;"; +END; +SQL, + $this->quoteStringLiteral($unquotedTableName), + $this->quoteStringLiteral($this->getCreateIndexSQL($idx, $quotedTableName)), + ); $sequenceName = $this->getIdentitySequenceName( $tableIdentifier->isQuoted() ? $quotedTableName : $unquotedTableName, - $nameIdentifier->isQuoted() ? $quotedName : $unquotedName, ); $sequence = new Sequence($sequenceName, $start); $sql[] = $this->getCreateSequenceSQL($sequence); - $sql[] = 'CREATE TRIGGER ' . $autoincrementIdentifierName . ' + $sql[] = sprintf( + <<<'SQL' +CREATE TRIGGER %1$s BEFORE INSERT - ON ' . $quotedTableName . ' + ON %2$s FOR EACH ROW DECLARE last_Sequence NUMBER; last_InsertID NUMBER; BEGIN - IF (:NEW.' . $quotedName . ' IS NULL OR :NEW.' . $quotedName . ' = 0) THEN - SELECT ' . $sequenceName . '.NEXTVAL INTO :NEW.' . $quotedName . ' FROM DUAL; + IF (:NEW.%3$s IS NULL OR :NEW.%3$s = 0) THEN + SELECT %4$s.NEXTVAL INTO :NEW.%3$s FROM DUAL; ELSE SELECT NVL(Last_Number, 0) INTO last_Sequence - FROM User_Sequences - WHERE Sequence_Name = \'' . $sequence->getName() . '\'; - SELECT :NEW.' . $quotedName . ' INTO last_InsertID FROM DUAL; + FROM USER_SEQUENCES + WHERE Sequence_Name = %5$s; + SELECT :NEW.%3$s INTO last_InsertID FROM DUAL; WHILE (last_InsertID > last_Sequence) LOOP - SELECT ' . $sequenceName . '.NEXTVAL INTO last_Sequence FROM DUAL; + SELECT %4$s.NEXTVAL INTO last_Sequence FROM DUAL; END LOOP; - SELECT ' . $sequenceName . '.NEXTVAL INTO last_Sequence FROM DUAL; END IF; -END;'; +END; +SQL, + $autoincrementIdentifierName, + $quotedTableName, + $quotedName, + $sequenceName, + $this->quoteStringLiteral($sequence->getName()), + ); return $sql; } @@ -607,13 +444,12 @@ public function getCreateAutoincrementSql($name, $table, $start = 1) * * @return string[] */ - public function getDropAutoincrementSql($table) + public function getDropAutoincrementSql(string $table): array { $table = $this->normalizeIdentifier($table); $autoincrementIdentifierName = $this->getAutoincrementIdentifierName($table); $identitySequenceName = $this->getIdentitySequenceName( $table->isQuoted() ? $table->getQuotedName($this) : $table->getName(), - '', ); return [ @@ -631,7 +467,7 @@ public function getDropAutoincrementSql($table) * * @param string $name The identifier to normalize. */ - private function normalizeIdentifier($name): Identifier + private function normalizeIdentifier(string $name): Identifier { $identifier = new Identifier($name); @@ -643,6 +479,8 @@ private function normalizeIdentifier($name): Identifier * * if the new string exceeds max identifier length, * keeps $suffix, cuts from $identifier as much as the part exceeding. + * + * @param non-empty-string $suffix */ private function addSuffix(string $identifier, string $suffix): string { @@ -669,191 +507,62 @@ private function getAutoincrementIdentifierName(Identifier $table): string : $identifierName; } - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTableForeignKeysSQL($table) + public function getDropForeignKeySQL(string $foreignKey, string $table): string { - $table = $this->normalizeIdentifier($table); - $table = $this->quoteStringLiteral($table->getName()); - - return "SELECT alc.constraint_name, - alc.DELETE_RULE, - cols.column_name \"local_column\", - cols.position, - ( - SELECT r_cols.table_name - FROM user_cons_columns r_cols - WHERE alc.r_constraint_name = r_cols.constraint_name - AND r_cols.position = cols.position - ) AS \"references_table\", - ( - SELECT r_cols.column_name - FROM user_cons_columns r_cols - WHERE alc.r_constraint_name = r_cols.constraint_name - AND r_cols.position = cols.position - ) AS \"foreign_column\" - FROM user_cons_columns cols - JOIN user_constraints alc - ON alc.constraint_name = cols.constraint_name - AND alc.constraint_type = 'R' - AND alc.table_name = " . $table . ' - ORDER BY cols.constraint_name ASC, cols.position ASC'; - } - - /** - * @deprecated - * - * {@inheritDoc} - */ - public function getListTableConstraintsSQL($table) - { - $table = $this->normalizeIdentifier($table); - $table = $this->quoteStringLiteral($table->getName()); - - return 'SELECT * FROM user_constraints WHERE table_name = ' . $table; - } - - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTableColumnsSQL($table, $database = null) - { - $table = $this->normalizeIdentifier($table); - $table = $this->quoteStringLiteral($table->getName()); - - $tabColumnsTableName = 'user_tab_columns'; - $colCommentsTableName = 'user_col_comments'; - $tabColumnsOwnerCondition = ''; - $colCommentsOwnerCondition = ''; - - if ($database !== null && $database !== '/') { - $database = $this->normalizeIdentifier($database); - $database = $this->quoteStringLiteral($database->getName()); - $tabColumnsTableName = 'all_tab_columns'; - $colCommentsTableName = 'all_col_comments'; - $tabColumnsOwnerCondition = ' AND c.owner = ' . $database; - $colCommentsOwnerCondition = ' AND d.OWNER = c.OWNER'; - } - - return sprintf( - <<<'SQL' -SELECT c.*, - ( - SELECT d.comments - FROM %s d - WHERE d.TABLE_NAME = c.TABLE_NAME%s - AND d.COLUMN_NAME = c.COLUMN_NAME - ) AS comments -FROM %s c -WHERE c.table_name = %s%s -ORDER BY c.column_id -SQL - , - $colCommentsTableName, - $colCommentsOwnerCondition, - $tabColumnsTableName, - $table, - $tabColumnsOwnerCondition, - ); - } - - /** - * {@inheritDoc} - */ - public function getDropForeignKeySQL($foreignKey, $table) - { - if ($foreignKey instanceof ForeignKeyConstraint) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $foreignKey as a ForeignKeyConstraint object to %s is deprecated.' - . ' Pass it as a quoted name instead.', - __METHOD__, - ); - } else { - $foreignKey = new Identifier($foreignKey); - } - - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - } else { - $table = new Identifier($table); - } - - $foreignKey = $foreignKey->getQuotedName($this); - $table = $table->getQuotedName($this); - - return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey; + return $this->getDropConstraintSQL($foreignKey, $table); } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey): string { - $referentialAction = ''; + $sql = ''; if ($foreignKey->hasOption('onDelete')) { $referentialAction = $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onDelete')); + + if ($referentialAction !== '') { + $sql .= ' ON DELETE ' . $referentialAction; + } } - if ($referentialAction !== '') { - return ' ON DELETE ' . $referentialAction; + $deferrabilitySQL = $this->getConstraintDeferrabilitySQL($foreignKey); + + if ($deferrabilitySQL !== '') { + $sql .= $deferrabilitySQL; } - return ''; + return $sql; } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function getForeignKeyReferentialActionSQL($action) + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getForeignKeyReferentialActionSQL(string $action): string { $action = strtoupper($action); - switch ($action) { - case 'RESTRICT': // RESTRICT is not supported, therefore falling back to NO ACTION. - case 'NO ACTION': - // NO ACTION cannot be declared explicitly, - // therefore returning empty string to indicate to OMIT the referential clause. - return ''; - - case 'CASCADE': - case 'SET NULL': - return $action; - - default: - // SET DEFAULT is not supported, throw exception instead. - throw new InvalidArgumentException('Invalid foreign key action: ' . $action); + if ($action === 'RESTRICT') { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6707', + 'Relying on automatic conversion of RESTRICT to NO ACTION for Oracle is deprecated.' + . ' Use NO ACTION explicitly instead.', + ); } + + return match ($action) { + 'RESTRICT', + 'NO ACTION' => '', + 'CASCADE', + 'SET NULL' => $action, + default => throw new InvalidArgumentException(sprintf('Invalid foreign key action "%s".', $action)), + }; } - /** - * {@inheritDoc} - */ - public function getCreateDatabaseSQL($name) + public function getCreateDatabaseSQL(string $name): string { return 'CREATE USER ' . $name; } - /** - * {@inheritDoc} - */ - public function getDropDatabaseSQL($name) + public function getDropDatabaseSQL(string $name): string { return 'DROP USER ' . $name . ' CASCADE'; } @@ -861,25 +570,19 @@ public function getDropDatabaseSQL($name) /** * {@inheritDoc} */ - public function getAlterTableSQL(TableDiff $diff) + public function getAlterTableSQL(TableDiff $diff): array { - $sql = []; - $commentsSQL = []; - $columnSql = []; - - $fields = []; + $sql = []; + $commentsSQL = []; + $addColumnSQL = []; - $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); + $tableNameSQL = $diff->getOldTable()->getQuotedName($this); foreach ($diff->getAddedColumns() as $column) { - if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { - continue; - } - - $fields[] = $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); - $comment = $this->getColumnComment($column); + $addColumnSQL[] = $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + $comment = $column->getComment(); - if ($comment === null || $comment === '') { + if ($comment === '') { continue; } @@ -890,25 +593,34 @@ public function getAlterTableSQL(TableDiff $diff) ); } - if (count($fields) > 0) { - $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ADD (' . implode(', ', $fields) . ')'; + if (count($addColumnSQL) > 0) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ADD (' . implode(', ', $addColumnSQL) . ')'; } - $fields = []; - foreach ($diff->getModifiedColumns() as $columnDiff) { - if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { - continue; - } - + $modifyColumnSQL = []; + foreach ($diff->getChangedColumns() as $columnDiff) { $newColumn = $columnDiff->getNewColumn(); + $oldColumn = $columnDiff->getOldColumn(); + + // Column names in Oracle are case insensitive and automatically uppercased on the server. + if ($columnDiff->hasNameChanged()) { + $newColumnName = $newColumn->getQuotedName($this); + $oldColumnName = $oldColumn->getQuotedName($this); + + $sql = array_merge( + $sql, + $this->getRenameColumnSQL($tableNameSQL, $oldColumnName, $newColumnName), + ); + } + $countChangedProperties = $columnDiff->countChangedProperties(); // Do not generate column alteration clause if type is binary and only fixed property has changed. // Oracle only supports binary type columns with variable length. // Avoids unnecessary table alteration statements. if ( $newColumn->getType() instanceof BinaryType && $columnDiff->hasFixedChanged() && - count($columnDiff->changedProperties) === 1 + $countChangedProperties === 1 ) { continue; } @@ -918,85 +630,52 @@ public function getAlterTableSQL(TableDiff $diff) /** * Do not add query part if only comment has changed */ - if (! ($columnHasChangedComment && count($columnDiff->changedProperties) === 1)) { + if ($countChangedProperties > ($columnHasChangedComment ? 1 : 0)) { $newColumnProperties = $newColumn->toArray(); - if (! $columnDiff->hasNotNullChanged()) { - unset($newColumnProperties['notnull']); - } + $oldSQL = $this->getColumnDeclarationSQL('', $oldColumn->toArray()); + $newSQL = $this->getColumnDeclarationSQL('', $newColumnProperties); - $fields[] = $newColumn->getQuotedName($this) . $this->getColumnDeclarationSQL('', $newColumnProperties); + if ($newSQL !== $oldSQL) { + if (! $columnDiff->hasNotNullChanged()) { + unset($newColumnProperties['notnull']); + $newSQL = $this->getColumnDeclarationSQL('', $newColumnProperties); + } + + $modifyColumnSQL[] = $newColumn->getQuotedName($this) . $newSQL; + } } - if (! $columnHasChangedComment) { + if (! $columnDiff->hasCommentChanged()) { continue; } $commentsSQL[] = $this->getCommentOnColumnSQL( $tableNameSQL, $newColumn->getQuotedName($this), - $this->getColumnComment($newColumn), + $newColumn->getComment(), ); } - if (count($fields) > 0) { - $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' MODIFY (' . implode(', ', $fields) . ')'; - } - - foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { - if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { - continue; - } - - $oldColumnName = new Identifier($oldColumnName); - - $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) - . ' TO ' . $column->getQuotedName($this); + if (count($modifyColumnSQL) > 0) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' MODIFY (' . implode(', ', $modifyColumnSQL) . ')'; } - $fields = []; + $dropColumnSQL = []; foreach ($diff->getDroppedColumns() as $column) { - if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { - continue; - } - - $fields[] = $column->getQuotedName($this); - } - - if (count($fields) > 0) { - $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' DROP (' . implode(', ', $fields) . ')'; + $dropColumnSQL[] = $column->getQuotedName($this); } - $tableSql = []; - - if (! $this->onSchemaAlterTable($diff, $tableSql)) { - $sql = array_merge($sql, $commentsSQL); - - $newName = $diff->getNewName(); - - if ($newName !== false) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5663', - 'Generation of "rename table" SQL using %s is deprecated. Use getRenameTableSQL() instead.', - __METHOD__, - ); - - $sql[] = sprintf( - 'ALTER TABLE %s RENAME TO %s', - $tableNameSQL, - $newName->getQuotedName($this), - ); - } - - $sql = array_merge( - $this->getPreAlterTableIndexForeignKeySQL($diff), - $sql, - $this->getPostAlterTableIndexForeignKeySQL($diff), - ); + if (count($dropColumnSQL) > 0) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' DROP (' . implode(', ', $dropColumnSQL) . ')'; } - return array_merge($sql, $tableSql, $columnSql); + return array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $commentsSQL, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); } /** @@ -1004,10 +683,10 @@ public function getAlterTableSQL(TableDiff $diff) * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ - public function getColumnDeclarationSQL($name, array $column) + public function getColumnDeclarationSQL(string $name, array $column): string { if (isset($column['columnDefinition'])) { - $columnDef = $this->getCustomTypeDeclarationSQL($column); + $declaration = $column['columnDefinition']; } else { $default = $this->getDefaultValueDeclarationSQL($column); @@ -1017,43 +696,19 @@ public function getColumnDeclarationSQL($name, array $column) $notnull = $column['notnull'] ? ' NOT NULL' : ' NULL'; } - if (! empty($column['unique'])) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5656', - 'The usage of the "unique" column property is deprecated. Use unique constraints instead.', - ); - - $unique = ' ' . $this->getUniqueFieldDeclarationSQL(); - } else { - $unique = ''; - } - - if (! empty($column['check'])) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5656', - 'The usage of the "check" column property is deprecated.', - ); - - $check = ' ' . $column['check']; - } else { - $check = ''; - } - - $typeDecl = $column['type']->getSQLDeclaration($column, $this); - $columnDef = $typeDecl . $default . $notnull . $unique . $check; + $typeDecl = $column['type']->getSQLDeclaration($column, $this); + $declaration = $typeDecl . $default . $notnull; } - return $name . ' ' . $columnDef; + return $name . ' ' . $declaration; } /** * {@inheritDoc} */ - protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) + protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array { - if (strpos($tableName, '.') !== false) { + if (str_contains($tableName, '.')) { [$schema] = explode('.', $tableName); $oldIndexName = $schema . '.' . $oldIndexName; } @@ -1061,29 +716,8 @@ protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)]; } - /** - * {@inheritDoc} - * - * @deprecated - */ - public function usesSequenceEmulatedIdentityColumns() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5513', - '%s is deprecated.', - __METHOD__, - ); - - return true; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the OraclePlatform class hierarchy. - */ - public function getIdentitySequenceName($tableName, $columnName) + /** @internal The method should be only used by the {@see OraclePlatform} class. */ + protected function getIdentitySequenceName(string $tableName): string { $table = new Identifier($tableName); @@ -1099,144 +733,73 @@ public function getIdentitySequenceName($tableName, $columnName) return $identitySequenceIdentifier->getQuotedName($this); } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function supportsCommentOnStatement() + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsCommentOnStatement(): bool { return true; } - /** - * {@inheritDoc} - */ - public function getName() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4749', - 'OraclePlatform::getName() is deprecated. Identify platforms by their class.', - ); - - return 'oracle'; - } - - /** - * {@inheritDoc} - */ - protected function doModifyLimitQuery($query, $limit, $offset) + protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string { - if ($limit === null && $offset <= 0) { - return $query; + if ($offset > 0) { + $query .= sprintf(' OFFSET %d ROWS', $offset); } - if (preg_match('/^\s*SELECT/i', $query) === 1) { - if (preg_match('/\sFROM\s/i', $query) === 0) { - $query .= ' FROM dual'; - } - - $columns = ['a.*']; - - if ($offset > 0) { - $columns[] = 'ROWNUM AS doctrine_rownum'; - } - - $query = sprintf('SELECT %s FROM (%s) a', implode(', ', $columns), $query); - - if ($limit !== null) { - $query .= sprintf(' WHERE ROWNUM <= %d', $offset + $limit); - } - - if ($offset > 0) { - $query = sprintf('SELECT * FROM (%s) WHERE doctrine_rownum >= %d', $query, $offset + 1); - } + if ($limit !== null) { + $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit); } return $query; } - /** - * {@inheritDoc} - */ - public function getCreateTemporaryTableSnippetSQL() + public function getCreateTemporaryTableSnippetSQL(): string { return 'CREATE GLOBAL TEMPORARY TABLE'; } - /** - * {@inheritDoc} - */ - public function getDateTimeTzFormatString() + public function getDateTimeTzFormatString(): string { return 'Y-m-d H:i:sP'; } - /** - * {@inheritDoc} - */ - public function getDateFormatString() + public function getDateFormatString(): string { return 'Y-m-d 00:00:00'; } - /** - * {@inheritDoc} - */ - public function getTimeFormatString() + public function getTimeFormatString(): string { return '1900-01-01 H:i:s'; } - /** - * {@inheritDoc} - */ - public function getMaxIdentifierLength() + public function getMaxIdentifierLength(): int { - return 30; + return 128; } - /** - * {@inheritDoc} - */ - public function supportsSequences() + public function supportsSequences(): bool { return true; } - /** - * {@inheritDoc} - */ - public function supportsReleaseSavepoints() + public function supportsReleaseSavepoints(): bool { return false; } - /** - * {@inheritDoc} - */ - public function getTruncateTableSQL($tableName, $cascade = false) + public function getTruncateTableSQL(string $tableName, bool $cascade = false): string { $tableIdentifier = new Identifier($tableName); return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this); } - /** - * {@inheritDoc} - */ - public function getDummySelectSQL() + public function getDummySelectSQL(string $expression = '1'): string { - $expression = func_num_args() > 0 ? func_get_arg(0) : '1'; - return sprintf('SELECT %s FROM DUAL', $expression); } - /** - * {@inheritDoc} - */ - protected function initializeDoctrineTypeMappings() + protected function initializeDoctrineTypeMappings(): void { $this->doctrineTypeMapping = [ 'binary_double' => Types::FLOAT, @@ -1256,6 +819,7 @@ protected function initializeDoctrineTypeMappings() 'nvarchar2' => Types::STRING, 'pls_integer' => Types::BOOLEAN, 'raw' => Types::BINARY, + 'real' => Types::SMALLFLOAT, 'rowid' => Types::STRING, 'timestamp' => Types::DATETIME_MUTABLE, 'timestamptz' => Types::DATETIMETZ_MUTABLE, @@ -1265,61 +829,35 @@ protected function initializeDoctrineTypeMappings() ]; } - /** - * {@inheritDoc} - */ - public function releaseSavePoint($savepoint) + public function releaseSavePoint(string $savepoint): string { return ''; } - /** - * {@inheritDoc} - * - * @deprecated Implement {@see createReservedKeywordsList()} instead. - */ - protected function getReservedKeywordsClass() + /** @deprecated */ + protected function createReservedKeywordsList(): KeywordList { Deprecation::triggerIfCalledFromOutside( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4510', - 'OraclePlatform::getReservedKeywordsClass() is deprecated,' - . ' use OraclePlatform::createReservedKeywordsList() instead.', + 'https://github.com/doctrine/dbal/pull/6607', + '%s is deprecated.', + __METHOD__, ); - return Keywords\OracleKeywords::class; + return new OracleKeywords(); } /** * {@inheritDoc} */ - public function getBlobTypeDeclarationSQL(array $column) + public function getBlobTypeDeclarationSQL(array $column): string { return 'BLOB'; } - /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */ - public function getListTableCommentsSQL(string $table, ?string $database = null): string + public function createMetadataProvider(Connection $connection): OracleMetadataProvider { - $tableCommentsName = 'user_tab_comments'; - $ownerCondition = ''; - - if ($database !== null && $database !== '/') { - $tableCommentsName = 'all_tab_comments'; - $ownerCondition = ' AND owner = ' . $this->quoteStringLiteral( - $this->normalizeIdentifier($database)->getName(), - ); - } - - return sprintf( - <<<'SQL' -SELECT comments FROM %s WHERE table_name = %s%s -SQL - , - $tableCommentsName, - $this->quoteStringLiteral($this->normalizeIdentifier($table)->getName()), - $ownerCondition, - ); + return new OracleMetadataProvider($connection, $this); } public function createSchemaManager(Connection $connection): OracleSchemaManager diff --git a/doctrine/dbal/src/Platforms/PostgreSQL/PostgreSQLMetadataProvider.php b/doctrine/dbal/src/Platforms/PostgreSQL/PostgreSQLMetadataProvider.php new file mode 100644 index 000000000..6b6e91a19 --- /dev/null +++ b/doctrine/dbal/src/Platforms/PostgreSQL/PostgreSQLMetadataProvider.php @@ -0,0 +1,729 @@ + 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, + ); + } +} diff --git a/doctrine/dbal/src/Platforms/PostgreSQL100Platform.php b/doctrine/dbal/src/Platforms/PostgreSQL100Platform.php deleted file mode 100644 index 6ee1f90f2..000000000 --- a/doctrine/dbal/src/Platforms/PostgreSQL100Platform.php +++ /dev/null @@ -1,36 +0,0 @@ -useBooleanTrueFalseStrings = (bool) $flag; - } - - /** - * {@inheritDoc} - */ - public function getSubstringExpression($string, $start, $length = null) - { - if ($length === null) { - return 'SUBSTRING(' . $string . ' FROM ' . $start . ')'; - } - - return 'SUBSTRING(' . $string . ' FROM ' . $start . ' FOR ' . $length . ')'; + $this->useBooleanTrueFalseStrings = $flag; } - /** - * {@inheritDoc} - * - * @deprecated Generate dates within the application. - */ - public function getNowExpression() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4753', - 'PostgreSQLPlatform::getNowExpression() is deprecated. Generate dates within the application.', - ); - - return 'LOCALTIMESTAMP(0)'; - } - - /** - * {@inheritDoc} - */ - public function getRegexpExpression() + public function getRegexpExpression(): string { return 'SIMILAR TO'; } - /** - * {@inheritDoc} - */ - public function getLocateExpression($str, $substr, $startPos = false) + public function getLocateExpression(string $string, string $substring, ?string $start = null): string { - if ($startPos !== false) { - $str = $this->getSubstringExpression($str, $startPos); + if ($start !== null) { + $string = $this->getSubstringExpression($string, $start); - return 'CASE WHEN (POSITION(' . $substr . ' IN ' . $str . ') = 0) THEN 0' - . ' ELSE (POSITION(' . $substr . ' IN ' . $str . ') + ' . $startPos . ' - 1) END'; + return 'CASE WHEN (POSITION(' . $substring . ' IN ' . $string . ') = 0) THEN 0' + . ' ELSE (POSITION(' . $substring . ' IN ' . $string . ') + ' . $start . ' - 1) END'; } - return 'POSITION(' . $substr . ' IN ' . $str . ')'; + return sprintf('POSITION(%s IN %s)', $substring, $string); } - /** - * {@inheritDoc} - */ - protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit) - { + protected function getDateArithmeticIntervalExpression( + string $date, + string $operator, + string $interval, + DateIntervalUnit $unit, + ): string { if ($unit === DateIntervalUnit::QUARTER) { - $interval = $this->multiplyInterval((string) $interval, 3); + $interval = $this->multiplyInterval($interval, 3); $unit = DateIntervalUnit::MONTH; } - return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)"; + return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit->value . "')::interval)"; } - /** - * {@inheritDoc} - */ - public function getDateDiffExpression($date1, $date2) + public function getDateDiffExpression(string $date1, string $date2): string { return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))'; } @@ -156,159 +123,41 @@ public function getCurrentDatabaseExpression(): string return 'CURRENT_DATABASE()'; } - /** - * {@inheritDoc} - */ - public function supportsSequences() + public function supportsSequences(): bool { return true; } - /** - * {@inheritDoc} - */ - public function supportsSchemas() + public function supportsSchemas(): bool { return true; } - /** - * {@inheritDoc} - * - * @deprecated - */ - public function getDefaultSchemaName() + public function supportsIdentityColumns(): bool { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5513', - '%s is deprecated.', - __METHOD__, - ); - - return 'public'; - } - - /** - * {@inheritDoc} - */ - public function supportsIdentityColumns() - { - return true; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function supportsPartialIndexes() - { - return true; - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function usesSequenceEmulatedIdentityColumns() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5513', - '%s is deprecated.', - __METHOD__, - ); - return true; } - /** - * {@inheritDoc} - * - * @deprecated - */ - public function getIdentitySequenceName($tableName, $columnName) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5513', - '%s is deprecated.', - __METHOD__, - ); - - return $tableName . '_' . $columnName . '_seq'; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function supportsCommentOnStatement() + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsPartialIndexes(): bool { return true; } - /** - * {@inheritDoc} - * - * @deprecated - */ - public function hasNativeGuidType() + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsCommentOnStatement(): bool { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - return true; } - public function createSelectSQLBuilder(): SelectSQLBuilder - { - return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', null); - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. - */ - public function getListDatabasesSQL() + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListDatabasesSQL(): string { return 'SELECT datname FROM pg_database'; } - /** - * {@inheritDoc} - * - * @deprecated Use {@see PostgreSQLSchemaManager::listSchemaNames()} instead. - */ - public function getListNamespacesSQL() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4503', - 'PostgreSQLPlatform::getListNamespacesSQL() is deprecated,' - . ' use PostgreSQLSchemaManager::listSchemaNames() instead.', - ); - - return "SELECT schema_name AS nspname - FROM information_schema.schemata - WHERE schema_name NOT LIKE 'pg\_%' - AND schema_name != 'information_schema'"; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. - */ - public function getListSequencesSQL($database) + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListSequencesSQL(string $database): string { return 'SELECT sequence_name AS relname, sequence_schema AS schemaname, @@ -320,29 +169,8 @@ public function getListSequencesSQL($database) AND sequence_schema != 'information_schema'"; } - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTablesSQL() - { - return "SELECT quote_ident(table_name) AS table_name, - table_schema AS schema_name - FROM information_schema.tables - WHERE table_schema NOT LIKE 'pg\_%' - AND table_schema != 'information_schema' - AND table_name != 'geometry_columns' - AND table_name != 'spatial_ref_sys' - AND table_type != 'VIEW'"; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. - */ - public function getListViewsSQL($database) + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListViewsSQL(string $database): string { return 'SELECT quote_ident(table_name) AS viewname, table_schema AS schemaname, @@ -351,148 +179,8 @@ public function getListViewsSQL($database) WHERE view_definition IS NOT NULL'; } - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * @param string $table - * @param string|null $database - * - * @return string - */ - public function getListTableForeignKeysSQL($table, $database = null) - { - return 'SELECT quote_ident(r.conname) as conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef - FROM pg_catalog.pg_constraint r - WHERE r.conrelid = - ( - SELECT c.oid - FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n - WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace - ) - AND r.contype = 'f'"; - } - - /** - * @deprecated - * - * {@inheritDoc} - */ - public function getListTableConstraintsSQL($table) - { - $table = new Identifier($table); - $table = $this->quoteStringLiteral($table->getName()); - - return sprintf( - <<<'SQL' -SELECT - quote_ident(relname) as relname -FROM - pg_class -WHERE oid IN ( - SELECT indexrelid - FROM pg_index, pg_class - WHERE pg_class.relname = %s - AND pg_class.oid = pg_index.indrelid - AND (indisunique = 't' OR indisprimary = 't') - ) -SQL - , - $table, - ); - } - - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTableIndexesSQL($table, $database = null) - { - return 'SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary, - pg_index.indkey, pg_index.indrelid, - pg_get_expr(indpred, indrelid) AS where - FROM pg_class, pg_index - WHERE oid IN ( - SELECT indexrelid - FROM pg_index si, pg_class sc, pg_namespace sn - WHERE ' . $this->getTableWhereClause($table, 'sc', 'sn') . ' - AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid - ) AND pg_index.indexrelid = oid'; - } - - /** - * @param string $table - * @param string $classAlias - * @param string $namespaceAlias - */ - private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n'): string - { - $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND "; - if (strpos($table, '.') !== false) { - [$schema, $table] = explode('.', $table); - $schema = $this->quoteStringLiteral($schema); - } else { - $schema = 'ANY(current_schemas(false))'; - } - - $table = new Identifier($table); - $table = $this->quoteStringLiteral($table->getName()); - - return $whereClause . sprintf( - '%s.relname = %s AND %s.nspname = %s', - $classAlias, - $table, - $namespaceAlias, - $schema, - ); - } - - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTableColumnsSQL($table, $database = null) - { - return "SELECT - a.attnum, - quote_ident(a.attname) AS field, - t.typname AS type, - format_type(a.atttypid, a.atttypmod) AS complete_type, - (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation, - (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type, - (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM - pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type, - a.attnotnull AS isnotnull, - (SELECT 't' - FROM pg_index - WHERE c.oid = pg_index.indrelid - AND pg_index.indkey[0] = a.attnum - AND pg_index.indisprimary = 't' - ) AS pri, - (SELECT pg_get_expr(adbin, adrelid) - FROM pg_attrdef - WHERE c.oid = pg_attrdef.adrelid - AND pg_attrdef.adnum=a.attnum - ) AS default, - (SELECT pg_description.description - FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid - ) AS comment - FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n - WHERE " . $this->getTableWhereClause($table, 'c', 'n') . ' - AND a.attnum > 0 - AND a.attrelid = c.oid - AND a.atttypid = t.oid - AND n.oid = c.relnamespace - ORDER BY a.attnum'; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey): string { $query = ''; @@ -502,19 +190,10 @@ public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey); - if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) { - $query .= ' DEFERRABLE'; - } else { - $query .= ' NOT DEFERRABLE'; - } + $deferrabilitySQL = $this->getConstraintDeferrabilitySQL($foreignKey); - if ( - ($foreignKey->hasOption('feferred') && $foreignKey->getOption('feferred') !== false) - || ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) - ) { - $query .= ' INITIALLY DEFERRED'; - } else { - $query .= ' INITIALLY IMMEDIATE'; + if ($deferrabilitySQL !== '') { + $query .= $deferrabilitySQL; } return $query; @@ -523,21 +202,16 @@ public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey /** * {@inheritDoc} */ - public function getAlterTableSQL(TableDiff $diff) + public function getAlterTableSQL(TableDiff $diff): array { $sql = []; $commentsSQL = []; - $columnSql = []; - $table = $diff->getOldTable() ?? $diff->getName($this); + $table = $diff->getOldTable(); $tableNameSQL = $table->getQuotedName($this); foreach ($diff->getAddedColumns() as $addedColumn) { - if ($this->onSchemaAlterTableAddColumn($addedColumn, $diff, $columnSql)) { - continue; - } - $query = 'ADD ' . $this->getColumnDeclarationSQL( $addedColumn->getQuotedName($this), $addedColumn->toArray(), @@ -545,9 +219,9 @@ public function getAlterTableSQL(TableDiff $diff) $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; - $comment = $this->getColumnComment($addedColumn); + $comment = $addedColumn->getComment(); - if ($comment === null || $comment === '') { + if ($comment === '') { continue; } @@ -559,42 +233,28 @@ public function getAlterTableSQL(TableDiff $diff) } foreach ($diff->getDroppedColumns() as $droppedColumn) { - if ($this->onSchemaAlterTableRemoveColumn($droppedColumn, $diff, $columnSql)) { - continue; - } - $query = 'DROP ' . $droppedColumn->getQuotedName($this); $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; } - foreach ($diff->getModifiedColumns() as $columnDiff) { - if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { - continue; - } - - if ($this->isUnchangedBinaryColumn($columnDiff)) { - continue; - } - - $oldColumn = $columnDiff->getOldColumn() ?? $columnDiff->getOldColumnName(); + foreach ($diff->getChangedColumns() as $columnDiff) { + $oldColumn = $columnDiff->getOldColumn(); $newColumn = $columnDiff->getNewColumn(); $oldColumnName = $oldColumn->getQuotedName($this); + $newColumnName = $newColumn->getQuotedName($this); - if ( - $columnDiff->hasTypeChanged() - || $columnDiff->hasPrecisionChanged() - || $columnDiff->hasScaleChanged() - || $columnDiff->hasFixedChanged() - ) { - $type = $newColumn->getType(); - - // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type - $columnDefinition = $newColumn->toArray(); - $columnDefinition['autoincrement'] = false; + if ($columnDiff->hasNameChanged()) { + $sql = array_merge( + $sql, + $this->getRenameColumnSQL($tableNameSQL, $oldColumnName, $newColumnName), + ); + } - // here was a server version check before, but DBAL API does not support this anymore. - $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this); + $newTypeSQLDeclaration = $this->getTypeSQLDeclaration($newColumn); + $oldTypeSQLDeclaration = $this->getTypeSQLDeclaration($oldColumn); + if ($oldTypeSQLDeclaration !== $newTypeSQLDeclaration) { + $query = 'ALTER ' . $newColumnName . ' TYPE ' . $newTypeSQLDeclaration; $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; } @@ -603,143 +263,61 @@ public function getAlterTableSQL(TableDiff $diff) ? ' DROP DEFAULT' : ' SET' . $this->getDefaultValueDeclarationSQL($newColumn->toArray()); - $query = 'ALTER ' . $oldColumnName . $defaultClause; + $query = 'ALTER ' . $newColumnName . $defaultClause; $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; } if ($columnDiff->hasNotNullChanged()) { - $query = 'ALTER ' . $oldColumnName . ' ' . ($newColumn->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL'; + $query = 'ALTER ' . $newColumnName . ' ' . ($newColumn->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL'; $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; } if ($columnDiff->hasAutoIncrementChanged()) { if ($newColumn->getAutoincrement()) { - // add autoincrement - $seqName = $this->getIdentitySequenceName( - $table->getName(), - $oldColumnName, - ); - - $sql[] = 'CREATE SEQUENCE ' . $seqName; - $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM ' - . $tableNameSQL . '))'; - $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')"; + $query = 'ADD GENERATED BY DEFAULT AS IDENTITY'; } else { - // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have - $query = 'ALTER ' . $oldColumnName . ' DROP DEFAULT'; + $query = 'DROP IDENTITY'; } - $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ALTER ' . $newColumnName . ' ' . $query; } - $oldComment = $this->getOldColumnComment($columnDiff); - $newComment = $this->getColumnComment($newColumn); - - if ( - $columnDiff->hasCommentChanged() - || ($columnDiff->getOldColumn() !== null && $oldComment !== $newComment) - ) { - $commentsSQL[] = $this->getCommentOnColumnSQL( - $tableNameSQL, - $newColumn->getQuotedName($this), - $newComment, - ); - } - - if (! $columnDiff->hasLengthChanged()) { + if (! $columnDiff->hasCommentChanged()) { continue; } - $query = 'ALTER ' . $oldColumnName . ' TYPE ' - . $newColumn->getType()->getSQLDeclaration($newColumn->toArray(), $this); - $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; - } - - foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { - if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { - continue; - } - - $oldColumnName = new Identifier($oldColumnName); - - $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) - . ' TO ' . $column->getQuotedName($this); - } - - $tableSql = []; - - if (! $this->onSchemaAlterTable($diff, $tableSql)) { - $sql = array_merge($sql, $commentsSQL); - - $newName = $diff->getNewName(); - - if ($newName !== false) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5663', - 'Generation of "rename table" SQL using %s is deprecated. Use getRenameTableSQL() instead.', - __METHOD__, - ); - - $sql[] = sprintf( - 'ALTER TABLE %s RENAME TO %s', - $tableNameSQL, - $newName->getQuotedName($this), - ); - } - - $sql = array_merge( - $this->getPreAlterTableIndexForeignKeySQL($diff), - $sql, - $this->getPostAlterTableIndexForeignKeySQL($diff), + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $newColumn->getQuotedName($this), + $newColumn->getComment(), ); } - return array_merge($sql, $tableSql, $columnSql); + return array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $commentsSQL, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); } - /** - * Checks whether a given column diff is a logically unchanged binary type column. - * - * Used to determine whether a column alteration for a binary type column can be skipped. - * Doctrine's {@see BinaryType} and {@see BlobType} are mapped to the same database column type on this platform - * as this platform does not have a native VARBINARY/BINARY column type. Therefore the comparator - * might detect differences for binary type columns which do not have to be propagated - * to database as there actually is no difference at database level. - */ - private function isUnchangedBinaryColumn(ColumnDiff $columnDiff): bool + private function getTypeSQLDeclaration(Column $column): string { - $newColumnType = $columnDiff->getNewColumn()->getType(); - - if (! $newColumnType instanceof BinaryType && ! $newColumnType instanceof BlobType) { - return false; - } - - $oldColumn = $columnDiff->getOldColumn() instanceof Column ? $columnDiff->getOldColumn() : null; - - if ($oldColumn !== null) { - $oldColumnType = $oldColumn->getType(); + $type = $column->getType(); - if (! $oldColumnType instanceof BinaryType && ! $oldColumnType instanceof BlobType) { - return false; - } - - return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0; - } - - if ($columnDiff->hasTypeChanged()) { - return false; - } + // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type + $columnDefinition = $column->toArray(); + $columnDefinition['autoincrement'] = false; - return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0; + return $type->getSQLDeclaration($columnDefinition, $this); } /** * {@inheritDoc} */ - protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) + protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array { - if (strpos($tableName, '.') !== false) { + if (str_contains($tableName, '.')) { [$schema] = explode('.', $tableName); $oldIndexName = $schema . '.' . $oldIndexName; } @@ -747,125 +325,84 @@ protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)]; } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function getCommentOnColumnSQL($tableName, $columnName, $comment) - { - $tableName = new Identifier($tableName); - $columnName = new Identifier($columnName); - $comment = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment); - - return sprintf( - 'COMMENT ON COLUMN %s.%s IS %s', - $tableName->getQuotedName($this), - $columnName->getQuotedName($this), - $comment, - ); - } - - /** - * {@inheritDoc} - */ - public function getCreateSequenceSQL(Sequence $sequence) + public function getCreateSequenceSQL(Sequence $sequence): string { return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . ' INCREMENT BY ' . $sequence->getAllocationSize() . ' MINVALUE ' . $sequence->getInitialValue() . ' START ' . $sequence->getInitialValue() . - $this->getSequenceCacheSQL($sequence); + $this->getSequenceCacheSQL($sequence->getCacheSize()); } - /** - * {@inheritDoc} - */ - public function getAlterSequenceSQL(Sequence $sequence) + public function getAlterSequenceSQL(Sequence $sequence): string { return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . ' INCREMENT BY ' . $sequence->getAllocationSize() . - $this->getSequenceCacheSQL($sequence); + $this->getSequenceCacheSQL($sequence->getCacheSize()); } /** * Cache definition for sequences */ - private function getSequenceCacheSQL(Sequence $sequence): string + private function getSequenceCacheSQL(?int $cacheSize): string { - if ($sequence->getCache() > 1) { - return ' CACHE ' . $sequence->getCache(); + if ($cacheSize > 1) { + return ' CACHE ' . $cacheSize; } return ''; } - /** - * {@inheritDoc} - */ - public function getDropSequenceSQL($sequence) + public function getDropSequenceSQL(string $name): string { - return parent::getDropSequenceSQL($sequence) . ' CASCADE'; + return parent::getDropSequenceSQL($name) . ' CASCADE'; } - /** - * {@inheritDoc} - */ - public function getDropForeignKeySQL($foreignKey, $table) + public function getDropForeignKeySQL(string $foreignKey, string $table): string { return $this->getDropConstraintSQL($foreignKey, $table); } - /** - * {@inheritDoc} - */ - public function getDropIndexSQL($index, $table = null) + public function getDropIndexSQL(string $name, string $table): string { - if ($index instanceof Index && $index->isPrimary() && $table !== null) { - $constraintName = $index->getName() === 'primary' ? $this->tableName($table) . '_pkey' : $index->getName(); - - return $this->getDropConstraintSQL($constraintName, $table); + if (str_ends_with($table, '"')) { + $primaryKeyName = substr($table, 0, -1) . '_pkey"'; + } else { + $primaryKeyName = $table . '_pkey'; } - if ($index === '"primary"' && $table !== null) { - $constraintName = $this->tableName($table) . '_pkey'; + if ($name === '"primary"' || $name === $primaryKeyName) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6867', + 'Building the SQL for dropping primary key constraint via %s() is deprecated. Use' + . ' getDropConstraintSQL() instead.', + __METHOD__, + ); - return $this->getDropConstraintSQL($constraintName, $table); + return $this->getDropConstraintSQL($primaryKeyName, $table); } - if ($table !== null) { - $indexName = $index instanceof Index ? $index->getQuotedName($this) : $index; - $tableName = $table instanceof Table ? $table->getQuotedName($this) : $table; - - if (strpos($tableName, '.') !== false) { - [$schema] = explode('.', $tableName); - $index = $schema . '.' . $indexName; - } + if (str_contains($table, '.')) { + [$schema] = explode('.', $table); + $name = $schema . '.' . $name; } - return parent::getDropIndexSQL($index, $table); - } - - /** - * @param Table|string|null $table - * - * @return string - */ - private function tableName($table) - { - return $table instanceof Table ? $table->getName() : (string) $table; + return parent::getDropIndexSQL($name, $table); } /** * {@inheritDoc} */ - protected function _getCreateTableSQL($name, array $columns, array $options = []) + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array { + $this->validateCreateTableOptions($options, __METHOD__); + $queryFields = $this->getColumnDeclarationListSQL($columns); - if (isset($options['primary']) && ! empty($options['primary'])) { + if (! empty($options['primary'])) { $keyColumns = array_unique(array_values($options['primary'])); - $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + $queryFields .= ', PRIMARY KEY (' . implode(', ', $keyColumns) . ')'; } $unlogged = isset($options['unlogged']) && $options['unlogged'] === true ? ' UNLOGGED' : ''; @@ -874,7 +411,7 @@ protected function _getCreateTableSQL($name, array $columns, array $options = [] $sql = [$query]; - if (isset($options['indexes']) && ! empty($options['indexes'])) { + if (! empty($options['indexes'])) { foreach ($options['indexes'] as $index) { $sql[] = $this->getCreateIndexSQL($index, $name); } @@ -882,7 +419,7 @@ protected function _getCreateTableSQL($name, array $columns, array $options = [] if (isset($options['uniqueConstraints'])) { foreach ($options['uniqueConstraints'] as $uniqueConstraint) { - $sql[] = $this->getCreateConstraintSQL($uniqueConstraint, $name); + $sql[] = $this->getCreateUniqueConstraintSQL($uniqueConstraint, $name); } } @@ -905,11 +442,9 @@ protected function _getCreateTableSQL($name, array $columns, array $options = [] * @param mixed $value The value to convert. * @param callable $callback The callback function to use for converting the real boolean value. * - * @return mixed - * * @throws UnexpectedValueException */ - private function convertSingleBooleanValue($value, $callback) + private function convertSingleBooleanValue(mixed $value, callable $callback): mixed { if ($value === null) { return $callback(null); @@ -934,7 +469,10 @@ private function convertSingleBooleanValue($value, $callback) return $callback(true); } - throw new UnexpectedValueException(sprintf("Unrecognized boolean literal '%s'", $value)); + throw new UnexpectedValueException(sprintf( + 'Unrecognized boolean literal, %s given.', + $value, + )); } /** @@ -946,10 +484,8 @@ private function convertSingleBooleanValue($value, $callback) * * @param mixed $item The value(s) to convert. * @param callable $callback The callback function to use for converting the real boolean value(s). - * - * @return mixed */ - private function doConvertBooleans($item, $callback) + private function doConvertBooleans(mixed $item, callable $callback): mixed { if (is_array($item)) { foreach ($item as $key => $value) { @@ -967,7 +503,7 @@ private function doConvertBooleans($item, $callback) * * Postgres wants boolean values converted to the strings 'true'/'false'. */ - public function convertBooleans($item) + public function convertBooleans(mixed $item): mixed { if (! $this->useBooleanTrueFalseStrings) { return parent::convertBooleans($item); @@ -976,7 +512,7 @@ public function convertBooleans($item) return $this->doConvertBooleans( $item, /** @param mixed $value */ - static function ($value) { + static function ($value): string { if ($value === null) { return 'NULL'; } @@ -986,10 +522,7 @@ static function ($value) { ); } - /** - * {@inheritDoc} - */ - public function convertBooleansToDatabaseValue($item) + public function convertBooleansToDatabaseValue(mixed $item): mixed { if (! $this->useBooleanTrueFalseStrings) { return parent::convertBooleansToDatabaseValue($item); @@ -1005,35 +538,27 @@ static function ($value): ?int { } /** - * {@inheritDoc} - * * @param T $item * * @return (T is null ? null : bool) * * @template T */ - public function convertFromBoolean($item) + public function convertFromBoolean(mixed $item): ?bool { - if ($item !== null && in_array(strtolower($item), $this->booleanLiterals['false'], true)) { + if (in_array($item, $this->booleanLiterals['false'], true)) { return false; } return parent::convertFromBoolean($item); } - /** - * {@inheritDoc} - */ - public function getSequenceNextValSQL($sequence) + public function getSequenceNextValSQL(string $sequence): string { return "SELECT NEXTVAL('" . $sequence . "')"; } - /** - * {@inheritDoc} - */ - public function getSetTransactionIsolationSQL($level) + public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string { return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); @@ -1042,7 +567,7 @@ public function getSetTransactionIsolationSQL($level) /** * {@inheritDoc} */ - public function getBooleanTypeDeclarationSQL(array $column) + public function getBooleanTypeDeclarationSQL(array $column): string { return 'BOOLEAN'; } @@ -1050,43 +575,31 @@ public function getBooleanTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getIntegerTypeDeclarationSQL(array $column) + public function getIntegerTypeDeclarationSQL(array $column): string { - if (! empty($column['autoincrement'])) { - return 'SERIAL'; - } - - return 'INT'; + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ - public function getBigIntTypeDeclarationSQL(array $column) + public function getBigIntTypeDeclarationSQL(array $column): string { - if (! empty($column['autoincrement'])) { - return 'BIGSERIAL'; - } - - return 'BIGINT'; + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ - public function getSmallIntTypeDeclarationSQL(array $column) + public function getSmallIntTypeDeclarationSQL(array $column): string { - if (! empty($column['autoincrement'])) { - return 'SMALLSERIAL'; - } - - return 'SMALLINT'; + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ - public function getGuidTypeDeclarationSQL(array $column) + public function getGuidTypeDeclarationSQL(array $column): string { return 'UUID'; } @@ -1094,7 +607,7 @@ public function getGuidTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getDateTimeTypeDeclarationSQL(array $column) + public function getDateTimeTypeDeclarationSQL(array $column): string { return 'TIMESTAMP(0) WITHOUT TIME ZONE'; } @@ -1102,7 +615,7 @@ public function getDateTimeTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getDateTimeTzTypeDeclarationSQL(array $column) + public function getDateTimeTzTypeDeclarationSQL(array $column): string { return 'TIMESTAMP(0) WITH TIME ZONE'; } @@ -1110,7 +623,7 @@ public function getDateTimeTzTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getDateTypeDeclarationSQL(array $column) + public function getDateTypeDeclarationSQL(array $column): string { return 'DATE'; } @@ -1118,7 +631,7 @@ public function getDateTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getTimeTypeDeclarationSQL(array $column) + public function getTimeTypeDeclarationSQL(array $column): string { return 'TIME(0) WITHOUT TIME ZONE'; } @@ -1126,70 +639,55 @@ public function getTimeTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - protected function _getCommonIntegerTypeDeclarationSQL(array $column) + protected function _getCommonIntegerTypeDeclarationSQL(array $column): string { + if (! empty($column['autoincrement'])) { + return ' GENERATED BY DEFAULT AS IDENTITY'; + } + return ''; } - /** - * {@inheritDoc} - */ - protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string { - return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(255)') - : ($length > 0 ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'); + $sql = 'VARCHAR'; + + if ($length !== null) { + $sql .= sprintf('(%d)', $length); + } + + return $sql; } - /** - * {@inheritDoc} - */ - protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed) + protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string { return 'BYTEA'; } - /** - * {@inheritDoc} - */ - public function getClobTypeDeclarationSQL(array $column) + protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string { - return 'TEXT'; + return 'BYTEA'; } /** * {@inheritDoc} */ - public function getName() + public function getClobTypeDeclarationSQL(array $column): string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4749', - 'PostgreSQLPlatform::getName() is deprecated. Identify platforms by their class.', - ); - - return 'postgresql'; + return 'TEXT'; } - /** - * {@inheritDoc} - */ - public function getDateTimeTzFormatString() + public function getDateTimeTzFormatString(): string { return 'Y-m-d H:i:sO'; } - /** - * {@inheritDoc} - */ - public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName) + public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string { return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)'; } - /** - * {@inheritDoc} - */ - public function getTruncateTableSQL($tableName, $cascade = false) + public function getTruncateTableSQL(string $tableName, bool $cascade = false): string { $tableIdentifier = new Identifier($tableName); $sql = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this); @@ -1203,6 +701,8 @@ public function getTruncateTableSQL($tableName, $cascade = false) /** * Get the snippet used to retrieve the default value for a given column + * + * @internal The method should be only used from within the {@see PostgreSQLSchemaManager} class hierarchy. */ public function getDefaultColumnValueSQLSnippet(): string { @@ -1214,18 +714,7 @@ public function getDefaultColumnValueSQLSnippet(): string SQL; } - /** - * {@inheritDoc} - */ - public function getReadLockSQL() - { - return 'FOR SHARE'; - } - - /** - * {@inheritDoc} - */ - protected function initializeDoctrineTypeMappings() + protected function initializeDoctrineTypeMappings(): void { $this->doctrineTypeMapping = [ 'bigint' => Types::BIGINT, @@ -1236,12 +725,10 @@ protected function initializeDoctrineTypeMappings() 'bytea' => Types::BLOB, 'char' => Types::STRING, 'date' => Types::DATE_MUTABLE, - 'datetime' => Types::DATETIME_MUTABLE, 'decimal' => Types::DECIMAL, - 'double' => Types::FLOAT, 'double precision' => Types::FLOAT, 'float' => Types::FLOAT, - 'float4' => Types::FLOAT, + 'float4' => Types::SMALLFLOAT, 'float8' => Types::FLOAT, 'inet' => Types::STRING, 'int' => Types::INTEGER, @@ -1257,7 +744,7 @@ protected function initializeDoctrineTypeMappings() 'serial' => Types::INTEGER, 'serial4' => Types::INTEGER, 'serial8' => Types::BIGINT, - 'real' => Types::FLOAT, + 'real' => Types::SMALLFLOAT, 'smallint' => Types::SMALLINT, 'text' => Types::TEXT, 'time' => Types::TIME_MUTABLE, @@ -1267,95 +754,27 @@ protected function initializeDoctrineTypeMappings() 'tsvector' => Types::TEXT, 'uuid' => Types::GUID, 'varchar' => Types::STRING, - 'year' => Types::DATE_MUTABLE, '_varchar' => Types::STRING, ]; } - /** - * {@inheritDoc} - * - * @deprecated - */ - public function getVarcharMaxLength() + /** @deprecated */ + protected function createReservedKeywordsList(): KeywordList { Deprecation::triggerIfCalledFromOutside( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'PostgreSQLPlatform::getVarcharMaxLength() is deprecated.', - ); - - return 65535; - } - - /** - * {@inheritDoc} - */ - public function getBinaryMaxLength() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'PostgreSQLPlatform::getBinaryMaxLength() is deprecated.', - ); - - return 0; - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function getBinaryDefaultLength() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'Relying on the default binary column length is deprecated, specify the length explicitly.', - ); - - return 0; - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function hasNativeJsonType() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', + 'https://github.com/doctrine/dbal/pull/6607', '%s is deprecated.', __METHOD__, ); - return true; - } - - /** - * {@inheritDoc} - * - * @deprecated Implement {@see createReservedKeywordsList()} instead. - */ - protected function getReservedKeywordsClass() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4510', - 'PostgreSQLPlatform::getReservedKeywordsClass() is deprecated,' - . ' use PostgreSQLPlatform::createReservedKeywordsList() instead.', - ); - - return Keywords\PostgreSQL94Keywords::class; + return new PostgreSQLKeywords(); } /** * {@inheritDoc} */ - public function getBlobTypeDeclarationSQL(array $column) + public function getBlobTypeDeclarationSQL(array $column): string { return 'BYTEA'; } @@ -1365,7 +784,7 @@ public function getBlobTypeDeclarationSQL(array $column) * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ - public function getDefaultValueDeclarationSQL($column) + public function getDefaultValueDeclarationSQL(array $column): string { if (isset($column['autoincrement']) && $column['autoincrement'] === true) { return ''; @@ -1374,12 +793,8 @@ public function getDefaultValueDeclarationSQL($column) return parent::getDefaultValueDeclarationSQL($column); } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function supportsColumnCollation() + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsColumnCollation(): bool { return true; } @@ -1387,40 +802,32 @@ public function supportsColumnCollation() /** * {@inheritDoc} */ - public function getJsonTypeDeclarationSQL(array $column) + public function getJsonTypeDeclarationSQL(array $column): string { if (! empty($column['jsonb'])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6939', + 'The "jsonb" column platform option is deprecated. Use the "JSONB" type instead.', + ); + return 'JSONB'; } return 'JSON'; } - private function getOldColumnComment(ColumnDiff $columnDiff): ?string + /** + * {@inheritDoc} + */ + public function getJsonbTypeDeclarationSQL(array $column): string { - $oldColumn = $columnDiff->getOldColumn(); - - if ($oldColumn !== null) { - return $this->getColumnComment($oldColumn); - } - - return null; + return 'JSONB'; } - /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */ - public function getListTableMetadataSQL(string $table, ?string $schema = null): string + public function createMetadataProvider(Connection $connection): PostgreSQLMetadataProvider { - if ($schema !== null) { - $table = $schema . '.' . $table; - } - - return sprintf( - <<<'SQL' -SELECT obj_description(%s::regclass) AS table_comment; -SQL - , - $this->quoteStringLiteral($table), - ); + return new PostgreSQLMetadataProvider($connection, $this); } public function createSchemaManager(Connection $connection): PostgreSQLSchemaManager diff --git a/doctrine/dbal/src/Platforms/SQLServer/Comparator.php b/doctrine/dbal/src/Platforms/SQLServer/Comparator.php index 41ef422bf..079e70eaf 100644 --- a/doctrine/dbal/src/Platforms/SQLServer/Comparator.php +++ b/doctrine/dbal/src/Platforms/SQLServer/Comparator.php @@ -1,9 +1,12 @@ databaseCollation = $databaseCollation; + public function __construct( + SQLServerPlatform $platform, + private readonly string $databaseCollation, + ComparatorConfig $config = new ComparatorConfig(), + ) { + parent::__construct($platform, $config); } - public function compareTables(Table $fromTable, Table $toTable): TableDiff + public function compareTables(Table $oldTable, Table $newTable): TableDiff { return parent::compareTables( - $this->normalizeColumns($fromTable), - $this->normalizeColumns($toTable), - ); - } - - /** - * {@inheritDoc} - */ - public function diffTable(Table $fromTable, Table $toTable) - { - return parent::diffTable( - $this->normalizeColumns($fromTable), - $this->normalizeColumns($toTable), + $this->normalizeColumns($oldTable), + $this->normalizeColumns($newTable), ); } diff --git a/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php b/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php index dff12221c..ea6bb6027 100644 --- a/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php +++ b/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php @@ -14,12 +14,10 @@ final class SQLServerSelectSQLBuilder implements SelectSQLBuilder { - private SQLServerPlatform $platform; - /** @internal The SQL builder should be instantiated only by database platforms. */ - public function __construct(SQLServerPlatform $platform) - { - $this->platform = $platform; + public function __construct( + private readonly SQLServerPlatform $platform, + ) { } public function buildSQL(SelectQuery $query): string diff --git a/doctrine/dbal/src/Platforms/SQLServer/SQLServerMetadataProvider.php b/doctrine/dbal/src/Platforms/SQLServer/SQLServerMetadataProvider.php new file mode 100644 index 000000000..1835e6a5e --- /dev/null +++ b/doctrine/dbal/src/Platforms/SQLServer/SQLServerMetadataProvider.php @@ -0,0 +1,658 @@ +connection->iterateColumn($sql) as $databaseName) { + yield new DatabaseMetadataRow($databaseName); + } + } + + /** {@inheritDoc} */ + public function getAllSchemaNames(): iterable + { + $sql = <<<'SQL' + SELECT name + FROM sys.schemas + WHERE name NOT LIKE 'db_%' + AND name NOT IN ('guest', 'INFORMATION_SCHEMA', 'sys') + SQL; + + foreach ($this->connection->iterateColumn($sql) as $schemaName) { + yield new SchemaMetadataRow($schemaName); + } + } + + /** {@inheritDoc} */ + public function getAllTableNames(): iterable + { + $sql = sprintf( + <<<'SQL' + SELECT s.name, + t.name + FROM sys.tables AS t + JOIN sys.schemas AS s + ON t.schema_id = s.schema_id + WHERE %s + AND %s + ORDER BY s.name, + t.name + SQL, + $this->buildSchemaNamePredicate('s.name'), + $this->buildTableNamePredicate('t.name'), + ); + + 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://learn.microsoft.com/en-us/sql/relational-databases/system-catalog-views/extended-properties-catalog-views-sys-extended-properties + * + * @return iterable + * + * @throws Exception + */ + private function getTableColumns(?string $schemaName, ?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT scm.name, + tbl.name, + col.name, + type.name, + col.max_length, + col.is_nullable, + def.definition, + def.name, + col.precision, + col.scale, + col.is_identity, + col.collation_name, + -- CAST avoids driver error for sql_variant type + CAST(prop.value AS NVARCHAR(MAX)) + FROM sys.columns AS col + JOIN sys.types AS type + ON col.user_type_id = type.user_type_id + JOIN sys.tables AS tbl + ON col.object_id = tbl.object_id + JOIN sys.schemas AS scm + ON tbl.schema_id = scm.schema_id + LEFT JOIN sys.default_constraints def + ON col.default_object_id = def.object_id + AND col.object_id = def.parent_object_id + LEFT JOIN sys.extended_properties AS prop + ON tbl.object_id = prop.major_id + AND col.column_id = prop.minor_id + AND prop.name = N'MS_Description' + WHERE %s + ORDER BY scm.name, + tbl.name, + col.column_id + SQL, + $this->buildTableQueryPredicate('scm', $schemaName, 'tbl', $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 + { + [ + $schemaName, + $tableName, + $columnName, + $dbType, + $length, + $isNullable, + $defaultExpression, + $defaultConstraintName, + $precision, + $scale, + $isIdentity, + $collationName, + $description, + ] = $row; + + $length = (int) $length; + + switch ($dbType) { + case 'nchar': + case 'ntext': + // Unicode data requires 2 bytes per character + $length = (int) ($length / 2); + break; + + case 'nvarchar': + if ($length === -1) { + break; + } + + // Unicode data requires 2 bytes per character + $length = (int) ($length / 2); + break; + + case 'varchar': + // TEXT type is returned as VARCHAR(MAX) with a length of -1 + if ($length === -1) { + $dbType = 'text'; + } + + break; + + case 'varbinary': + if ($length === -1) { + $dbType = 'blob'; + } + + break; + } + + $type = $this->platform->getDoctrineTypeMapping($dbType); + + $editor = Column::editor() + ->setQuotedName($columnName) + ->setTypeName( + $this->platform->getDoctrineTypeMapping($dbType), + ) + ->setNotNull(! $isNullable) + ->setAutoincrement((bool) $isIdentity); + + if ($precision !== null) { + $editor->setPrecision((int) $precision); + } + + if ($scale !== null) { + $editor->setScale((int) $scale); + } + + if ($dbType === 'char' || $dbType === 'nchar' || $dbType === 'binary') { + $editor->setFixed(true); + } + + if ($description !== null) { + $editor->setComment($description); + } + + if ($length !== 0 && ($type === 'text' || $type === 'string' || $type === 'binary')) { + $editor->setLength($length); + } + + if ($defaultExpression !== null) { + $editor + ->setDefaultValue($this->parseDefaultExpression($defaultExpression)) + ->setDefaultConstraintName($defaultConstraintName); + } + + $editor->setCollation($collationName); + + return new TableColumnMetadataRow($schemaName, $tableName, $editor->create()); + } + + private function parseDefaultExpression(string $value): ?string + { + while (preg_match('/^\((.*)\)$/s', $value, $matches) === 1) { + $value = $matches[1]; + } + + if ($value === 'NULL') { + return null; + } + + if (preg_match('/^\'(.*)\'$/s', $value, $matches) === 1) { + $value = str_replace("''", "'", $matches[1]); + } + + if ($value === 'getdate()') { + return $this->platform->getCurrentTimestampSQL(); + } + + return $value; + } + + /** {@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); + } + + /** + * @return iterable + * + * @throws Exception + */ + private function getIndexColumns(?string $schemaName, ?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT s.name, + t.name, + i.name, + i.is_unique, + i.type, + c.name + FROM sys.tables AS t + JOIN sys.schemas AS s + ON t.schema_id = s.schema_id + JOIN sys.indexes AS i + ON t.object_id = i.object_id + JOIN sys.index_columns AS idxcol + ON i.object_id = idxcol.object_id + AND i.index_id = idxcol.index_id + JOIN sys.columns AS c + ON idxcol.object_id = c.object_id + AND idxcol.column_id = c.column_id + WHERE %s + AND i.is_primary_key = 0 + ORDER BY s.name, + t.name, + i.name, + idxcol.key_ordinal + SQL, + $this->buildTableQueryPredicate('s', $schemaName, 't', $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: (int) $row[4] === 1, + predicate: null, + 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); + } + + /** + * @return iterable + * + * @throws Exception + */ + private function getPrimaryKeyConstraintColumns(?string $schemaName, ?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT s.name, + t.name, + i.name, + i.type, + c.name + FROM sys.schemas s + INNER JOIN sys.tables t + ON t.schema_id = s.schema_id + INNER JOIN sys.indexes i + ON i.object_id = t.object_id + AND i.is_primary_key = 1 + INNER JOIN sys.index_columns ic + ON ic.object_id = t.object_id + AND ic.index_id = i.index_id + INNER JOIN sys.columns c + ON c.object_id = t.object_id + AND c.column_id = ic.column_id + WHERE %s + ORDER BY s.name, + t.name, + ic.key_ordinal + SQL, + $this->buildTableQueryPredicate('s', $schemaName, 't', $tableName, $params), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new PrimaryKeyConstraintColumnRow( + schemaName: $row[0], + tableName: $row[1], + constraintName: $row[2], + isClustered: (int) $row[3] === 1, + columnName: $row[4], + ); + } + } + + /** {@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); + } + + /** + * @return iterable + * + * @throws Exception + */ + private function getForeignKeyConstraintColumns(?string $schemaName, ?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT pks.name, + pkt.name, + fk.name, + fks.name, + fkt.name, + fk.update_referential_action_desc, + fk.delete_referential_action_desc, + pkc.name, + fkc.name + FROM sys.foreign_keys AS fk + JOIN sys.foreign_key_columns AS c + ON fk.object_id = c.constraint_object_id + JOIN sys.tables AS pkt + ON pkt.object_id = fk.parent_object_id + JOIN sys.schemas AS pks + ON pks.schema_id = pkt.schema_id + JOIN sys.columns AS pkc + ON pkc.object_id = c.parent_object_id + AND pkc.column_id = c.parent_column_id + JOIN sys.tables AS fkt + ON fkt.object_id = fk.referenced_object_id + JOIN sys.schemas AS fks + ON fks.schema_id = fkt.schema_id + JOIN sys.columns AS fkc + ON fkc.object_id = c.referenced_object_id + AND fkc.column_id = c.referenced_column_id + WHERE %s + ORDER BY pks.name, + pkt.name, + fk.name, + c.constraint_column_id +SQL, + $this->buildTableQueryPredicate('pks', $schemaName, 'pkt', $tableName, $params), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new ForeignKeyConstraintColumnMetadataRow( + referencingSchemaName: $row[0], + referencingTableName: $row[1], + id: null, + name: $row[2], + referencedSchemaName: $row[3], + referencedTableName: $row[4], + matchType: MatchType::SIMPLE, + onUpdateAction: $this->createReferentialAction($row[5]), + onDeleteAction: $this->createReferentialAction($row[6]), + isDeferrable: false, + isDeferred: false, + referencingColumnName: $row[7], + referencedColumnName: $row[8], + ); + } + } + + private function createReferentialAction(string $value): ReferentialAction + { + $action = ReferentialAction::tryFrom(str_replace('_', ' ', $value)); + assert($action !== null); + + return $action; + } + + /** {@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); + } + + /** + * @return iterable + * + * @throws Exception + */ + private function getTableOptions(?string $schemaName, ?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT scm.name, + tbl.name, + p.value + FROM sys.tables AS tbl + JOIN sys.schemas AS scm + ON tbl.schema_id = scm.schema_id + LEFT JOIN sys.extended_properties AS p + ON p.major_id = tbl.object_id + AND p.minor_id = 0 + AND p.class = 1 + AND p.name = N'MS_Description' + WHERE %s + SQL, + $this->buildTableQueryPredicate('scm', $schemaName, 'tbl', $tableName, $params), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new TableMetadataRow($row[0], $row[1], [ + 'comment' => $row[2], + ]); + } + } + + /** + * @param list $params + * + * @return non-empty-string + */ + private function buildTableQueryPredicate( + string $schemaRelation, + ?string $schemaName, + string $tableRelation, + ?string $tableName, + array &$params, + ): string { + assert(($tableName === null) === ($schemaName === null)); + + $conditions = []; + + if ($tableName !== null && $schemaName !== null) { + $conditions = [sprintf('%s.name = ?', $schemaRelation)]; + $params[] = $schemaName; + + $conditions[] = sprintf('%s.name = ?', $tableRelation); + $params[] = $tableName; + } + + $conditions[] = $this->buildSchemaNamePredicate($schemaRelation . '.name'); + $conditions[] = $this->buildTableNamePredicate($tableRelation . '.name'); + + return implode(' AND ', $conditions); + } + + /** + * {@inheritDoc} + * + * @link https://learn.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-views-transact-sql + */ + public function getAllViews(): iterable + { + $sql = sprintf( + <<<'SQL' + SELECT s.name, + v.name, + m.definition + FROM sys.views v + JOIN sys.schemas s + ON v.schema_id = s.schema_id + JOIN sys.sql_modules m + ON v.object_id = m.object_id + WHERE %s + ORDER BY s.name, + v.name + SQL, + $this->buildSchemaNamePredicate('s.name'), + ); + + foreach ($this->connection->iterateNumeric($sql) as $row) { + yield new ViewMetadataRow(...$row); + } + } + + /** + * {@inheritDoc} + * + * @link https://learn.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-sequences-transact-sql + */ + public function getAllSequences(): iterable + { + $sql = <<<'SQL' + SELECT scm.name, + seq.name, + seq.increment, + seq.start_value + FROM sys.sequences AS seq + JOIN sys.schemas AS scm + ON scm.schema_id = seq.schema_id + SQL; + + 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 buildSchemaNamePredicate(string $columnName): string + { + return sprintf( + "%1\$s NOT LIKE 'db\_%%' AND %1\$s NOT IN ('guest', 'INFORMATION_SCHEMA', 'sys')", + $columnName, + ); + } + + private function buildTableNamePredicate(string $columnName): string + { + // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams + return sprintf("%s != 'sysdiagrams'", $columnName); + } +} diff --git a/doctrine/dbal/src/Platforms/SQLServer2012Platform.php b/doctrine/dbal/src/Platforms/SQLServer2012Platform.php deleted file mode 100644 index a8ba2fa0b..000000000 --- a/doctrine/dbal/src/Platforms/SQLServer2012Platform.php +++ /dev/null @@ -1,13 +0,0 @@ -getConvertExpression('date', 'GETDATE()'); } - /** - * {@inheritDoc} - */ - public function getCurrentTimeSQL() + public function getCurrentTimeSQL(): string { return $this->getConvertExpression('time', 'GETDATE()'); } @@ -82,101 +82,53 @@ public function getCurrentTimeSQL() * @param string $dataType The target native data type. Alias data types cannot be used. * @param string $expression The SQL expression to convert. */ - private function getConvertExpression($dataType, $expression): string + private function getConvertExpression(string $dataType, string $expression): string { return sprintf('CONVERT(%s, %s)', $dataType, $expression); } - /** - * {@inheritDoc} - */ - protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit) - { + protected function getDateArithmeticIntervalExpression( + string $date, + string $operator, + string $interval, + DateIntervalUnit $unit, + ): string { $factorClause = ''; if ($operator === '-') { $factorClause = '-1 * '; } - return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')'; + return 'DATEADD(' . $unit->value . ', ' . $factorClause . $interval . ', ' . $date . ')'; } - /** - * {@inheritDoc} - */ - public function getDateDiffExpression($date1, $date2) + public function getDateDiffExpression(string $date1, string $date2): string { return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')'; } - /** - * {@inheritDoc} - * - * Microsoft SQL Server prefers "autoincrement" identity columns - * since sequences can only be emulated with a table. - * - * @deprecated - */ - public function prefersIdentityColumns() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/1519', - 'SQLServerPlatform::prefersIdentityColumns() is deprecated.', - ); - - return true; - } - /** * {@inheritDoc} * * Microsoft SQL Server supports this through AUTO_INCREMENT columns. */ - public function supportsIdentityColumns() + public function supportsIdentityColumns(): bool { return true; } - /** - * {@inheritDoc} - */ - public function supportsReleaseSavepoints() + public function supportsReleaseSavepoints(): bool { return false; } - /** - * {@inheritDoc} - */ - public function supportsSchemas() + public function supportsSchemas(): bool { return true; } - /** - * {@inheritDoc} - * - * @deprecated - */ - public function getDefaultSchemaName() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5513', - '%s is deprecated.', - __METHOD__, - ); - - return 'dbo'; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function supportsColumnCollation() + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsColumnCollation(): bool { return true; } @@ -200,12 +152,8 @@ public function getCreateSequenceSQL(Sequence $sequence): string ' MINVALUE ' . $sequence->getInitialValue(); } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. - */ - public function getListSequencesSQL($database) + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListSequencesSQL(string $database): string { return 'SELECT seq.name, CAST( @@ -217,93 +165,28 @@ public function getListSequencesSQL($database) FROM sys.sequences AS seq'; } - /** - * {@inheritDoc} - */ - public function getSequenceNextValSQL($sequence) + public function getSequenceNextValSQL(string $sequence): string { return 'SELECT NEXT VALUE FOR ' . $sequence; } - /** - * {@inheritDoc} - * - * @deprecated - */ - public function hasNativeGuidType() + public function getDropForeignKeySQL(string $foreignKey, string $table): string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - - return true; + return $this->getDropConstraintSQL($foreignKey, $table); } - /** - * {@inheritDoc} - */ - public function getDropForeignKeySQL($foreignKey, $table) + public function getDropIndexSQL(string $name, string $table): string { - if (! $foreignKey instanceof ForeignKeyConstraint) { - $foreignKey = new Identifier($foreignKey); - } - - if (! $table instanceof Table) { - $table = new Identifier($table); - } - - $foreignKey = $foreignKey->getQuotedName($this); - $table = $table->getQuotedName($this); - - return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey; + return 'DROP INDEX ' . $name . ' ON ' . $table; } /** * {@inheritDoc} */ - public function getDropIndexSQL($index, $table = null) + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array { - if ($index instanceof Index) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $index as an Index object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $index = $index->getQuotedName($this); - } elseif (! is_string($index)) { - throw new InvalidArgumentException( - __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.', - ); - } - - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as an Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $table = $table->getQuotedName($this); - } elseif (! is_string($table)) { - throw new InvalidArgumentException( - __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.', - ); - } + $this->validateCreateTableOptions($options, __METHOD__); - return 'DROP INDEX ' . $index . ' ON ' . $table; - } - - /** - * {@inheritDoc} - */ - protected function _getCreateTableSQL($name, array $columns, array $options = []) - { $defaultConstraintsSql = []; $commentsSql = []; @@ -322,25 +205,23 @@ protected function _getCreateTableSQL($name, array $columns, array $options = [] // Build default constraints SQL statements. if (isset($column['default'])) { $defaultConstraintsSql[] = 'ALTER TABLE ' . $name . - ' ADD' . $this->getDefaultConstraintDeclarationSQL($name, $column); + ' ADD' . $this->getDefaultConstraintDeclarationSQL($column); } - if (empty($column['comment']) && ! is_numeric($column['comment'])) { - continue; + if (! empty($column['comment']) || is_numeric($column['comment'])) { + $commentsSql[] = $this->getCreateColumnCommentSQL($name, $column['name'], $column['comment']); } - - $commentsSql[] = $this->getCreateColumnCommentSQL($name, $column['name'], $column['comment']); } $columnListSql = $this->getColumnDeclarationListSQL($columns); - if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { - foreach ($options['uniqueConstraints'] as $constraintName => $definition) { - $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($constraintName, $definition); + if (! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $definition) { + $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($definition); } } - if (isset($options['primary']) && ! empty($options['primary'])) { + if (! empty($options['primary'])) { $flags = ''; if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) { $flags = ' NONCLUSTERED'; @@ -361,7 +242,7 @@ protected function _getCreateTableSQL($name, array $columns, array $options = [] $sql = [$query]; - if (isset($options['indexes']) && ! empty($options['indexes'])) { + if (! empty($options['indexes'])) { foreach ($options['indexes'] as $index) { $sql[] = $this->getCreateIndexSQL($index, $name); } @@ -376,31 +257,23 @@ protected function _getCreateTableSQL($name, array $columns, array $options = [] return array_merge($sql, $commentsSql, $defaultConstraintsSql); } - /** - * {@inheritDoc} - */ - public function getCreatePrimaryKeySQL(Index $index, $table) + /** @deprecated */ + public function getCreatePrimaryKeySQL(Index $index, string $table): string { - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $identifier = $table->getQuotedName($this); - } else { - $identifier = $table; - } + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6867', + '%s() is deprecated.', + __METHOD__, + ); - $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY'; + $sql = 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY'; if ($index->hasFlag('nonclustered')) { $sql .= ' NONCLUSTERED'; } - return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')'; + return $sql . ' (' . implode(', ', $index->getQuotedColumns($this)) . ')'; } private function unquoteSingleIdentifier(string $possiblyQuotedName): string @@ -421,15 +294,15 @@ private function unquoteSingleIdentifier(string $possiblyQuotedName): string * as column comments are stored in the same property there when * specifying a column's "Description" attribute. * - * @param string $tableName The quoted table name to which the column belongs. - * @param string $columnName The quoted column name to create the comment for. - * @param string|null $comment The column's comment. + * @internal The method should be only used by the {@see SQLServerPlatform} class. * - * @return string + * @param string $tableName The quoted table name to which the column belongs. + * @param string $columnName The quoted column name to create the comment for. + * @param string $comment The column's comment. */ - protected function getCreateColumnCommentSQL($tableName, $columnName, $comment) + protected function getCreateColumnCommentSQL(string $tableName, string $columnName, string $comment): string { - if (strpos($tableName, '.') !== false) { + if (str_contains($tableName, '.')) { [$schemaName, $tableName] = explode('.', $tableName); } else { $schemaName = 'dbo'; @@ -450,33 +323,22 @@ protected function getCreateColumnCommentSQL($tableName, $columnName, $comment) /** * Returns the SQL snippet for declaring a default constraint. * - * @internal The method should be only used from within the SQLServerPlatform class hierarchy. + * @internal The method should be only used by the {@see SQLServerPlatform} class. * - * @param string $table Name of the table to return the default constraint declaration for. * @param mixed[] $column Column definition. - * - * @return string - * - * @throws InvalidArgumentException */ - public function getDefaultConstraintDeclarationSQL($table, array $column) + protected function getDefaultConstraintDeclarationSQL(array $column): string { if (! isset($column['default'])) { - throw new InvalidArgumentException("Incomplete column definition. 'default' required."); + throw new InvalidArgumentException('Incomplete column definition. "default" required.'); } $columnName = new Identifier($column['name']); - return ' CONSTRAINT ' . - $this->generateDefaultConstraintName($table, $column['name']) . - $this->getDefaultValueDeclarationSQL($column) . - ' FOR ' . $columnName->getQuotedName($this); + return $this->getDefaultValueDeclarationSQL($column) . ' FOR ' . $columnName->getQuotedName($this); } - /** - * {@inheritDoc} - */ - public function getCreateIndexSQL(Index $index, $table) + public function getCreateIndexSQL(Index $index, string $table): string { $constraint = parent::getCreateIndexSQL($index, $table); @@ -487,10 +349,7 @@ public function getCreateIndexSQL(Index $index, $table) return $constraint; } - /** - * {@inheritDoc} - */ - protected function getCreateIndexSQLFlags(Index $index) + protected function getCreateIndexSQLFlags(Index $index): string { $type = ''; if ($index->isUnique()) { @@ -508,10 +367,8 @@ protected function getCreateIndexSQLFlags(Index $index) /** * Extend unique key constraint with required filters - * - * @param string $sql */ - private function _appendUniqueConstraintDefinition($sql, Index $index): string + private function _appendUniqueConstraintDefinition(string $sql, Index $index): string { $fields = []; @@ -525,38 +382,30 @@ private function _appendUniqueConstraintDefinition($sql, Index $index): string /** * {@inheritDoc} */ - public function getAlterTableSQL(TableDiff $diff) + public function getAlterTableSQL(TableDiff $diff): array { $queryParts = []; $sql = []; - $columnSql = []; $commentsSql = []; - $table = $diff->getOldTable() ?? $diff->getName($this); + $table = $diff->getOldTable(); $tableName = $table->getName(); foreach ($diff->getAddedColumns() as $column) { - if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { - continue; - } - $columnProperties = $column->toArray(); $addColumnSql = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnProperties); if (isset($columnProperties['default'])) { - $addColumnSql .= ' CONSTRAINT ' . $this->generateDefaultConstraintName( - $tableName, - $column->getQuotedName($this), - ) . $this->getDefaultValueDeclarationSQL($columnProperties); + $addColumnSql .= $this->getDefaultValueDeclarationSQL($columnProperties); } $queryParts[] = $addColumnSql; - $comment = $this->getColumnComment($column); + $comment = $column->getComment(); - if (empty($comment) && ! is_numeric($comment)) { + if ($comment === '') { continue; } @@ -568,170 +417,105 @@ public function getAlterTableSQL(TableDiff $diff) } foreach ($diff->getDroppedColumns() as $column) { - if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { - continue; + if ($column->getDefault() !== null) { + $queryParts[] = $this->getAlterTableDropDefaultConstraintClause($column); } $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); } - foreach ($diff->getModifiedColumns() as $columnDiff) { - if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { - continue; - } - - $newColumn = $columnDiff->getNewColumn(); - $newComment = $this->getColumnComment($newColumn); - $hasNewComment = ! empty($newComment) || is_numeric($newComment); - - $oldColumn = $columnDiff->getOldColumn(); - - if ($oldColumn instanceof Column) { - $oldComment = $this->getColumnComment($oldColumn); - $hasOldComment = ! empty($oldComment) || is_numeric($oldComment); - - if ($hasOldComment && $hasNewComment && $oldComment !== $newComment) { - $commentsSql[] = $this->getAlterColumnCommentSQL( - $tableName, - $newColumn->getQuotedName($this), - $newComment, - ); - } elseif ($hasOldComment && ! $hasNewComment) { - $commentsSql[] = $this->getDropColumnCommentSQL( - $tableName, - $newColumn->getQuotedName($this), - ); - } elseif (! $hasOldComment && $hasNewComment) { - $commentsSql[] = $this->getCreateColumnCommentSQL( - $tableName, - $newColumn->getQuotedName($this), - $newComment, - ); - } - } - - // Do not add query part if only comment has changed. - if ($columnDiff->hasCommentChanged() && count($columnDiff->changedProperties) === 1) { - continue; - } + $tableNameSQL = $table->getQuotedName($this); - $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff); + foreach ($diff->getChangedColumns() as $columnDiff) { + $newColumn = $columnDiff->getNewColumn(); + $oldColumn = $columnDiff->getOldColumn(); + $nameChanged = $columnDiff->hasNameChanged(); - if ($requireDropDefaultConstraint) { - $oldColumn = $columnDiff->getOldColumn(); + if ($nameChanged) { + // sp_rename accepts the old name as a qualified name, so it should be quoted. + $oldColumnNameSQL = $oldColumn->getQuotedName($this); - if ($oldColumn !== null) { - $oldColumnName = $oldColumn->getName(); - } else { - $oldColumnName = $columnDiff->oldColumnName; - } + // sp_rename accepts the new name as a literal value, so it cannot be quoted. + $newColumnName = $newColumn->getName(); - $queryParts[] = $this->getAlterTableDropDefaultConstraintClause($tableName, $oldColumnName); + $sql = array_merge( + $sql, + $this->getRenameColumnSQL($tableNameSQL, $oldColumnNameSQL, $newColumnName), + ); } - $columnProperties = $newColumn->toArray(); + $newComment = $newColumn->getComment(); + $hasNewComment = $newComment !== ''; - $queryParts[] = 'ALTER COLUMN ' . - $this->getColumnDeclarationSQL($newColumn->getQuotedName($this), $columnProperties); + $oldComment = $oldColumn->getComment(); + $hasOldComment = $oldComment !== ''; - if ( - ! isset($columnProperties['default']) - || (! $requireDropDefaultConstraint && ! $columnDiff->hasDefaultChanged()) - ) { - continue; + if ($hasOldComment && $hasNewComment && $oldComment !== $newComment) { + $commentsSql[] = $this->getAlterColumnCommentSQL( + $tableName, + $newColumn->getQuotedName($this), + $newComment, + ); + } elseif ($hasOldComment && ! $hasNewComment) { + $commentsSql[] = $this->getDropColumnCommentSQL( + $tableName, + $newColumn->getQuotedName($this), + ); + } elseif (! $hasOldComment && $hasNewComment) { + $commentsSql[] = $this->getCreateColumnCommentSQL( + $tableName, + $newColumn->getQuotedName($this), + $newComment, + ); } - $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($tableName, $newColumn); - } + $columnNameSQL = $newColumn->getQuotedName($this); - $tableNameSQL = $table->getQuotedName($this); + $newDeclarationSQL = $this->getColumnDeclarationSQL($columnNameSQL, $newColumn->toArray()); + $oldDeclarationSQL = $this->getColumnDeclarationSQL($columnNameSQL, $oldColumn->toArray()); + $declarationSQLChanged = $newDeclarationSQL !== $oldDeclarationSQL; - foreach ($diff->getRenamedColumns() as $oldColumnName => $newColumn) { - if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $newColumn, $diff, $columnSql)) { + $defaultChanged = $columnDiff->hasDefaultChanged(); + + if (! $declarationSQLChanged && ! $defaultChanged && ! $nameChanged) { continue; } - $oldColumnName = new Identifier($oldColumnName); + $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff); + + if ($requireDropDefaultConstraint) { + $queryParts[] = $this->getAlterTableDropDefaultConstraintClause($oldColumn); + } - $sql[] = "sp_rename '" . $tableNameSQL . '.' . $oldColumnName->getQuotedName($this) . - "', '" . $newColumn->getQuotedName($this) . "', 'COLUMN'"; + if ($declarationSQLChanged) { + $queryParts[] = 'ALTER COLUMN ' . $newDeclarationSQL; + } - // Recreate default constraint with new column name if necessary (for future reference). - if ($newColumn->getDefault() === null) { + if ( + $newColumn->getDefault() === null + || (! $requireDropDefaultConstraint && ! $defaultChanged) + ) { continue; } - $queryParts[] = $this->getAlterTableDropDefaultConstraintClause( - $tableName, - $oldColumnName->getQuotedName($this), - ); $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($tableName, $newColumn); } - $tableSql = []; - - if ($this->onSchemaAlterTable($diff, $tableSql)) { - return array_merge($tableSql, $columnSql); - } - foreach ($queryParts as $query) { $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; } - $sql = array_merge($sql, $commentsSql); - - $newName = $diff->getNewName(); - - if ($newName !== false) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5663', - 'Generation of "rename table" SQL using %s is deprecated. Use getRenameTableSQL() instead.', - __METHOD__, - ); - - $sql = array_merge($sql, $this->getRenameTableSQL($tableName, $newName->getName())); - } - - $sql = array_merge( + return array_merge( $this->getPreAlterTableIndexForeignKeySQL($diff), $sql, + $commentsSql, $this->getPostAlterTableIndexForeignKeySQL($diff), ); - - return array_merge($sql, $tableSql, $columnSql); } - /** - * {@inheritDoc} - */ - public function getRenameTableSQL(string $oldName, string $newName): array - { - return [ - sprintf('sp_rename %s, %s', $this->quoteStringLiteral($oldName), $this->quoteStringLiteral($newName)), - - /* Rename table's default constraints names - * to match the new table name. - * This is necessary to ensure that the default - * constraints can be referenced in future table - * alterations as the table name is encoded in - * default constraints' names. */ - sprintf( - <<<'SQL' - DECLARE @sql NVARCHAR(MAX) = N''; - SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' - + REPLACE(dc.name, '%s', '%s') + ''', ''OBJECT'';' - FROM sys.default_constraints dc - JOIN sys.tables tbl - ON dc.parent_object_id = tbl.object_id - WHERE tbl.name = %s; - EXEC sp_executesql @sql - SQL, - $this->generateIdentifierName($oldName), - $this->generateIdentifierName($newName), - $this->quoteStringLiteral($newName), - ), - ]; + public function getRenameTableSQL(string $oldName, string $newName): string + { + return $this->getRenameSQL($oldName, $newName); } /** @@ -740,23 +524,29 @@ public function getRenameTableSQL(string $oldName, string $newName): array * @param string $tableName The name of the table to generate the clause for. * @param Column $column The column to generate the clause for. */ - private function getAlterTableAddDefaultConstraintClause($tableName, Column $column): string + private function getAlterTableAddDefaultConstraintClause(string $tableName, Column $column): string { $columnDef = $column->toArray(); $columnDef['name'] = $column->getQuotedName($this); - return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef); + return 'ADD' . $this->getDefaultConstraintDeclarationSQL($columnDef); } /** * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement. - * - * @param string $tableName The name of the table to generate the clause for. - * @param string $columnName The name of the column to generate the clause for. */ - private function getAlterTableDropDefaultConstraintClause($tableName, $columnName): string + private function getAlterTableDropDefaultConstraintClause(Column $column): string { - return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName); + if (! $column->hasPlatformOption(self::OPTION_DEFAULT_CONSTRAINT_NAME)) { + throw new InvalidArgumentException( + 'Column ' . $column->getName() . ' was not properly introspected as it has a default value' + . ' but does not have the default constraint name.', + ); + } + + return 'DROP CONSTRAINT ' . $this->quoteSingleIdentifier( + $column->getPlatformOption(self::OPTION_DEFAULT_CONSTRAINT_NAME), + ); } /** @@ -769,17 +559,9 @@ private function getAlterTableDropDefaultConstraintClause($tableName, $columnNam */ private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff): bool { - $oldColumn = $columnDiff->getOldColumn(); - - // We can only decide whether to drop an existing default constraint - // if we know the original default value. - if (! $oldColumn instanceof Column) { - return false; - } - // We only need to drop an existing default constraint if we know the // column was defined with a default value before. - if ($oldColumn->getDefault() === null) { + if ($columnDiff->getOldColumn()->getDefault() === null) { return false; } @@ -805,15 +587,15 @@ private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff * as column comments are stored in the same property there when * specifying a column's "Description" attribute. * - * @param string $tableName The quoted table name to which the column belongs. - * @param string $columnName The quoted column name to alter the comment for. - * @param string|null $comment The column's comment. + * @internal The method should be only used by the {@see SQLServerPlatform} class. * - * @return string + * @param string $tableName The quoted table name to which the column belongs. + * @param string $columnName The quoted column name to alter the comment for. + * @param string $comment The column's comment. */ - protected function getAlterColumnCommentSQL($tableName, $columnName, $comment) + protected function getAlterColumnCommentSQL(string $tableName, string $columnName, string $comment): string { - if (strpos($tableName, '.') !== false) { + if (str_contains($tableName, '.')) { [$schemaName, $tableName] = explode('.', $tableName); } else { $schemaName = 'dbo'; @@ -842,14 +624,14 @@ protected function getAlterColumnCommentSQL($tableName, $columnName, $comment) * as column comments are stored in the same property there when * specifying a column's "Description" attribute. * + * @internal The method should be only used by the {@see SQLServerPlatform} class. + * * @param string $tableName The quoted table name to which the column belongs. * @param string $columnName The quoted column name to drop the comment for. - * - * @return string */ - protected function getDropColumnCommentSQL($tableName, $columnName) + protected function getDropColumnCommentSQL(string $tableName, string $columnName): string { - if (strpos($tableName, '.') !== false) { + if (str_contains($tableName, '.')) { [$schemaName, $tableName] = explode('.', $tableName); } else { $schemaName = 'dbo'; @@ -869,21 +651,39 @@ protected function getDropColumnCommentSQL($tableName, $columnName) /** * {@inheritDoc} */ - protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) - { - return [sprintf( - "EXEC sp_rename N'%s.%s', N'%s', N'INDEX'", - $tableName, - $oldIndexName, - $index->getQuotedName($this), - ), - ]; + protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array + { + return [$this->getRenameSQL($tableName . '.' . $oldIndexName, $index->getName(), 'INDEX')]; + } + + /** + * Returns the SQL for renaming a column + * + * @param string $tableName The table to rename the column on. + * @param string $oldColumnName The name of the column we want to rename. + * @param string $newColumnName The name we should rename it to. + * + * @return list The sequence of SQL statements for renaming the given column. + */ + protected function getRenameColumnSQL(string $tableName, string $oldColumnName, string $newColumnName): array + { + return [$this->getRenameSQL($tableName . '.' . $oldColumnName, $newColumnName)]; + } + + /** + * Returns the SQL statement that will execute sp_rename with the given arguments. + */ + private function getRenameSQL(string ...$arguments): string + { + return $this->getExecSQL('sp_rename', ...array_map(function (string $argument): string { + return $this->quoteNationalStringLiteral($argument); + }, $arguments)); } /** * Returns the SQL statement for adding an extended property to a database object. * - * @internal The method should be only used from within the SQLServerPlatform class hierarchy. + * @internal The method should be only used by the {@see SQLServerPlatform} class. * * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx * @@ -895,33 +695,38 @@ protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) * @param string|null $level1Name The name of the object at level 1 the property belongs to. * @param string|null $level2Type The type of the object at level 2 the property belongs to. * @param string|null $level2Name The name of the object at level 2 the property belongs to. - * - * @return string */ - public function getAddExtendedPropertySQL( - $name, - $value = null, - $level0Type = null, - $level0Name = null, - $level1Type = null, - $level1Name = null, - $level2Type = null, - $level2Name = null - ) { - return 'EXEC sp_addextendedproperty ' . - 'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral($value ?? '') . ', ' . - 'N' . $this->quoteStringLiteral($level0Type ?? '') . ', ' . $level0Name . ', ' . - 'N' . $this->quoteStringLiteral($level1Type ?? '') . ', ' . $level1Name . - ($level2Type !== null || $level2Name !== null - ? ', N' . $this->quoteStringLiteral($level2Type ?? '') . ', ' . $level2Name - : '' - ); + protected function getAddExtendedPropertySQL( + string $name, + ?string $value = null, + ?string $level0Type = null, + ?string $level0Name = null, + ?string $level1Type = null, + ?string $level1Name = null, + ?string $level2Type = null, + ?string $level2Name = null, + ): string { + $arguments = [ + $this->quoteNationalStringLiteral($name), + $this->quoteNationalStringLiteral($value ?? ''), + $this->quoteNationalStringLiteral($level0Type ?? ''), + $level0Name ?? '', + $this->quoteNationalStringLiteral($level1Type ?? ''), + $level1Name ?? '', + ]; + + if ($level2Type !== null || $level2Name !== null) { + $arguments[] = $this->quoteNationalStringLiteral($level2Type ?? ''); + $arguments[] = $level2Name ?? ''; + } + + return $this->getExecSQL('sp_addextendedproperty', ...$arguments); } /** * Returns the SQL statement for dropping an extended property from a database object. * - * @internal The method should be only used from within the SQLServerPlatform class hierarchy. + * @internal The method should be only used by the {@see SQLServerPlatform} class. * * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx * @@ -932,32 +737,36 @@ public function getAddExtendedPropertySQL( * @param string|null $level1Name The name of the object at level 1 the property belongs to. * @param string|null $level2Type The type of the object at level 2 the property belongs to. * @param string|null $level2Name The name of the object at level 2 the property belongs to. - * - * @return string */ - public function getDropExtendedPropertySQL( - $name, - $level0Type = null, - $level0Name = null, - $level1Type = null, - $level1Name = null, - $level2Type = null, - $level2Name = null - ) { - return 'EXEC sp_dropextendedproperty ' . - 'N' . $this->quoteStringLiteral($name) . ', ' . - 'N' . $this->quoteStringLiteral($level0Type ?? '') . ', ' . $level0Name . ', ' . - 'N' . $this->quoteStringLiteral($level1Type ?? '') . ', ' . $level1Name . - ($level2Type !== null || $level2Name !== null - ? ', N' . $this->quoteStringLiteral($level2Type ?? '') . ', ' . $level2Name - : '' - ); + protected function getDropExtendedPropertySQL( + string $name, + ?string $level0Type = null, + ?string $level0Name = null, + ?string $level1Type = null, + ?string $level1Name = null, + ?string $level2Type = null, + ?string $level2Name = null, + ): string { + $arguments = [ + $this->quoteNationalStringLiteral($name), + $this->quoteNationalStringLiteral($level0Type ?? ''), + $level0Name ?? '', + $this->quoteNationalStringLiteral($level1Type ?? ''), + $level1Name ?? '', + ]; + + if ($level2Type !== null || $level2Name !== null) { + $arguments[] = $this->quoteNationalStringLiteral($level2Type ?? ''); + $arguments[] = $level2Name ?? ''; + } + + return $this->getExecSQL('sp_dropextendedproperty', ...$arguments); } /** * Returns the SQL statement for updating an extended property of a database object. * - * @internal The method should be only used from within the SQLServerPlatform class hierarchy. + * @internal The method should be only used by the {@see SQLServerPlatform} class. * * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx * @@ -969,211 +778,88 @@ public function getDropExtendedPropertySQL( * @param string|null $level1Name The name of the object at level 1 the property belongs to. * @param string|null $level2Type The type of the object at level 2 the property belongs to. * @param string|null $level2Name The name of the object at level 2 the property belongs to. - * - * @return string */ - public function getUpdateExtendedPropertySQL( - $name, - $value = null, - $level0Type = null, - $level0Name = null, - $level1Type = null, - $level1Name = null, - $level2Type = null, - $level2Name = null - ) { - return 'EXEC sp_updateextendedproperty ' . - 'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral($value ?? '') . ', ' . - 'N' . $this->quoteStringLiteral($level0Type ?? '') . ', ' . $level0Name . ', ' . - 'N' . $this->quoteStringLiteral($level1Type ?? '') . ', ' . $level1Name . - ($level2Type !== null || $level2Name !== null - ? ', N' . $this->quoteStringLiteral($level2Type ?? '') . ', ' . $level2Name - : '' - ); - } + protected function getUpdateExtendedPropertySQL( + string $name, + ?string $value = null, + ?string $level0Type = null, + ?string $level0Name = null, + ?string $level1Type = null, + ?string $level1Name = null, + ?string $level2Type = null, + ?string $level2Name = null, + ): string { + $arguments = [ + $this->quoteNationalStringLiteral($name), + $this->quoteNationalStringLiteral($value ?? ''), + $this->quoteNationalStringLiteral($level0Type ?? ''), + $level0Name ?? '', + $this->quoteNationalStringLiteral($level1Type ?? ''), + $level1Name ?? '', + ]; - /** - * {@inheritDoc} - */ - public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName) - { - return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES'; - } + if ($level2Type !== null || $level2Name !== null) { + $arguments[] = $this->quoteNationalStringLiteral($level2Type ?? ''); + $arguments[] = $level2Name ?? ''; + } - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTablesSQL() - { - // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams - // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication - return 'SELECT name, SCHEMA_NAME (uid) AS schema_name FROM sysobjects' - . " WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name"; + return $this->getExecSQL('sp_updateextendedproperty', ...$arguments); } /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * Returns the SQL statement that will execute the given stored procedure with the given arguments. * - * {@inheritDoc} + * @param string $procedureName The name of the stored procedure to execute. + * @param string ...$arguments The SQL fragments representing the arguments to pass to the stored procedure. */ - public function getListTableColumnsSQL($table, $database = null) - { - return "SELECT col.name, - type.name AS type, - col.max_length AS length, - ~col.is_nullable AS notnull, - def.definition AS [default], - col.scale, - col.precision, - col.is_identity AS autoincrement, - col.collation_name AS collation, - CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type - FROM sys.columns AS col - JOIN sys.types AS type - ON col.user_type_id = type.user_type_id - JOIN sys.objects AS obj - ON col.object_id = obj.object_id - JOIN sys.schemas AS scm - ON obj.schema_id = scm.schema_id - LEFT JOIN sys.default_constraints def - ON col.default_object_id = def.object_id - AND col.object_id = def.parent_object_id - LEFT JOIN sys.extended_properties AS prop - ON obj.object_id = prop.major_id - AND col.column_id = prop.minor_id - AND prop.name = 'MS_Description' - WHERE obj.type = 'U' - AND " . $this->getTableWhereClause($table, 'scm.name', 'obj.name'); + private function getExecSQL(string $procedureName, string ...$arguments): string + { + return 'EXEC ' . $this->quoteSingleIdentifier($procedureName) . ' ' . implode(', ', $arguments); } - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * @param string $table - * @param string|null $database - * - * @return string - */ - public function getListTableForeignKeysSQL($table, $database = null) - { - return 'SELECT f.name AS ForeignKey, - SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName, - OBJECT_NAME (f.parent_object_id) AS TableName, - COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName, - SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName, - OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName, - COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName, - f.delete_referential_action_desc, - f.update_referential_action_desc - FROM sys.foreign_keys AS f - INNER JOIN sys.foreign_key_columns AS fc - INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id - ON f.OBJECT_ID = fc.constraint_object_id - WHERE ' . - $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)') . - ' ORDER BY fc.constraint_column_id'; + private function quoteNationalStringLiteral(string $value): string + { + return 'N' . $this->quoteStringLiteral($value); } - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTableIndexesSQL($table, $database = null) - { - return "SELECT idx.name AS key_name, - col.name AS column_name, - ~idx.is_unique AS non_unique, - idx.is_primary_key AS [primary], - CASE idx.type - WHEN '1' THEN 'clustered' - WHEN '2' THEN 'nonclustered' - ELSE NULL - END AS flags - FROM sys.tables AS tbl - JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id - JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id - JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id - JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id - WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . ' - ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC'; + public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string + { + return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES'; } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. - */ - public function getListViewsSQL($database) + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListViewsSQL(string $database): string { return "SELECT name, definition FROM sysobjects INNER JOIN sys.sql_modules ON sysobjects.id = sys.sql_modules.object_id WHERE type = 'V' ORDER BY name"; } - /** - * Returns the where clause to filter schema and table name in a query. - * - * @param string $table The full qualified name of the table. - * @param string $schemaColumn The name of the column to compare the schema to in the where clause. - * @param string $tableColumn The name of the column to compare the table to in the where clause. - */ - private function getTableWhereClause($table, $schemaColumn, $tableColumn): string - { - if (strpos($table, '.') !== false) { - [$schema, $table] = explode('.', $table); - $schema = $this->quoteStringLiteral($schema); - $table = $this->quoteStringLiteral($table); - } else { - $schema = 'SCHEMA_NAME()'; - $table = $this->quoteStringLiteral($table); - } - - return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema); - } - - /** - * {@inheritDoc} - */ - public function getLocateExpression($str, $substr, $startPos = false) + public function getLocateExpression(string $string, string $substring, ?string $start = null): string { - if ($startPos === false) { - return 'CHARINDEX(' . $substr . ', ' . $str . ')'; + if ($start === null) { + return sprintf('CHARINDEX(%s, %s)', $substring, $string); } - return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')'; + return sprintf('CHARINDEX(%s, %s, %s)', $substring, $string, $start); } - /** - * {@inheritDoc} - */ - public function getModExpression($expression1, $expression2) + public function getModExpression(string $dividend, string $divisor): string { - return $expression1 . ' % ' . $expression2; + return $dividend . ' % ' . $divisor; } - /** - * {@inheritDoc} - */ - public function getTrimExpression($str, $mode = TrimMode::UNSPECIFIED, $char = false) - { - if ($char === false) { - switch ($mode) { - case TrimMode::LEADING: - $trimFn = 'LTRIM'; - break; - - case TrimMode::TRAILING: - $trimFn = 'RTRIM'; - break; - - default: - return 'LTRIM(RTRIM(' . $str . '))'; - } - - return $trimFn . '(' . $str . ')'; + public function getTrimExpression( + string $str, + TrimMode $mode = TrimMode::UNSPECIFIED, + ?string $char = null, + ): string { + if ($char === null) { + return match ($mode) { + TrimMode::LEADING => 'LTRIM(' . $str . ')', + TrimMode::TRAILING => 'RTRIM(' . $str . ')', + default => 'LTRIM(RTRIM(' . $str . '))', + }; } $pattern = "'%[^' + " . $char . " + ']%'"; @@ -1192,59 +878,29 @@ public function getTrimExpression($str, $mode = TrimMode::UNSPECIFIED, $char = f . ') - 1, null))) - 1, null))'; } - /** - * {@inheritDoc} - */ - public function getConcatExpression() + public function getConcatExpression(string ...$string): string { - return sprintf('CONCAT(%s)', implode(', ', func_get_args())); + return sprintf('CONCAT(%s)', implode(', ', $string)); } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. - */ - public function getListDatabasesSQL() + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListDatabasesSQL(): string { return 'SELECT * FROM sys.databases'; } - /** - * {@inheritDoc} - * - * @deprecated Use {@see SQLServerSchemaManager::listSchemaNames()} instead. - */ - public function getListNamespacesSQL() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4503', - 'SQLServerPlatform::getListNamespacesSQL() is deprecated,' - . ' use SQLServerSchemaManager::listSchemaNames() instead.', - ); - - return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')"; - } - - /** - * {@inheritDoc} - */ - public function getSubstringExpression($string, $start, $length = null) + public function getSubstringExpression(string $string, string $start, ?string $length = null): string { - if ($length !== null) { - return 'SUBSTRING(' . $string . ', ' . $start . ', ' . $length . ')'; + if ($length === null) { + return sprintf('SUBSTRING(%s, %s, LEN(%s) - %s + 1)', $string, $start, $string, $start); } - return 'SUBSTRING(' . $string . ', ' . $start . ', LEN(' . $string . ') - ' . $start . ' + 1)'; + return sprintf('SUBSTRING(%s, %s, %s)', $string, $start, $length); } - /** - * {@inheritDoc} - */ - public function getLengthExpression($column) + public function getLengthExpression(string $string): string { - return 'LEN(' . $column . ')'; + return 'LEN(' . $string . ')'; } public function getCurrentDatabaseExpression(): string @@ -1252,10 +908,7 @@ public function getCurrentDatabaseExpression(): string return 'DB_NAME()'; } - /** - * {@inheritDoc} - */ - public function getSetTransactionIsolationSQL($level) + public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string { return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); } @@ -1263,7 +916,7 @@ public function getSetTransactionIsolationSQL($level) /** * {@inheritDoc} */ - public function getIntegerTypeDeclarationSQL(array $column) + public function getIntegerTypeDeclarationSQL(array $column): string { return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } @@ -1271,7 +924,7 @@ public function getIntegerTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getBigIntTypeDeclarationSQL(array $column) + public function getBigIntTypeDeclarationSQL(array $column): string { return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } @@ -1279,7 +932,7 @@ public function getBigIntTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getSmallIntTypeDeclarationSQL(array $column) + public function getSmallIntTypeDeclarationSQL(array $column): string { return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } @@ -1287,7 +940,7 @@ public function getSmallIntTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getGuidTypeDeclarationSQL(array $column) + public function getGuidTypeDeclarationSQL(array $column): string { return 'UNIQUEIDENTIFIER'; } @@ -1295,83 +948,49 @@ public function getGuidTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getDateTimeTzTypeDeclarationSQL(array $column) + public function getDateTimeTzTypeDeclarationSQL(array $column): string { return 'DATETIMEOFFSET(6)'; } - /** - * {@inheritDoc} - */ - public function getAsciiStringTypeDeclarationSQL(array $column): string + protected function getCharTypeDeclarationSQLSnippet(?int $length): string { - $length = $column['length'] ?? null; + $sql = 'NCHAR'; - if (empty($column['fixed'])) { - return sprintf('VARCHAR(%d)', $length ?? 255); + if ($length !== null) { + $sql .= sprintf('(%d)', $length); } - return sprintf('CHAR(%d)', $length ?? 255); + return $sql; } - /** - * {@inheritDoc} - */ - protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string { - if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'Relying on the default string column length on SQL Server is deprecated' - . ', specify the length explicitly.', - ); + if ($length === null) { + throw ColumnLengthRequired::new($this, 'NVARCHAR'); } - return $fixed - ? 'NCHAR(' . ($length > 0 ? $length : 255) . ')' - : 'NVARCHAR(' . ($length > 0 ? $length : 255) . ')'; + return sprintf('NVARCHAR(%d)', $length); } /** * {@inheritDoc} */ - protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + public function getAsciiStringTypeDeclarationSQL(array $column): string { - if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'Relying on the default binary column length on SQL Server is deprecated' - . ', specify the length explicitly.', - ); - } - - return $fixed - ? 'BINARY(' . ($length > 0 ? $length : 255) . ')' - : 'VARBINARY(' . ($length > 0 ? $length : 255) . ')'; - } + $length = $column['length'] ?? null; - /** - * {@inheritDoc} - * - * @deprecated - */ - public function getBinaryMaxLength() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'SQLServerPlatform::getBinaryMaxLength() is deprecated.', - ); + if (empty($column['fixed'])) { + return parent::getVarcharTypeDeclarationSQLSnippet($length); + } - return 8000; + return parent::getCharTypeDeclarationSQLSnippet($length); } /** * {@inheritDoc} */ - public function getClobTypeDeclarationSQL(array $column) + public function getClobTypeDeclarationSQL(array $column): string { return 'VARCHAR(MAX)'; } @@ -1379,15 +998,19 @@ public function getClobTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - protected function _getCommonIntegerTypeDeclarationSQL(array $column) + protected function _getCommonIntegerTypeDeclarationSQL(array $column): string { - return ! empty($column['autoincrement']) ? ' IDENTITY' : ''; + if (! empty($column['autoincrement'])) { + return ' IDENTITY'; + } + + return ''; } /** * {@inheritDoc} */ - public function getDateTimeTypeDeclarationSQL(array $column) + public function getDateTimeTypeDeclarationSQL(array $column): string { // 3 - microseconds precision length // http://msdn.microsoft.com/en-us/library/ms187819.aspx @@ -1397,7 +1020,7 @@ public function getDateTimeTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getDateTypeDeclarationSQL(array $column) + public function getDateTypeDeclarationSQL(array $column): string { return 'DATE'; } @@ -1405,7 +1028,7 @@ public function getDateTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getTimeTypeDeclarationSQL(array $column) + public function getTimeTypeDeclarationSQL(array $column): string { return 'TIME(0)'; } @@ -1413,15 +1036,12 @@ public function getTimeTypeDeclarationSQL(array $column) /** * {@inheritDoc} */ - public function getBooleanTypeDeclarationSQL(array $column) + public function getBooleanTypeDeclarationSQL(array $column): string { return 'BIT'; } - /** - * {@inheritDoc} - */ - protected function doModifyLimitQuery($query, $limit, $offset) + protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string { if ($limit === null && $offset <= 0) { return $query; @@ -1454,18 +1074,13 @@ protected function doModifyLimitQuery($query, $limit, $offset) return $query; } - /** - * {@inheritDoc} - */ - public function convertBooleans($item) + public function convertBooleans(mixed $item): mixed { if (is_array($item)) { foreach ($item as $key => $value) { - if (! is_bool($value) && ! is_numeric($value)) { - continue; + if (is_bool($value) || is_numeric($value)) { + $item[$key] = (int) (bool) $value; } - - $item[$key] = (int) (bool) $value; } } elseif (is_bool($item) || is_numeric($item)) { $item = (int) (bool) $item; @@ -1474,66 +1089,37 @@ public function convertBooleans($item) return $item; } - /** - * {@inheritDoc} - */ - public function getCreateTemporaryTableSnippetSQL() + public function getCreateTemporaryTableSnippetSQL(): string { return 'CREATE TABLE'; } - /** - * {@inheritDoc} - */ - public function getTemporaryTableName($tableName) + public function getTemporaryTableName(string $tableName): string { return '#' . $tableName; } - /** - * {@inheritDoc} - */ - public function getDateTimeFormatString() + public function getDateTimeFormatString(): string { return 'Y-m-d H:i:s.u'; } - /** - * {@inheritDoc} - */ - public function getDateFormatString() + public function getDateFormatString(): string { return 'Y-m-d'; } - /** - * {@inheritDoc} - */ - public function getTimeFormatString() + public function getTimeFormatString(): string { return 'H:i:s'; } - /** - * {@inheritDoc} - */ - public function getDateTimeTzFormatString() + public function getDateTimeTzFormatString(): string { return 'Y-m-d H:i:s.u P'; } - /** - * {@inheritDoc} - */ - public function getName() - { - return 'mssql'; - } - - /** - * {@inheritDoc} - */ - protected function initializeDoctrineTypeMappings() + protected function initializeDoctrineTypeMappings(): void { $this->doctrineTypeMapping = [ 'bigint' => Types::BIGINT, @@ -1556,7 +1142,7 @@ protected function initializeDoctrineTypeMappings() 'ntext' => Types::TEXT, 'numeric' => Types::DECIMAL, 'nvarchar' => Types::STRING, - 'real' => Types::FLOAT, + 'real' => Types::SMALLFLOAT, 'smalldatetime' => Types::DATETIME_MUTABLE, 'smallint' => Types::SMALLINT, 'smallmoney' => Types::INTEGER, @@ -1571,102 +1157,68 @@ protected function initializeDoctrineTypeMappings() ]; } - /** - * {@inheritDoc} - */ - public function createSavePoint($savepoint) + public function createSavePoint(string $savepoint): string { return 'SAVE TRANSACTION ' . $savepoint; } - /** - * {@inheritDoc} - */ - public function releaseSavePoint($savepoint) + public function releaseSavePoint(string $savepoint): string { return ''; } - /** - * {@inheritDoc} - */ - public function rollbackSavePoint($savepoint) + public function rollbackSavePoint(string $savepoint): string { return 'ROLLBACK TRANSACTION ' . $savepoint; } - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function getForeignKeyReferentialActionSQL($action) + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getForeignKeyReferentialActionSQL(string $action): string { // RESTRICT is not supported, therefore falling back to NO ACTION. if (strtoupper($action) === 'RESTRICT') { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6707', + 'Relying on automatic conversion of RESTRICT to NO ACTION for SQL Server is deprecated.' + . ' Use NO ACTION explicitly instead.', + ); + return 'NO ACTION'; } return parent::getForeignKeyReferentialActionSQL($action); } - public function appendLockHint(string $fromClause, int $lockMode): string - { - switch ($lockMode) { - case LockMode::NONE: - case LockMode::OPTIMISTIC: - return $fromClause; - - case LockMode::PESSIMISTIC_READ: - return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)'; - - case LockMode::PESSIMISTIC_WRITE: - return $fromClause . ' WITH (UPDLOCK, ROWLOCK)'; - - default: - throw InvalidLockMode::fromLockMode($lockMode); - } - } - - /** - * {@inheritDoc} - * - * @deprecated This API is not portable. - */ - public function getForUpdateSQL() + public function appendLockHint(string $fromClause, LockMode $lockMode): string { - return ' '; + return match ($lockMode) { + LockMode::NONE, + LockMode::OPTIMISTIC => $fromClause, + LockMode::PESSIMISTIC_READ => $fromClause . ' WITH (HOLDLOCK, ROWLOCK)', + LockMode::PESSIMISTIC_WRITE => $fromClause . ' WITH (UPDLOCK, ROWLOCK)', + }; } - /** - * {@inheritDoc} - * - * @deprecated Implement {@see createReservedKeywordsList()} instead. - */ - protected function getReservedKeywordsClass() + /** @deprecated */ + protected function createReservedKeywordsList(): KeywordList { Deprecation::triggerIfCalledFromOutside( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4510', - 'SQLServerPlatform::getReservedKeywordsClass() is deprecated,' - . ' use SQLServerPlatform::createReservedKeywordsList() instead.', + 'https://github.com/doctrine/dbal/pull/6607', + '%s is deprecated.', + __METHOD__, ); - return Keywords\SQLServer2012Keywords::class; + return new SQLServerKeywords(); } - /** - * {@inheritDoc} - */ - public function quoteSingleIdentifier($str) + public function quoteSingleIdentifier(string $str): string { return '[' . str_replace(']', ']]', $str) . ']'; } - /** - * {@inheritDoc} - */ - public function getTruncateTableSQL($tableName, $cascade = false) + public function getTruncateTableSQL(string $tableName, bool $cascade = false): string { $tableIdentifier = new Identifier($tableName); @@ -1676,7 +1228,7 @@ public function getTruncateTableSQL($tableName, $cascade = false) /** * {@inheritDoc} */ - public function getBlobTypeDeclarationSQL(array $column) + public function getBlobTypeDeclarationSQL(array $column): string { return 'VARBINARY(MAX)'; } @@ -1686,53 +1238,27 @@ public function getBlobTypeDeclarationSQL(array $column) * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ - public function getColumnDeclarationSQL($name, array $column) + public function getColumnDeclarationSQL(string $name, array $column): string { if (isset($column['columnDefinition'])) { - $columnDef = $this->getCustomTypeDeclarationSQL($column); + $declaration = $column['columnDefinition']; } else { $collation = ! empty($column['collation']) ? ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : ''; $notnull = ! empty($column['notnull']) ? ' NOT NULL' : ''; - if (! empty($column['unique'])) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5656', - 'The usage of the "unique" column property is deprecated. Use unique constraints instead.', - ); - - $unique = ' ' . $this->getUniqueFieldDeclarationSQL(); - } else { - $unique = ''; - } - - if (! empty($column['check'])) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5656', - 'The usage of the "check" column property is deprecated.', - ); - - $check = ' ' . $column['check']; - } else { - $check = ''; - } - - $typeDecl = $column['type']->getSQLDeclaration($column, $this); - $columnDef = $typeDecl . $collation . $notnull . $unique . $check; + $typeDecl = $column['type']->getSQLDeclaration($column, $this); + $declaration = $typeDecl . $collation . $notnull; } - return $name . ' ' . $columnDef; + return $name . ' ' . $declaration; } /** - * {@inheritDoc} - * * SQL Server does not support quoting collation identifiers. */ - public function getColumnCollationDeclarationSQL($collation) + public function getColumnCollationDeclarationSQL(string $collation): string { return 'COLLATE ' . $collation; } @@ -1747,36 +1273,17 @@ public function columnsEqual(Column $column1, Column $column2): bool === $this->getDefaultValueDeclarationSQL($column2->toArray()); } - protected function getLikeWildcardCharacters(): string - { - return parent::getLikeWildcardCharacters() . '[]^'; - } - /** - * Returns a unique default constraint name for a table and column. + * The [ character is used in SQL Server's extended pattern syntax to define character ranges or sets. * - * @param string $table Name of the table to generate the unique default constraint name for. - * @param string $column Name of the column in the table to generate the unique default constraint name for. + * @link https://learn.microsoft.com/en-us/sql/t-sql/language-elements/like-transact-sql#pattern */ - private function generateDefaultConstraintName($table, $column): string - { - return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column); - } - - /** - * Returns a hash value for a given identifier. - * - * @param string $identifier Identifier to generate a hash value for. - */ - private function generateIdentifierName($identifier): string + protected function getLikeWildcardCharacters(): string { - // Always generate name for unquoted identifiers to ensure consistency. - $identifier = new Identifier($identifier); - - return strtoupper(dechex(crc32($identifier->getName()))); + return parent::getLikeWildcardCharacters() . '['; } - protected function getCommentOnTableSQL(string $tableName, ?string $comment): string + protected function getCommentOnTableSQL(string $tableName, string $comment): string { if (str_contains($tableName, '.')) { [$schemaName, $tableName] = explode('.', $tableName); @@ -1794,26 +1301,7 @@ protected function getCommentOnTableSQL(string $tableName, ?string $comment): st ); } - /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */ - public function getListTableMetadataSQL(string $table): string - { - return sprintf( - <<<'SQL' - SELECT - p.value AS [table_comment] - FROM - sys.tables AS tbl - INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1 - WHERE - (tbl.name=N%s and SCHEMA_NAME(tbl.schema_id)=N'dbo' and p.name=N'MS_Description') - SQL - , - $this->quoteStringLiteral($table), - ); - } - - /** @param string $query */ - private function shouldAddOrderBy($query): bool + private function shouldAddOrderBy(string $query): bool { // Find the position of the last instance of ORDER BY and ensure it is not within a parenthetical statement // but can be in a newline @@ -1841,6 +1329,11 @@ private function shouldAddOrderBy($query): bool return true; } + public function createMetadataProvider(Connection $connection): SQLServerMetadataProvider + { + return new SQLServerMetadataProvider($connection, $this); + } + public function createSchemaManager(Connection $connection): SQLServerSchemaManager { return new SQLServerSchemaManager($connection, $this); diff --git a/doctrine/dbal/src/Platforms/SQLite/Comparator.php b/doctrine/dbal/src/Platforms/SQLite/Comparator.php index 5218871c6..d14aefb29 100644 --- a/doctrine/dbal/src/Platforms/SQLite/Comparator.php +++ b/doctrine/dbal/src/Platforms/SQLite/Comparator.php @@ -1,9 +1,12 @@ normalizeColumns($fromTable), - $this->normalizeColumns($toTable), - ); - } - - /** - * {@inheritDoc} - */ - public function diffTable(Table $fromTable, Table $toTable) - { - return parent::diffTable( - $this->normalizeColumns($fromTable), - $this->normalizeColumns($toTable), + $this->normalizeColumns($oldTable), + $this->normalizeColumns($newTable), ); } diff --git a/doctrine/dbal/src/Platforms/SQLite/SQLiteMetadataProvider.php b/doctrine/dbal/src/Platforms/SQLite/SQLiteMetadataProvider.php new file mode 100644 index 000000000..04ed05421 --- /dev/null +++ b/doctrine/dbal/src/Platforms/SQLite/SQLiteMetadataProvider.php @@ -0,0 +1,710 @@ +getTableNames() as $tableName) { + yield new TableMetadataRow(null, $tableName, []); + } + } + + /** + * @return iterable + * + * @throws Exception + */ + private function getTableNames(): iterable + { + $sql = sprintf( + <<<'SQL' + SELECT name + FROM sqlite_master + WHERE type = 'table' + AND %s + SQL, + $this->buildTableNamePredicate('name'), + ); + + yield from $this->connection->iterateColumn($sql); + } + + /** {@inheritDoc} */ + public function getTableColumnsForAllTables(): iterable + { + return $this->getTableColumns(null); + } + + /** {@inheritDoc} */ + public function getTableColumnsForTable(?string $schemaName, string $tableName): iterable + { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getTableColumns($tableName); + } + + /** + * @link https://www.sqlite.org/pragma.html#pragma_table_info + * + * @return iterable + * + * @throws Exception + */ + private function getTableColumns(?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT t.name, + c.name, + c.type, + c."notnull", + c.dflt_value + FROM sqlite_master t + JOIN pragma_table_info(t.name) c + WHERE %s + ORDER BY t.name, + c.cid + SQL, + $this->buildTableQueryPredicate($tableName, $params), + ); + + $rows = $sqlByTableName = []; + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + [$tableName] = $row; + $rows[] = $row; + + $sqlByTableName[$tableName] ??= $this->getCreateTableSQL($tableName); + } + + foreach ($rows as $row) { + yield $this->createTableColumn($row, $sqlByTableName); + } + } + + /** + * @param list $row + * @param array $sqlByTableName + * + * @throws TypesException + */ + private function createTableColumn(array $row, array $sqlByTableName): TableColumnMetadataRow + { + [$tableName, $columnName, $type, $notNull, $defaultExpression] = $row; + + $matchResult = preg_match('/^([A-Z\s]+?)(?:\s*\((\d+)(?:,\s*(\d+))?\))?$/i', $type, $matches); + assert($matchResult === 1); + + $editor = Column::editor() + ->setQuotedName($columnName); + + $dbType = strtolower($matches[1]); + + if (str_contains($dbType, ' unsigned')) { + $dbType = str_replace(' unsigned', '', $dbType); + $editor->setUnsigned(true); + } + + $typeName = $this->platform->getDoctrineTypeMapping($dbType); + + $editor->setTypeName($typeName); + + if ($dbType === 'char') { + $editor->setFixed(true); + } + + if (isset($matches[2])) { + if (isset($matches[3])) { + $editor + ->setPrecision((int) $matches[2]) + ->setScale((int) $matches[3]); + } else { + $editor->setLength((int) $matches[2]); + } + } + + if ($defaultExpression !== null) { + $editor->setDefaultValue( + $this->parseDefaultExpression($defaultExpression), + ); + } + + $tableSQL = $sqlByTableName[$tableName]; + + $editor + ->setAutoincrement( + $this->parseColumnAutoIncrementFromSQL($columnName, $tableSQL), + ) + ->setComment( + $this->parseColumnCommentFromSQL($columnName, $tableSQL), + ) + ->setNotNull((bool) $notNull); + + if ($typeName === Types::STRING || $typeName === Types::TEXT) { + $editor->setCollation( + $this->parseColumnCollationFromSQL($columnName, $tableSQL) ?? 'BINARY', + ); + } + + return new TableColumnMetadataRow(null, $tableName, $editor->create()); + } + + private function parseDefaultExpression(string $value): ?string + { + if ($value === 'NULL') { + return null; + } + + if (preg_match('/^\'(.*)\'$/s', $value, $matches) === 1) { + $value = str_replace("''", "'", $matches[1]); + } + + return $value; + } + + /** @link https://www.sqlite.org/autoinc.html#the_autoincrement_keyword */ + private function parseColumnAutoIncrementFromSQL(string $column, string $sql): bool + { + $pattern = '/' . $this->buildIdentifierPattern($column) . 'INTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT/i'; + + return preg_match($pattern, $sql) === 1; + } + + /** @return ?non-empty-string */ + private function parseColumnCollationFromSQL(string $column, string $sql): ?string + { + $pattern = '{' . $this->buildIdentifierPattern($column) + . '[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is'; + + if (preg_match($pattern, $sql, $match) !== 1) { + return null; + } + + assert(strlen($match[1]) > 0); + + return $match[1]; + } + + private function parseColumnCommentFromSQL(string $column, string $sql): string + { + $pattern = '{[\s(,]' . $this->buildIdentifierPattern($column) . '(?:\([^)]*?\)|[^,(])*?,?(\s*--[^\n]*\n?)+}i'; + + if (preg_match($pattern, $sql, $match) !== 1) { + return ''; + } + + $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n")); + assert(is_string($comment)); + + return $comment; + } + + /** + * Returns a regular expression pattern that matches the given unquoted or quoted identifier. + */ + private function buildIdentifierPattern(string $identifier): string + { + return '(?:' . implode('|', array_map( + static function (string $sql): string { + return '\W' . preg_quote($sql, '/') . '\W'; + }, + [ + $identifier, + $this->platform->quoteSingleIdentifier($identifier), + ], + )) . ')'; + } + + /** {@inheritDoc} */ + public function getIndexColumnsForAllTables(): iterable + { + return $this->getIndexColumns(null); + } + + /** {@inheritDoc} */ + public function getIndexColumnsForTable(?string $schemaName, string $tableName): iterable + { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getIndexColumns($tableName); + } + + /** + * @link https://www.sqlite.org/pragma.html#pragma_index_info + * @link https://www.sqlite.org/pragma.html#pragma_index_list + * @link https://www.sqlite.org/fileformat2.html#internal_schema_objects + * + * @return iterable + * + * @throws Exception + */ + private function getIndexColumns(?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT t.name, + i.name, + i."unique", + c.name + FROM sqlite_master t + JOIN pragma_index_list(t.name) i + JOIN pragma_index_info(i.name) c + WHERE %s + AND i.name NOT LIKE 'sqlite_%%' + ORDER BY t.name, + i.name, + c.seqno + SQL, + $this->buildTableQueryPredicate($tableName, $params), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new IndexColumnMetadataRow( + schemaName: null, + tableName: $row[0], + indexName: $row[1], + type: $row[2] ? IndexType::UNIQUE : IndexType::REGULAR, + isClustered: false, + predicate: null, + columnName: $row[3], + columnLength: null, + ); + } + } + + /** {@inheritDoc} */ + public function getPrimaryKeyConstraintColumnsForAllTables(): iterable + { + return $this->getPrimaryKeyConstraintColumns(null); + } + + /** {@inheritDoc} */ + public function getPrimaryKeyConstraintColumnsForTable( + ?string $schemaName, + string $tableName, + ): iterable { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getPrimaryKeyConstraintColumns($tableName); + } + + /** + * @link https://www.sqlite.org/pragma.html#pragma_table_info + * + * @return iterable + * + * @throws Exception + */ + private function getPrimaryKeyConstraintColumns(?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT t.name, + c.name + FROM sqlite_master t + JOIN pragma_table_info(t.name) c + WHERE %s + AND c.pk > 0 + ORDER BY t.name, + c.pk + SQL, + $this->buildTableQueryPredicate($tableName, $params), + ); + + foreach ($this->connection->iterateNumeric($sql, $params) as $row) { + yield new PrimaryKeyConstraintColumnRow( + schemaName: null, + tableName: $row[0], + constraintName: null, + isClustered: true, + columnName: $row[1], + ); + } + } + + /** {@inheritDoc} */ + public function getForeignKeyConstraintColumnsForAllTables(): iterable + { + return $this->getForeignKeyConstraintColumns(null); + } + + /** {@inheritDoc} */ + public function getForeignKeyConstraintColumnsForTable( + ?string $schemaName, + string $tableName, + ): iterable { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getForeignKeyConstraintColumns($tableName); + } + + /** + * @link https://sqlite.org/pragma.html#pragma_foreign_key_list + * + * @return iterable + * + * @throws Exception + */ + private function getForeignKeyConstraintColumns(?string $tableName): iterable + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT t.name, + c.id, + c."table", + c.on_update, + c.on_delete, + c."from", + c."to" + FROM sqlite_master t + JOIN pragma_foreign_key_list(t.name) c + WHERE %s + ORDER BY t.name, + c.id DESC, + c.seq +SQL, + $this->buildTableQueryPredicate($tableName, $params), + ); + + return $this->generateForeignKeyConstraintColumns( + $this->connection->iterateNumeric($sql, $params), + ); + } + + /** + * @param iterable> $rows + * + * @return iterable + * + * @throws Exception + */ + private function generateForeignKeyConstraintColumns(iterable $rows): iterable + { + $currentTableName = null; + $currentDetails = []; + + foreach ($rows as $row) { + $tableName = $row[0]; + $id = $row[1]; + + if ($tableName !== $currentTableName) { + $currentDetails = $this->getForeignKeyConstraintDetails($tableName); + $currentTableName = $tableName; + } + + // SQLite identifies foreign keys in reverse order of appearance in SQL + $details = $currentDetails[count($currentDetails) - $id - 1]; + + $name = $details->getName(); + + if ($row[6] !== null) { + $referencedColumnNames = [$row[6]]; + } else { + // inferring a shorthand form for the foreign key constraint, + // where the referenced column names are omitted + $referencedColumnNames = []; + + foreach ($this->getPrimaryKeyConstraintColumns($row[2]) as $primaryKeyConstraintColumn) { + $referencedColumnNames[] = $primaryKeyConstraintColumn->getColumnName(); + } + + if (count($referencedColumnNames) === 0) { + throw UnsupportedSchema::sqliteMissingForeignKeyConstraintReferencedColumns( + $name, + $tableName, + $row[2], + ); + } + } + + foreach ($referencedColumnNames as $referencedColumnName) { + yield new ForeignKeyConstraintColumnMetadataRow( + referencingSchemaName: null, + referencingTableName: $tableName, + id: $row[1], + name: $name, + referencedSchemaName: null, + referencedTableName: $row[2], + matchType: MatchType::SIMPLE, + onUpdateAction: $this->createReferentialAction($row[3]), + onDeleteAction: $this->createReferentialAction($row[4]), + isDeferrable: $details->isDeferrable(), + isDeferred: $details->isDeferred(), + referencingColumnName: $row[5], + referencedColumnName: $referencedColumnName, + ); + } + } + } + + private function createReferentialAction(string $value): ReferentialAction + { + $action = ReferentialAction::tryFrom($value); + assert($action !== null); + + return $action; + } + + /** + * @return list + * + * @throws Exception + */ + private function getForeignKeyConstraintDetails(string $tableName): array + { + $sql = $this->getCreateTableSQL($tableName); + + if ( + preg_match_all( + '# + (?:CONSTRAINT\s+(\S+)\s+)? + (?:FOREIGN\s+KEY[^)]+\)\s*)? + REFERENCES\s+\S+\s*(?:\([^)]+\))? + (?: + [^,]*? + (NOT\s+DEFERRABLE|DEFERRABLE) + (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))? + )?#isx', + $sql, + $matches, + ) === 0 + ) { + return []; + } + + $names = $matches[1]; + $deferrable = $matches[2]; + $deferred = $matches[3]; + $details = []; + + for ($i = 0, $count = count($matches[0]); $i < $count; $i++) { + $details[] = new ForeignKeyConstraintDetails( + $this->parseOptionallyQuotedName($names[$i]), + strcasecmp($deferrable[$i], 'deferrable') === 0, + strcasecmp($deferred[$i], 'deferred') === 0, + ); + } + + return $details; + } + + /** @return ?non-empty-string */ + private function parseOptionallyQuotedName(string $sql): ?string + { + if ($sql === '') { + return null; + } + + if (str_starts_with($sql, '"') && str_ends_with($sql, '"')) { + $name = str_replace('""', '"', substr($sql, 1, -1)); + assert(strlen($name) > 0); + + return $name; + } + + return $sql; + } + + /** @throws Exception */ + private function getCreateTableSQL(string $tableName): string + { + $sql = $this->connection->fetchOne( + <<<'SQL' + SELECT sql + FROM sqlite_master + WHERE type = 'table' + AND name = ? + SQL, + [$tableName], + ); + + assert($sql !== false); + + return $sql; + } + + /** {@inheritDoc} */ + public function getTableOptionsForAllTables(): iterable + { + return $this->getTableOptions($this->getTableNames()); + } + + /** {@inheritDoc} */ + public function getTableOptionsForTable( + ?string $schemaName, + string $tableName, + ): iterable { + if ($schemaName !== null) { + throw UnsupportedName::fromNonNullSchemaName($schemaName, __METHOD__); + } + + return $this->getTableOptions([$tableName]); + } + + /** + * @param iterable $tableNames + * + * @return iterable + * + * @throws Exception + */ + private function getTableOptions(iterable $tableNames): iterable + { + foreach ($tableNames as $tableName) { + yield new TableMetadataRow(null, $tableName, [ + 'comment' => $this->parseTableCommentFromSQL( + $tableName, + $this->getCreateTableSQL($tableName), + ), + ]); + } + } + + /** + * @param list $params + * + * @return non-empty-string + */ + private function buildTableQueryPredicate(?string $tableName, array &$params): string + { + $conditions = [ + "t.type = 'table'", + $this->buildTableNamePredicate('t.name'), + ]; + + if ($tableName !== null) { + $conditions[] = 't.name = ?'; + $params[] = $tableName; + } + + return implode(' AND ', $conditions); + } + + private function parseTableCommentFromSQL(string $table, string $sql): ?string + { + $pattern = sprintf( + <<<'PATTERN' + /CREATE\s+TABLE%s + ( # Start capture + (?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end + # of the line(s) + )/ix + PATTERN, + $this->buildIdentifierPattern($table), + ); + + if (preg_match($pattern, $sql, $match) !== 1) { + return null; + } + + $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n")); + + return $comment === '' ? null : $comment; + } + + /** {@inheritDoc} */ + public function getAllViews(): iterable + { + $sql = <<<'SQL' + SELECT name, + sql + FROM sqlite_master + WHERE type = 'view' + ORDER BY name + SQL; + + foreach ($this->connection->iterateNumeric($sql) as $row) { + yield new ViewMetadataRow(null, ...$row); + } + } + + /** {@inheritDoc} */ + public function getAllSequences(): iterable + { + throw NotSupported::new(__METHOD__); + } + + private function buildTableNamePredicate(string $columnName): string + { + return sprintf("%s NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')", $columnName); + } +} diff --git a/doctrine/dbal/src/Platforms/SQLite/SQLiteMetadataProvider/ForeignKeyConstraintDetails.php b/doctrine/dbal/src/Platforms/SQLite/SQLiteMetadataProvider/ForeignKeyConstraintDetails.php new file mode 100644 index 000000000..521762e3e --- /dev/null +++ b/doctrine/dbal/src/Platforms/SQLite/SQLiteMetadataProvider/ForeignKeyConstraintDetails.php @@ -0,0 +1,38 @@ +name; + } + + public function isDeferrable(): bool + { + return $this->isDeferrable; + } + + public function isDeferred(): bool + { + return $this->isDeferred; + } +} diff --git a/doctrine/dbal/src/Platforms/SQLitePlatform.php b/doctrine/dbal/src/Platforms/SQLitePlatform.php new file mode 100644 index 000000000..6fcf04883 --- /dev/null +++ b/doctrine/dbal/src/Platforms/SQLitePlatform.php @@ -0,0 +1,976 @@ + 'TRIM', + TrimMode::LEADING => 'LTRIM', + TrimMode::TRAILING => 'RTRIM', + }; + + $arguments = [$str]; + + if ($char !== null) { + $arguments[] = $char; + } + + return sprintf('%s(%s)', $trimFn, implode(', ', $arguments)); + } + + public function getSubstringExpression(string $string, string $start, ?string $length = null): string + { + if ($length === null) { + return sprintf('SUBSTR(%s, %s)', $string, $start); + } + + return sprintf('SUBSTR(%s, %s, %s)', $string, $start, $length); + } + + public function getLocateExpression(string $string, string $substring, ?string $start = null): string + { + if ($start === null || $start === '1') { + return sprintf('INSTR(%s, %s)', $string, $substring); + } + + return sprintf( + 'CASE WHEN INSTR(SUBSTR(%1$s, %3$s), %2$s) > 0 THEN INSTR(SUBSTR(%1$s, %3$s), %2$s) + %3$s - 1 ELSE 0 END', + $string, + $substring, + $start, + ); + } + + protected function getDateArithmeticIntervalExpression( + string $date, + string $operator, + string $interval, + DateIntervalUnit $unit, + ): string { + switch ($unit) { + case DateIntervalUnit::WEEK: + $interval = $this->multiplyInterval($interval, 7); + $unit = DateIntervalUnit::DAY; + break; + + case DateIntervalUnit::QUARTER: + $interval = $this->multiplyInterval($interval, 3); + $unit = DateIntervalUnit::MONTH; + break; + } + + return 'DATETIME(' . $date . ',' . $this->getConcatExpression( + $this->quoteStringLiteral($operator), + $interval, + $this->quoteStringLiteral(' ' . $unit->value), + ) . ')'; + } + + public function getDateDiffExpression(string $date1, string $date2): string + { + return sprintf("JULIANDAY(%s, 'start of day') - JULIANDAY(%s, 'start of day')", $date1, $date2); + } + + /** + * {@inheritDoc} + * + * The DBAL doesn't support databases on the SQLite platform. The expression here always returns a fixed string + * as an indicator of an implicitly selected database. + * + * @link https://www.sqlite.org/lang_select.html + * @see Connection::getDatabase() + */ + public function getCurrentDatabaseExpression(): string + { + return "'main'"; + } + + /** @link https://www2.sqlite.org/cvstrac/wiki?p=UnsupportedSql */ + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, null, null); + } + + protected function _getTransactionIsolationLevelSQL(TransactionIsolationLevel $level): string + { + return match ($level) { + TransactionIsolationLevel::READ_UNCOMMITTED => '0', + TransactionIsolationLevel::READ_COMMITTED, + TransactionIsolationLevel::REPEATABLE_READ, + TransactionIsolationLevel::SERIALIZABLE => '1', + }; + } + + public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string + { + return 'PRAGMA read_uncommitted = ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column): string + { + return 'BOOLEAN'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column): string + { + return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column): string + { + // SQLite autoincrement is implicit for INTEGER PKs, but not for BIGINT fields. + if (! empty($column['autoincrement'])) { + return $this->getIntegerTypeDeclarationSQL($column); + } + + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column): string + { + // SQLite autoincrement is implicit for INTEGER PKs, but not for SMALLINT fields. + if (! empty($column['autoincrement'])) { + return $this->getIntegerTypeDeclarationSQL($column); + } + + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column): string + { + return 'DATETIME'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column): string + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column): string + { + return 'TIME'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column): string + { + // SQLite autoincrement is only possible for the primary key + if (! empty($column['autoincrement'])) { + return ' PRIMARY KEY AUTOINCREMENT'; + } + + return ! empty($column['unsigned']) ? ' UNSIGNED' : ''; + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array + { + $this->validateCreateTableOptions($options, __METHOD__); + + if ($this->hasAutoIncrementColumn($columns, $options)) { + $options['primary'] = []; + } + + $queryFields = $this->getColumnDeclarationListSQL($columns); + + if (! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $definition) { + $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($definition); + } + } + + if (! empty($options['primary'])) { + $keyColumns = array_unique(array_values($options['primary'])); + $queryFields .= ', PRIMARY KEY (' . implode(', ', $keyColumns) . ')'; + } + + if (isset($options['foreignKeys'])) { + foreach ($options['foreignKeys'] as $foreignKey) { + $queryFields .= ', ' . $this->getForeignKeyDeclarationSQL($foreignKey); + } + } + + $tableComment = ''; + if (isset($options['comment'])) { + $tableComment = $this->getInlineCommentSQL($options['comment']); + } + + $query = ['CREATE TABLE ' . $name . ' ' . $tableComment . '(' . $queryFields . ')']; + + if (isset($options['alter']) && $options['alter'] === true) { + return $query; + } + + if (! empty($options['indexes'])) { + foreach ($options['indexes'] as $indexDef) { + $query[] = $this->getCreateIndexSQL($indexDef, $name); + } + } + + return $query; + } + + /** + * @param list $columns + * @param CreateTableParameters $options + */ + private function hasAutoIncrementColumn(array $columns, array $options): bool + { + $primaryKeyColumnNames = array_fill_keys($options['primary'] ?? [], true); + + foreach ($columns as $column) { + if (empty($column['autoincrement'])) { + continue; + } + + if (! isset($primaryKeyColumnNames[$column['name']])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6849', + 'Declaring a column that is not part of the primary key as auto-increment is deprecated.', + ); + } elseif (count($primaryKeyColumnNames) > 1) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6849', + 'Declaring a column that is part of a composite primary key as auto-increment is deprecated.', + ); + } + + return true; + } + + return false; + } + + protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string + { + return 'BLOB'; + } + + protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string + { + $sql = 'VARCHAR'; + + if ($length !== null) { + $sql .= sprintf('(%d)', $length); + } + + return $sql; + } + + protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string + { + return 'BLOB'; + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column): string + { + return 'CLOB'; + } + + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListViewsSQL(string $database): string + { + return "SELECT name, sql FROM sqlite_master WHERE type='view' AND sql NOT NULL"; + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey): string + { + $query = parent::getAdvancedForeignKeyOptionsSQL($foreignKey); + + if (! $foreignKey->hasOption('deferrable') || $foreignKey->getOption('deferrable') === false) { + $query .= ' NOT'; + } + + $query .= ' DEFERRABLE'; + $query .= ' INITIALLY'; + + if ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) { + $query .= ' DEFERRED'; + } else { + $query .= ' IMMEDIATE'; + } + + return $query; + } + + public function supportsIdentityColumns(): bool + { + return true; + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsColumnCollation(): bool + { + return true; + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsInlineColumnComments(): bool + { + return true; + } + + public function getTruncateTableSQL(string $tableName, bool $cascade = false): string + { + $tableIdentifier = new Identifier($tableName); + + return 'DELETE FROM ' . $tableIdentifier->getQuotedName($this); + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getInlineColumnCommentSQL(string $comment): string + { + return $this->getInlineCommentSQL($comment); + } + + private function getInlineCommentSQL(string $comment): string + { + if ($comment === '') { + return ''; + } + + return '--' . str_replace("\n", "\n--", $comment) . "\n"; + } + + protected function initializeDoctrineTypeMappings(): void + { + $this->doctrineTypeMapping = [ + 'bigint' => 'bigint', + 'bigserial' => 'bigint', + 'blob' => 'blob', + 'boolean' => 'boolean', + 'char' => 'string', + 'clob' => 'text', + 'date' => 'date', + 'datetime' => 'datetime', + 'decimal' => 'decimal', + 'double' => 'float', + 'double precision' => 'float', + 'float' => 'float', + 'image' => 'string', + 'int' => 'integer', + 'integer' => 'integer', + 'longtext' => 'text', + 'longvarchar' => 'string', + 'mediumint' => 'integer', + 'mediumtext' => 'text', + 'ntext' => 'string', + 'numeric' => 'decimal', + 'nvarchar' => 'string', + 'real' => 'smallfloat', + 'serial' => 'integer', + 'smallint' => 'smallint', + 'string' => 'string', + 'text' => 'text', + 'time' => 'time', + 'timestamp' => 'datetime', + 'tinyint' => 'boolean', + 'tinytext' => 'text', + 'varchar' => 'string', + 'varchar2' => 'string', + ]; + } + + /** @deprecated */ + protected function createReservedKeywordsList(): KeywordList + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6607', + '%s is deprecated.', + __METHOD__, + ); + + return new SQLiteKeywords(); + } + + /** + * {@inheritDoc} + */ + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff): array + { + return []; + } + + /** + * {@inheritDoc} + */ + protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff): array + { + $table = $diff->getOldTable(); + + $sql = []; + + foreach ($this->getIndexesInAlteredTable($diff) as $index) { + if (! $index->isPrimary()) { + $sql[] = $this->getCreateIndexSQL($index, $table->getQuotedName($this)); + } + } + + return $sql; + } + + protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string + { + if ($limit === null && $offset > 0) { + $limit = -1; + } + + return parent::doModifyLimitQuery($query, $limit, $offset); + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column): string + { + return 'BLOB'; + } + + public function getTemporaryTableName(string $tableName): string + { + return $tableName; + } + + /** + * {@inheritDoc} + */ + public function getCreateTablesSQL(array $tables): array + { + $sql = []; + + foreach ($tables as $table) { + $sql = array_merge($sql, $this->getCreateTableSQL($table)); + } + + return $sql; + } + + /** {@inheritDoc} */ + public function getCreateIndexSQL(Index $index, string $table): string + { + $name = $index->getQuotedName($this); + $columns = $index->getColumns(); + + if (count($columns) === 0) { + throw new InvalidArgumentException(sprintf( + 'Incomplete or invalid index definition %s on table %s', + $name, + $table, + )); + } + + if ($index->isPrimary()) { + return $this->getCreatePrimaryKeySQL($index, $table); + } + + if (strpos($table, '.') !== false) { + [$schema, $table] = explode('.', $table); + $name = $schema . '.' . $name; + } + + $query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table; + $query .= ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index); + + return $query; + } + + /** + * {@inheritDoc} + */ + public function getDropTablesSQL(array $tables): array + { + $sql = []; + + foreach ($tables as $table) { + $sql[] = $this->getDropTableSQL($table->getQuotedName($this)); + } + + return $sql; + } + + /** @deprecated */ + public function getCreatePrimaryKeySQL(Index $index, string $table): string + { + throw NotSupported::new(__METHOD__); + } + + public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, string $table): string + { + throw NotSupported::new(__METHOD__); + } + + public function getDropForeignKeySQL(string $foreignKey, string $table): string + { + throw NotSupported::new(__METHOD__); + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff): array + { + $sql = $this->getSimpleAlterTableSQL($diff); + if ($sql !== false) { + return $sql; + } + + $table = $diff->getOldTable(); + + $columns = []; + $oldColumnNames = []; + $newColumnNames = []; + + foreach ($table->getColumns() as $column) { + $columnName = strtolower($column->getName()); + $columns[$columnName] = $column; + $oldColumnNames[$columnName] = $newColumnNames[$columnName] = $column->getQuotedName($this); + } + + foreach ($diff->getDroppedColumns() as $column) { + $columnName = strtolower($column->getName()); + if (! isset($columns[$columnName])) { + continue; + } + + unset( + $columns[$columnName], + $oldColumnNames[$columnName], + $newColumnNames[$columnName], + ); + } + + foreach ($diff->getChangedColumns() as $columnDiff) { + $oldColumnName = strtolower($columnDiff->getOldColumn()->getName()); + $newColumn = $columnDiff->getNewColumn(); + + $columns = $this->replaceColumn( + $table->getName(), + $columns, + $oldColumnName, + $newColumn, + ); + + if (isset($newColumnNames[$oldColumnName])) { + $newColumnNames[$oldColumnName] = $newColumn->getQuotedName($this); + } + } + + foreach ($diff->getAddedColumns() as $column) { + $columns[strtolower($column->getName())] = $column; + } + + $tableName = $table->getName(); + $pos = strpos($tableName, '.'); + if ($pos !== false) { + $tableName = substr($tableName, $pos + 1); + } + + $dataTable = new Table('__temp__' . $tableName); + + $newTable = new Table( + $table->getQuotedName($this), + $columns, + $this->getPrimaryIndexInAlteredTable($diff), + [], + $this->getForeignKeysInAlteredTable($diff), + $table->getOptions(), + ); + + $newTable->addOption('alter', true); + + $sql = $this->getPreAlterTableIndexForeignKeySQL($diff); + + $sql[] = sprintf( + 'CREATE TEMPORARY TABLE %s AS SELECT %s FROM %s', + $dataTable->getQuotedName($this), + implode(', ', $oldColumnNames), + $table->getQuotedName($this), + ); + $sql[] = $this->getDropTableSQL($table->getQuotedName($this)); + + $sql = array_merge($sql, $this->getCreateTableSQL($newTable)); + $sql[] = sprintf( + 'INSERT INTO %s (%s) SELECT %s FROM %s', + $newTable->getQuotedName($this), + implode(', ', $newColumnNames), + implode(', ', $oldColumnNames), + $dataTable->getQuotedName($this), + ); + $sql[] = $this->getDropTableSQL($dataTable->getQuotedName($this)); + + return array_merge($sql, $this->getPostAlterTableIndexForeignKeySQL($diff)); + } + + /** + * Replace the column with the given name with the new column. + * + * @param array $columns + * + * @return array + */ + private function replaceColumn(string $tableName, array $columns, string $columnName, Column $column): array + { + $keys = array_keys($columns); + $index = array_search($columnName, $keys, true); + + if ($index === false) { + throw ColumnDoesNotExist::new($columnName, $tableName); + } + + $values = array_values($columns); + + $keys[$index] = strtolower($column->getName()); + $values[$index] = $column; + + return array_combine($keys, $values); + } + + /** @return list|false */ + private function getSimpleAlterTableSQL(TableDiff $diff): array|false + { + if ( + count($diff->getChangedColumns()) > 0 + || count($diff->getDroppedColumns()) > 0 + || count($diff->getAddedIndexes()) > 0 + || count($diff->getModifiedIndexes()) > 0 + || count($diff->getDroppedIndexes()) > 0 + || count($diff->getRenamedIndexes()) > 0 + || count($diff->getAddedForeignKeys()) > 0 + || count($diff->getModifiedForeignKeys()) > 0 + || count($diff->getDroppedForeignKeys()) > 0 + ) { + return false; + } + + $table = $diff->getOldTable(); + + $sql = []; + + foreach ($diff->getAddedColumns() as $column) { + $definition = $column->toArray(); + + $type = $definition['type']; + + switch (true) { + case isset($definition['columnDefinition']): + case $definition['autoincrement']: + case $definition['comment'] !== '': + case $type instanceof Types\DateTimeType && $definition['default'] === $this->getCurrentTimestampSQL(): + case $type instanceof Types\DateType && $definition['default'] === $this->getCurrentDateSQL(): + case $type instanceof Types\TimeType && $definition['default'] === $this->getCurrentTimeSQL(): + return false; + } + + $definition['name'] = $column->getQuotedName($this); + + $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' ADD COLUMN ' + . $this->getColumnDeclarationSQL($definition['name'], $definition); + } + + return $sql; + } + + /** + * Based on the table diff, returns a map where the keys are the lower-case old column names and the values are the + * new column names. If the column was dropped, it is not present in the map. + * + * @return array + */ + private function getDiffColumnNameMap(TableDiff $diff): array + { + $oldTable = $diff->getOldTable(); + + $map = []; + + foreach ($oldTable->getColumns() as $column) { + $columnName = $column->getName(); + $map[strtolower($columnName)] = $columnName; + } + + foreach ($diff->getDroppedColumns() as $column) { + unset($map[strtolower($column->getName())]); + } + + foreach ($diff->getChangedColumns() as $columnDiff) { + $columnName = $columnDiff->getOldColumn()->getName(); + $map[strtolower($columnName)] = $columnDiff->getNewColumn()->getName(); + } + + foreach ($diff->getAddedColumns() as $column) { + $columnName = $column->getName(); + $map[strtolower($columnName)] = $columnName; + } + + // @phpstan-ignore return.type + return $map; + } + + /** @return array */ + private function getIndexesInAlteredTable(TableDiff $diff): array + { + $oldTable = $diff->getOldTable(); + $indexes = $oldTable->getIndexes(); + $nameMap = $this->getDiffColumnNameMap($diff); + + foreach ($indexes as $key => $index) { + $indexName = $index->getName(); + foreach ($diff->getRenamedIndexes() as $oldIndexName => $renamedIndex) { + if (strtolower($indexName) === strtolower($oldIndexName)) { + unset($indexes[$key]); + } + } + + $changed = false; + $indexColumns = []; + foreach ($index->getColumns() as $columnName) { + $normalizedColumnName = strtolower($columnName); + if (! isset($nameMap[$normalizedColumnName])) { + unset($indexes[$key]); + continue 2; + } + + $indexColumns[] = $nameMap[$normalizedColumnName]; + if ($columnName !== $nameMap[$normalizedColumnName]) { + $changed = true; + } + } + + if (! $changed) { + continue; + } + + $indexes[$key] = new Index( + $index->getName(), + $indexColumns, + $index->isUnique(), + $index->isPrimary(), + $index->getFlags(), + ); + } + + foreach ($diff->getDroppedIndexes() as $index) { + $indexName = $index->getName(); + + if ($indexName !== '') { + unset($indexes[strtolower($indexName)]); + } + } + + foreach ( + array_merge( + $diff->getModifiedIndexes(), + $diff->getAddedIndexes(), + $diff->getRenamedIndexes(), + ) as $index + ) { + $indexName = $index->getName(); + + if ($indexName !== '') { + $indexes[strtolower($indexName)] = $index; + } else { + $indexes[] = $index; + } + } + + return $indexes; + } + + /** @return array */ + private function getForeignKeysInAlteredTable(TableDiff $diff): array + { + $oldTable = $diff->getOldTable(); + $foreignKeys = $oldTable->getForeignKeys(); + $nameMap = $this->getDiffColumnNameMap($diff); + + foreach ($foreignKeys as $key => $constraint) { + $changed = false; + $localColumns = []; + foreach ($constraint->getLocalColumns() as $columnName) { + $normalizedColumnName = strtolower($columnName); + if (! isset($nameMap[$normalizedColumnName])) { + unset($foreignKeys[$key]); + continue 2; + } + + $localColumns[] = $nameMap[$normalizedColumnName]; + if ($columnName !== $nameMap[$normalizedColumnName]) { + $changed = true; + } + } + + if (! $changed) { + continue; + } + + $foreignKeys[$key] = new ForeignKeyConstraint( + $localColumns, // @phpstan-ignore argument.type + $constraint->getForeignTableName(), + $constraint->getForeignColumns(), // @phpstan-ignore argument.type + $constraint->getName(), + $constraint->getOptions(), + ); + } + + foreach ($diff->getDroppedForeignKeys() as $constraint) { + $constraintName = $constraint->getName(); + + if ($constraintName !== '') { + unset($foreignKeys[strtolower($constraintName)]); + } + } + + foreach (array_merge($diff->getModifiedForeignKeys(), $diff->getAddedForeignKeys()) as $constraint) { + $constraintName = $constraint->getName(); + + if ($constraintName !== '') { + $foreignKeys[strtolower($constraintName)] = $constraint; + } else { + $foreignKeys[] = $constraint; + } + } + + return $foreignKeys; + } + + /** @return array */ + private function getPrimaryIndexInAlteredTable(TableDiff $diff): array + { + $primaryIndex = []; + + foreach ($this->getIndexesInAlteredTable($diff) as $index) { + if ($index->isPrimary()) { + $primaryIndex = [$index->getName() => $index]; + } + } + + return $primaryIndex; + } + + public function createMetadataProvider(Connection $connection): SQLiteMetadataProvider + { + return new SQLiteMetadataProvider($connection, $this); + } + + public function createSchemaManager(Connection $connection): SQLiteSchemaManager + { + return new SQLiteSchemaManager($connection, $this); + } + + /** + * Returns the union select query part surrounded by parenthesis if possible for platform. + */ + public function getUnionSelectPartSQL(string $subQuery): string + { + return $subQuery; + } +} diff --git a/doctrine/dbal/src/Platforms/SqlitePlatform.php b/doctrine/dbal/src/Platforms/SqlitePlatform.php deleted file mode 100644 index b2e1280d0..000000000 --- a/doctrine/dbal/src/Platforms/SqlitePlatform.php +++ /dev/null @@ -1,1545 +0,0 @@ - 0 THEN INSTR(SUBSTR(' . $str . ', ' . $startPos . '), ' . $substr . ') + ' . $startPos - . ' - 1 ELSE 0 END'; - } - - /** - * {@inheritDoc} - */ - protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit) - { - switch ($unit) { - case DateIntervalUnit::SECOND: - case DateIntervalUnit::MINUTE: - case DateIntervalUnit::HOUR: - return 'DATETIME(' . $date . ",'" . $operator . $interval . ' ' . $unit . "')"; - } - - switch ($unit) { - case DateIntervalUnit::WEEK: - $interval = $this->multiplyInterval((string) $interval, 7); - $unit = DateIntervalUnit::DAY; - break; - - case DateIntervalUnit::QUARTER: - $interval = $this->multiplyInterval((string) $interval, 3); - $unit = DateIntervalUnit::MONTH; - break; - } - - if (! is_numeric($interval)) { - $interval = "' || " . $interval . " || '"; - } - - return 'DATE(' . $date . ",'" . $operator . $interval . ' ' . $unit . "')"; - } - - /** - * {@inheritDoc} - */ - public function getDateDiffExpression($date1, $date2) - { - return sprintf("JULIANDAY(%s, 'start of day') - JULIANDAY(%s, 'start of day')", $date1, $date2); - } - - /** - * {@inheritDoc} - * - * The DBAL doesn't support databases on the SQLite platform. The expression here always returns a fixed string - * as an indicator of an implicitly selected database. - * - * @link https://www.sqlite.org/lang_select.html - * @see Connection::getDatabase() - */ - public function getCurrentDatabaseExpression(): string - { - return "'main'"; - } - - /** @link https://www2.sqlite.org/cvstrac/wiki?p=UnsupportedSql */ - public function createSelectSQLBuilder(): SelectSQLBuilder - { - return new DefaultSelectSQLBuilder($this, null, null); - } - - /** - * {@inheritDoc} - */ - protected function _getTransactionIsolationLevelSQL($level) - { - switch ($level) { - case TransactionIsolationLevel::READ_UNCOMMITTED: - return '0'; - - case TransactionIsolationLevel::READ_COMMITTED: - case TransactionIsolationLevel::REPEATABLE_READ: - case TransactionIsolationLevel::SERIALIZABLE: - return '1'; - - default: - return parent::_getTransactionIsolationLevelSQL($level); - } - } - - /** - * {@inheritDoc} - */ - public function getSetTransactionIsolationSQL($level) - { - return 'PRAGMA read_uncommitted = ' . $this->_getTransactionIsolationLevelSQL($level); - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function prefersIdentityColumns() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/1519', - 'SqlitePlatform::prefersIdentityColumns() is deprecated.', - ); - - return true; - } - - /** - * {@inheritDoc} - */ - public function getBooleanTypeDeclarationSQL(array $column) - { - return 'BOOLEAN'; - } - - /** - * {@inheritDoc} - */ - public function getIntegerTypeDeclarationSQL(array $column) - { - return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($column); - } - - /** - * {@inheritDoc} - */ - public function getBigIntTypeDeclarationSQL(array $column) - { - // SQLite autoincrement is implicit for INTEGER PKs, but not for BIGINT columns - if (! empty($column['autoincrement'])) { - return $this->getIntegerTypeDeclarationSQL($column); - } - - return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); - } - - /** - * @deprecated Use {@see getSmallIntTypeDeclarationSQL()} instead. - * - * @param array $column - * - * @return string - */ - public function getTinyIntTypeDeclarationSQL(array $column) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5511', - '%s is deprecated. Use getSmallIntTypeDeclarationSQL() instead.', - __METHOD__, - ); - - // SQLite autoincrement is implicit for INTEGER PKs, but not for TINYINT columns - if (! empty($column['autoincrement'])) { - return $this->getIntegerTypeDeclarationSQL($column); - } - - return 'TINYINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); - } - - /** - * {@inheritDoc} - */ - public function getSmallIntTypeDeclarationSQL(array $column) - { - // SQLite autoincrement is implicit for INTEGER PKs, but not for SMALLINT columns - if (! empty($column['autoincrement'])) { - return $this->getIntegerTypeDeclarationSQL($column); - } - - return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); - } - - /** - * @deprecated Use {@see getIntegerTypeDeclarationSQL()} instead. - * - * @param array $column - * - * @return string - */ - public function getMediumIntTypeDeclarationSQL(array $column) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5511', - '%s is deprecated. Use getIntegerTypeDeclarationSQL() instead.', - __METHOD__, - ); - - // SQLite autoincrement is implicit for INTEGER PKs, but not for MEDIUMINT columns - if (! empty($column['autoincrement'])) { - return $this->getIntegerTypeDeclarationSQL($column); - } - - return 'MEDIUMINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); - } - - /** - * {@inheritDoc} - */ - public function getDateTimeTypeDeclarationSQL(array $column) - { - return 'DATETIME'; - } - - /** - * {@inheritDoc} - */ - public function getDateTypeDeclarationSQL(array $column) - { - return 'DATE'; - } - - /** - * {@inheritDoc} - */ - public function getTimeTypeDeclarationSQL(array $column) - { - return 'TIME'; - } - - /** - * {@inheritDoc} - */ - protected function _getCommonIntegerTypeDeclarationSQL(array $column) - { - // sqlite autoincrement is only possible for the primary key - if (! empty($column['autoincrement'])) { - return ' PRIMARY KEY AUTOINCREMENT'; - } - - return ! empty($column['unsigned']) ? ' UNSIGNED' : ''; - } - - /** - * Disables schema emulation. - * - * Schema emulation is enabled by default to maintain backwards compatibility. - * Disable it to opt-in to the behavior of DBAL 4. - * - * @deprecated Will be removed in DBAL 4.0. - */ - public function disableSchemaEmulation(): void - { - $this->schemaEmulationEnabled = false; - } - - private function emulateSchemaNamespacing(string $tableName): string - { - return $this->schemaEmulationEnabled - ? str_replace('.', '__', $tableName) - : $tableName; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey) - { - return parent::getForeignKeyDeclarationSQL(new ForeignKeyConstraint( - $foreignKey->getQuotedLocalColumns($this), - $this->emulateSchemaNamespacing($foreignKey->getQuotedForeignTableName($this)), - $foreignKey->getQuotedForeignColumns($this), - $foreignKey->getName(), - $foreignKey->getOptions(), - )); - } - - /** - * {@inheritDoc} - */ - protected function _getCreateTableSQL($name, array $columns, array $options = []) - { - $name = $this->emulateSchemaNamespacing($name); - $queryFields = $this->getColumnDeclarationListSQL($columns); - - if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { - foreach ($options['uniqueConstraints'] as $constraintName => $definition) { - $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($constraintName, $definition); - } - } - - $queryFields .= $this->getNonAutoincrementPrimaryKeyDefinition($columns, $options); - - if (isset($options['foreignKeys'])) { - foreach ($options['foreignKeys'] as $foreignKey) { - $queryFields .= ', ' . $this->getForeignKeyDeclarationSQL($foreignKey); - } - } - - $tableComment = ''; - if (isset($options['comment'])) { - $comment = trim($options['comment'], " '"); - - $tableComment = $this->getInlineTableCommentSQL($comment); - } - - $query = ['CREATE TABLE ' . $name . ' ' . $tableComment . '(' . $queryFields . ')']; - - if (isset($options['alter']) && $options['alter'] === true) { - return $query; - } - - if (isset($options['indexes']) && ! empty($options['indexes'])) { - foreach ($options['indexes'] as $indexDef) { - $query[] = $this->getCreateIndexSQL($indexDef, $name); - } - } - - if (isset($options['unique']) && ! empty($options['unique'])) { - foreach ($options['unique'] as $indexDef) { - $query[] = $this->getCreateIndexSQL($indexDef, $name); - } - } - - return $query; - } - - /** - * Generate a PRIMARY KEY definition if no autoincrement value is used - * - * @param mixed[][] $columns - * @param mixed[] $options - */ - private function getNonAutoincrementPrimaryKeyDefinition(array $columns, array $options): string - { - if (empty($options['primary'])) { - return ''; - } - - $keyColumns = array_unique(array_values($options['primary'])); - - foreach ($keyColumns as $keyColumn) { - if (! empty($columns[$keyColumn]['autoincrement'])) { - return ''; - } - } - - return ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; - } - - /** - * {@inheritDoc} - */ - protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) - { - return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(255)') - : ($length > 0 ? 'VARCHAR(' . $length . ')' : 'TEXT'); - } - - /** - * {@inheritDoc} - */ - protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed) - { - return 'BLOB'; - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function getBinaryMaxLength() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'SqlitePlatform::getBinaryMaxLength() is deprecated.', - ); - - return 0; - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function getBinaryDefaultLength() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3263', - 'Relying on the default binary column length is deprecated, specify the length explicitly.', - ); - - return 0; - } - - /** - * {@inheritDoc} - */ - public function getClobTypeDeclarationSQL(array $column) - { - return 'CLOB'; - } - - /** - * @deprecated - * - * {@inheritDoc} - */ - public function getListTableConstraintsSQL($table) - { - $table = $this->emulateSchemaNamespacing($table); - - return sprintf( - "SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name = %s AND sql NOT NULL ORDER BY name", - $this->quoteStringLiteral($table), - ); - } - - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTableColumnsSQL($table, $database = null) - { - $table = $this->emulateSchemaNamespacing($table); - - return sprintf('PRAGMA table_info(%s)', $this->quoteStringLiteral($table)); - } - - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTableIndexesSQL($table, $database = null) - { - $table = $this->emulateSchemaNamespacing($table); - - return sprintf('PRAGMA index_list(%s)', $this->quoteStringLiteral($table)); - } - - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * {@inheritDoc} - */ - public function getListTablesSQL() - { - return 'SELECT name FROM sqlite_master' - . " WHERE type = 'table'" - . " AND name != 'sqlite_sequence'" - . " AND name != 'geometry_columns'" - . " AND name != 'spatial_ref_sys'" - . ' UNION ALL SELECT name FROM sqlite_temp_master' - . " WHERE type = 'table' ORDER BY name"; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. - */ - public function getListViewsSQL($database) - { - return "SELECT name, sql FROM sqlite_master WHERE type='view' AND sql NOT NULL"; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) - { - $query = parent::getAdvancedForeignKeyOptionsSQL($foreignKey); - - if (! $foreignKey->hasOption('deferrable') || $foreignKey->getOption('deferrable') === false) { - $query .= ' NOT'; - } - - $query .= ' DEFERRABLE'; - $query .= ' INITIALLY'; - - if ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) { - $query .= ' DEFERRED'; - } else { - $query .= ' IMMEDIATE'; - } - - return $query; - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function supportsCreateDropDatabase() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5513', - '%s is deprecated.', - __METHOD__, - ); - - return false; - } - - /** - * {@inheritDoc} - */ - public function supportsIdentityColumns() - { - return true; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function supportsColumnCollation() - { - return true; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function supportsInlineColumnComments() - { - return true; - } - - /** - * {@inheritDoc} - */ - public function getName() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4749', - 'SqlitePlatform::getName() is deprecated. Identify platforms by their class.', - ); - - return 'sqlite'; - } - - /** - * {@inheritDoc} - */ - public function getTruncateTableSQL($tableName, $cascade = false) - { - $tableIdentifier = new Identifier($tableName); - $tableName = $this->emulateSchemaNamespacing($tableIdentifier->getQuotedName($this)); - - return 'DELETE FROM ' . $tableName; - } - - /** - * User-defined function for Sqlite that is used with PDO::sqliteCreateFunction(). - * - * @deprecated The driver will use {@see sqrt()} in the next major release. - * - * @param int|float $value - * - * @return float - */ - public static function udfSqrt($value) - { - return sqrt($value); - } - - /** - * User-defined function for Sqlite that implements MOD(a, b). - * - * @deprecated The driver will use {@see UserDefinedFunctions::mod()} in the next major release. - * - * @param int $a - * @param int $b - * - * @return int - */ - public static function udfMod($a, $b) - { - return UserDefinedFunctions::mod($a, $b); - } - - /** - * @deprecated The driver will use {@see UserDefinedFunctions::locate()} in the next major release. - * - * @param string $str - * @param string $substr - * @param int $offset - * - * @return int - */ - public static function udfLocate($str, $substr, $offset = 0) - { - return UserDefinedFunctions::locate($str, $substr, $offset); - } - - /** - * {@inheritDoc} - * - * @deprecated This API is not portable. - */ - public function getForUpdateSQL() - { - return ''; - } - - /** - * {@inheritDoc} - * - * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. - */ - public function getInlineColumnCommentSQL($comment) - { - return '--' . str_replace("\n", "\n--", $comment) . "\n"; - } - - private function getInlineTableCommentSQL(string $comment): string - { - return $this->getInlineColumnCommentSQL($comment); - } - - /** - * {@inheritDoc} - */ - protected function initializeDoctrineTypeMappings() - { - $this->doctrineTypeMapping = [ - 'bigint' => Types\Types::BIGINT, - 'bigserial' => Types\Types::BIGINT, - 'blob' => Types\Types::BLOB, - 'boolean' => Types\Types::BOOLEAN, - 'char' => Types\Types::STRING, - 'clob' => Types\Types::TEXT, - 'date' => Types\Types::DATE_MUTABLE, - 'datetime' => Types\Types::DATETIME_MUTABLE, - 'decimal' => Types\Types::DECIMAL, - 'double' => Types\Types::FLOAT, - 'double precision' => Types\Types::FLOAT, - 'float' => Types\Types::FLOAT, - 'image' => Types\Types::STRING, - 'int' => Types\Types::INTEGER, - 'integer' => Types\Types::INTEGER, - 'longtext' => Types\Types::TEXT, - 'longvarchar' => Types\Types::STRING, - 'mediumint' => Types\Types::INTEGER, - 'mediumtext' => Types\Types::TEXT, - 'ntext' => Types\Types::STRING, - 'numeric' => Types\Types::DECIMAL, - 'nvarchar' => Types\Types::STRING, - 'real' => Types\Types::FLOAT, - 'serial' => Types\Types::INTEGER, - 'smallint' => Types\Types::SMALLINT, - 'text' => Types\Types::TEXT, - 'time' => Types\Types::TIME_MUTABLE, - 'timestamp' => Types\Types::DATETIME_MUTABLE, - 'tinyint' => Types\Types::BOOLEAN, - 'tinytext' => Types\Types::TEXT, - 'varchar' => Types\Types::STRING, - 'varchar2' => Types\Types::STRING, - ]; - } - - /** - * {@inheritDoc} - * - * @deprecated Implement {@see createReservedKeywordsList()} instead. - */ - protected function getReservedKeywordsClass() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4510', - 'SqlitePlatform::getReservedKeywordsClass() is deprecated,' - . ' use SqlitePlatform::createReservedKeywordsList() instead.', - ); - - return Keywords\SQLiteKeywords::class; - } - - /** - * {@inheritDoc} - */ - protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) - { - return []; - } - - /** - * {@inheritDoc} - */ - protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff) - { - $table = $diff->getOldTable(); - - if (! $table instanceof Table) { - throw new Exception( - 'Sqlite platform requires for alter table the table diff with reference to original table schema', - ); - } - - $sql = []; - $tableName = $diff->getNewName(); - - if ($tableName === false) { - $tableName = $diff->getName($this); - } - - foreach ($this->getIndexesInAlteredTable($diff, $table) as $index) { - if ($index->isPrimary()) { - continue; - } - - $sql[] = $this->getCreateIndexSQL($index, $tableName->getQuotedName($this)); - } - - return $sql; - } - - /** - * {@inheritDoc} - */ - protected function doModifyLimitQuery($query, $limit, $offset) - { - if ($limit === null && $offset > 0) { - return sprintf('%s LIMIT -1 OFFSET %d', $query, $offset); - } - - return parent::doModifyLimitQuery($query, $limit, $offset); - } - - /** - * {@inheritDoc} - */ - public function getBlobTypeDeclarationSQL(array $column) - { - return 'BLOB'; - } - - /** - * {@inheritDoc} - */ - public function getTemporaryTableName($tableName) - { - $tableName = $this->emulateSchemaNamespacing($tableName); - - return $tableName; - } - - /** - * {@inheritDoc} - * - * @deprecated - * - * Sqlite Platform emulates schema by underscoring each dot and generating tables - * into the default database. - * - * This hack is implemented to be able to use SQLite as testdriver when - * using schema supporting databases. - */ - public function canEmulateSchemas() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4805', - 'SqlitePlatform::canEmulateSchemas() is deprecated.', - ); - - return $this->schemaEmulationEnabled; - } - - /** - * {@inheritDoc} - */ - public function getCreateTablesSQL(array $tables): array - { - $sql = []; - - foreach ($tables as $table) { - $sql = array_merge($sql, $this->getCreateTableSQL($table)); - } - - return $sql; - } - - /** - * {@inheritDoc} - */ - public function getCreateIndexSQL(Index $index, $table) - { - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $table = $table->getQuotedName($this); - } - - $name = $index->getQuotedName($this); - $columns = $index->getColumns(); - - if (count($columns) === 0) { - throw new InvalidArgumentException(sprintf( - 'Incomplete or invalid index definition %s on table %s', - $name, - $table, - )); - } - - if ($index->isPrimary()) { - return $this->getCreatePrimaryKeySQL($index, $table); - } - - if (strpos($table, '.') !== false) { - [$schema, $table] = explode('.', $table, 2); - $name = $schema . '.' . $name; - } - - $query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table; - $query .= ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')' . $this->getPartialIndexSQL($index); - - return $query; - } - - /** - * {@inheritDoc} - */ - public function getDropTablesSQL(array $tables): array - { - $sql = []; - - foreach ($tables as $table) { - $sql[] = $this->getDropTableSQL($table->getQuotedName($this)); - } - - return $sql; - } - - /** - * {@inheritDoc} - */ - public function getCreatePrimaryKeySQL(Index $index, $table) - { - throw new Exception('Sqlite platform does not support alter primary key.'); - } - - /** - * {@inheritDoc} - */ - public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table) - { - throw new Exception('Sqlite platform does not support alter foreign key.'); - } - - /** - * {@inheritDoc} - */ - public function getDropForeignKeySQL($foreignKey, $table) - { - throw new Exception('Sqlite platform does not support alter foreign key.'); - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function getCreateConstraintSQL(Constraint $constraint, $table) - { - throw new Exception('Sqlite platform does not support alter constraint.'); - } - - /** - * {@inheritDoc} - * - * @param int|null $createFlags - * @phpstan-param int-mask-of|null $createFlags - */ - public function getCreateTableSQL(Table $table, $createFlags = null) - { - $createFlags = $createFlags ?? self::CREATE_INDEXES | self::CREATE_FOREIGNKEYS; - - return parent::getCreateTableSQL($table, $createFlags); - } - - /** - * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. - * - * @param string $table - * @param string|null $database - * - * @return string - */ - public function getListTableForeignKeysSQL($table, $database = null) - { - $table = $this->emulateSchemaNamespacing($table); - - return sprintf('PRAGMA foreign_key_list(%s)', $this->quoteStringLiteral($table)); - } - - /** - * {@inheritDoc} - */ - public function getAlterTableSQL(TableDiff $diff) - { - $sql = $this->getSimpleAlterTableSQL($diff); - if ($sql !== false) { - return $sql; - } - - $table = $diff->getOldTable(); - - if (! $table instanceof Table) { - throw new Exception( - 'Sqlite platform requires for alter table the table diff with reference to original table schema', - ); - } - - $columns = []; - $oldColumnNames = []; - $newColumnNames = []; - $columnSql = []; - - foreach ($table->getColumns() as $columnName => $column) { - $columnName = strtolower($columnName); - $columns[$columnName] = $column; - $oldColumnNames[$columnName] = $newColumnNames[$columnName] = $column->getQuotedName($this); - } - - foreach ($diff->getDroppedColumns() as $column) { - if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { - continue; - } - - $columnName = strtolower($column->getName()); - if (! isset($columns[$columnName])) { - continue; - } - - unset( - $columns[$columnName], - $oldColumnNames[$columnName], - $newColumnNames[$columnName], - ); - } - - foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { - if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { - continue; - } - - $oldColumnName = strtolower($oldColumnName); - - $columns = $this->replaceColumn( - $table->getName(), - $columns, - $oldColumnName, - $column, - ); - - if (! isset($newColumnNames[$oldColumnName])) { - continue; - } - - $newColumnNames[$oldColumnName] = $column->getQuotedName($this); - } - - foreach ($diff->getModifiedColumns() as $columnDiff) { - if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { - continue; - } - - $oldColumn = $columnDiff->getOldColumn() ?? $columnDiff->getOldColumnName(); - - $oldColumnName = strtolower($oldColumn->getName()); - - $columns = $this->replaceColumn( - $table->getName(), - $columns, - $oldColumnName, - $columnDiff->getNewColumn(), - ); - - if (! isset($newColumnNames[$oldColumnName])) { - continue; - } - - $newColumnNames[$oldColumnName] = $columnDiff->getNewColumn()->getQuotedName($this); - } - - foreach ($diff->getAddedColumns() as $column) { - if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { - continue; - } - - $columns[strtolower($column->getName())] = $column; - } - - $sql = []; - $tableSql = []; - if (! $this->onSchemaAlterTable($diff, $tableSql)) { - $tableName = $table->getName(); - if (strpos($tableName, '.') !== false) { - [, $tableName] = explode('.', $tableName, 2); - } - - $dataTable = new Table('__temp__' . $tableName); - - $newTable = new Table( - $table->getQuotedName($this), - $columns, - $this->getPrimaryIndexInAlteredTable($diff, $table), - [], - $this->getForeignKeysInAlteredTable($diff, $table), - $table->getOptions(), - ); - $newTable->addOption('alter', true); - - $sql = $this->getPreAlterTableIndexForeignKeySQL($diff); - - $sql[] = sprintf( - 'CREATE TEMPORARY TABLE %s AS SELECT %s FROM %s', - $dataTable->getQuotedName($this), - implode(', ', $oldColumnNames), - $table->getQuotedName($this), - ); - $sql[] = $this->getDropTableSQL($table); - - $sql = array_merge($sql, $this->getCreateTableSQL($newTable)); - $sql[] = sprintf( - 'INSERT INTO %s (%s) SELECT %s FROM %s', - $newTable->getQuotedName($this), - implode(', ', $newColumnNames), - implode(', ', $oldColumnNames), - $dataTable->getQuotedName($this), - ); - $sql[] = $this->getDropTableSQL($dataTable->getQuotedName($this)); - - $newName = $diff->getNewName(); - - if ($newName !== false) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5663', - 'Generation of "rename table" SQL using %s is deprecated. Use getRenameTableSQL() instead.', - __METHOD__, - ); - - $sql[] = sprintf( - 'ALTER TABLE %s RENAME TO %s', - $newTable->getQuotedName($this), - $newName->getQuotedName($this), - ); - } - - $sql = array_merge($sql, $this->getPostAlterTableIndexForeignKeySQL($diff)); - } - - return array_merge($sql, $tableSql, $columnSql); - } - - /** - * Replace the column with the given name with the new column. - * - * @param string $tableName - * @param array $columns - * @param string $columnName - * - * @return array - * - * @throws Exception - */ - private function replaceColumn($tableName, array $columns, $columnName, Column $column): array - { - $keys = array_keys($columns); - $index = array_search($columnName, $keys, true); - - if ($index === false) { - throw SchemaException::columnDoesNotExist($columnName, $tableName); - } - - $values = array_values($columns); - - $keys[$index] = strtolower($column->getName()); - $values[$index] = $column; - - return array_combine($keys, $values); - } - - /** - * @return string[]|false - * - * @throws Exception - */ - private function getSimpleAlterTableSQL(TableDiff $diff) - { - // Suppress changes on integer type autoincrement columns. - foreach ($diff->getModifiedColumns() as $columnDiff) { - $oldColumn = $columnDiff->getOldColumn(); - - if ($oldColumn === null) { - continue; - } - - $newColumn = $columnDiff->getNewColumn(); - - if (! $newColumn->getAutoincrement() || ! $newColumn->getType() instanceof IntegerType) { - continue; - } - - $oldColumnName = $oldColumn->getName(); - - if (! $columnDiff->hasTypeChanged() && $columnDiff->hasUnsignedChanged()) { - unset($diff->changedColumns[$oldColumnName]); - - continue; - } - - $fromColumnType = $oldColumn->getType(); - - if (! ($fromColumnType instanceof Types\SmallIntType) && ! ($fromColumnType instanceof Types\BigIntType)) { - continue; - } - - unset($diff->changedColumns[$oldColumnName]); - } - - if ( - count($diff->getModifiedColumns()) > 0 - || count($diff->getDroppedColumns()) > 0 - || count($diff->getRenamedColumns()) > 0 - || count($diff->getAddedIndexes()) > 0 - || count($diff->getModifiedIndexes()) > 0 - || count($diff->getDroppedIndexes()) > 0 - || count($diff->getRenamedIndexes()) > 0 - || count($diff->getAddedForeignKeys()) > 0 - || count($diff->getModifiedForeignKeys()) > 0 - || count($diff->getDroppedForeignKeys()) > 0 - ) { - return false; - } - - $table = $diff->getOldTable() ?? $diff->getName($this); - - $sql = []; - $tableSql = []; - $columnSql = []; - - foreach ($diff->getAddedColumns() as $column) { - if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { - continue; - } - - $definition = array_merge([ - 'unique' => null, - 'autoincrement' => null, - 'default' => null, - ], $column->toArray()); - - $type = $definition['type']; - - switch (true) { - case isset($definition['columnDefinition']) || $definition['autoincrement'] || $definition['unique']: - case $type instanceof Types\DateTimeType && $definition['default'] === $this->getCurrentTimestampSQL(): - case $type instanceof Types\DateType && $definition['default'] === $this->getCurrentDateSQL(): - case $type instanceof Types\TimeType && $definition['default'] === $this->getCurrentTimeSQL(): - return false; - } - - $definition['name'] = $column->getQuotedName($this); - if ($type instanceof Types\StringType) { - $definition['length'] ??= 255; - } - - $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' ADD COLUMN ' - . $this->getColumnDeclarationSQL($definition['name'], $definition); - } - - if (! $this->onSchemaAlterTable($diff, $tableSql)) { - if ($diff->newName !== false) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5663', - 'Generation of SQL that renames a table using %s is deprecated.' - . ' Use getRenameTableSQL() instead.', - __METHOD__, - ); - - $newTable = new Identifier($diff->newName); - - $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' RENAME TO ' - . $newTable->getQuotedName($this); - } - } - - return array_merge($sql, $tableSql, $columnSql); - } - - /** @return string[] */ - private function getColumnNamesInAlteredTable(TableDiff $diff, Table $fromTable): array - { - $columns = []; - - foreach ($fromTable->getColumns() as $columnName => $column) { - $columns[strtolower($columnName)] = $column->getName(); - } - - foreach ($diff->getDroppedColumns() as $column) { - $columnName = strtolower($column->getName()); - if (! isset($columns[$columnName])) { - continue; - } - - unset($columns[$columnName]); - } - - foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { - $columnName = $column->getName(); - $columns[strtolower($oldColumnName)] = $columnName; - $columns[strtolower($columnName)] = $columnName; - } - - foreach ($diff->getModifiedColumns() as $columnDiff) { - $oldColumn = $columnDiff->getOldColumn() ?? $columnDiff->getOldColumnName(); - - $oldColumnName = $oldColumn->getName(); - $newColumnName = $columnDiff->getNewColumn()->getName(); - $columns[strtolower($oldColumnName)] = $newColumnName; - $columns[strtolower($newColumnName)] = $newColumnName; - } - - foreach ($diff->getAddedColumns() as $column) { - $columnName = $column->getName(); - $columns[strtolower($columnName)] = $columnName; - } - - return $columns; - } - - /** @return Index[] */ - private function getIndexesInAlteredTable(TableDiff $diff, Table $fromTable): array - { - $indexes = $fromTable->getIndexes(); - $columnNames = $this->getColumnNamesInAlteredTable($diff, $fromTable); - - foreach ($indexes as $key => $index) { - foreach ($diff->getRenamedIndexes() as $oldIndexName => $renamedIndex) { - if (strtolower($key) !== strtolower($oldIndexName)) { - continue; - } - - unset($indexes[$key]); - } - - $changed = false; - $indexColumns = []; - foreach ($index->getColumns() as $columnName) { - $normalizedColumnName = strtolower($columnName); - if (! isset($columnNames[$normalizedColumnName])) { - unset($indexes[$key]); - continue 2; - } - - $indexColumns[] = $columnNames[$normalizedColumnName]; - if ($columnName === $columnNames[$normalizedColumnName]) { - continue; - } - - $changed = true; - } - - if (! $changed) { - continue; - } - - $indexes[$key] = new Index( - $index->getName(), - $indexColumns, - $index->isUnique(), - $index->isPrimary(), - $index->getFlags(), - ); - } - - foreach ($diff->getDroppedIndexes() as $index) { - $indexName = strtolower($index->getName()); - if (strlen($indexName) === 0 || ! isset($indexes[$indexName])) { - continue; - } - - unset($indexes[$indexName]); - } - - foreach ( - array_merge( - $diff->getModifiedIndexes(), - $diff->getAddedIndexes(), - $diff->getRenamedIndexes(), - ) as $index - ) { - $indexName = strtolower($index->getName()); - if (strlen($indexName) > 0) { - $indexes[$indexName] = $index; - } else { - $indexes[] = $index; - } - } - - return $indexes; - } - - /** @return ForeignKeyConstraint[] */ - private function getForeignKeysInAlteredTable(TableDiff $diff, Table $fromTable): array - { - $foreignKeys = $fromTable->getForeignKeys(); - $columnNames = $this->getColumnNamesInAlteredTable($diff, $fromTable); - - foreach ($foreignKeys as $key => $constraint) { - $changed = false; - $localColumns = []; - foreach ($constraint->getLocalColumns() as $columnName) { - $normalizedColumnName = strtolower($columnName); - if (! isset($columnNames[$normalizedColumnName])) { - unset($foreignKeys[$key]); - continue 2; - } - - $localColumns[] = $columnNames[$normalizedColumnName]; - if ($columnName === $columnNames[$normalizedColumnName]) { - continue; - } - - $changed = true; - } - - if (! $changed) { - continue; - } - - $foreignKeys[$key] = new ForeignKeyConstraint( - $localColumns, - $constraint->getForeignTableName(), - $constraint->getForeignColumns(), - $constraint->getName(), - $constraint->getOptions(), - ); - } - - foreach ($diff->getDroppedForeignKeys() as $constraint) { - if (! $constraint instanceof ForeignKeyConstraint) { - $constraint = new Identifier($constraint); - } - - $constraintName = strtolower($constraint->getName()); - if (strlen($constraintName) === 0 || ! isset($foreignKeys[$constraintName])) { - continue; - } - - unset($foreignKeys[$constraintName]); - } - - foreach (array_merge($diff->getModifiedForeignKeys(), $diff->getAddedForeignKeys()) as $constraint) { - $constraintName = strtolower($constraint->getName()); - if (strlen($constraintName) > 0) { - $foreignKeys[$constraintName] = $constraint; - } else { - $foreignKeys[] = $constraint; - } - } - - return $foreignKeys; - } - - /** @return Index[] */ - private function getPrimaryIndexInAlteredTable(TableDiff $diff, Table $fromTable): array - { - $primaryIndex = []; - - foreach ($this->getIndexesInAlteredTable($diff, $fromTable) as $index) { - if (! $index->isPrimary()) { - continue; - } - - $primaryIndex = [$index->getName() => $index]; - } - - return $primaryIndex; - } - - public function createSchemaManager(Connection $connection): SqliteSchemaManager - { - return new SqliteSchemaManager($connection, $this); - } -} diff --git a/doctrine/dbal/src/Platforms/TrimMode.php b/doctrine/dbal/src/Platforms/TrimMode.php index 01356c0d3..31c237543 100644 --- a/doctrine/dbal/src/Platforms/TrimMode.php +++ b/doctrine/dbal/src/Platforms/TrimMode.php @@ -4,18 +4,10 @@ namespace Doctrine\DBAL\Platforms; -final class TrimMode +enum TrimMode { - public const UNSPECIFIED = 0; - - public const LEADING = 1; - - public const TRAILING = 2; - - public const BOTH = 3; - - /** @codeCoverageIgnore */ - private function __construct() - { - } + case UNSPECIFIED; + case LEADING; + case TRAILING; + case BOTH; } diff --git a/doctrine/dbal/src/Portability/Connection.php b/doctrine/dbal/src/Portability/Connection.php index fc1bdd9ad..4a821d830 100644 --- a/doctrine/dbal/src/Portability/Connection.php +++ b/doctrine/dbal/src/Portability/Connection.php @@ -1,11 +1,11 @@ converter = $converter; } - public function prepare(string $sql): DriverStatement + public function prepare(string $sql): Statement { return new Statement( parent::prepare($sql), @@ -35,7 +31,7 @@ public function prepare(string $sql): DriverStatement ); } - public function query(string $sql): DriverResult + public function query(string $sql): Result { return new Result( parent::query($sql), diff --git a/doctrine/dbal/src/Portability/Converter.php b/doctrine/dbal/src/Portability/Converter.php index effbf986f..d26e85f68 100644 --- a/doctrine/dbal/src/Portability/Converter.php +++ b/doctrine/dbal/src/Portability/Converter.php @@ -4,11 +4,15 @@ namespace Doctrine\DBAL\Portability; +use Closure; + use function array_change_key_case; use function array_map; use function array_reduce; use function is_string; use function rtrim; +use function strtolower; +use function strtoupper; use const CASE_LOWER; use const CASE_UPPER; @@ -18,23 +22,13 @@ final class Converter public const CASE_LOWER = CASE_LOWER; public const CASE_UPPER = CASE_UPPER; - /** @var callable */ - private $convertNumeric; - - /** @var callable */ - private $convertAssociative; - - /** @var callable */ - private $convertOne; - - /** @var callable */ - private $convertAllNumeric; - - /** @var callable */ - private $convertAllAssociative; - - /** @var callable */ - private $convertFirstColumn; + private readonly Closure $convertNumeric; + private readonly Closure $convertAssociative; + private readonly Closure $convertOne; + private readonly Closure $convertAllNumeric; + private readonly Closure $convertAllAssociative; + private readonly Closure $convertFirstColumn; + private readonly Closure $convertColumnName; /** * @param bool $convertEmptyStringToNull Whether each empty string should @@ -50,13 +44,19 @@ public function __construct(bool $convertEmptyStringToNull, bool $rightTrimStrin $convertNumeric = $this->createConvertRow($convertValue, null); $convertAssociative = $this->createConvertRow($convertValue, $case); - $this->convertNumeric = $this->createConvert($convertNumeric, [self::class, 'id']); - $this->convertAssociative = $this->createConvert($convertAssociative, [self::class, 'id']); - $this->convertOne = $this->createConvert($convertValue, [self::class, 'id']); + $this->convertNumeric = $this->createConvert($convertNumeric); + $this->convertAssociative = $this->createConvert($convertAssociative); + $this->convertOne = $this->createConvert($convertValue); + + $this->convertAllNumeric = $this->createConvertAll($convertNumeric); + $this->convertAllAssociative = $this->createConvertAll($convertAssociative); + $this->convertFirstColumn = $this->createConvertAll($convertValue); - $this->convertAllNumeric = $this->createConvertAll($convertNumeric, [self::class, 'id']); - $this->convertAllAssociative = $this->createConvertAll($convertAssociative, [self::class, 'id']); - $this->convertFirstColumn = $this->createConvertAll($convertValue, [self::class, 'id']); + $this->convertColumnName = match ($case) { + null => static fn (string $name) => $name, + self::CASE_LOWER => strtolower(...), + self::CASE_UPPER => strtoupper(...), + }; } /** @@ -64,7 +64,7 @@ public function __construct(bool $convertEmptyStringToNull, bool $rightTrimStrin * * @return list|false */ - public function convertNumeric($row) + public function convertNumeric(array|false $row): array|false { return ($this->convertNumeric)($row); } @@ -74,17 +74,12 @@ public function convertNumeric($row) * * @return array|false */ - public function convertAssociative($row) + public function convertAssociative(array|false $row): array|false { return ($this->convertAssociative)($row); } - /** - * @param mixed|false $value - * - * @return mixed|false - */ - public function convertOne($value) + public function convertOne(mixed $value): mixed { return ($this->convertOne)($value); } @@ -119,6 +114,11 @@ public function convertFirstColumn(array $data): array return ($this->convertFirstColumn)($data); } + public function convertColumnName(string $name): string + { + return ($this->convertColumnName)($name); + } + /** * @param T $value * @@ -126,7 +126,7 @@ public function convertFirstColumn(array $data): array * * @template T */ - private static function id($value) + private static function id(mixed $value): mixed { return $value; } @@ -138,7 +138,7 @@ private static function id($value) * * @template T */ - private static function convertEmptyStringToNull($value) + private static function convertEmptyStringToNull(mixed $value): mixed { if ($value === '') { return null; @@ -155,7 +155,7 @@ private static function convertEmptyStringToNull($value) * * @template T */ - private static function rightTrimString($value) + private static function rightTrimString(mixed $value): mixed { if (! is_string($value)) { return $value; @@ -170,18 +170,18 @@ private static function rightTrimString($value) * @param bool $convertEmptyStringToNull Whether each empty string should be converted to NULL * @param bool $rightTrimString Whether each string should right-trimmed * - * @return callable|null The resulting function or NULL if no conversion is needed + * @return Closure|null The resulting function or NULL if no conversion is needed */ - private function createConvertValue(bool $convertEmptyStringToNull, bool $rightTrimString): ?callable + private function createConvertValue(bool $convertEmptyStringToNull, bool $rightTrimString): ?Closure { $functions = []; if ($convertEmptyStringToNull) { - $functions[] = [self::class, 'convertEmptyStringToNull']; + $functions[] = self::convertEmptyStringToNull(...); } if ($rightTrimString) { - $functions[] = [self::class, 'rightTrimString']; + $functions[] = self::rightTrimString(...); } return $this->compose(...$functions); @@ -190,12 +190,12 @@ private function createConvertValue(bool $convertEmptyStringToNull, bool $rightT /** * Creates a function that will convert each array-row retrieved from the database * - * @param callable|null $function The function that will convert each value + * @param Closure|null $function The function that will convert each value * @param self::CASE_LOWER|self::CASE_UPPER|null $case Column name case * - * @return callable|null The resulting function or NULL if no conversion is needed + * @return Closure|null The resulting function or NULL if no conversion is needed */ - private function createConvertRow(?callable $function, ?int $case): ?callable + private function createConvertRow(?Closure $function, ?int $case): ?Closure { $functions = []; @@ -204,9 +204,7 @@ private function createConvertRow(?callable $function, ?int $case): ?callable } if ($case !== null) { - $functions[] = static function (array $row) use ($case): array { - return array_change_key_case($row, $case); - }; + $functions[] = static fn (array $row): array => array_change_key_case($row, $case); } return $this->compose(...$functions); @@ -216,13 +214,12 @@ private function createConvertRow(?callable $function, ?int $case): ?callable * Creates a function that will be applied to the return value of Statement::fetch*() * or an identity function if no conversion is needed * - * @param callable|null $function The function that will convert each tow - * @param callable $id Identity function + * @param Closure|null $function The function that will convert each tow */ - private function createConvert(?callable $function, callable $id): callable + private function createConvert(?Closure $function): Closure { if ($function === null) { - return $id; + return self::id(...); } return /** @@ -232,7 +229,7 @@ private function createConvert(?callable $function, callable $id): callable * * @template T */ - static function ($value) use ($function) { + static function (mixed $value) use ($function): mixed { if ($value === false) { return false; } @@ -245,13 +242,12 @@ static function ($value) use ($function) { * Creates a function that will be applied to the return value of Statement::fetchAll*() * or an identity function if no transformation is required * - * @param callable|null $function The function that will transform each value - * @param callable $id Identity function + * @param Closure|null $function The function that will transform each value */ - private function createConvertAll(?callable $function, callable $id): callable + private function createConvertAll(?Closure $function): Closure { if ($function === null) { - return $id; + return self::id(...); } return $this->createMapper($function); @@ -260,27 +256,27 @@ private function createConvertAll(?callable $function, callable $id): callable /** * Creates a function that maps each value of the array using the given function * - * @param callable $function The function that maps each value of the array + * @param Closure $function The function that maps each value of the array + * + * @return Closure(array):array */ - private function createMapper(callable $function): callable + private function createMapper(Closure $function): Closure { - return static function (array $array) use ($function): array { - return array_map($function, $array); - }; + return static fn (array $array): array => array_map($function, $array); } /** * Creates a composition of the given set of functions * - * @param callable(T):T ...$functions The functions to compose + * @param Closure(T):T ...$functions The functions to compose * - * @return callable(T):T|null + * @return Closure(T):T|null * * @template T */ - private function compose(callable ...$functions): ?callable + private function compose(Closure ...$functions): ?Closure { - return array_reduce($functions, static function (?callable $carry, callable $item): callable { + return array_reduce($functions, static function (?Closure $carry, Closure $item): Closure { if ($carry === null) { return $item; } @@ -292,9 +288,7 @@ private function compose(callable ...$functions): ?callable * * @template T */ - static function ($value) use ($carry, $item) { - return $item($carry($value)); - }; + static fn (mixed $value): mixed => $item($carry($value)); }); } } diff --git a/doctrine/dbal/src/Portability/Driver.php b/doctrine/dbal/src/Portability/Driver.php index a5ac9679a..d2f7de6d4 100644 --- a/doctrine/dbal/src/Portability/Driver.php +++ b/doctrine/dbal/src/Portability/Driver.php @@ -1,70 +1,61 @@ mode = $mode; - $this->case = $case; } /** * {@inheritDoc} + * + * @throws PlatformException + * @throws Exception */ public function connect( #[SensitiveParameter] - array $params - ) { + array $params, + ): ConnectionInterface { $connection = parent::connect($params); $portability = (new OptimizeFlags())( - $this->getDatabasePlatform(), + $this->getDatabasePlatform($connection), $this->mode, ); $case = null; - if ($this->case !== 0 && ($portability & Connection::PORTABILITY_FIX_CASE) !== 0) { - $nativeConnection = null; - if (method_exists($connection, 'getNativeConnection')) { - try { - $nativeConnection = $connection->getNativeConnection(); - } catch (LogicException $e) { - } - } + if ($this->case !== null && ($portability & Connection::PORTABILITY_FIX_CASE) !== 0) { + $nativeConnection = $connection->getNativeConnection(); + + $case = match ($this->case) { + ColumnCase::LOWER => CASE_LOWER, + ColumnCase::UPPER => CASE_UPPER, + }; if ($nativeConnection instanceof PDO) { $portability &= ~Connection::PORTABILITY_FIX_CASE; - $nativeConnection->setAttribute( - PDO::ATTR_CASE, - $this->case === ColumnCase::LOWER ? PDO::CASE_LOWER : PDO::CASE_UPPER, - ); - } else { - $case = $this->case === ColumnCase::LOWER ? Converter::CASE_LOWER : Converter::CASE_UPPER; + $nativeConnection->setAttribute(PDO::ATTR_CASE, $case); } } diff --git a/doctrine/dbal/src/Portability/Middleware.php b/doctrine/dbal/src/Portability/Middleware.php index fae2c5567..b97897c97 100644 --- a/doctrine/dbal/src/Portability/Middleware.php +++ b/doctrine/dbal/src/Portability/Middleware.php @@ -10,21 +10,8 @@ final class Middleware implements MiddlewareInterface { - private int $mode; - - /** @var 0|ColumnCase::LOWER|ColumnCase::UPPER */ - private int $case; - - /** - * @param 0|ColumnCase::LOWER|ColumnCase::UPPER $case Determines how the column case will be treated. - * 0: The case will be left as is in the database. - * {@see ColumnCase::LOWER}: The case will be lowercased. - * {@see ColumnCase::UPPER}: The case will be uppercased. - */ - public function __construct(int $mode, int $case) + public function __construct(private readonly int $mode, private readonly ?ColumnCase $case) { - $this->mode = $mode; - $this->case = $case; } public function wrap(DriverInterface $driver): DriverInterface diff --git a/doctrine/dbal/src/Portability/OptimizeFlags.php b/doctrine/dbal/src/Portability/OptimizeFlags.php index 884a936c7..c985d4bfd 100644 --- a/doctrine/dbal/src/Portability/OptimizeFlags.php +++ b/doctrine/dbal/src/Portability/OptimizeFlags.php @@ -8,7 +8,7 @@ use Doctrine\DBAL\Platforms\DB2Platform; use Doctrine\DBAL\Platforms\OraclePlatform; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; -use Doctrine\DBAL\Platforms\SqlitePlatform; +use Doctrine\DBAL\Platforms\SQLitePlatform; use Doctrine\DBAL\Platforms\SQLServerPlatform; final class OptimizeFlags @@ -23,7 +23,7 @@ final class OptimizeFlags DB2Platform::class => 0, OraclePlatform::class => Connection::PORTABILITY_EMPTY_TO_NULL, PostgreSQLPlatform::class => 0, - SqlitePlatform::class => 0, + SQLitePlatform::class => 0, SQLServerPlatform::class => 0, ]; diff --git a/doctrine/dbal/src/Portability/Result.php b/doctrine/dbal/src/Portability/Result.php index da1eca985..bb1208fab 100644 --- a/doctrine/dbal/src/Portability/Result.php +++ b/doctrine/dbal/src/Portability/Result.php @@ -9,40 +9,27 @@ final class Result extends AbstractResultMiddleware { - private Converter $converter; - /** @internal The result can be only instantiated by the portability connection or statement. */ - public function __construct(ResultInterface $result, Converter $converter) + public function __construct(ResultInterface $result, private readonly Converter $converter) { parent::__construct($result); - - $this->converter = $converter; } - /** - * {@inheritDoc} - */ - public function fetchNumeric() + public function fetchNumeric(): array|false { return $this->converter->convertNumeric( parent::fetchNumeric(), ); } - /** - * {@inheritDoc} - */ - public function fetchAssociative() + public function fetchAssociative(): array|false { return $this->converter->convertAssociative( parent::fetchAssociative(), ); } - /** - * {@inheritDoc} - */ - public function fetchOne() + public function fetchOne(): mixed { return $this->converter->convertOne( parent::fetchOne(), @@ -78,4 +65,11 @@ public function fetchFirstColumn(): array parent::fetchFirstColumn(), ); } + + public function getColumnName(int $index): string + { + return $this->converter->convertColumnName( + parent::getColumnName($index), + ); + } } diff --git a/doctrine/dbal/src/Portability/Statement.php b/doctrine/dbal/src/Portability/Statement.php index 8fcd79d4a..de0c76fc4 100644 --- a/doctrine/dbal/src/Portability/Statement.php +++ b/doctrine/dbal/src/Portability/Statement.php @@ -1,5 +1,7 @@ Statement and applies portability measures. */ - public function __construct(DriverStatement $stmt, Converter $converter) + public function __construct(DriverStatement $stmt, private readonly Converter $converter) { parent::__construct($stmt); - - $this->converter = $converter; } - /** - * {@inheritDoc} - */ - public function execute($params = null): ResultInterface + public function execute(): ResultInterface { return new Result( - parent::execute($params), + parent::execute(), $this->converter, ); } diff --git a/doctrine/dbal/src/Query.php b/doctrine/dbal/src/Query.php index e4ab80e7c..5ea162bf9 100644 --- a/doctrine/dbal/src/Query.php +++ b/doctrine/dbal/src/Query.php @@ -4,41 +4,22 @@ namespace Doctrine\DBAL; -use Doctrine\DBAL\Types\Type; - /** * An SQL query together with its bound parameters. + * + * @phpstan-import-type WrapperParameterType from Connection */ final class Query { /** - * The SQL query. - */ - private string $sql; - - /** - * The parameters bound to the query. - * - * @var array + * @param array $params + * @phpstan-param array $types */ - private array $params; - - /** - * The types of the parameters bound to the query. - * - * @var array - */ - private array $types; - - /** - * @param array $params - * @param array $types - */ - public function __construct(string $sql, array $params, array $types) - { - $this->sql = $sql; - $this->params = $params; - $this->types = $types; + public function __construct( + private readonly string $sql, + private readonly array $params, + private readonly array $types, + ) { } public function getSQL(): string @@ -52,7 +33,7 @@ public function getParams(): array return $this->params; } - /** @return array */ + /** @phpstan-return array */ public function getTypes(): array { return $this->types; diff --git a/doctrine/dbal/src/Query/CommonTableExpression.php b/doctrine/dbal/src/Query/CommonTableExpression.php new file mode 100644 index 000000000..0e37bd4c7 --- /dev/null +++ b/doctrine/dbal/src/Query/CommonTableExpression.php @@ -0,0 +1,27 @@ +type = $type; - - $this->addMultiple($parts); - - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3864', - 'Do not use CompositeExpression constructor directly, use static and() and or() factory methods.', - ); - } - - /** - * @param self|string $part - * @param self|string ...$parts - */ - public static function and($part, ...$parts): self - { - return new self(self::TYPE_AND, array_merge([$part], $parts)); - } - - /** - * @param self|string $part - * @param self|string ...$parts + * @var array */ - public static function or($part, ...$parts): self - { - return new self(self::TYPE_OR, array_merge([$part], $parts)); + private array $parts; + + /** @internal Use the and() / or() factory methods. */ + public function __construct( + private readonly string $type, + self|string $part, + self|string ...$parts, + ) { + $this->parts = array_merge([$part], array_values($parts)); } - /** - * Adds multiple parts to composite expression. - * - * @deprecated This class will be made immutable. Use with() instead. - * - * @param self[]|string[] $parts - * - * @return CompositeExpression - */ - public function addMultiple(array $parts = []) + public static function and(self|string $part, self|string ...$parts): self { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3844', - 'CompositeExpression::addMultiple() is deprecated, use CompositeExpression::with() instead.', - ); - - foreach ($parts as $part) { - $this->add($part); - } - - return $this; + return new self(self::TYPE_AND, $part, ...$parts); } - /** - * Adds an expression to composite expression. - * - * @deprecated This class will be made immutable. Use with() instead. - * - * @param mixed $part - * - * @return CompositeExpression - */ - public function add($part) + public static function or(self|string $part, self|string ...$parts): self { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3844', - 'CompositeExpression::add() is deprecated, use CompositeExpression::with() instead.', - ); - - if ($part === null) { - return $this; - } - - if ($part instanceof self && count($part) === 0) { - return $this; - } - - $this->parts[] = $part; - - return $this; + return new self(self::TYPE_OR, $part, ...$parts); } /** * Returns a new CompositeExpression with the given parts added. - * - * @param self|string $part - * @param self|string ...$parts */ - public function with($part, ...$parts): self + public function with(self|string $part, self|string ...$parts): self { $that = clone $this; - $that->parts = array_merge($that->parts, [$part], $parts); + $that->parts = array_merge($that->parts, [$part], array_values($parts)); return $that; } @@ -148,21 +69,17 @@ public function with($part, ...$parts): self /** * Retrieves the amount of expressions on composite expression. * - * @return int * @phpstan-return int<0, max> */ - #[ReturnTypeWillChange] - public function count() + public function count(): int { return count($this->parts); } /** * Retrieves the string representation of this composite expression. - * - * @return string */ - public function __toString() + public function __toString(): string { if ($this->count() === 1) { return (string) $this->parts[0]; @@ -173,10 +90,8 @@ public function __toString() /** * Returns the type of this composite expression (AND/OR). - * - * @return string */ - public function getType() + public function getType(): string { return $this->type; } diff --git a/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php b/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php index d532b2894..55e14960c 100644 --- a/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php +++ b/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php @@ -1,13 +1,12 @@ '; - public const LT = '<'; - public const LTE = '<='; - public const GT = '>'; - public const GTE = '>='; - - /** - * The DBAL Connection. - */ - private Connection $connection; + final public const EQ = '='; + final public const NEQ = '<>'; + final public const LT = '<'; + final public const LTE = '<='; + final public const GT = '>'; + final public const GTE = '>='; /** * Initializes a new ExpressionBuilder. * * @param Connection $connection The DBAL Connection. */ - public function __construct(Connection $connection) + public function __construct(private readonly Connection $connection) { - $this->connection = $connection; } /** * Creates a conjunction of the given expressions. - * - * @param string|CompositeExpression $expression - * @param string|CompositeExpression ...$expressions */ - public function and($expression, ...$expressions): CompositeExpression - { + public function and( + string|CompositeExpression $expression, + string|CompositeExpression ...$expressions, + ): CompositeExpression { return CompositeExpression::and($expression, ...$expressions); } /** * Creates a disjunction of the given expressions. - * - * @param string|CompositeExpression $expression - * @param string|CompositeExpression ...$expressions */ - public function or($expression, ...$expressions): CompositeExpression - { + public function or( + string|CompositeExpression $expression, + string|CompositeExpression ...$expressions, + ): CompositeExpression { return CompositeExpression::or($expression, ...$expressions); } - /** - * @deprecated Use `and()` instead. - * - * @param mixed $x Optional clause. Defaults = null, but requires - * at least one defined when converting to string. - * - * @return CompositeExpression - */ - public function andX($x = null) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3851', - 'ExpressionBuilder::andX() is deprecated, use ExpressionBuilder::and() instead.', - ); - - return new CompositeExpression(CompositeExpression::TYPE_AND, func_get_args()); - } - - /** - * @deprecated Use `or()` instead. - * - * @param mixed $x Optional clause. Defaults = null, but requires - * at least one defined when converting to string. - * - * @return CompositeExpression - */ - public function orX($x = null) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/3851', - 'ExpressionBuilder::orX() is deprecated, use ExpressionBuilder::or() instead.', - ); - - return new CompositeExpression(CompositeExpression::TYPE_OR, func_get_args()); - } - /** * Creates a comparison expression. * - * @param mixed $x The left expression. - * @param string $operator One of the ExpressionBuilder::* constants. - * @param mixed $y The right expression. - * - * @return string + * @param string $x The left expression. + * @param string $operator The comparison operator. + * @param string $y The right expression. */ - public function comparison($x, $operator, $y) + public function comparison(string $x, string $operator, string $y): string { return $x . ' ' . $operator . ' ' . $y; } @@ -122,12 +73,10 @@ public function comparison($x, $operator, $y) * // u.id = ? * $expr->eq('u.id', '?'); * - * @param mixed $x The left expression. - * @param mixed $y The right expression. - * - * @return string + * @param string $x The left expression. + * @param string $y The right expression. */ - public function eq($x, $y) + public function eq(string $x, string $y): string { return $this->comparison($x, self::EQ, $y); } @@ -141,12 +90,10 @@ public function eq($x, $y) * // u.id <> 1 * $q->where($q->expr()->neq('u.id', '1')); * - * @param mixed $x The left expression. - * @param mixed $y The right expression. - * - * @return string + * @param string $x The left expression. + * @param string $y The right expression. */ - public function neq($x, $y) + public function neq(string $x, string $y): string { return $this->comparison($x, self::NEQ, $y); } @@ -160,12 +107,10 @@ public function neq($x, $y) * // u.id < ? * $q->where($q->expr()->lt('u.id', '?')); * - * @param mixed $x The left expression. - * @param mixed $y The right expression. - * - * @return string + * @param string $x The left expression. + * @param string $y The right expression. */ - public function lt($x, $y) + public function lt(string $x, string $y): string { return $this->comparison($x, self::LT, $y); } @@ -179,12 +124,10 @@ public function lt($x, $y) * // u.id <= ? * $q->where($q->expr()->lte('u.id', '?')); * - * @param mixed $x The left expression. - * @param mixed $y The right expression. - * - * @return string + * @param string $x The left expression. + * @param string $y The right expression. */ - public function lte($x, $y) + public function lte(string $x, string $y): string { return $this->comparison($x, self::LTE, $y); } @@ -198,12 +141,10 @@ public function lte($x, $y) * // u.id > ? * $q->where($q->expr()->gt('u.id', '?')); * - * @param mixed $x The left expression. - * @param mixed $y The right expression. - * - * @return string + * @param string $x The left expression. + * @param string $y The right expression. */ - public function gt($x, $y) + public function gt(string $x, string $y): string { return $this->comparison($x, self::GT, $y); } @@ -217,12 +158,10 @@ public function gt($x, $y) * // u.id >= ? * $q->where($q->expr()->gte('u.id', '?')); * - * @param mixed $x The left expression. - * @param mixed $y The right expression. - * - * @return string + * @param string $x The left expression. + * @param string $y The right expression. */ - public function gte($x, $y) + public function gte(string $x, string $y): string { return $this->comparison($x, self::GTE, $y); } @@ -231,10 +170,8 @@ public function gte($x, $y) * Creates an IS NULL expression with the given arguments. * * @param string $x The expression to be restricted by IS NULL. - * - * @return string */ - public function isNull($x) + public function isNull(string $x): string { return $x . ' IS NULL'; } @@ -243,40 +180,34 @@ public function isNull($x) * Creates an IS NOT NULL expression with the given arguments. * * @param string $x The expression to be restricted by IS NOT NULL. - * - * @return string */ - public function isNotNull($x) + public function isNotNull(string $x): string { return $x . ' IS NOT NULL'; } /** - * Creates a LIKE() comparison expression with the given arguments. - * - * @param string $x The expression to be inspected by the LIKE comparison - * @param mixed $y The pattern to compare against + * Creates a LIKE comparison expression. * - * @return string + * @param string $expression The expression to be inspected by the LIKE comparison + * @param string $pattern The pattern to compare against */ - public function like($x, $y/*, ?string $escapeChar = null */) + public function like(string $expression, string $pattern, ?string $escapeChar = null): string { - return $this->comparison($x, 'LIKE', $y) . - (func_num_args() >= 3 ? sprintf(' ESCAPE %s', func_get_arg(2)) : ''); + return $this->comparison($expression, 'LIKE', $pattern) . + ($escapeChar !== null ? sprintf(' ESCAPE %s', $escapeChar) : ''); } /** - * Creates a NOT LIKE() comparison expression with the given arguments. + * Creates a NOT LIKE comparison expression * - * @param string $x The expression to be inspected by the NOT LIKE comparison - * @param mixed $y The pattern to compare against - * - * @return string + * @param string $expression The expression to be inspected by the NOT LIKE comparison + * @param string $pattern The pattern to compare against */ - public function notLike($x, $y/*, ?string $escapeChar = null */) + public function notLike(string $expression, string $pattern, ?string $escapeChar = null): string { - return $this->comparison($x, 'NOT LIKE', $y) . - (func_num_args() >= 3 ? sprintf(' ESCAPE %s', func_get_arg(2)) : ''); + return $this->comparison($expression, 'NOT LIKE', $pattern) . + ($escapeChar !== null ? sprintf(' ESCAPE %s', $escapeChar) : ''); } /** @@ -284,10 +215,8 @@ public function notLike($x, $y/*, ?string $escapeChar = null */) * * @param string $x The SQL expression to be matched against the set. * @param string|string[] $y The SQL expression or an array of SQL expressions representing the set. - * - * @return string */ - public function in($x, $y) + public function in(string $x, string|array $y): string { return $this->comparison($x, 'IN', '(' . implode(', ', (array) $y) . ')'); } @@ -297,27 +226,22 @@ public function in($x, $y) * * @param string $x The SQL expression to be matched against the set. * @param string|string[] $y The SQL expression or an array of SQL expressions representing the set. - * - * @return string */ - public function notIn($x, $y) + public function notIn(string $x, string|array $y): string { return $this->comparison($x, 'NOT IN', '(' . implode(', ', (array) $y) . ')'); } /** - * Builds an SQL literal from a given input parameter. + * Creates an SQL literal expression from the string. * * The usage of this method is discouraged. Use prepared statements * or {@see AbstractPlatform::quoteStringLiteral()} instead. * - * @param mixed $input The parameter to be quoted. - * @param int|null $type The type of the parameter. - * - * @return string + * @throws Exception */ - public function literal($input, $type = null) + public function literal(string $input): string { - return $this->connection->quote($input, $type); + return $this->connection->quote($input); } } diff --git a/doctrine/dbal/src/Query/ForUpdate.php b/doctrine/dbal/src/Query/ForUpdate.php index fe54df909..62c169d2f 100644 --- a/doctrine/dbal/src/Query/ForUpdate.php +++ b/doctrine/dbal/src/Query/ForUpdate.php @@ -4,17 +4,17 @@ namespace Doctrine\DBAL\Query; +use Doctrine\DBAL\Query\ForUpdate\ConflictResolutionMode; + /** @internal */ -final class ForUpdate +final readonly class ForUpdate { - private int $conflictResolutionMode; - - public function __construct(int $conflictResolutionMode) - { - $this->conflictResolutionMode = $conflictResolutionMode; + public function __construct( + private ConflictResolutionMode $conflictResolutionMode, + ) { } - public function getConflictResolutionMode(): int + public function getConflictResolutionMode(): ConflictResolutionMode { return $this->conflictResolutionMode; } diff --git a/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php b/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php index f968f7b94..f45f774bd 100644 --- a/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php +++ b/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php @@ -4,24 +4,15 @@ namespace Doctrine\DBAL\Query\ForUpdate; -final class ConflictResolutionMode +enum ConflictResolutionMode { /** * Wait for the row to be unlocked */ - public const ORDINARY = 0; + case ORDINARY; /** * Skip the row if it is locked */ - public const SKIP_LOCKED = 1; - - /** - * This class cannot be instantiated. - * - * @codeCoverageIgnore - */ - private function __construct() - { - } + case SKIP_LOCKED; } diff --git a/doctrine/dbal/src/Query/From.php b/doctrine/dbal/src/Query/From.php new file mode 100644 index 000000000..48e6b75c8 --- /dev/null +++ b/doctrine/dbal/src/Query/From.php @@ -0,0 +1,15 @@ +maxResults = $maxResults; - $this->firstResult = $firstResult; + public function __construct( + private readonly ?int $maxResults, + private readonly int $firstResult, + ) { } public function isDefined(): bool diff --git a/doctrine/dbal/src/Query/QueryBuilder.php b/doctrine/dbal/src/Query/QueryBuilder.php index 208579c93..77e0b972b 100644 --- a/doctrine/dbal/src/Query/QueryBuilder.php +++ b/doctrine/dbal/src/Query/QueryBuilder.php @@ -1,34 +1,34 @@ |array + */ + private array $params = []; - /** @deprecated */ - public const DELETE = 1; + /** + * The parameter type map of this query. + * + * @phpstan-var WrapperParameterTypeArray + */ + private array $types = []; - /** @deprecated */ - public const UPDATE = 2; + /** + * The type of query this is. Can be select, update or delete. + */ + private QueryType $type = QueryType::SELECT; - /** @deprecated */ - public const INSERT = 3; + /** + * The index of the first result to retrieve. + */ + private int $firstResult = 0; - /** @deprecated */ - public const STATE_DIRTY = 0; + /** + * The maximum number of results to retrieve or NULL to retrieve all results. + */ + private ?int $maxResults = null; - /** @deprecated */ - public const STATE_CLEAN = 1; + /** + * The counter of bound parameters used with {@see bindValue). + * + * @var int<0, max> + */ + private int $boundCounter = 0; /** - * The DBAL Connection. + * The SELECT parts of the query. + * + * @var string[] */ - private Connection $connection; + private array $select = []; - /* - * The default values of SQL parts collection + /** + * Whether this is a SELECT DISTINCT query. */ - private const SQL_PARTS_DEFAULTS = [ - 'select' => [], - 'distinct' => false, - 'from' => [], - 'join' => [], - 'set' => [], - 'where' => null, - 'groupBy' => [], - 'having' => null, - 'orderBy' => [], - 'values' => [], - 'for_update' => null, - ]; + private bool $distinct = false; /** - * The array of SQL parts collected. + * The FROM parts of a SELECT query. * - * @var mixed[] + * @var From[] */ - private array $sqlParts = self::SQL_PARTS_DEFAULTS; + private array $from = []; /** - * The complete SQL string for this query. + * The table name for an INSERT, UPDATE or DELETE query. */ - private ?string $sql = null; + private ?string $table = null; /** - * The query parameters. + * The list of joins, indexed by from alias. * - * @var list|array + * @var array */ - private $params = []; + private array $join = []; /** - * The parameter type map of this query. + * The SET parts of an UPDATE query. * - * @var array|array + * @var string[] */ - private array $paramTypes = []; + private array $set = []; /** - * The type of query this is. Can be select, update or delete. + * The WHERE part of a SELECT, UPDATE or DELETE query. + */ + private string|CompositeExpression|null $where = null; + + /** + * The GROUP BY part of a SELECT query. * - * @phpstan-var self::SELECT|self::DELETE|self::UPDATE|self::INSERT + * @var string[] + */ + private array $groupBy = []; + + /** + * The HAVING part of a SELECT query. */ - private int $type = self::SELECT; + private string|CompositeExpression|null $having = null; /** - * The state of the query object. Can be dirty or clean. + * The ORDER BY parts of a SELECT query. * - * @phpstan-var self::STATE_* + * @var string[] */ - private int $state = self::STATE_CLEAN; + private array $orderBy = []; + + private ?ForUpdate $forUpdate = null; /** - * The index of the first result to retrieve. + * The values of an INSERT query. + * + * @var array */ - private int $firstResult = 0; + private array $values = []; /** - * The maximum number of results to retrieve or NULL to retrieve all results. + * The QueryBuilder for the union parts. + * + * @var Union[] */ - private ?int $maxResults = null; + private array $unionParts = []; /** - * The counter of bound parameters used with {@see bindValue). + * The common table expression parts. + * + * @var CommonTableExpression[] */ - private int $boundCounter = 0; + private array $commonTableExpressions = []; /** * The query cache profile used for caching results. @@ -149,9 +180,8 @@ class QueryBuilder * * @param Connection $connection The DBAL Connection. */ - public function __construct(Connection $connection) + public function __construct(private readonly Connection $connection) { - $this->connection = $connection; } /** @@ -167,69 +197,18 @@ public function __construct(Connection $connection) * * For more complex expression construction, consider storing the expression * builder object in a local variable. - * - * @return ExpressionBuilder */ - public function expr() + public function expr(): ExpressionBuilder { - return $this->connection->getExpressionBuilder(); + return $this->connection->createExpressionBuilder(); } /** - * Gets the type of the currently built query. - * - * @deprecated If necessary, track the type of the query being built outside of the builder. - * - * @return int + * Returns a fresh query builder instance that can be used to build a subquery. */ - public function getType() + public function sub(): self { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5551', - 'Relying on the type of the query being built is deprecated.' - . ' If necessary, track the type of the query being built outside of the builder.', - ); - - return $this->type; - } - - /** - * Gets the associated DBAL Connection for this query builder. - * - * @deprecated Use the connection used to instantiate the builder instead. - * - * @return Connection - */ - public function getConnection() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5780', - '%s is deprecated. Use the connection used to instantiate the builder instead.', - __METHOD__, - ); - - return $this->connection; - } - - /** - * Gets the state of this query builder instance. - * - * @deprecated The builder state is an internal concern. - * - * @return int Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN. - * @phpstan-return self::STATE_* - */ - public function getState() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5551', - 'Relying on the query builder state is deprecated as it is an internal concern.', - ); - - return $this->state; + return $this->connection->createQueryBuilder(); } /** @@ -240,7 +219,7 @@ public function getState() * * @throws Exception */ - public function fetchAssociative() + public function fetchAssociative(): array|false { return $this->executeQuery()->fetchAssociative(); } @@ -253,7 +232,7 @@ public function fetchAssociative() * * @throws Exception */ - public function fetchNumeric() + public function fetchNumeric(): array|false { return $this->executeQuery()->fetchNumeric(); } @@ -266,7 +245,7 @@ public function fetchNumeric() * * @throws Exception */ - public function fetchOne() + public function fetchOne(): mixed { return $this->executeQuery()->fetchOne(); } @@ -344,7 +323,7 @@ public function executeQuery(): Result return $this->connection->executeQuery( $this->getSQL(), $this->params, - $this->paramTypes, + $this->types, $this->resultCacheProfile, ); } @@ -354,43 +333,13 @@ public function executeQuery(): Result * * Should be used for INSERT, UPDATE and DELETE * - * @return int The number of affected rows. - * - * @throws Exception - */ - public function executeStatement(): int - { - return $this->connection->executeStatement($this->getSQL(), $this->params, $this->paramTypes); - } - - /** - * Executes this query using the bound parameters and their types. - * - * @deprecated Use {@see executeQuery()} or {@see executeStatement()} instead. - * - * @return Result|int|string + * @return int|numeric-string The number of affected rows. * * @throws Exception */ - public function execute() + public function executeStatement(): int|string { - if ($this->type === self::SELECT) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4578', - 'QueryBuilder::execute() is deprecated, use QueryBuilder::executeQuery() for SQL queries instead.', - ); - - return $this->executeQuery(); - } - - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4578', - 'QueryBuilder::execute() is deprecated, use QueryBuilder::executeStatement() for SQL statements instead.', - ); - - return $this->connection->executeStatement($this->getSQL(), $this->params, $this->paramTypes); + return $this->connection->executeStatement($this->getSQL(), $this->params, $this->types); } /** @@ -404,35 +353,18 @@ public function execute() * * * @return string The SQL query string. + * + * @throws Exception */ - public function getSQL() + public function getSQL(): string { - if ($this->sql !== null && $this->state === self::STATE_CLEAN) { - return $this->sql; - } - - switch ($this->type) { - case self::INSERT: - $sql = $this->getSQLForInsert(); - break; - - case self::DELETE: - $sql = $this->getSQLForDelete(); - break; - - case self::UPDATE: - $sql = $this->getSQLForUpdate(); - break; - - case self::SELECT: - $sql = $this->getSQLForSelect(); - break; - } - - $this->state = self::STATE_CLEAN; - $this->sql = $sql; - - return $sql; + return $this->sql ??= match ($this->type) { + QueryType::INSERT => $this->getSQLForInsert(), + QueryType::DELETE => $this->getSQLForDelete(), + QueryType::UPDATE => $this->getSQLForUpdate(), + QueryType::SELECT => $this->getSQLForSelect(), + QueryType::UNION => $this->getSQLForUnion(), + }; } /** @@ -446,26 +378,17 @@ public function getSQL() * ->setParameter('user_id', 1); * * - * @param int|string $key Parameter position or name - * @param mixed $value Parameter value - * @param int|string|Type|null $type Parameter type + * @param int<0, max>|string $key Parameter position or name * * @return $this This QueryBuilder instance. */ - public function setParameter($key, $value, $type = ParameterType::STRING) - { - if ($type !== null) { - $this->paramTypes[$key] = $type; - } else { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5550', - 'Using NULL as prepared statement parameter type is deprecated.' - . 'Omit or use ParameterType::STRING instead', - ); - } - + public function setParameter( + int|string $key, + mixed $value, + string|ParameterType|Type|ArrayParameterType $type = ParameterType::STRING, + ): self { $this->params[$key] = $value; + $this->types[$key] = $type; return $this; } @@ -484,15 +407,15 @@ public function setParameter($key, $value, $type = ParameterType::STRING) * )); * * - * @param list|array $params Parameters to set - * @param array|array $types Parameter types + * @param list|array $params + * @phpstan-param WrapperParameterTypeArray $types * * @return $this This QueryBuilder instance. */ - public function setParameters(array $params, array $types = []) + public function setParameters(array $params, array $types = []): self { - $this->paramTypes = $types; - $this->params = $params; + $this->params = $params; + $this->types = $types; return $this; } @@ -502,7 +425,7 @@ public function setParameters(array $params, array $types = []) * * @return list|array The currently defined query parameters */ - public function getParameters() + public function getParameters(): array { return $this->params; } @@ -510,11 +433,11 @@ public function getParameters() /** * Gets a (previously set) query parameter of the query being constructed. * - * @param mixed $key The key (index or name) of the bound parameter. + * @param string|int $key The key (index or name) of the bound parameter. * * @return mixed The value of the bound parameter. */ - public function getParameter($key) + public function getParameter(string|int $key): mixed { return $this->params[$key] ?? null; } @@ -522,24 +445,21 @@ public function getParameter($key) /** * Gets all defined query parameter types for the query being constructed indexed by parameter index or name. * - * @return array|array The currently defined - * query parameter types + * @phpstan-return WrapperParameterTypeArray */ - public function getParameterTypes() + public function getParameterTypes(): array { - return $this->paramTypes; + return $this->types; } /** * Gets a (previously set) query parameter type of the query being constructed. * * @param int|string $key The key of the bound parameter type - * - * @return int|string|Type The value of the bound parameter type */ - public function getParameterType($key) + public function getParameterType(int|string $key): string|ParameterType|Type|ArrayParameterType { - return $this->paramTypes[$key] ?? ParameterType::STRING; + return $this->types[$key] ?? ParameterType::STRING; } /** @@ -549,11 +469,12 @@ public function getParameterType($key) * * @return $this This QueryBuilder instance. */ - public function setFirstResult($firstResult) + public function setFirstResult(int $firstResult): self { - $this->state = self::STATE_DIRTY; $this->firstResult = $firstResult; + $this->sql = null; + return $this; } @@ -562,7 +483,7 @@ public function setFirstResult($firstResult) * * @return int The position of the first result. */ - public function getFirstResult() + public function getFirstResult(): int { return $this->firstResult; } @@ -574,11 +495,12 @@ public function getFirstResult() * * @return $this This QueryBuilder instance. */ - public function setMaxResults($maxResults) + public function setMaxResults(?int $maxResults): self { - $this->state = self::STATE_DIRTY; $this->maxResults = $maxResults; + $this->sql = null; + return $this; } @@ -588,7 +510,7 @@ public function setMaxResults($maxResults) * * @return int|null The maximum number of results. */ - public function getMaxResults() + public function getMaxResults(): ?int { return $this->maxResults; } @@ -598,61 +520,94 @@ public function getMaxResults() * * @return $this */ - public function forUpdate(int $conflictResolutionMode = ConflictResolutionMode::ORDINARY): self + public function forUpdate(ConflictResolutionMode $conflictResolutionMode = ConflictResolutionMode::ORDINARY): self { - $this->state = self::STATE_DIRTY; + $this->forUpdate = new ForUpdate($conflictResolutionMode); - $this->sqlParts['for_update'] = new ForUpdate($conflictResolutionMode); + $this->sql = null; return $this; } /** - * Either appends to or replaces a single, generic query part. + * Specifies union parts to be used to build a UNION query. + * Replaces any previously specified parts. * - * The available parts are: 'select', 'from', 'set', 'where', - * 'groupBy', 'having' and 'orderBy'. + * + * $qb = $conn->createQueryBuilder() + * ->union('SELECT 1 AS field1') + * ->addUnion('SELECT 2 AS field1') + * ->addUnion('SELECT 3 AS field1'); + * * - * @param string $sqlPartName - * @param mixed $sqlPart - * @param bool $append + * @return $this + */ + public function union(string|QueryBuilder $part): self + { + $this->type = QueryType::UNION; + + $this->unionParts = [new Union($part)]; + + $this->sql = null; + + return $this; + } + + /** + * Add parts to be used to build a UNION query. * - * @return $this This QueryBuilder instance. + * + * $qb = $conn->createQueryBuilder() + * ->union('SELECT 1 AS field1') + * ->addUnion('SELECT 2 AS field1') + * ->addUnion('SELECT 3 AS field1'); + * + * + * @return $this + * + * @throws QueryException */ - public function add($sqlPartName, $sqlPart, $append = false) + public function addUnion(string|QueryBuilder $part, UnionType $type = UnionType::DISTINCT): self { - $isArray = is_array($sqlPart); - $isMultiple = is_array($this->sqlParts[$sqlPartName]); + $this->type = QueryType::UNION; - if ($isMultiple && ! $isArray) { - $sqlPart = [$sqlPart]; + if (count($this->unionParts) === 0) { + throw new QueryException('No initial UNION part set, use union() to set one first.'); } - $this->state = self::STATE_DIRTY; - - if ($append) { - if ( - $sqlPartName === 'orderBy' - || $sqlPartName === 'groupBy' - || $sqlPartName === 'select' - || $sqlPartName === 'set' - ) { - foreach ($sqlPart as $part) { - $this->sqlParts[$sqlPartName][] = $part; - } - } elseif ($isArray && is_array($sqlPart[key($sqlPart)])) { - $key = key($sqlPart); - $this->sqlParts[$sqlPartName][$key][] = $sqlPart[$key]; - } elseif ($isMultiple) { - $this->sqlParts[$sqlPartName][] = $sqlPart; - } else { - $this->sqlParts[$sqlPartName] = $sqlPart; - } + $this->unionParts[] = new Union($part, $type); - return $this; - } + $this->sql = null; - $this->sqlParts[$sqlPartName] = $sqlPart; + return $this; + } + + /** + * Add a Common Table Expression to be used for a select query. + * + * + * // WITH cte_name AS (SELECT id AS column1 FROM table_a) + * $qb = $conn->createQueryBuilder() + * ->with('cte_name', 'SELECT id AS column1 FROM table_a'); + * + * // WITH cte_name(column1) AS (SELECT id AS column1 FROM table_a) + * $qb = $conn->createQueryBuilder() + * ->with('cte_name', 'SELECT id AS column1 FROM table_a', ['column1']); + * + * + * @param string $name The name of the CTE + * @param string[]|null $columns The optional columns list to select in the CTE. + * If not provided, the columns are inferred from the CTE. + * + * @return $this This QueryBuilder instance. + * + * @throws QueryException Setting an empty array as columns is not allowed. + */ + public function with(string $name, string|QueryBuilder $part, ?array $columns = null): self + { + $this->commonTableExpressions[] = new CommonTableExpression($name, $part, $columns); + + $this->sql = null; return $this; } @@ -661,8 +616,6 @@ public function add($sqlPartName, $sqlPart, $append = false) * Specifies an item that is to be returned in the query result. * Replaces any previously specified selections, if any. * - * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument. - * * * $qb = $conn->createQueryBuilder() * ->select('u.id', 'p.id') @@ -670,31 +623,19 @@ public function add($sqlPartName, $sqlPart, $append = false) * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); * * - * @param string|string[]|null $select The selection expression. USING AN ARRAY OR NULL IS DEPRECATED. - * Pass each value as an individual argument. + * @param string ...$expressions The selection expressions. * * @return $this This QueryBuilder instance. */ - public function select($select = null/*, string ...$selects*/) + public function select(string ...$expressions): self { - $this->type = self::SELECT; + $this->type = QueryType::SELECT; - if ($select === null) { - return $this; - } + $this->select = $expressions; - if (is_array($select)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3837', - 'Passing an array for the first argument to QueryBuilder::select() is deprecated, ' . - 'pass each value as an individual variadic argument instead.', - ); - } + $this->sql = null; - $selects = is_array($select) ? $select : func_get_args(); - - return $this->add('select', $selects); + return $this; } /** @@ -709,10 +650,10 @@ public function select($select = null/*, string ...$selects*/) * * @return $this This QueryBuilder instance. */ - public function distinct(/* bool $distinct = true */): self + public function distinct(bool $distinct = true): self { - $this->sqlParts['distinct'] = func_num_args() < 1 || func_get_arg(0); - $this->state = self::STATE_DIRTY; + $this->distinct = $distinct; + $this->sql = null; return $this; } @@ -720,8 +661,6 @@ public function distinct(/* bool $distinct = true */): self /** * Adds an item that is to be returned in the query result. * - * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument. - * * * $qb = $conn->createQueryBuilder() * ->select('u.id') @@ -730,31 +669,20 @@ public function distinct(/* bool $distinct = true */): self * ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id'); * * - * @param string|string[]|null $select The selection expression. USING AN ARRAY OR NULL IS DEPRECATED. - * Pass each value as an individual argument. + * @param string $expression The selection expression. + * @param string ...$expressions Additional selection expressions. * * @return $this This QueryBuilder instance. */ - public function addSelect($select = null/*, string ...$selects*/) + public function addSelect(string $expression, string ...$expressions): self { - $this->type = self::SELECT; - - if ($select === null) { - return $this; - } + $this->type = QueryType::SELECT; - if (is_array($select)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3837', - 'Passing an array for the first argument to QueryBuilder::addSelect() is deprecated, ' . - 'pass each value as an individual variadic argument instead.', - ); - } + $this->select = array_merge($this->select, [$expression], $expressions); - $selects = is_array($select) ? $select : func_get_args(); + $this->sql = null; - return $this->add('select', $selects, true); + return $this; } /** @@ -763,28 +691,24 @@ public function addSelect($select = null/*, string ...$selects*/) * * * $qb = $conn->createQueryBuilder() - * ->delete('users', 'u') + * ->delete('users u') * ->where('u.id = :user_id') * ->setParameter(':user_id', 1); * * - * @param string $delete The table whose rows are subject to the deletion. - * @param string $alias The table alias used in the constructed query. + * @param string $table The table whose rows are subject to the deletion. * * @return $this This QueryBuilder instance. */ - public function delete($delete = null, $alias = null) + public function delete(string $table): self { - $this->type = self::DELETE; + $this->type = QueryType::DELETE; - if ($delete === null) { - return $this; - } + $this->table = $table; + + $this->sql = null; - return $this->add('from', [ - 'table' => $delete, - 'alias' => $alias, - ]); + return $this; } /** @@ -793,28 +717,24 @@ public function delete($delete = null, $alias = null) * * * $qb = $conn->createQueryBuilder() - * ->update('counters', 'c') + * ->update('counters c') * ->set('c.value', 'c.value + 1') * ->where('c.id = ?'); * * - * @param string $update The table whose rows are subject to the update. - * @param string $alias The table alias used in the constructed query. + * @param string $table The table whose rows are subject to the update. * * @return $this This QueryBuilder instance. */ - public function update($update = null, $alias = null) + public function update(string $table): self { - $this->type = self::UPDATE; + $this->type = QueryType::UPDATE; - if ($update === null) { - return $this; - } + $this->table = $table; - return $this->add('from', [ - 'table' => $update, - 'alias' => $alias, - ]); + $this->sql = null; + + return $this; } /** @@ -832,19 +752,19 @@ public function update($update = null, $alias = null) * ); * * - * @param string $insert The table into which the rows should be inserted. + * @param string $table The table into which the rows should be inserted. * * @return $this This QueryBuilder instance. */ - public function insert($insert = null) + public function insert(string $table): self { - $this->type = self::INSERT; + $this->type = QueryType::INSERT; - if ($insert === null) { - return $this; - } + $this->table = $table; - return $this->add('from', ['table' => $insert]); + $this->sql = null; + + return $this; } /** @@ -857,17 +777,18 @@ public function insert($insert = null) * ->from('users', 'u') * * - * @param string $from The table. + * @param string $table The table. * @param string|null $alias The alias of the table. * * @return $this This QueryBuilder instance. */ - public function from($from, $alias = null) + public function from(string $table, ?string $alias = null): self { - return $this->add('from', [ - 'table' => $from, - 'alias' => $alias, - ], true); + $this->from[] = new From($table, $alias); + + $this->sql = null; + + return $this; } /** @@ -880,14 +801,14 @@ public function from($from, $alias = null) * ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1'); * * - * @param string $fromAlias The alias that points to a from clause. - * @param string $join The table name to join. - * @param string $alias The alias of the join table. - * @param string $condition The condition for the join. + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string|null $condition The condition for the join. * * @return $this This QueryBuilder instance. */ - public function join($fromAlias, $join, $alias, $condition = null) + public function join(string $fromAlias, string $join, string $alias, ?string $condition = null): self { return $this->innerJoin($fromAlias, $join, $alias, $condition); } @@ -902,23 +823,20 @@ public function join($fromAlias, $join, $alias, $condition = null) * ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); * * - * @param string $fromAlias The alias that points to a from clause. - * @param string $join The table name to join. - * @param string $alias The alias of the join table. - * @param string $condition The condition for the join. + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string|null $condition The condition for the join. * * @return $this This QueryBuilder instance. */ - public function innerJoin($fromAlias, $join, $alias, $condition = null) + public function innerJoin(string $fromAlias, string $join, string $alias, ?string $condition = null): self { - return $this->add('join', [ - $fromAlias => [ - 'joinType' => 'inner', - 'joinTable' => $join, - 'joinAlias' => $alias, - 'joinCondition' => $condition, - ], - ], true); + $this->join[$fromAlias][] = Join::inner($join, $alias, $condition); + + $this->sql = null; + + return $this; } /** @@ -931,23 +849,20 @@ public function innerJoin($fromAlias, $join, $alias, $condition = null) * ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); * * - * @param string $fromAlias The alias that points to a from clause. - * @param string $join The table name to join. - * @param string $alias The alias of the join table. - * @param string $condition The condition for the join. + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string|null $condition The condition for the join. * * @return $this This QueryBuilder instance. */ - public function leftJoin($fromAlias, $join, $alias, $condition = null) + public function leftJoin(string $fromAlias, string $join, string $alias, ?string $condition = null): self { - return $this->add('join', [ - $fromAlias => [ - 'joinType' => 'left', - 'joinTable' => $join, - 'joinAlias' => $alias, - 'joinCondition' => $condition, - ], - ], true); + $this->join[$fromAlias][] = Join::left($join, $alias, $condition); + + $this->sql = null; + + return $this; } /** @@ -960,23 +875,20 @@ public function leftJoin($fromAlias, $join, $alias, $condition = null) * ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); * * - * @param string $fromAlias The alias that points to a from clause. - * @param string $join The table name to join. - * @param string $alias The alias of the join table. - * @param string $condition The condition for the join. + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string|null $condition The condition for the join. * * @return $this This QueryBuilder instance. */ - public function rightJoin($fromAlias, $join, $alias, $condition = null) + public function rightJoin(string $fromAlias, string $join, string $alias, ?string $condition = null): self { - return $this->add('join', [ - $fromAlias => [ - 'joinType' => 'right', - 'joinTable' => $join, - 'joinAlias' => $alias, - 'joinCondition' => $condition, - ], - ], true); + $this->join[$fromAlias][] = Join::right($join, $alias, $condition); + + $this->sql = null; + + return $this; } /** @@ -984,7 +896,7 @@ public function rightJoin($fromAlias, $join, $alias, $condition = null) * * * $qb = $conn->createQueryBuilder() - * ->update('counters', 'c') + * ->update('counters c') * ->set('c.value', 'c.value + 1') * ->where('c.id = ?'); * @@ -994,9 +906,13 @@ public function rightJoin($fromAlias, $join, $alias, $condition = null) * * @return $this This QueryBuilder instance. */ - public function set($key, $value) + public function set(string $key, string $value): self { - return $this->add('set', $key . ' = ' . $value, true); + $this->set[] = $key . ' = ' . $value; + + $this->sql = null; + + return $this; } /** @@ -1016,22 +932,23 @@ public function set($key, $value) * $or->add($qb->expr()->eq('c.id', 1)); * $or->add($qb->expr()->eq('c.id', 2)); * - * $qb->update('counters', 'c') + * $qb->update('counters c') * ->set('c.value', 'c.value + 1') * ->where($or); * * - * @param mixed $predicates The restriction predicates. + * @param string|CompositeExpression $predicate The WHERE clause predicate. + * @param string|CompositeExpression ...$predicates Additional WHERE clause predicates. * * @return $this This QueryBuilder instance. */ - public function where($predicates) + public function where(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self { - if (! (func_num_args() === 1 && $predicates instanceof CompositeExpression)) { - $predicates = CompositeExpression::and(...func_get_args()); - } + $this->where = $this->createPredicate($predicate, ...$predicates); + + $this->sql = null; - return $this->add('where', $predicates); + return $this; } /** @@ -1048,23 +965,23 @@ public function where($predicates) * * @see where() * - * @param mixed $where The query restrictions. + * @param string|CompositeExpression $predicate The predicate to append. + * @param string|CompositeExpression ...$predicates Additional predicates to append. * * @return $this This QueryBuilder instance. */ - public function andWhere($where) + public function andWhere(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self { - $args = func_get_args(); - $where = $this->getQueryPart('where'); + $this->where = $this->appendToPredicate( + $this->where, + CompositeExpression::TYPE_AND, + $predicate, + ...$predicates, + ); - if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_AND) { - $where = $where->with(...$args); - } else { - array_unshift($args, $where); - $where = CompositeExpression::and(...$args); - } + $this->sql = null; - return $this->add('where', $where, true); + return $this; } /** @@ -1081,31 +998,24 @@ public function andWhere($where) * * @see where() * - * @param mixed $where The WHERE statement. + * @param string|CompositeExpression $predicate The predicate to append. + * @param string|CompositeExpression ...$predicates Additional predicates to append. * * @return $this This QueryBuilder instance. */ - public function orWhere($where) + public function orWhere(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self { - $args = func_get_args(); - $where = $this->getQueryPart('where'); + $this->where = $this->appendToPredicate($this->where, CompositeExpression::TYPE_OR, $predicate, ...$predicates); - if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_OR) { - $where = $where->with(...$args); - } else { - array_unshift($args, $where); - $where = CompositeExpression::or(...$args); - } + $this->sql = null; - return $this->add('where', $where, true); + return $this; } /** - * Specifies a grouping over the results of the query. + * Specifies one or more grouping expressions over the results of the query. * Replaces any previously specified groupings, if any. * - * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument. - * * * $qb = $conn->createQueryBuilder() * ->select('u.name') @@ -1113,35 +1023,22 @@ public function orWhere($where) * ->groupBy('u.id'); * * - * @param string|string[] $groupBy The grouping expression. USING AN ARRAY IS DEPRECATED. - * Pass each value as an individual argument. + * @param string $expression The grouping expression + * @param string ...$expressions Additional grouping expressions * * @return $this This QueryBuilder instance. */ - public function groupBy($groupBy/*, string ...$groupBys*/) + public function groupBy(string $expression, string ...$expressions): self { - if (is_array($groupBy) && count($groupBy) === 0) { - return $this; - } + $this->groupBy = array_merge([$expression], $expressions); - if (is_array($groupBy)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3837', - 'Passing an array for the first argument to QueryBuilder::groupBy() is deprecated, ' . - 'pass each value as an individual variadic argument instead.', - ); - } - - $groupBy = is_array($groupBy) ? $groupBy : func_get_args(); + $this->sql = null; - return $this->add('groupBy', $groupBy, false); + return $this; } /** - * Adds a grouping expression to the query. - * - * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument. + * Adds one or more grouping expressions to the query. * * * $qb = $conn->createQueryBuilder() @@ -1151,29 +1048,18 @@ public function groupBy($groupBy/*, string ...$groupBys*/) * ->addGroupBy('u.createdAt'); * * - * @param string|string[] $groupBy The grouping expression. USING AN ARRAY IS DEPRECATED. - * Pass each value as an individual argument. + * @param string $expression The grouping expression + * @param string ...$expressions Additional grouping expressions * * @return $this This QueryBuilder instance. */ - public function addGroupBy($groupBy/*, string ...$groupBys*/) + public function addGroupBy(string $expression, string ...$expressions): self { - if (is_array($groupBy) && count($groupBy) === 0) { - return $this; - } + $this->groupBy = array_merge($this->groupBy, [$expression], $expressions); - if (is_array($groupBy)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3837', - 'Passing an array for the first argument to QueryBuilder::addGroupBy() is deprecated, ' . - 'pass each value as an individual variadic argument instead.', - ); - } + $this->sql = null; - $groupBy = is_array($groupBy) ? $groupBy : func_get_args(); - - return $this->add('groupBy', $groupBy, true); + return $this; } /** @@ -1195,9 +1081,9 @@ public function addGroupBy($groupBy/*, string ...$groupBys*/) * * @return $this This QueryBuilder instance. */ - public function setValue($column, $value) + public function setValue(string $column, string $value): self { - $this->sqlParts['values'][$column] = $value; + $this->values[$column] = $value; return $this; } @@ -1217,220 +1103,161 @@ public function setValue($column, $value) * ); * * - * @param mixed[] $values The values to specify for the insert query indexed by column names. + * @param array $values The values to specify for the insert query indexed by column names. * * @return $this This QueryBuilder instance. */ - public function values(array $values) + public function values(array $values): self { - return $this->add('values', $values); + $this->values = $values; + + $this->sql = null; + + return $this; } /** * Specifies a restriction over the groups of the query. * Replaces any previous having restrictions, if any. * - * @param mixed $having The restriction over the groups. + * @param string|CompositeExpression $predicate The HAVING clause predicate. + * @param string|CompositeExpression ...$predicates Additional HAVING clause predicates. * * @return $this This QueryBuilder instance. */ - public function having($having) + public function having(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self { - if (! (func_num_args() === 1 && $having instanceof CompositeExpression)) { - $having = CompositeExpression::and(...func_get_args()); - } + $this->having = $this->createPredicate($predicate, ...$predicates); + + $this->sql = null; - return $this->add('having', $having); + return $this; } /** * Adds a restriction over the groups of the query, forming a logical * conjunction with any existing having restrictions. * - * @param mixed $having The restriction to append. + * @param string|CompositeExpression $predicate The predicate to append. + * @param string|CompositeExpression ...$predicates Additional predicates to append. * * @return $this This QueryBuilder instance. */ - public function andHaving($having) + public function andHaving(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self { - $args = func_get_args(); - $having = $this->getQueryPart('having'); + $this->having = $this->appendToPredicate( + $this->having, + CompositeExpression::TYPE_AND, + $predicate, + ...$predicates, + ); - if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_AND) { - $having = $having->with(...$args); - } else { - array_unshift($args, $having); - $having = CompositeExpression::and(...$args); - } + $this->sql = null; - return $this->add('having', $having); + return $this; } /** * Adds a restriction over the groups of the query, forming a logical * disjunction with any existing having restrictions. * - * @param mixed $having The restriction to add. + * @param string|CompositeExpression $predicate The predicate to append. + * @param string|CompositeExpression ...$predicates Additional predicates to append. * * @return $this This QueryBuilder instance. */ - public function orHaving($having) + public function orHaving(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self { - $args = func_get_args(); - $having = $this->getQueryPart('having'); + $this->having = $this->appendToPredicate( + $this->having, + CompositeExpression::TYPE_OR, + $predicate, + ...$predicates, + ); - if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_OR) { - $having = $having->with(...$args); - } else { - array_unshift($args, $having); - $having = CompositeExpression::or(...$args); - } + $this->sql = null; - return $this->add('having', $having); + return $this; } /** - * Specifies an ordering for the query results. - * Replaces any previously specified orderings, if any. - * - * @param string $sort The ordering expression. - * @param string $order The ordering direction. - * - * @return $this This QueryBuilder instance. + * Creates a CompositeExpression from one or more predicates combined by the AND logic. */ - public function orderBy($sort, $order = null) - { - return $this->add('orderBy', $sort . ' ' . ($order ?? 'ASC'), false); - } + private function createPredicate( + string|CompositeExpression $predicate, + string|CompositeExpression ...$predicates, + ): string|CompositeExpression { + if (count($predicates) === 0) { + return $predicate; + } - /** - * Adds an ordering to the query results. - * - * @param string $sort The ordering expression. - * @param string $order The ordering direction. - * - * @return $this This QueryBuilder instance. - */ - public function addOrderBy($sort, $order = null) - { - return $this->add('orderBy', $sort . ' ' . ($order ?? 'ASC'), true); + return new CompositeExpression(CompositeExpression::TYPE_AND, $predicate, ...$predicates); } /** - * Gets a query part by its name. - * - * @deprecated The query parts are implementation details and should not be relied upon. - * - * @param string $queryPartName - * - * @return mixed + * Appends the given predicates combined by the given type of logic to the current predicate. */ - public function getQueryPart($queryPartName) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6179', - 'Getting query parts is deprecated as they are implementation details.', - ); - - return $this->sqlParts[$queryPartName]; - } + private function appendToPredicate( + string|CompositeExpression|null $currentPredicate, + string $type, + string|CompositeExpression ...$predicates, + ): string|CompositeExpression { + if ($currentPredicate instanceof CompositeExpression && $currentPredicate->getType() === $type) { + return $currentPredicate->with(...$predicates); + } - /** - * Gets all query parts. - * - * @deprecated The query parts are implementation details and should not be relied upon. - * - * @return mixed[] - */ - public function getQueryParts() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6179', - 'Getting query parts is deprecated as they are implementation details.', - ); + if ($currentPredicate !== null) { + array_unshift($predicates, $currentPredicate); + } elseif (count($predicates) === 1) { + return $predicates[0]; + } - return $this->sqlParts; + return new CompositeExpression($type, ...$predicates); } /** - * Resets SQL parts. - * - * @deprecated Use the dedicated reset*() methods instead. + * Specifies an ordering for the query results. + * Replaces any previously specified orderings, if any. * - * @param string[]|null $queryPartNames + * @param string $sort The ordering expression. + * @param string|null $order The ordering direction. * * @return $this This QueryBuilder instance. */ - public function resetQueryParts($queryPartNames = null) + public function orderBy(string $sort, ?string $order = null): self { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6193', - '%s() is deprecated, instead use dedicated reset methods for the parts that shall be reset.', - __METHOD__, - ); - - $queryPartNames ??= array_keys($this->sqlParts); + $orderBy = $sort; - foreach ($queryPartNames as $queryPartName) { - $this->sqlParts[$queryPartName] = self::SQL_PARTS_DEFAULTS[$queryPartName]; + if ($order !== null) { + $orderBy .= ' ' . $order; } - $this->state = self::STATE_DIRTY; + $this->orderBy = [$orderBy]; + + $this->sql = null; return $this; } /** - * Resets a single SQL part. - * - * @deprecated Use the dedicated reset*() methods instead. + * Adds an ordering to the query results. * - * @param string $queryPartName + * @param string $sort The ordering expression. + * @param string|null $order The ordering direction. * * @return $this This QueryBuilder instance. */ - public function resetQueryPart($queryPartName) + public function addOrderBy(string $sort, ?string $order = null): self { - if ($queryPartName === 'distinct') { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6193', - 'Calling %s() with "distinct" is deprecated, call distinct(false) instead.', - __METHOD__, - ); - - return $this->distinct(false); - } - - $newMethodName = 'reset' . ucfirst($queryPartName); - if (array_key_exists($queryPartName, self::SQL_PARTS_DEFAULTS) && method_exists($this, $newMethodName)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6193', - 'Calling %s() with "%s" is deprecated, call %s() instead.', - __METHOD__, - $queryPartName, - $newMethodName, - ); + $orderBy = $sort; - return $this->$newMethodName(); + if ($order !== null) { + $orderBy .= ' ' . $order; } - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6193', - 'Calling %s() with "%s" is deprecated without replacement.', - __METHOD__, - $queryPartName, - $newMethodName, - ); - - $this->sqlParts[$queryPartName] = self::SQL_PARTS_DEFAULTS[$queryPartName]; + $this->orderBy[] = $orderBy; - $this->state = self::STATE_DIRTY; + $this->sql = null; return $this; } @@ -1442,9 +1269,8 @@ public function resetQueryPart($queryPartName) */ public function resetWhere(): self { - $this->sqlParts['where'] = self::SQL_PARTS_DEFAULTS['where']; - - $this->state = self::STATE_DIRTY; + $this->where = null; + $this->sql = null; return $this; } @@ -1456,9 +1282,8 @@ public function resetWhere(): self */ public function resetGroupBy(): self { - $this->sqlParts['groupBy'] = self::SQL_PARTS_DEFAULTS['groupBy']; - - $this->state = self::STATE_DIRTY; + $this->groupBy = []; + $this->sql = null; return $this; } @@ -1470,9 +1295,8 @@ public function resetGroupBy(): self */ public function resetHaving(): self { - $this->sqlParts['having'] = self::SQL_PARTS_DEFAULTS['having']; - - $this->state = self::STATE_DIRTY; + $this->having = null; + $this->sql = null; return $this; } @@ -1484,9 +1308,8 @@ public function resetHaving(): self */ public function resetOrderBy(): self { - $this->sqlParts['orderBy'] = self::SQL_PARTS_DEFAULTS['orderBy']; - - $this->state = self::STATE_DIRTY; + $this->orderBy = []; + $this->sql = null; return $this; } @@ -1494,25 +1317,39 @@ public function resetOrderBy(): self /** @throws Exception */ private function getSQLForSelect(): string { - return $this->connection->getDatabasePlatform() + if (count($this->select) === 0) { + throw new QueryException('No SELECT expressions given. Please use select() or addSelect().'); + } + + $databasePlatform = $this->connection->getDatabasePlatform(); + $selectParts = []; + if (count($this->commonTableExpressions) > 0) { + $selectParts[] = $databasePlatform + ->createWithSQLBuilder() + ->buildSQL(...$this->commonTableExpressions); + } + + $selectParts[] = $databasePlatform ->createSelectSQLBuilder() ->buildSQL( new SelectQuery( - $this->sqlParts['distinct'], - $this->sqlParts['select'], + $this->distinct, + $this->select, $this->getFromClauses(), - $this->sqlParts['where'], - $this->sqlParts['groupBy'], - $this->sqlParts['having'], - $this->sqlParts['orderBy'], + $this->where !== null ? (string) $this->where : null, + $this->groupBy, + $this->having !== null ? (string) $this->having : null, + $this->orderBy, new Limit($this->maxResults, $this->firstResult), - $this->sqlParts['for_update'], + $this->forUpdate, ), ); + + return implode(' ', $selectParts); } /** - * @return string[] + * @return array * * @throws QueryException */ @@ -1521,14 +1358,13 @@ private function getFromClauses(): array $fromClauses = []; $knownAliases = []; - // Loop through all FROM clauses - foreach ($this->sqlParts['from'] as $from) { - if ($from['alias'] === null) { - $tableSql = $from['table']; - $tableReference = $from['table']; + foreach ($this->from as $from) { + if ($from->alias === null || $from->alias === $from->table) { + $tableSql = $from->table; + $tableReference = $from->table; } else { - $tableSql = $from['table'] . ' ' . $from['alias']; - $tableReference = $from['alias']; + $tableSql = $from->table . ' ' . $from->alias; + $tableReference = $from->alias; } $knownAliases[$tableReference] = true; @@ -1542,15 +1378,15 @@ private function getFromClauses(): array } /** - * @param array $knownAliases + * @param array $knownAliases * * @throws QueryException */ private function verifyAllAliasesAreKnown(array $knownAliases): void { - foreach ($this->sqlParts['join'] as $fromAlias => $joins) { + foreach ($this->join as $fromAlias => $joins) { if (! isset($knownAliases[$fromAlias])) { - throw QueryException::unknownAlias($fromAlias, array_keys($knownAliases)); + throw UnknownAlias::new($fromAlias, array_keys($knownAliases)); } } } @@ -1560,9 +1396,9 @@ private function verifyAllAliasesAreKnown(array $knownAliases): void */ private function getSQLForInsert(): string { - return 'INSERT INTO ' . $this->sqlParts['from']['table'] . - ' (' . implode(', ', array_keys($this->sqlParts['values'])) . ')' . - ' VALUES(' . implode(', ', $this->sqlParts['values']) . ')'; + return 'INSERT INTO ' . $this->table . + ' (' . implode(', ', array_keys($this->values)) . ')' . + ' VALUES(' . implode(', ', $this->values) . ')'; } /** @@ -1570,12 +1406,14 @@ private function getSQLForInsert(): string */ private function getSQLForUpdate(): string { - $table = $this->sqlParts['from']['table'] - . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : ''); + $query = 'UPDATE ' . $this->table + . ' SET ' . implode(', ', $this->set); + + if ($this->where !== null) { + $query .= ' WHERE ' . $this->where; + } - return 'UPDATE ' . $table - . ' SET ' . implode(', ', $this->sqlParts['set']) - . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : ''); + return $query; } /** @@ -1583,11 +1421,49 @@ private function getSQLForUpdate(): string */ private function getSQLForDelete(): string { - $table = $this->sqlParts['from']['table'] - . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : ''); + $query = 'DELETE FROM ' . $this->table; - return 'DELETE FROM ' . $table - . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : ''); + if ($this->where !== null) { + $query .= ' WHERE ' . $this->where; + } + + return $query; + } + + /** + * Converts this instance into a UNION string in SQL. + * + * @throws Exception + */ + private function getSQLForUnion(): string + { + $countUnions = count($this->unionParts); + if ($countUnions < 2) { + throw new QueryException( + 'Insufficient UNION parts give, need at least 2.' + . ' Please use union() and addUnion() to set enough UNION parts.', + ); + } + + $databasePlatform = $this->connection->getDatabasePlatform(); + $unionParts = []; + if (count($this->commonTableExpressions) > 0) { + $unionParts[] = $databasePlatform + ->createWithSQLBuilder() + ->buildSQL(...$this->commonTableExpressions); + } + + $unionParts[] = $databasePlatform + ->createUnionSQLBuilder() + ->buildSQL( + new UnionQuery( + $this->unionParts, + $this->orderBy, + new Limit($this->maxResults, $this->firstResult), + ), + ); + + return implode(' ', $unionParts); } /** @@ -1595,8 +1471,10 @@ private function getSQLForDelete(): string * the final SQL query being constructed. * * @return string The string representation of this QueryBuilder. + * + * @throws Exception */ - public function __toString() + public function __toString(): string { return $this->getSQL(); } @@ -1621,14 +1499,15 @@ public function __toString() * * @link http://www.zetacomponents.org * - * @param mixed $value - * @param int|string|Type|null $type - * @param string $placeHolder The name to bind with. The string must start with a colon ':'. + * @param string|null $placeHolder The name to bind with. The string must start with a colon ':'. * * @return string the placeholder name used. */ - public function createNamedParameter($value, $type = ParameterType::STRING, $placeHolder = null) - { + public function createNamedParameter( + mixed $value, + string|ParameterType|Type|ArrayParameterType $type = ParameterType::STRING, + ?string $placeHolder = null, + ): string { if ($placeHolder === null) { $this->boundCounter++; $placeHolder = ':dcValue' . $this->boundCounter; @@ -1655,14 +1534,11 @@ public function createNamedParameter($value, $type = ParameterType::STRING, $pla * ->where('u.username = ' . $qb->createPositionalParameter('Foo', ParameterType::STRING)) * ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', ParameterType::STRING)) * - * - * @param mixed $value - * @param int|string|Type|null $type - * - * @return string */ - public function createPositionalParameter($value, $type = ParameterType::STRING) - { + public function createPositionalParameter( + mixed $value, + string|ParameterType|Type|ArrayParameterType $type = ParameterType::STRING, + ): string { $this->setParameter($this->boundCounter, $value, $type); $this->boundCounter++; @@ -1670,33 +1546,34 @@ public function createPositionalParameter($value, $type = ParameterType::STRING) } /** - * @param string $fromAlias - * @param array $knownAliases + * @param array $knownAliases * * @throws QueryException */ - private function getSQLForJoins($fromAlias, array &$knownAliases): string + private function getSQLForJoins(string $fromAlias, array &$knownAliases): string { $sql = ''; - if (isset($this->sqlParts['join'][$fromAlias])) { - foreach ($this->sqlParts['join'][$fromAlias] as $join) { - if (array_key_exists($join['joinAlias'], $knownAliases)) { - throw QueryException::nonUniqueAlias((string) $join['joinAlias'], array_keys($knownAliases)); - } - - $sql .= ' ' . strtoupper($join['joinType']) - . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias']; - if ($join['joinCondition'] !== null) { - $sql .= ' ON ' . $join['joinCondition']; - } + if (! isset($this->join[$fromAlias])) { + return $sql; + } - $knownAliases[$join['joinAlias']] = true; + foreach ($this->join[$fromAlias] as $join) { + if (array_key_exists($join->alias, $knownAliases)) { + throw NonUniqueAlias::new($join->alias, array_keys($knownAliases)); } - foreach ($this->sqlParts['join'][$fromAlias] as $join) { - $sql .= $this->getSQLForJoins($join['joinAlias'], $knownAliases); + $sql .= ' ' . $join->type . ' JOIN ' . $join->table . ' ' . $join->alias; + + if ($join->condition !== null) { + $sql .= ' ON ' . $join->condition; } + + $knownAliases[$join->alias] = true; + } + + foreach ($this->join[$fromAlias] as $join) { + $sql .= $this->getSQLForJoins($join->alias, $knownAliases); } return $sql; @@ -1704,31 +1581,31 @@ private function getSQLForJoins($fromAlias, array &$knownAliases): string /** * Deep clone of all expression objects in the SQL parts. - * - * @return void */ public function __clone() { - foreach ($this->sqlParts as $part => $elements) { - if (is_array($this->sqlParts[$part])) { - foreach ($this->sqlParts[$part] as $idx => $element) { - if (! is_object($element)) { - continue; - } - - $this->sqlParts[$part][$idx] = clone $element; - } - } elseif (is_object($elements)) { - $this->sqlParts[$part] = clone $elements; + foreach ($this->from as $key => $from) { + $this->from[$key] = clone $from; + } + + foreach ($this->join as $fromAlias => $joins) { + foreach ($joins as $key => $join) { + $this->join[$fromAlias][$key] = clone $join; } } + if (is_object($this->where)) { + $this->where = clone $this->where; + } + + if (is_object($this->having)) { + $this->having = clone $this->having; + } + foreach ($this->params as $name => $param) { - if (! is_object($param)) { - continue; + if (is_object($param)) { + $this->params[$name] = clone $param; } - - $this->params[$name] = clone $param; } } diff --git a/doctrine/dbal/src/Query/QueryException.php b/doctrine/dbal/src/Query/QueryException.php index bfb5eafc3..34b9527a8 100644 --- a/doctrine/dbal/src/Query/QueryException.php +++ b/doctrine/dbal/src/Query/QueryException.php @@ -1,36 +1,11 @@ distinct = $distinct; - $this->columns = $columns; - $this->from = $from; - $this->where = $where; - $this->groupBy = $groupBy; - $this->having = $having; - $this->orderBy = $orderBy; - $this->limit = $limit; - $this->forUpdate = $forUpdate; } public function isDistinct(): bool diff --git a/doctrine/dbal/src/Query/Union.php b/doctrine/dbal/src/Query/Union.php new file mode 100644 index 000000000..33a5a6dcd --- /dev/null +++ b/doctrine/dbal/src/Query/Union.php @@ -0,0 +1,15 @@ +unionParts; + } + + /** @return string[] */ + public function getOrderBy(): array + { + return $this->orderBy; + } + + public function getLimit(): Limit + { + return $this->limit; + } +} diff --git a/doctrine/dbal/src/Query/UnionType.php b/doctrine/dbal/src/Query/UnionType.php new file mode 100644 index 000000000..e7c0df694 --- /dev/null +++ b/doctrine/dbal/src/Query/UnionType.php @@ -0,0 +1,11 @@ +result = $result; - $this->connection = $connection; } /** @@ -33,7 +31,7 @@ public function __construct(DriverResult $result, Connection $connection) * * @throws Exception */ - public function fetchNumeric() + public function fetchNumeric(): array|false { try { return $this->result->fetchNumeric(); @@ -49,7 +47,7 @@ public function fetchNumeric() * * @throws Exception */ - public function fetchAssociative() + public function fetchAssociative(): array|false { try { return $this->result->fetchAssociative(); @@ -61,11 +59,9 @@ public function fetchAssociative() /** * Returns the first value of the next row of the result or FALSE if there are no more rows. * - * @return mixed|false - * * @throws Exception */ - public function fetchOne() + public function fetchOne(): mixed { try { return $this->result->fetchOne(); @@ -119,8 +115,10 @@ public function fetchAllKeyValue(): array $data = []; - foreach ($this->fetchAllNumeric() as [$key, $value]) { - $data[$key] = $value; + foreach ($this->fetchAllNumeric() as $row) { + assert(count($row) >= 2); + [$key, $value] = $row; + $data[$key] = $value; } return $data; @@ -192,7 +190,10 @@ public function iterateKeyValue(): Traversable { $this->ensureHasKeyValue(); - foreach ($this->iterateNumeric() as [$key, $value]) { + foreach ($this->iterateNumeric() as $row) { + assert(count($row) >= 2); + [$key, $value] = $row; + yield $key => $value; } } @@ -224,8 +225,20 @@ public function iterateColumn(): Traversable } } - /** @throws Exception */ - public function rowCount(): int + /** + * Returns the number of rows affected by the DELETE, INSERT, or UPDATE statement that produced the result. + * + * If the statement executed a SELECT query or a similar platform-specific SQL (e.g. DESCRIBE, SHOW, etc.), + * some database drivers may return the number of rows returned by that query. However, this behaviour + * is not guaranteed for all drivers and should not be relied on in portable applications. + * + * If the number of rows exceeds {@see PHP_INT_MAX}, it might be returned as string if the driver supports it. + * + * @return int|numeric-string + * + * @throws Exception + */ + public function rowCount(): int|string { try { return $this->result->rowCount(); @@ -244,96 +257,41 @@ public function columnCount(): int } } - public function free(): void - { - $this->result->free(); - } - - /** @throws Exception */ - private function ensureHasKeyValue(): void - { - $columnCount = $this->columnCount(); - - if ($columnCount < 2) { - throw NoKeyValue::fromColumnCount($columnCount); - } - } - /** - * BC layer for a wide-spread use-case of old DBAL APIs + * Returns the name of the column in the result set for the given 0-based index. * - * @deprecated Use {@see fetchNumeric()}, {@see fetchAssociative()} or {@see fetchOne()} instead. - * - * @phpstan-param FetchMode::* $mode - * - * @return mixed + * If the index is not a valid column index ({@see columnCount}), an exception will be thrown. * * @throws Exception */ - public function fetch(int $mode = FetchMode::ASSOCIATIVE) + public function getColumnName(int $index): string { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4007', - '%s is deprecated, please use fetchNumeric(), fetchAssociative() or fetchOne() instead.', - __METHOD__, - ); - - if (func_num_args() > 1) { - throw new LogicException('Only invocations with one argument are still supported by this legacy API.'); - } - - if ($mode === FetchMode::ASSOCIATIVE) { - return $this->fetchAssociative(); - } - - if ($mode === FetchMode::NUMERIC) { - return $this->fetchNumeric(); + if (! method_exists($this->result, 'getColumnName')) { + throw new LogicException(sprintf( + 'The driver result %s does not support accessing the column name.', + get_debug_type($this->result), + )); } - if ($mode === FetchMode::COLUMN) { - return $this->fetchOne(); + try { + return $this->result->getColumnName($index); + } catch (DriverException $e) { + throw $this->connection->convertException($e); } - - throw new LogicException('Only fetch modes declared on Doctrine\DBAL\FetchMode are supported by legacy API.'); } - /** - * BC layer for a wide-spread use-case of old DBAL APIs - * - * @deprecated Use {@see fetchAllNumeric()}, {@see fetchAllAssociative()} or {@see fetchFirstColumn()} instead. - * - * @phpstan-param FetchMode::* $mode - * - * @return list - * - * @throws Exception - */ - public function fetchAll(int $mode = FetchMode::ASSOCIATIVE): array + public function free(): void { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4007', - '%s is deprecated, please use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.', - __METHOD__, - ); - - if (func_num_args() > 1) { - throw new LogicException('Only invocations with one argument are still supported by this legacy API.'); - } - - if ($mode === FetchMode::ASSOCIATIVE) { - return $this->fetchAllAssociative(); - } + $this->result->free(); + } - if ($mode === FetchMode::NUMERIC) { - return $this->fetchAllNumeric(); - } + /** @throws Exception */ + private function ensureHasKeyValue(): void + { + $columnCount = $this->columnCount(); - if ($mode === FetchMode::COLUMN) { - return $this->fetchFirstColumn(); + if ($columnCount < 2) { + throw NoKeyValue::fromColumnCount($columnCount); } - - throw new LogicException('Only fetch modes declared on Doctrine\DBAL\FetchMode are supported by legacy API.'); } } diff --git a/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php b/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php index 74579b3e0..598a6adf0 100644 --- a/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php +++ b/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php @@ -1,8 +1,9 @@ platform = $platform; } - /** - * @return list - * - * @throws Exception - */ + /** @return list */ public function buildSQL(Schema $schema): array { return array_merge( @@ -37,8 +31,6 @@ public function buildSQL(Schema $schema): array * @param string[] $namespaces * * @return list - * - * @throws Exception */ private function buildNamespaceStatements(array $namespaces): array { @@ -57,8 +49,6 @@ private function buildNamespaceStatements(array $namespaces): array * @param Table[] $tables * * @return list - * - * @throws Exception */ private function buildTableStatements(array $tables): array { @@ -69,8 +59,6 @@ private function buildTableStatements(array $tables): array * @param Sequence[] $sequences * * @return list - * - * @throws Exception */ private function buildSequenceStatements(array $sequences): array { diff --git a/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php b/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php index 9dcb7a197..a30120e5e 100644 --- a/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php +++ b/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php @@ -1,9 +1,12 @@ platform = $platform; - $this->forUpdateSQL = $forUpdateSQL; - $this->skipLockedSQL = $skipLockedSQL; + public function __construct( + private readonly AbstractPlatform $platform, + private readonly ?string $forUpdateSQL, + private readonly ?string $skipLockedSQL, + ) { } /** @throws Exception */ @@ -76,14 +75,14 @@ public function buildSQL(SelectQuery $query): string if ($forUpdate !== null) { if ($this->forUpdateSQL === null) { - throw Exception::notSupported('FOR UPDATE'); + throw NotSupported::new('FOR UPDATE'); } $sql .= ' ' . $this->forUpdateSQL; if ($forUpdate->getConflictResolutionMode() === ConflictResolutionMode::SKIP_LOCKED) { if ($this->skipLockedSQL === null) { - throw Exception::notSupported('SKIP LOCKED'); + throw NotSupported::new('SKIP LOCKED'); } $sql .= ' ' . $this->skipLockedSQL; diff --git a/doctrine/dbal/src/SQL/Builder/DefaultUnionSQLBuilder.php b/doctrine/dbal/src/SQL/Builder/DefaultUnionSQLBuilder.php new file mode 100644 index 000000000..289382d96 --- /dev/null +++ b/doctrine/dbal/src/SQL/Builder/DefaultUnionSQLBuilder.php @@ -0,0 +1,48 @@ +getUnionParts() as $union) { + if ($union->type !== null) { + $parts[] = $union->type === UnionType::ALL + ? $this->platform->getUnionAllSQL() + : $this->platform->getUnionDistinctSQL(); + } + + $parts[] = $this->platform->getUnionSelectPartSQL((string) $union->query); + } + + $orderBy = $query->getOrderBy(); + if (count($orderBy) > 0) { + $parts[] = 'ORDER BY ' . implode(', ', $orderBy); + } + + $sql = implode(' ', $parts); + $limit = $query->getLimit(); + + if ($limit->isDefined()) { + $sql = $this->platform->modifyLimitQuery($sql, $limit->getMaxResults(), $limit->getFirstResult()); + } + + return $sql; + } +} diff --git a/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php b/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php index 8de742a31..c0384897f 100644 --- a/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php +++ b/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php @@ -1,8 +1,9 @@ platform = $platform; } - /** - * @return list - * - * @throws Exception - */ + /** @return list */ public function buildSQL(Schema $schema): array { return array_merge( @@ -46,15 +40,13 @@ private function buildTableStatements(array $tables): array * @param list $sequences * * @return list - * - * @throws Exception */ private function buildSequenceStatements(array $sequences): array { $statements = []; foreach ($sequences as $sequence) { - $statements[] = $this->platform->getDropSequenceSQL($sequence); + $statements[] = $this->platform->getDropSequenceSQL($sequence->getQuotedName($this->platform)); } return $statements; diff --git a/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php b/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php index ddbf73c03..c013f96a8 100644 --- a/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php +++ b/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php @@ -1,5 +1,7 @@ name]; + if ($part->columns !== null && count($part->columns) > 0) { + $ctePart[] = ' (' . implode(', ', $part->columns) . ')'; + } + + $ctePart[] = ' AS (' . $part->query . ')'; + $cteParts[] = implode('', $ctePart); + } + + return 'WITH ' . implode(', ', $cteParts); + } +} diff --git a/doctrine/dbal/src/SQL/Parser.php b/doctrine/dbal/src/SQL/Parser.php index 418af799f..c332a1ee5 100644 --- a/doctrine/dbal/src/SQL/Parser.php +++ b/doctrine/dbal/src/SQL/Parser.php @@ -1,5 +1,7 @@ Table($tableName)); if you want to rename the table, you have to make sure this does not get * recreated during schema migration. + * + * @internal This class should be extended only by DBAL itself. + * + * @template N of Name */ abstract class AbstractAsset { - /** @var string */ - protected $_name = ''; + protected string $_name = ''; + + /** + * Indicates whether the object name has been initialized. + */ + protected bool $isNameInitialized = false; /** * Namespace of the asset. If none isset the default namespace is assumed. * - * @var string|null + * @deprecated Use {@see NamedObject::getObjectName()} and {@see OptionallyQualifiedName::getQualifier()} instead. */ - protected $_namespace; + protected ?string $_namespace = null; + + /** @deprecated */ + protected bool $_quoted = false; + + /** @var list */ + private array $identifiers = []; - /** @var bool */ - protected $_quoted = false; + private bool $validateFuture = false; + + public function __construct(?string $name = null) + { + if ($name === null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6610', + 'Not passing $name to %s is deprecated.', + __METHOD__, + ); + + return; + } + + $this->_setName($name); + } /** - * Sets the name of this asset. + * Returns a parser for parsing the object name. * - * @param string $name + * @deprecated Parse the name in the constructor instead. + * + * @return Parser + */ + protected function getNameParser(): Parser + { + throw NotImplemented::fromMethod(static::class, __FUNCTION__); + } + + /** + * Sets the object name. + * + * @deprecated Set the name in the constructor instead. + * + * @param ?N $name + */ + protected function setName(?Name $name): void + { + throw NotImplemented::fromMethod(static::class, __FUNCTION__); + } + + /** + * Sets the name of this asset. * - * @return void + * @deprecated Use the constructor instead. */ - protected function _setName($name) + protected function _setName(string $name): void { + $this->isNameInitialized = false; + + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6610', + '%s is deprecated. Use the constructor instead.', + __METHOD__, + ); + + $input = $name; + if ($this->isIdentifierQuoted($name)) { $this->_quoted = true; $name = $this->trimQuotes($name); } - if (strpos($name, '.') !== false) { + if (str_contains($name, '.')) { $parts = explode('.', $name); $this->_namespace = $parts[0]; $name = $parts[1]; } $this->_name = $name; + + $this->validateFuture = false; + + if ($input !== '') { + try { + $parsedName = $this->getNameParser()->parse($input); + } catch (NotImplemented $e) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6592', + 'Unable to parse object name: %s.', + $e->getMessage(), + ); + + return; + } catch (Throwable $e) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6592', + 'Unable to parse object name: %s.', + $e->getMessage(), + ); + + return; + } + } else { + $parsedName = null; + } + + try { + $this->setName($parsedName); + } catch (Throwable $e) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6646', + 'Using invalid database object names is deprecated: %s.', + $e->getMessage(), + ); + + return; + } + + $this->isNameInitialized = true; + + if ($parsedName === null) { + $this->identifiers = []; + + return; + } + + if ($parsedName instanceof UnqualifiedName) { + $identifiers = [$parsedName->getIdentifier()]; + } elseif ($parsedName instanceof OptionallyQualifiedName) { + $unqualifiedName = $parsedName->getUnqualifiedName(); + $qualifier = $parsedName->getQualifier(); + + $identifiers = $qualifier !== null + ? [$qualifier, $unqualifiedName] + : [$unqualifiedName]; + } elseif ($parsedName instanceof GenericName) { + $identifiers = $parsedName->getIdentifiers(); + } else { + return; + } + + switch (count($identifiers)) { + case 1: + $namespace = null; + $name = $identifiers[0]; + break; + + case 2: + [$namespace, $name] = $identifiers; + break; + + default: + return; + } + + $this->identifiers = $identifiers; + $this->validateFuture = true; + + $futureName = $name->getValue(); + $futureNamespace = $namespace?->getValue(); + + if ($this->_name !== $futureName) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6592', + 'Instead of "%s", this name will be interpreted as "%s" in 5.0', + $this->_name, + $futureName, + ); + } + + if ($this->_namespace === $futureNamespace) { + return; + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6592', + 'Instead of %s, the namespace in this name will be interpreted as %s in 5.0.', + $this->_namespace !== null ? sprintf('"%s"', $this->_namespace) : 'null', + $futureNamespace !== null ? sprintf('"%s"', $futureNamespace) : 'null', + ); } /** * Is this asset in the default namespace? * - * @param string $defaultNamespaceName - * - * @return bool + * @deprecated */ - public function isInDefaultNamespace($defaultNamespaceName) + public function isInDefaultNamespace(string $defaultNamespaceName): bool { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6664', + '%s is deprecated and will be removed in 5.0.', + __METHOD__, + ); + return $this->_namespace === $defaultNamespaceName || $this->_namespace === null; } @@ -78,10 +262,18 @@ public function isInDefaultNamespace($defaultNamespaceName) * * If NULL is returned this means the default namespace is used. * - * @return string|null + * @deprecated Use {@see NamedObject::getObjectName()} and {@see OptionallyQualifiedName::getQualifier()} instead. */ - public function getNamespaceName() + public function getNamespaceName(): ?string { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6664', + '%s is deprecated and will be removed in 5.0. Use NamedObject::getObjectName()' + . ' and OptionallyQualifiedName::getQualifier() instead.', + __METHOD__, + ); + return $this->_namespace; } @@ -89,12 +281,17 @@ public function getNamespaceName() * The shortest name is stripped of the default namespace. All other * namespaced elements are returned as full-qualified names. * - * @param string|null $defaultNamespaceName - * - * @return string + * @deprecated Use {@see NamedObject::getObjectName()} instead. */ - public function getShortestName($defaultNamespaceName) + public function getShortestName(?string $defaultNamespaceName): string { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6657', + '%s is deprecated and will be removed in 5.0.', + __METHOD__, + ); + $shortestName = $this->getName(); if ($this->_namespace === $defaultNamespaceName) { $shortestName = $this->_name; @@ -104,67 +301,48 @@ public function getShortestName($defaultNamespaceName) } /** - * The normalized name is full-qualified and lower-cased. Lower-casing is - * actually wrong, but we have to do it to keep our sanity. If you are - * using database objects that only differentiate in the casing (FOO vs - * Foo) then you will NOT be able to use Doctrine Schema abstraction. - * - * Every non-namespaced element is prefixed with the default namespace - * name which is passed as argument to this method. - * - * @deprecated Use {@see getNamespaceName()} and {@see getName()} instead. - * - * @param string $defaultNamespaceName + * Checks if this asset's name is quoted. * - * @return string + * @deprecated Depending on the concrete class of the object, use {@see NamedObject::getObjectName()} or + * {@see OptionallyNamedObject::getObjectName()} to get the name. Then, depending on the type of the + * name, use {@see UnqualifiedName::getIdentifier()}, {@see OptionallyQualifiedName::getQualifier()}, + * or {@see OptionallyQualifiedName::getUnqualifiedName()} to get the corresponding identifiers. Then, + * use {@see Identifier::$isQuoted()}. */ - public function getFullQualifiedName($defaultNamespaceName) + public function isQuoted(): bool { Deprecation::triggerIfCalledFromOutside( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4814', - 'AbstractAsset::getFullQualifiedName() is deprecated.' - . ' Use AbstractAsset::getNamespaceName() and ::getName() instead.', + 'https://github.com/doctrine/dbal/pull/7084', + '%s is deprecated and will be removed in 5.0.', + __METHOD__, ); - $name = $this->getName(); - if ($this->_namespace === null) { - $name = $defaultNamespaceName . '.' . $name; - } - - return strtolower($name); - } - - /** - * Checks if this asset's name is quoted. - * - * @return bool - */ - public function isQuoted() - { return $this->_quoted; } /** * Checks if this identifier is quoted. * - * @param string $identifier - * - * @return bool + * @deprecated Parse the name and introspect its identifiers individually using {@see Identifier::isQuoted()} + * instead. */ - protected function isIdentifierQuoted($identifier) + protected function isIdentifierQuoted(string $identifier): bool { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6677', + '%s is deprecated and will be removed in 5.0.', + __METHOD__, + ); + return isset($identifier[0]) && ($identifier[0] === '`' || $identifier[0] === '"' || $identifier[0] === '['); } /** * Trim quotes from the identifier. - * - * @param string $identifier - * - * @return string */ - protected function trimQuotes($identifier) + protected function trimQuotes(string $identifier): string { return str_replace(['`', '"', '[', ']'], '', $identifier); } @@ -172,10 +350,19 @@ protected function trimQuotes($identifier) /** * Returns the name of this schema asset. * - * @return string + * @deprecated Use {@see NamedObject::getObjectName()} or {@see OptionallyQualifiedName::getObjectName()} instead. + * In SQL context, convert the resulting {@see Name} to SQL using {@see Name::toSQL()}. In other + * contexts, convert the resulting name to string using {@see Name::toString()}. */ - public function getName() + public function getName(): string { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/7094', + '%s is deprecated and will be removed in 5.0.', + __METHOD__, + ); + if ($this->_namespace !== null) { return $this->_namespace . '.' . $this->_name; } @@ -187,17 +374,63 @@ public function getName() * Gets the quoted representation of this asset but only if it was defined with one. Otherwise * return the plain unquoted value as inserted. * - * @return string + * @deprecated Use {@see NamedObject::getObjectName()} or {@see OptionallyQualifiedName::getObjectName()} followed + * by {@see Name::toSQL()} instead. */ - public function getQuotedName(AbstractPlatform $platform) + public function getQuotedName(AbstractPlatform $platform): string { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6674', + '%s is deprecated and will be removed in 5.0.', + __METHOD__, + ); + $keywords = $platform->getReservedKeywordsList(); - $parts = explode('.', $this->getName()); - foreach ($parts as $k => $v) { - $parts[$k] = $this->_quoted || $keywords->isKeyword($v) ? $platform->quoteIdentifier($v) : $v; + $folding = $platform->getUnquotedIdentifierFolding(); + $parts = $normalizedParts = []; + + foreach (explode('.', $this->getName()) as $identifier) { + $isQuoted = $this->_quoted || $keywords->isKeyword($identifier); + + if (! $isQuoted) { + $parts[] = $identifier; + + /** @phpstan-ignore argument.type */ + $normalizedParts[] = $folding->foldUnquotedIdentifier($identifier); + } else { + $parts[] = $platform->quoteSingleIdentifier($identifier); + $normalizedParts[] = $identifier; + } + } + + $name = implode('.', $parts); + + if ($this->validateFuture) { + $futureParts = array_map(static function (Identifier $identifier) use ($folding): string { + $value = $identifier->getValue(); + + if (! $identifier->isQuoted()) { + $value = $folding->foldUnquotedIdentifier($value); + } + + return $value; + }, $this->identifiers); + + if ($normalizedParts !== $futureParts) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6592', + 'Relying on implicitly quoted identifiers preserving their original case is deprecated. ' + . 'The current name %s will become %s in 5.0. ' + . 'Please quote the name if the case needs to be preserved.', + $name, + implode('.', array_map([$platform, 'quoteSingleIdentifier'], $futureParts)), + ); + } } - return implode('.', $parts); + return $name; } /** @@ -207,13 +440,12 @@ public function getQuotedName(AbstractPlatform $platform) * however building idents automatically for foreign keys, composite keys or such can easily create * very long names. * - * @param string[] $columnNames - * @param string $prefix - * @param int $maxSize + * @param array $columnNames + * @param positive-int $maxSize * - * @return string + * @return non-empty-string */ - protected function _generateIdentifierName($columnNames, $prefix = '', $maxSize = 30) + protected function _generateIdentifierName(array $columnNames, string $prefix = '', int $maxSize = 30): string { $hash = implode('', array_map(static function ($column): string { return dechex(crc32($column)); diff --git a/doctrine/dbal/src/Schema/AbstractNamedObject.php b/doctrine/dbal/src/Schema/AbstractNamedObject.php new file mode 100644 index 000000000..54fe5124a --- /dev/null +++ b/doctrine/dbal/src/Schema/AbstractNamedObject.php @@ -0,0 +1,58 @@ + + * @implements NamedObject + */ +abstract class AbstractNamedObject extends AbstractAsset implements NamedObject +{ + /** + * The name of the database object. + * + * Until the validity of the name is enforced, this property isn't guaranteed to be always initialized. The property + * can be accessed only if {@see $isNameInitialized} is set to true. + * + * @var N + */ + protected Name $name; + + public function __construct(string $name) + { + parent::__construct($name); + } + + /** + * Returns the object name. + * + * @return N + * + * @throws InvalidState + */ + public function getObjectName(): Name + { + if (! $this->isNameInitialized) { + throw InvalidState::objectNameNotInitialized(); + } + + return $this->name; + } + + protected function setName(?Name $name): void + { + if ($name === null) { + throw InvalidName::fromEmpty(); + } + + $this->name = $name; + } +} diff --git a/doctrine/dbal/src/Schema/AbstractOptionallyNamedObject.php b/doctrine/dbal/src/Schema/AbstractOptionallyNamedObject.php new file mode 100644 index 000000000..c1fe096ad --- /dev/null +++ b/doctrine/dbal/src/Schema/AbstractOptionallyNamedObject.php @@ -0,0 +1,46 @@ + + * @implements OptionallyNamedObject + */ +abstract class AbstractOptionallyNamedObject extends AbstractAsset implements OptionallyNamedObject +{ + /** + * The name of the database object. + * + * Until the validity of the name is enforced, this property isn't guaranteed to be always initialized. The property + * can be accessed only if {@see $isNameInitialized} is set to true. + * + * @var ?N + */ + protected ?Name $name; + + public function __construct(?string $name) + { + parent::__construct($name ?? ''); + } + + public function getObjectName(): ?Name + { + if (! $this->isNameInitialized) { + throw InvalidState::objectNameNotInitialized(); + } + + return $this->name; + } + + protected function setName(?Name $name): void + { + $this->name = $name; + } +} diff --git a/doctrine/dbal/src/Schema/AbstractSchemaManager.php b/doctrine/dbal/src/Schema/AbstractSchemaManager.php index 2e38bb88c..134c335af 100644 --- a/doctrine/dbal/src/Schema/AbstractSchemaManager.php +++ b/doctrine/dbal/src/Schema/AbstractSchemaManager.php @@ -1,15 +1,21 @@ null value means that there is no + * schema currently selected within the connection. * - * @var T - */ - protected $_platform; - - /** @param T $platform */ - public function __construct(Connection $connection, AbstractPlatform $platform) - { - $this->_conn = $connection; - $this->_platform = $platform; - } - - /** - * Returns the associated platform. + * The property should be accessed only when {@link $currentSchemaDetermined} is set to true. If the + * currently used database platform doesn't support schemas, the property will remain uninitialized. * - * @deprecated Use {@link Connection::getDatabasePlatform()} instead. + * The property is initialized only once. If the underlying connection switches to a different schema, a new schema + * manager instance will have to be created to reflect this change. * - * @return T + * @var ?non-empty-string */ - public function getDatabasePlatform() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5387', - 'AbstractSchemaManager::getDatabasePlatform() is deprecated.' - . ' Use Connection::getDatabasePlatform() instead.', - ); - - return $this->_platform; - } + private ?string $currentSchemaName; /** - * Tries any method on the schema manager. Normally a method throws an - * exception when your DBMS doesn't support it or if an error occurs. - * This method allows you to try and method on your SchemaManager - * instance and will return false if it does not work or is not supported. - * - * - * $result = $sm->tryMethod('dropView', 'view_name'); - * - * - * @deprecated - * - * @return mixed + * Indicates whether the current schema has been determined. */ - public function tryMethod() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4897', - 'AbstractSchemaManager::tryMethod() is deprecated.', - ); + private bool $currentSchemaDetermined = false; - $args = func_get_args(); - $method = $args[0]; - unset($args[0]); - $args = array_values($args); - - $callback = [$this, $method]; - assert(is_callable($callback)); - - try { - return call_user_func_array($callback, $args); - } catch (Throwable $e) { - return false; - } - } - - /** - * Lists the available databases for this connection. - * - * @return string[] - * - * @throws Exception - */ - public function listDatabases() + /** @param T $platform */ + public function __construct(protected Connection $connection, protected AbstractPlatform $platform) { - $sql = $this->_platform->getListDatabasesSQL(); - - $databases = $this->_conn->fetchAllAssociative($sql); - - return $this->_getPortableDatabasesList($databases); } /** - * Returns a list of all namespaces in the current database. + * Lists the available databases for this connection. * - * @deprecated Use {@see listSchemaNames()} instead. + * @deprecated Use {@see introspectDatabaseNames()} instead. * - * @return string[] + * @return array * * @throws Exception */ - public function listNamespaceNames() + public function listDatabases(): array { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4503', - 'AbstractSchemaManager::listNamespaceNames() is deprecated,' - . ' use AbstractSchemaManager::listSchemaNames() instead.', - ); - - $sql = $this->_platform->getListNamespacesSQL(); - - $namespaces = $this->_conn->fetchAllAssociative($sql); - - return $this->getPortableNamespacesList($namespaces); + return array_map(function (array $row): string { + return $this->_getPortableDatabaseDefinition($row); + }, $this->connection->fetchAllAssociative( + $this->platform->getListDatabasesSQL(), + )); } /** * Returns a list of the names of all schemata in the current database. * + * @deprecated Use {@see introspectSchemaNames()} instead. + * * @return list * * @throws Exception */ public function listSchemaNames(): array { - throw Exception::notSupported(__METHOD__); + throw NotSupported::new(__METHOD__); } /** * Lists the available sequences for this connection. * - * @param string|null $database + * @deprecated Use {@see introspectSequences()} instead. * - * @return Sequence[] + * @return list * * @throws Exception */ - public function listSequences($database = null) + public function listSequences(): array { - if ($database === null) { - $database = $this->getDatabase(__METHOD__); - } else { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5284', - 'Passing $database to AbstractSchemaManager::listSequences() is deprecated.', - ); - } - - $sql = $this->_platform->getListSequencesSQL($database); - - $sequences = $this->_conn->fetchAllAssociative($sql); - - return $this->filterAssetNames($this->_getPortableSequencesList($sequences)); + return $this->filterAssetNames( + array_map(function (array $row): Sequence { + return $this->_getPortableSequenceDefinition($row); + }, $this->connection->fetchAllAssociative( + $this->platform->getListSequencesSQL( + $this->getDatabase(__METHOD__), + ), + )), + ); } /** @@ -203,57 +125,23 @@ public function listSequences($database = null) * of a table. Where a RDBMS specifies more details, these are held * in the platformDetails array. * - * @param string $table The name of the table. - * @param string|null $database + * @deprecated Use {@see introspectTableColumns()}, {@see introspectTableColumnsByUnquotedName()} + * or {@see introspectTableColumnsByQuotedName()} instead. * - * @return Column[] + * @return array * * @throws Exception */ - public function listTableColumns($table, $database = null) + public function listTableColumns(string $table): array { - if ($database === null) { - $database = $this->getDatabase(__METHOD__); - } else { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5284', - 'Passing $database to AbstractSchemaManager::listTableColumns() is deprecated.', - ); - } + $this->validateTableName($table, __METHOD__); - $sql = $this->_platform->getListTableColumnsSQL($table, $database); - - $tableColumns = $this->_conn->fetchAllAssociative($sql); - - return $this->_getPortableTableColumnList($table, $database, $tableColumns); - } - - /** - * @param string $table - * @param string|null $database - * - * @return Column[] - * - * @throws Exception - */ - protected function doListTableColumns($table, $database = null): array - { - if ($database === null) { - $database = $this->getDatabase(__METHOD__); - } else { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5284', - 'Passing $database to AbstractSchemaManager::doListTableColumns() is deprecated.', - ); - } + $database = $this->getDatabase(__METHOD__); return $this->_getPortableTableColumnList( $table, $database, - $this->selectTableColumns($database, $this->normalizeName($table)) - ->fetchAllAssociative(), + $this->fetchTableColumns($database, $this->normalizeName($table)), ); } @@ -262,38 +150,22 @@ protected function doListTableColumns($table, $database = null): array * * Keys of the portable indexes list are all lower-cased. * - * @param string $table The name of the table. + * @deprecated Use {@see introspectTableIndexes()}, {@see introspectTableIndexesByUnquotedName()} + * or {@see introspectTableIndexesByQuotedName()} * - * @return Index[] + * @return array * * @throws Exception */ - public function listTableIndexes($table) + public function listTableIndexes(string $table): array { - $sql = $this->_platform->getListTableIndexesSQL($table, $this->_conn->getDatabase()); + $this->validateTableName($table, __METHOD__); - $tableIndexes = $this->_conn->fetchAllAssociative($sql); - - return $this->_getPortableTableIndexesList($tableIndexes, $table); - } - - /** - * @param string $table - * - * @return Index[] - * - * @throws Exception - */ - protected function doListTableIndexes($table): array - { $database = $this->getDatabase(__METHOD__); $table = $this->normalizeName($table); return $this->_getPortableTableIndexesList( - $this->selectIndexColumns( - $database, - $table, - )->fetchAllAssociative(), + $this->fetchIndexColumns($database, $table), $table, ); } @@ -301,61 +173,40 @@ protected function doListTableIndexes($table): array /** * Returns true if all the given tables exist. * - * The usage of a string $tableNames is deprecated. Pass a one-element array instead. - * - * @param string|string[] $names - * - * @return bool + * @param array $names * * @throws Exception */ - public function tablesExist($names) + public function tablesExist(array $names): bool { - if (is_string($names)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/3580', - 'The usage of a string $tableNames in AbstractSchemaManager::tablesExist() is deprecated. ' . - 'Pass a one-element array instead.', - ); - } - - $names = array_map('strtolower', (array) $names); + $names = array_map('strtolower', $names); return count($names) === count(array_intersect($names, array_map('strtolower', $this->listTableNames()))); } - /** - * Returns a list of all tables in the current database. - * - * @return string[] - * - * @throws Exception - */ - public function listTableNames() + /** @throws Exception */ + public function tableExists(string $tableName): bool { - $sql = $this->_platform->getListTablesSQL(); - - $tables = $this->_conn->fetchAllAssociative($sql); - $tableNames = $this->_getPortableTablesList($tables); - - return $this->filterAssetNames($tableNames); + return $this->tablesExist([$tableName]); } /** - * @return list + * Returns a list of all tables in the current database. + * + * @deprecated Use {@see introspectTableNames()} instead. + * + * @return list * * @throws Exception */ - protected function doListTableNames(): array + public function listTableNames(): array { - $database = $this->getDatabase(__METHOD__); - return $this->filterAssetNames( - $this->_getPortableTablesList( - $this->selectTableNames($database) - ->fetchAllAssociative(), - ), + array_map(function (array $row): string { + return $this->_getPortableTableDefinition($row); + }, $this->selectTableNames( + $this->getDatabase(__METHOD__), + )->fetchAllAssociative()), ); } @@ -363,16 +214,15 @@ protected function doListTableNames(): array * Filters asset names if they are configured to return only a subset of all * the found elements. * - * @param mixed[] $assetNames + * @param list $assetNames + * + * @return list * - * @return mixed[] + * @template N */ - protected function filterAssetNames($assetNames) + private function filterAssetNames(array $assetNames): array { - $filter = $this->_conn->getConfiguration()->getSchemaAssetsFilter(); - if ($filter === null) { - return $assetNames; - } + $filter = $this->connection->getConfiguration()->getSchemaAssetsFilter(); return array_values(array_filter($assetNames, $filter)); } @@ -380,28 +230,13 @@ protected function filterAssetNames($assetNames) /** * Lists the tables for this connection. * - * @return list
+ * @deprecated Use {@see introspectTables()} instead. * - * @throws Exception - */ - public function listTables() - { - $tableNames = $this->listTableNames(); - - $tables = []; - foreach ($tableNames as $tableName) { - $tables[] = $this->introspectTable($tableName); - } - - return $tables; - } - - /** * @return list
* * @throws Exception */ - protected function doListTables(): array + public function listTables(): array { $database = $this->getDatabase(__METHOD__); @@ -410,11 +245,14 @@ protected function doListTables(): array $foreignKeyColumnsByTable = $this->fetchForeignKeyColumnsByTable($database); $tableOptionsByTable = $this->fetchTableOptionsByTable($database); - $filter = $this->_conn->getConfiguration()->getSchemaAssetsFilter(); + $filter = $this->connection->getConfiguration()->getSchemaAssetsFilter(); $tables = []; + $configuration = $this->createSchemaConfig() + ->toTableConfiguration(); + foreach ($tableColumnsByTable as $tableName => $tableColumns) { - if ($filter !== null && ! $filter($tableName)) { + if (! $filter($tableName)) { continue; } @@ -425,6 +263,7 @@ protected function doListTables(): array [], $this->_getPortableTableForeignKeysList($foreignKeyColumnsByTable[$tableName] ?? []), $tableOptionsByTable[$tableName] ?? [], + $configuration, ); } @@ -432,68 +271,49 @@ protected function doListTables(): array } /** - * @deprecated Use {@see introspectTable()} instead. + * Returns the current schema name used by the schema manager connection. * - * @param string $name + * The null value means that there is no schema currently selected within the connection or the + * corresponding database platform doesn't support schemas. * - * @return Table + * @return ?non-empty-string * * @throws Exception */ - public function listTableDetails($name) + final protected function getCurrentSchemaName(): ?string { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5595', - '%s is deprecated. Use introspectTable() instead.', - __METHOD__, - ); - - $columns = $this->listTableColumns($name); - $foreignKeys = []; - - if ($this->_platform->supportsForeignKeyConstraints()) { - $foreignKeys = $this->listTableForeignKeys($name); + if (! $this->platform->supportsSchemas()) { + return null; } - $indexes = $this->listTableIndexes($name); + if (! $this->currentSchemaDetermined) { + $this->currentSchemaName = $this->determineCurrentSchemaName(); + $this->currentSchemaDetermined = true; + } - return new Table($name, $columns, $indexes, [], $foreignKeys); + return $this->currentSchemaName; } /** - * @param string $name + * Determines the name of the current schema. + * + * If the corresponding database platform supports schemas, the schema manager must implement this method. + * + * @return ?non-empty-string * * @throws Exception */ - protected function doListTableDetails($name): Table + protected function determineCurrentSchemaName(): ?string { - $database = $this->getDatabase(__METHOD__); - - $normalizedName = $this->normalizeName($name); - - $tableOptionsByTable = $this->fetchTableOptionsByTable($database, $normalizedName); - - if ($this->_platform->supportsForeignKeyConstraints()) { - $foreignKeys = $this->listTableForeignKeys($name); - } else { - $foreignKeys = []; - } - - return new Table( - $name, - $this->listTableColumns($name, $database), - $this->listTableIndexes($name), - [], - $foreignKeys, - $tableOptionsByTable[$normalizedName] ?? [], - ); + throw NotSupported::new(__METHOD__); } /** * An extension point for those platforms where case sensitivity of the object name depends on whether it's quoted. * * Such platforms should convert a possibly quoted name into a value of the corresponding case. + * + * @deprecated Use {@see Identifier::toNormalizedValue()} instead. */ protected function normalizeName(string $name): string { @@ -502,869 +322,945 @@ protected function normalizeName(string $name): string return $identifier->getName(); } + private function validateTableName(string $input, string $methodName): void + { + $parser = Parsers::getOptionallyQualifiedNameParser(); + + try { + $tableName = $parser->parse($input); + } catch (Throwable $e) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6768', + 'Unable to parse table name passed to %s(): %s.', + $methodName, + $e->getMessage(), + ); + + return; + } + + if ($tableName->getQualifier() === null || $this->platform->supportsSchemas()) { + return; + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6768', + 'Relying on %s() not parsing an unquoted table name containing a dot while working with %s is' + . ' deprecated. Pass a quoted name instead.', + $methodName, + $this->platform::class, + ); + } + /** * Selects names of tables in the specified database. * * @throws Exception - * - * @abstract */ - protected function selectTableNames(string $databaseName): Result - { - throw Exception::notSupported(__METHOD__); - } + abstract protected function selectTableNames(string $databaseName): Result; /** * Selects definitions of table columns in the specified database. If the table name is specified, narrows down * the selection to this table. * * @throws Exception + */ + abstract protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result; + + /** + * Selects definitions of index columns in the specified database. If the table name is specified, narrows down + * the selection to this table. + * + * @throws Exception + */ + abstract protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result; + + /** + * Selects definitions of foreign key columns in the specified database. If the table name is specified, + * narrows down the selection to this table. + * + * @throws Exception + */ + abstract protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result; + + /** + * Fetches definitions of table columns in the specified database. If the table name is specified, narrows down + * the selection to this table. * - * @abstract + * @return list> + * + * @throws Exception */ - protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + protected function fetchTableColumns(string $databaseName, ?string $tableName = null): array { - throw Exception::notSupported(__METHOD__); + return $this->selectTableColumns($databaseName, $tableName)->fetchAllAssociative(); } /** - * Selects definitions of index columns in the specified database. If the table name is specified, narrows down + * Fetches definitions of index columns in the specified database. If the table name is specified, narrows down * the selection to this table. * + * @return list> + * * @throws Exception */ - protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + protected function fetchIndexColumns(string $databaseName, ?string $tableName = null): array { - throw Exception::notSupported(__METHOD__); + return $this->selectIndexColumns($databaseName, $tableName)->fetchAllAssociative(); } /** - * Selects definitions of foreign key columns in the specified database. If the table name is specified, + * Fetches definitions of foreign key columns in the specified database. If the table name is specified, * narrows down the selection to this table. * + * @return list> + * * @throws Exception */ - protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + protected function fetchForeignKeyColumns(string $databaseName, ?string $tableName = null): array { - throw Exception::notSupported(__METHOD__); + return $this->selectForeignKeyColumns($databaseName, $tableName)->fetchAllAssociative(); } /** * Fetches definitions of table columns in the specified database and returns them grouped by table name. * - * @return array>> + * @return array>> * * @throws Exception */ protected function fetchTableColumnsByTable(string $databaseName): array { - return $this->fetchAllAssociativeGrouped($this->selectTableColumns($databaseName)); + return $this->groupByTable($this->fetchTableColumns($databaseName)); } /** * Fetches definitions of index columns in the specified database and returns them grouped by table name. * - * @return array>> + * @return array>> * * @throws Exception */ protected function fetchIndexColumnsByTable(string $databaseName): array { - return $this->fetchAllAssociativeGrouped($this->selectIndexColumns($databaseName)); + return $this->groupByTable($this->fetchIndexColumns($databaseName)); } /** * Fetches definitions of foreign key columns in the specified database and returns them grouped by table name. * - * @return array>> + * @return array>> * * @throws Exception */ protected function fetchForeignKeyColumnsByTable(string $databaseName): array { - if (! $this->_platform->supportsForeignKeyConstraints()) { - return []; - } - - return $this->fetchAllAssociativeGrouped( - $this->selectForeignKeyColumns($databaseName), - ); + return $this->groupByTable($this->fetchForeignKeyColumns($databaseName)); } /** * Fetches table options for the tables in the specified database and returns them grouped by table name. * If the table name is specified, narrows down the selection to this table. * - * @return array> + * @param ?non-empty-string $tableName + * + * @return array> * * @throws Exception */ - protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array - { - throw Exception::notSupported(__METHOD__); - } + abstract protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array; /** * Introspects the table with the given name. * + * @deprecated Use {@see introspectTableByUnquotedName()} or {@see introspectTableByQuotedName()} instead. + * * @throws Exception */ public function introspectTable(string $name): Table { - $table = $this->listTableDetails($name); + $columns = $this->listTableColumns($name); - if ($table->getColumns() === []) { - throw SchemaException::tableDoesNotExist($name); + if ($columns === []) { + throw TableDoesNotExist::new($name); } - return $table; + return new Table( + $name, + $columns, + $this->listTableIndexes($name), + [], + $this->listTableForeignKeys($name), + $this->getTableOptions($name), + ); } /** * Lists the views this connection has. * - * @return View[] + * @deprecated Use {@see introspectViews()} instead. + * + * @return list * * @throws Exception */ - public function listViews() + public function listViews(): array { - $database = $this->_conn->getDatabase(); - $sql = $this->_platform->getListViewsSQL($database); - $views = $this->_conn->fetchAllAssociative($sql); - - return $this->_getPortableViewsList($views); + return array_map(function (array $row): View { + return $this->_getPortableViewDefinition($row); + }, $this->connection->fetchAllAssociative( + $this->platform->getListViewsSQL( + $this->getDatabase(__METHOD__), + ), + )); } /** * Lists the foreign keys for the given table. * - * @param string $table The name of the table. - * @param string|null $database + * @deprecated Use {@see introspectTableForeignKeyConstraints()}, + * {@see introspectTableForeignKeyConstraintsByUnquotedName()} + * or {@see introspectTableForeignKeyConstraintsByQuotedName()} instead. * - * @return ForeignKeyConstraint[] + * @return array * * @throws Exception */ - public function listTableForeignKeys($table, $database = null) + public function listTableForeignKeys(string $table): array { - if ($database === null) { - $database = $this->getDatabase(__METHOD__); - } else { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5284', - 'Passing $database to AbstractSchemaManager::listTableForeignKeys() is deprecated.', - ); - } + $this->validateTableName($table, __METHOD__); - $sql = $this->_platform->getListTableForeignKeysSQL($table, $database); - $tableForeignKeys = $this->_conn->fetchAllAssociative($sql); + $database = $this->getDatabase(__METHOD__); - return $this->_getPortableTableForeignKeysList($tableForeignKeys); + return $this->_getPortableTableForeignKeysList( + $this->fetchForeignKeyColumns( + $database, + $this->normalizeName($table), + ), + ); } /** - * @param string $table - * @param string|null $database - * - * @return ForeignKeyConstraint[] + * @return array * * @throws Exception */ - protected function doListTableForeignKeys($table, $database = null): array + private function getTableOptions(string $name): array { - if ($database === null) { - $database = $this->getDatabase(__METHOD__); - } else { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5284', - 'Passing $database to AbstractSchemaManager::listTableForeignKeys() is deprecated.', - ); - } + $this->validateTableName($name, __METHOD__); - return $this->_getPortableTableForeignKeysList( - $this->selectForeignKeyColumns( - $database, - $this->normalizeName($table), - )->fetchAllAssociative(), - ); - } + $normalizedName = $this->normalizeName($name); - /* drop*() Methods */ + return $this->fetchTableOptionsByTable( + $this->getDatabase(__METHOD__), + $normalizedName, // @phpstan-ignore argument.type + )[$normalizedName] ?? []; + } /** - * Drops a database. - * - * NOTE: You can not drop the database this SchemaManager is currently connected to. + * Introspects available databases and returns their names. * - * @param string $database The name of the database to drop. - * - * @return void + * @return list * * @throws Exception */ - public function dropDatabase($database) + public function introspectDatabaseNames(): array { - $this->_conn->executeStatement( - $this->_platform->getDropDatabaseSQL($database), - ); + return $this->createSchemaProvider()->getAllDatabaseNames(); } /** - * Drops a schema. + * Introspects schemas in the current database and returns their names. + * + * @return list * * @throws Exception */ - public function dropSchema(string $schemaName): void + public function introspectSchemaNames(): array { - $this->_conn->executeStatement( - $this->_platform->getDropSchemaSQL($schemaName), - ); + return $this->createSchemaProvider()->getAllSchemaNames(); } /** - * Drops the given table. - * - * @param string $name The name of the table to drop. + * Introspects tables in the current database and returns their names. * - * @return void + * @return list * * @throws Exception */ - public function dropTable($name) + public function introspectTableNames(): array { - $this->_conn->executeStatement( - $this->_platform->getDropTableSQL($name), - ); + $filter = $this->connection->getConfiguration()->getSchemaAssetsFilter(); + $tableNames = []; + + foreach ($this->createSchemaProvider()->getAllTableNames() as $tableName) { + if ($this->testTableName($tableName, $filter)) { + $tableNames[] = $tableName; + } + } + + return $tableNames; } /** - * Drops the index from the given table. + * Introspects tables in the current database and returns their definitions. * - * @param Index|string $index The name of the index. - * @param Table|string $table The name of the table. - * - * @return void + * @return list
* * @throws Exception */ - public function dropIndex($index, $table) + public function introspectTables(): array { - if ($index instanceof Index) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $index as an Index object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $index = $index->getQuotedName($this->_platform); - } - - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as an Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); + $filter = $this->connection->getConfiguration()->getSchemaAssetsFilter(); + $tables = []; - $table = $table->getQuotedName($this->_platform); + foreach ($this->createSchemaProvider()->getAllTables() as $table) { + if ($this->testTableName($table->getObjectName(), $filter)) { + $tables[] = $table; + } } - $this->_conn->executeStatement( - $this->_platform->getDropIndexSQL($index, $table), - ); + return $tables; } /** - * Drops the constraint from the given table. - * - * @deprecated Use {@see dropIndex()}, {@see dropForeignKey()} or {@see dropUniqueConstraint()} instead. - * - * @param Table|string $table The name of the table. - * - * @return void - * - * @throws Exception + * Tests whether the table name matches the filter. */ - public function dropConstraint(Constraint $constraint, $table) + private function testTableName(OptionallyQualifiedName $tableName, callable $filter): bool { - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); + $formattedName = $tableName->getUnqualifiedName()->getValue(); + $qualifier = $tableName->getQualifier(); - $table = $table->getQuotedName($this->_platform); + if ($qualifier !== null) { + $formattedName = $qualifier->getValue() . '.' . $formattedName; } - $this->_conn->executeStatement($this->_platform->getDropConstraintSQL( - $constraint->getQuotedName($this->_platform), - $table, - )); + return $filter($formattedName); } /** - * Drops a foreign key from a table. + * Introspects the table with the given name and returns its definition. If the name is unqualified, and the + * underlying database platform supports schemas, the current schema is used. * - * @param ForeignKeyConstraint|string $foreignKey The name of the foreign key. - * @param Table|string $table The name of the table with the foreign key. - * - * @return void + * This method is currently private due to the conflict with {@see introspectTable()}. Use + * {@see introspectTableByUnquotedName()} or {@see introspectTableByQuotedName()} instead. * * @throws Exception */ - public function dropForeignKey($foreignKey, $table) + private function introspectTable0(OptionallyQualifiedName $tableName): Table { - if ($foreignKey instanceof ForeignKeyConstraint) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $foreignKey as a ForeignKeyConstraint object to %s is deprecated.' - . ' Pass it as a quoted name instead.', - __METHOD__, - ); + $columns = $this->introspectTableColumns($tableName); - $foreignKey = $foreignKey->getQuotedName($this->_platform); + if ($columns === []) { + throw TableDoesNotExist::new($tableName->toString()); } - if ($table instanceof Table) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4798', - 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', - __METHOD__, - ); - - $table = $table->getQuotedName($this->_platform); - } + $options = $this->introspectTableOptions($tableName); + assert($options !== null); - $this->_conn->executeStatement( - $this->_platform->getDropForeignKeySQL($foreignKey, $table), - ); + return Table::editor() + ->setName($tableName) + ->setColumns(...$columns) + ->setPrimaryKeyConstraint($this->introspectTablePrimaryKeyConstraint($tableName)) + ->setIndexes(...$this->introspectTableIndexes($tableName)) + ->setForeignKeyConstraints(...$this->introspectTableForeignKeyConstraints($tableName)) + ->setOptions($options) + ->create(); } /** - * Drops a sequence with a given name. - * - * @param string $name The name of the sequence to drop. + * Introspects the table with the given unquoted name and schema name and returns its definition. If the schema name + * is omitted, and the underlying database platform supports schemas, the current schema is used. * - * @return void + * @param non-empty-string $tableName + * @param ?non-empty-string $schemaName * * @throws Exception */ - public function dropSequence($name) + public function introspectTableByUnquotedName(string $tableName, ?string $schemaName = null): Table { - $this->_conn->executeStatement( - $this->_platform->getDropSequenceSQL($name), + return $this->introspectTable0( + OptionallyQualifiedName::unquoted($tableName, $schemaName), ); } /** - * Drops the unique constraint from the given table. + * Introspects the table with the given quoted name and schema name and returns its definition. If the schema name + * is omitted, and the underlying database platform supports schemas, the current schema is used. + * + * @param non-empty-string $tableName + * @param ?non-empty-string $schemaName * * @throws Exception */ - public function dropUniqueConstraint(string $name, string $tableName): void + public function introspectTableByQuotedName(string $tableName, ?string $schemaName = null): Table { - $this->_conn->executeStatement( - $this->_platform->getDropUniqueConstraintSQL($name, $tableName), + return $this->introspectTable0( + OptionallyQualifiedName::quoted($tableName, $schemaName), ); } /** - * Drops a view. + * Introspects the columns of a given table and returns their definitions. If the name is unqualified, and the + * underlying database platform supports schemas, the current schema is used. * - * @param string $name The name of the view. + * Returns an empty value if the table does not exist. * - * @return void + * @return list * * @throws Exception */ - public function dropView($name) + public function introspectTableColumns(OptionallyQualifiedName $tableName): array { - $this->_conn->executeStatement( - $this->_platform->getDropViewSQL($name), + return $this->introspectTableObjects( + $tableName, + static function (SchemaProvider $schemaProvider, ?string $schemaName, string $tableName): array { + return $schemaProvider->getColumnsForTable($schemaName, $tableName); + }, ); } - /* create*() Methods */ - - /** @throws Exception */ - public function createSchemaObjects(Schema $schema): void - { - $this->_execSql($schema->toSql($this->_platform)); - } - /** - * Creates a new database. + * Introspects the columns of the table with the given unquoted name and schema name and returns their definitions. + * If the schema name is omitted and the underlying database platform supports schemas, the current schema is used. + * + * Returns an empty value if the table does not exist. * - * @param string $database The name of the database to create. + * @param non-empty-string $tableName + * @param ?non-empty-string $schemaName * - * @return void + * @return list * * @throws Exception */ - public function createDatabase($database) + public function introspectTableColumnsByUnquotedName(string $tableName, ?string $schemaName = null): array { - $this->_conn->executeStatement( - $this->_platform->getCreateDatabaseSQL($database), + return $this->introspectTableColumns( + OptionallyQualifiedName::unquoted($tableName, $schemaName), ); } /** - * Creates a new table. + * Introspects the columns of the table with the given quoted name and schema name and returns their definitions. If + * the schema name is omitted and the underlying database platform supports schemas, the current schema is used. * - * @return void + * Returns an empty value if the table does not exist. + * + * @param non-empty-string $tableName + * @param ?non-empty-string $schemaName + * + * @return list * * @throws Exception */ - public function createTable(Table $table) + public function introspectTableColumnsByQuotedName(string $tableName, ?string $schemaName = null): array { - $createFlags = AbstractPlatform::CREATE_INDEXES | AbstractPlatform::CREATE_FOREIGNKEYS; - $this->_execSql($this->_platform->getCreateTableSQL($table, $createFlags)); + return $this->introspectTableColumns( + OptionallyQualifiedName::quoted($tableName, $schemaName), + ); } /** - * Creates a new sequence. + * Introspects the indexes of a given table and returns their definitions. If the name is unqualified, and the + * underlying database platform supports schemas, the current schema is used. * - * @param Sequence $sequence + * Returns an empty value if the table does not exist. * - * @return void + * @return list * * @throws Exception */ - public function createSequence($sequence) + public function introspectTableIndexes(OptionallyQualifiedName $tableName): array { - $this->_conn->executeStatement( - $this->_platform->getCreateSequenceSQL($sequence), + return $this->introspectTableObjects( + $tableName, + static function (SchemaProvider $schemaProvider, ?string $schemaName, string $tableName): array { + return $schemaProvider->getIndexesForTable($schemaName, $tableName); + }, ); } /** - * Creates a constraint on a table. + * Introspects the indexes of the table with the given unquoted name and schema name and returns their definitions. + * If the schema name is omitted and the underlying database platform supports schemas, the current schema is used. * - * @deprecated Use {@see createIndex()}, {@see createForeignKey()} or {@see createUniqueConstraint()} instead. + * Returns an empty value if the table does not exist. * - * @param Table|string $table + * @param non-empty-string $tableName + * @param ?non-empty-string $schemaName * - * @return void + * @return list * * @throws Exception */ - public function createConstraint(Constraint $constraint, $table) + public function introspectTableIndexesByUnquotedName(string $tableName, ?string $schemaName = null): array { - $this->_conn->executeStatement( - $this->_platform->getCreateConstraintSQL($constraint, $table), + return $this->introspectTableIndexes( + OptionallyQualifiedName::unquoted($tableName, $schemaName), ); } /** - * Creates a new index on a table. + * Introspects the indexes of the table with the given quoted name and schema name and returns their definitions. If + * the schema name is omitted and the underlying database platform supports schemas, the current schema is used. + * + * Returns an empty value if the table does not exist. * - * @param Table|string $table The name of the table on which the index is to be created. + * @param non-empty-string $tableName + * @param ?non-empty-string $schemaName * - * @return void + * @return list * * @throws Exception */ - public function createIndex(Index $index, $table) + public function introspectTableIndexesByQuotedName(string $tableName, ?string $schemaName = null): array { - $this->_conn->executeStatement( - $this->_platform->getCreateIndexSQL($index, $table), + return $this->introspectTableIndexes( + OptionallyQualifiedName::quoted($tableName, $schemaName), ); } /** - * Creates a new foreign key. - * - * @param ForeignKeyConstraint $foreignKey The ForeignKey instance. - * @param Table|string $table The name of the table on which the foreign key is to be created. + * Introspects the primary key constraint of a given table and returns its definition. If the name is unqualified, + * and the underlying database platform supports schemas, the current schema is used. * - * @return void + * Returns null if the table does not exist or does not have a primary key constraint. * * @throws Exception */ - public function createForeignKey(ForeignKeyConstraint $foreignKey, $table) + public function introspectTablePrimaryKeyConstraint(OptionallyQualifiedName $tableName): ?PrimaryKeyConstraint { - $this->_conn->executeStatement( - $this->_platform->getCreateForeignKeySQL($foreignKey, $table), + return $this->introspectTableObjects( + $tableName, + static function ( + SchemaProvider $schemaProvider, + ?string $schemaName, + string $tableName, + ): ?PrimaryKeyConstraint { + return $schemaProvider->getPrimaryKeyConstraintForTable($schemaName, $tableName); + }, ); } /** - * Creates a unique constraint on a table. + * Introspects the foreign key constraints of a given table and returns their definitions. If the name is + * unqualified, and the underlying database platform supports schemas, the current schema is used. + * + * Returns an empty value if the table does not exist. + * + * @return list * * @throws Exception */ - public function createUniqueConstraint(UniqueConstraint $uniqueConstraint, string $tableName): void + public function introspectTableForeignKeyConstraints(OptionallyQualifiedName $tableName): array { - $this->_conn->executeStatement( - $this->_platform->getCreateUniqueConstraintSQL($uniqueConstraint, $tableName), + return $this->introspectTableObjects( + $tableName, + static function (SchemaProvider $schemaProvider, ?string $schemaName, string $tableName): array { + return $schemaProvider->getForeignKeyConstraintsForTable($schemaName, $tableName); + }, ); } /** - * Creates a new view. + * Introspects the foreign key constraints of the table with the given unquoted name and schema name and returns + * their definitions. If the name is unqualified, and the underlying database platform supports schemas, the current + * schema is used. + * + * Returns an empty value if the table does not exist. * - * @return void + * @param non-empty-string $tableName + * @param ?non-empty-string $schemaName + * + * @return list * * @throws Exception */ - public function createView(View $view) - { - $this->_conn->executeStatement( - $this->_platform->getCreateViewSQL( - $view->getQuotedName($this->_platform), - $view->getSql(), - ), + public function introspectTableForeignKeyConstraintsByUnquotedName( + string $tableName, + ?string $schemaName = null, + ): array { + return $this->introspectTableForeignKeyConstraints( + OptionallyQualifiedName::unquoted($tableName, $schemaName), ); } - /* dropAndCreate*() Methods */ - - /** @throws Exception */ - public function dropSchemaObjects(Schema $schema): void - { - $this->_execSql($schema->toDropSql($this->_platform)); - } - /** - * Drops and creates a constraint. - * - * @deprecated Use {@see dropIndex()} and {@see createIndex()}, - * {@see dropForeignKey()} and {@see createForeignKey()} - * or {@see dropUniqueConstraint()} and {@see createUniqueConstraint()} instead. + * Introspects the foreign key constraints of the table with the given quoted name and schema name and returns their + * definitions. If the name is unqualified, and the underlying database platform supports schemas, the current + * schema is used. * - * @see dropConstraint() - * @see createConstraint() + * Returns an empty value if the table does not exist. * - * @param Table|string $table + * @param non-empty-string $tableName + * @param ?non-empty-string $schemaName * - * @return void + * @return list * * @throws Exception */ - public function dropAndCreateConstraint(Constraint $constraint, $table) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4897', - 'AbstractSchemaManager::dropAndCreateConstraint() is deprecated.' - . ' Use AbstractSchemaManager::dropIndex() and AbstractSchemaManager::createIndex(),' - . ' AbstractSchemaManager::dropForeignKey() and AbstractSchemaManager::createForeignKey()' - . ' or AbstractSchemaManager::dropUniqueConstraint()' - . ' and AbstractSchemaManager::createUniqueConstraint() instead.', + public function introspectTableForeignKeyConstraintsByQuotedName( + string $tableName, + ?string $schemaName = null, + ): array { + return $this->introspectTableForeignKeyConstraints( + OptionallyQualifiedName::quoted($tableName, $schemaName), ); - - $this->tryMethod('dropConstraint', $constraint, $table); - $this->createConstraint($constraint, $table); } /** - * Drops and creates a new index on a table. + * @param callable(SchemaProvider, ?non-empty-string, non-empty-string): R $function * - * @deprecated Use {@see dropIndex()} and {@see createIndex()} instead. + * @return R * - * @param Table|string $table The name of the table on which the index is to be created. + * @throws Exception * - * @return void + * @template R + */ + private function introspectTableObjects(OptionallyQualifiedName $tableName, callable $function) + { + $folding = $this->platform->getUnquotedIdentifierFolding(); + $qualifier = $tableName->getQualifier(); + + if ($qualifier !== null) { + $schemaName = $qualifier->toNormalizedValue($folding); + } else { + $schemaName = $this->getCurrentSchemaName(); + } + + return $function( + $this->createSchemaProvider(), + $schemaName, + $tableName->getUnqualifiedName()->toNormalizedValue($folding), + ); + } + + /** + * @return ?array * * @throws Exception */ - public function dropAndCreateIndex(Index $index, $table) + private function introspectTableOptions(OptionallyQualifiedName $tableName): ?array { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4897', - 'AbstractSchemaManager::dropAndCreateIndex() is deprecated.' - . ' Use AbstractSchemaManager::dropIndex() and AbstractSchemaManager::createIndex() instead.', - ); + $folding = $this->platform->getUnquotedIdentifierFolding(); + $qualifier = $tableName->getQualifier(); - $this->tryMethod('dropIndex', $index->getQuotedName($this->_platform), $table); - $this->createIndex($index, $table); + if ($qualifier !== null) { + $schemaName = $qualifier->toNormalizedValue($folding); + } else { + $schemaName = $this->getCurrentSchemaName(); + } + + return $this->createSchemaProvider()->getOptionsForTable( + $schemaName, + $tableName->getUnqualifiedName()->toNormalizedValue($folding), + ); } /** - * Drops and creates a new foreign key. + * Introspects the views in the current database and returns their definitions. * - * @deprecated Use {@see dropForeignKey()} and {@see createForeignKey()} instead. + * @return list * - * @param ForeignKeyConstraint $foreignKey An associative array that defines properties - * of the foreign key to be created. - * @param Table|string $table The name of the table on which the foreign key is to be created. + * @throws Exception + */ + public function introspectViews(): array + { + return $this->createSchemaProvider()->getAllViews(); + } + + /** + * Introspects the sequences in the current database and returns their definitions. * - * @return void + * @return list * * @throws Exception */ - public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table) + public function introspectSequences(): array { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4897', - 'AbstractSchemaManager::dropAndCreateForeignKey() is deprecated.' - . ' Use AbstractSchemaManager::dropForeignKey() and AbstractSchemaManager::createForeignKey() instead.', + return $this->filterAssetNames( + $this->createSchemaProvider()->getAllSequences(), ); + } - $this->tryMethod('dropForeignKey', $foreignKey, $table); - $this->createForeignKey($foreignKey, $table); + /** @throws Exception */ + private function createSchemaProvider(): IntrospectingSchemaProvider + { + return new IntrospectingSchemaProvider( + $this->platform->createMetadataProvider($this->connection), + $this->getCurrentSchemaName(), + $this->createSchemaConfig()->toTableConfiguration(), + ); } + /* drop*() Methods */ + /** - * Drops and create a new sequence. - * - * @deprecated Use {@see dropSequence()} and {@see createSequence()} instead. + * Drops a database. * - * @return void + * NOTE: You can not drop the database this SchemaManager is currently connected to. * * @throws Exception */ - public function dropAndCreateSequence(Sequence $sequence) + public function dropDatabase(string $database): void { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4897', - 'AbstractSchemaManager::dropAndCreateSequence() is deprecated.' - . ' Use AbstractSchemaManager::dropSequence() and AbstractSchemaManager::createSequence() instead.', + $this->connection->executeStatement( + $this->platform->getDropDatabaseSQL($database), ); - - $this->tryMethod('dropSequence', $sequence->getQuotedName($this->_platform)); - $this->createSequence($sequence); } /** - * Drops and creates a new table. - * - * @deprecated Use {@see dropTable()} and {@see createTable()} instead. - * - * @return void + * Drops a schema. * * @throws Exception */ - public function dropAndCreateTable(Table $table) + public function dropSchema(string $schemaName): void { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4897', - 'AbstractSchemaManager::dropAndCreateTable() is deprecated.' - . ' Use AbstractSchemaManager::dropTable() and AbstractSchemaManager::createTable() instead.', + $this->connection->executeStatement( + $this->platform->getDropSchemaSQL($schemaName), ); - - $this->tryMethod('dropTable', $table->getQuotedName($this->_platform)); - $this->createTable($table); } /** - * Drops and creates a new database. - * - * @deprecated Use {@see dropDatabase()} and {@see createDatabase()} instead. - * - * @param string $database The name of the database to create. - * - * @return void + * Drops the given table. * * @throws Exception */ - public function dropAndCreateDatabase($database) + public function dropTable(string $name): void { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4897', - 'AbstractSchemaManager::dropAndCreateDatabase() is deprecated.' - . ' Use AbstractSchemaManager::dropDatabase() and AbstractSchemaManager::createDatabase() instead.', + $this->connection->executeStatement( + $this->platform->getDropTableSQL($name), ); - - $this->tryMethod('dropDatabase', $database); - $this->createDatabase($database); } /** - * Drops and creates a new view. - * - * @deprecated Use {@see dropView()} and {@see createView()} instead. - * - * @return void + * Drops the index from the given table. * * @throws Exception */ - public function dropAndCreateView(View $view) + public function dropIndex(string $index, string $table): void { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4897', - 'AbstractSchemaManager::dropAndCreateView() is deprecated.' - . ' Use AbstractSchemaManager::dropView() and AbstractSchemaManager::createView() instead.', + $this->connection->executeStatement( + $this->platform->getDropIndexSQL($index, $table), ); + } - $this->tryMethod('dropView', $view->getQuotedName($this->_platform)); - $this->createView($view); + /** + * Drops a foreign key from a table. + * + * @throws Exception + */ + public function dropForeignKey(string $name, string $table): void + { + $this->connection->executeStatement( + $this->platform->getDropForeignKeySQL($name, $table), + ); } /** - * Alters an existing schema. + * Drops a sequence with a given name. * * @throws Exception */ - public function alterSchema(SchemaDiff $schemaDiff): void + public function dropSequence(string $name): void { - $this->_execSql($this->_platform->getAlterSchemaSQL($schemaDiff)); + $this->connection->executeStatement( + $this->platform->getDropSequenceSQL($name), + ); } /** - * Migrates an existing schema to a new schema. + * Drops the unique constraint from the given table. * * @throws Exception */ - public function migrateSchema(Schema $toSchema): void + public function dropUniqueConstraint(string $name, string $tableName): void { - $schemaDiff = $this->createComparator() - ->compareSchemas($this->introspectSchema(), $toSchema); + $this->connection->executeStatement( + $this->platform->getDropUniqueConstraintSQL($name, $tableName), + ); + } - $this->alterSchema($schemaDiff); + /** + * Drops a view. + * + * @throws Exception + */ + public function dropView(string $name): void + { + $this->connection->executeStatement( + $this->platform->getDropViewSQL($name), + ); } - /* alterTable() Methods */ + /* create*() Methods */ + + /** @throws Exception */ + public function createSchemaObjects(Schema $schema): void + { + $this->executeStatements($schema->toSql($this->platform)); + } /** - * Alters an existing tables schema. - * - * @return void + * Creates a new database. * * @throws Exception */ - public function alterTable(TableDiff $tableDiff) + public function createDatabase(string $database): void { - $this->_execSql($this->_platform->getAlterTableSQL($tableDiff)); + $this->connection->executeStatement( + $this->platform->getCreateDatabaseSQL($database), + ); } /** - * Renames a given table to another name. - * - * @param string $name The current name of the table. - * @param string $newName The new name of the table. - * - * @return void + * Creates a new table. * * @throws Exception */ - public function renameTable($name, $newName) + public function createTable(Table $table): void { - $this->_execSql($this->_platform->getRenameTableSQL($name, $newName)); + $this->executeStatements($this->platform->getCreateTableSQL($table)); } /** - * Methods for filtering return values of list*() methods to convert - * the native DBMS data definition to a portable Doctrine definition + * Creates a new sequence. + * + * @throws Exception */ + public function createSequence(Sequence $sequence): void + { + $this->connection->executeStatement( + $this->platform->getCreateSequenceSQL($sequence), + ); + } /** - * @param mixed[] $databases + * Creates a new index on a table. * - * @return string[] + * @param string $table The name of the table on which the index is to be created. + * + * @throws Exception */ - protected function _getPortableDatabasesList($databases) + public function createIndex(Index $index, string $table): void { - $list = []; - foreach ($databases as $value) { - $list[] = $this->_getPortableDatabaseDefinition($value); - } - - return $list; + $this->connection->executeStatement( + $this->platform->getCreateIndexSQL($index, $table), + ); } /** - * Converts a list of namespace names from the native DBMS data definition to a portable Doctrine definition. - * - * @deprecated Use {@see listSchemaNames()} instead. + * Creates a new foreign key. * - * @param array> $namespaces The list of namespace names - * in the native DBMS data definition. + * @param ForeignKeyConstraint $foreignKey The ForeignKey instance. + * @param string $table The name of the table on which the foreign key is to be created. * - * @return string[] + * @throws Exception */ - protected function getPortableNamespacesList(array $namespaces) + public function createForeignKey(ForeignKeyConstraint $foreignKey, string $table): void { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4503', - 'AbstractSchemaManager::getPortableNamespacesList() is deprecated,' - . ' use AbstractSchemaManager::listSchemaNames() instead.', + $this->connection->executeStatement( + $this->platform->getCreateForeignKeySQL($foreignKey, $table), ); - - $namespacesList = []; - - foreach ($namespaces as $namespace) { - $namespacesList[] = $this->getPortableNamespaceDefinition($namespace); - } - - return $namespacesList; } /** - * @param mixed $database + * Creates a unique constraint on a table. * - * @return mixed + * @throws Exception */ - protected function _getPortableDatabaseDefinition($database) + public function createUniqueConstraint(UniqueConstraint $uniqueConstraint, string $tableName): void { - return $database; + $this->connection->executeStatement( + $this->platform->getCreateUniqueConstraintSQL($uniqueConstraint, $tableName), + ); } /** - * Converts a namespace definition from the native DBMS data definition to a portable Doctrine definition. - * - * @deprecated Use {@see listSchemaNames()} instead. - * - * @param array $namespace The native DBMS namespace definition. + * Creates a new view. * - * @return mixed + * @throws Exception */ - protected function getPortableNamespaceDefinition(array $namespace) + public function createView(View $view): void { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4503', - 'AbstractSchemaManager::getPortableNamespaceDefinition() is deprecated,' - . ' use AbstractSchemaManager::listSchemaNames() instead.', + $this->connection->executeStatement( + $this->platform->getCreateViewSQL( + $view->getQuotedName($this->platform), + $view->getSql(), + ), ); + } - return $namespace; + /** @throws Exception */ + public function dropSchemaObjects(Schema $schema): void + { + $this->executeStatements($schema->toDropSql($this->platform)); } /** - * @param mixed[][] $sequences - * - * @return Sequence[] + * Alters an existing schema. * * @throws Exception */ - protected function _getPortableSequencesList($sequences) + public function alterSchema(SchemaDiff $schemaDiff): void { - $list = []; + $this->executeStatements($this->platform->getAlterSchemaSQL($schemaDiff)); + } - foreach ($sequences as $value) { - $list[] = $this->_getPortableSequenceDefinition($value); - } + /** + * Migrates an existing schema to a new schema. + * + * @throws Exception + */ + public function migrateSchema(Schema $newSchema): void + { + $schemaDiff = $this->createComparator() + ->compareSchemas($this->introspectSchema(), $newSchema); - return $list; + $this->alterSchema($schemaDiff); } + /* alterTable() Methods */ + /** - * @param mixed[] $sequence + * Alters an existing tables schema. * - * @return Sequence + * @throws Exception + */ + public function alterTable(TableDiff $tableDiff): void + { + $this->executeStatements($this->platform->getAlterTableSQL($tableDiff)); + } + + /** + * Renames a given table to another name. * * @throws Exception */ - protected function _getPortableSequenceDefinition($sequence) + public function renameTable(string $name, string $newName): void + { + $this->connection->executeStatement( + $this->platform->getRenameTableSQL($name, $newName), + ); + } + + /** + * Methods for filtering return values of list*() methods to convert + * the native DBMS data definition to a portable Doctrine definition + */ + + /** @param array $database */ + protected function _getPortableDatabaseDefinition(array $database): string + { + throw NotSupported::new(__METHOD__); + } + + /** @param array $sequence */ + protected function _getPortableSequenceDefinition(array $sequence): Sequence { - throw Exception::notSupported('Sequences'); + throw NotSupported::new(__METHOD__); } /** @@ -1372,47 +1268,19 @@ protected function _getPortableSequenceDefinition($sequence) * * The name of the created column instance however is kept in its case. * - * @param string $table The name of the table. - * @param string $database - * @param mixed[][] $tableColumns + * @param array> $rows * - * @return Column[] + * @return array * - * @throws Exception + * @throws TypesException */ - protected function _getPortableTableColumnList($table, $database, $tableColumns) + protected function _getPortableTableColumnList(string $table, string $database, array $rows): array { - $eventManager = $this->_platform->getEventManager(); - $list = []; - foreach ($tableColumns as $tableColumn) { - $column = null; - $defaultPrevented = false; - - if ($eventManager !== null && $eventManager->hasListeners(Events::onSchemaColumnDefinition)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated. Use a custom schema manager instead.', - Events::onSchemaColumnDefinition, - ); - - $eventArgs = new SchemaColumnDefinitionEventArgs($tableColumn, $table, $database, $this->_conn); - $eventManager->dispatchEvent(Events::onSchemaColumnDefinition, $eventArgs); - - $defaultPrevented = $eventArgs->isDefaultPrevented(); - $column = $eventArgs->getColumn(); - } - - if (! $defaultPrevented) { - $column = $this->_getPortableTableColumnDefinition($tableColumn); - } - - if ($column === null) { - continue; - } + foreach ($rows as $row) { + $column = $this->_getPortableTableColumnDefinition($row); - $name = strtolower($column->getQuotedName($this->_platform)); + $name = strtolower($column->getQuotedName($this->platform)); $list[$name] = $column; } @@ -1422,30 +1290,25 @@ protected function _getPortableTableColumnList($table, $database, $tableColumns) /** * Gets Table Column Definition. * - * @param mixed[] $tableColumn + * @param array $tableColumn * - * @return Column - * - * @throws Exception + * @throws TypesException */ - abstract protected function _getPortableTableColumnDefinition($tableColumn); + abstract protected function _getPortableTableColumnDefinition(array $tableColumn): Column; /** * Aggregates and groups the index results according to the required data result. * - * @param mixed[][] $tableIndexes - * @param string|null $tableName - * - * @return Index[] + * @param array> $rows * - * @throws Exception + * @return array */ - protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) + protected function _getPortableTableIndexesList(array $rows, string $tableName): array { $result = []; - foreach ($tableIndexes as $tableIndex) { - $indexName = $keyName = $tableIndex['key_name']; - if ($tableIndex['primary']) { + foreach ($rows as $row) { + $indexName = $keyName = $row['key_name']; + if ($row['primary']) { $keyName = 'primary'; } @@ -1456,195 +1319,98 @@ protected function _getPortableTableIndexesList($tableIndexes, $tableName = null 'lengths' => [], ]; - if (isset($tableIndex['where'])) { - $options['where'] = $tableIndex['where']; + if (isset($row['where'])) { + $options['where'] = $row['where']; } $result[$keyName] = [ 'name' => $indexName, 'columns' => [], - 'unique' => ! $tableIndex['non_unique'], - 'primary' => $tableIndex['primary'], - 'flags' => $tableIndex['flags'] ?? [], + 'unique' => ! $row['non_unique'], + 'primary' => $row['primary'], + 'flags' => $row['flags'] ?? [], 'options' => $options, ]; } - $result[$keyName]['columns'][] = $tableIndex['column_name']; - $result[$keyName]['options']['lengths'][] = $tableIndex['length'] ?? null; + $result[$keyName]['columns'][] = $row['column_name']; + $result[$keyName]['options']['lengths'][] = $row['length'] ?? null; } - $eventManager = $this->_platform->getEventManager(); - $indexes = []; foreach ($result as $indexKey => $data) { - $index = null; - $defaultPrevented = false; - - if ($eventManager !== null && $eventManager->hasListeners(Events::onSchemaIndexDefinition)) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/5784', - 'Subscribing to %s events is deprecated. Use a custom schema manager instead.', - Events::onSchemaColumnDefinition, - ); - - $eventArgs = new SchemaIndexDefinitionEventArgs($data, $tableName, $this->_conn); - $eventManager->dispatchEvent(Events::onSchemaIndexDefinition, $eventArgs); - - $defaultPrevented = $eventArgs->isDefaultPrevented(); - $index = $eventArgs->getIndex(); - } - - if (! $defaultPrevented) { - $index = new Index( - $data['name'], - $data['columns'], - $data['unique'], - $data['primary'], - $data['flags'], - $data['options'], - ); - } - - if ($index === null) { - continue; - } - - $indexes[$indexKey] = $index; + $indexes[$indexKey] = new Index( + $data['name'], + $data['columns'], + $data['unique'], + $data['primary'], + $data['flags'], + $data['options'], + ); } return $indexes; } /** - * @param mixed[][] $tables + * @deprecated Use the schema name and the unqualified table name separately instead. * - * @return string[] - */ - protected function _getPortableTablesList($tables) - { - $list = []; - foreach ($tables as $value) { - $list[] = $this->_getPortableTableDefinition($value); - } - - return $list; - } - - /** - * @param mixed $table - * - * @return string - */ - protected function _getPortableTableDefinition($table) - { - return $table; - } - - /** - * @param mixed[][] $views + * @param array $table * - * @return View[] + * @return non-empty-string */ - protected function _getPortableViewsList($views) - { - $list = []; - foreach ($views as $value) { - $view = $this->_getPortableViewDefinition($value); - - if ($view === false) { - continue; - } - - $viewName = strtolower($view->getQuotedName($this->_platform)); - $list[$viewName] = $view; - } + abstract protected function _getPortableTableDefinition(array $table): string; - return $list; - } + /** @param array $view */ + abstract protected function _getPortableViewDefinition(array $view): View; /** - * @param mixed[] $view + * @param array> $rows * - * @return View|false + * @return array */ - protected function _getPortableViewDefinition($view) - { - return false; - } - - /** - * @param mixed[][] $tableForeignKeys - * - * @return ForeignKeyConstraint[] - */ - protected function _getPortableTableForeignKeysList($tableForeignKeys) + protected function _getPortableTableForeignKeysList(array $rows): array { $list = []; - foreach ($tableForeignKeys as $value) { + foreach ($rows as $value) { $list[] = $this->_getPortableTableForeignKeyDefinition($value); } return $list; } - /** - * @param mixed $tableForeignKey - * - * @return ForeignKeyConstraint - * - * @abstract - */ - protected function _getPortableTableForeignKeyDefinition($tableForeignKey) - { - return $tableForeignKey; - } + /** @param array $tableForeignKey */ + abstract protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint; /** - * @internal - * - * @param string[]|string $sql - * - * @return void + * @param array $sql * * @throws Exception */ - protected function _execSql($sql) + private function executeStatements(array $sql): void { - foreach ((array) $sql as $query) { - $this->_conn->executeStatement($query); + foreach ($sql as $query) { + $this->connection->executeStatement($query); } } /** - * Creates a schema instance for the current database. - * - * @deprecated Use {@link introspectSchema()} instead. - * - * @return Schema + * Returns a {@see Schema} instance representing the current database schema. * * @throws Exception */ - public function createSchema() + public function introspectSchema(): Schema { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5613', - '%s is deprecated. Use introspectSchema() instead.', - __METHOD__, - ); - $schemaNames = []; - if ($this->_platform->supportsSchemas()) { - $schemaNames = $this->listNamespaceNames(); + if ($this->platform->supportsSchemas()) { + $schemaNames = $this->listSchemaNames(); } $sequences = []; - if ($this->_platform->supportsSequences()) { + if ($this->platform->supportsSequences()) { $sequences = $this->listSequences(); } @@ -1653,34 +1419,18 @@ public function createSchema() return new Schema($tables, $sequences, $this->createSchemaConfig(), $schemaNames); } - /** - * Returns a {@see Schema} instance representing the current database schema. - * - * @throws Exception - */ - public function introspectSchema(): Schema - { - return $this->createSchema(); - } - /** * Creates the configuration for this schema. * - * @return SchemaConfig - * * @throws Exception */ - public function createSchemaConfig() + public function createSchemaConfig(): SchemaConfig { $schemaConfig = new SchemaConfig(); - $schemaConfig->setMaxIdentifierLength($this->_platform->getMaxIdentifierLength()); - - $searchPaths = $this->getSchemaSearchPaths(); - if (isset($searchPaths[0])) { - $schemaConfig->setName($searchPaths[0]); - } + $schemaConfig->setMaxIdentifierLength($this->platform->getMaxIdentifierLength()); + $schemaConfig->setName($this->getCurrentSchemaName()); - $params = $this->_conn->getParams(); + $params = $this->connection->getParams(); if (! isset($params['defaultTableOptions'])) { $params['defaultTableOptions'] = []; } @@ -1695,87 +1445,13 @@ public function createSchemaConfig() } /** - * The search path for namespaces in the currently connected database. - * - * The first entry is usually the default namespace in the Schema. All - * further namespaces contain tables/sequences which can also be addressed - * with a short, not full-qualified name. - * - * For databases that don't support subschema/namespaces this method - * returns the name of the currently connected database. - * - * @deprecated - * - * @return string[] + * @return non-empty-string * * @throws Exception */ - public function getSchemaSearchPaths() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4821', - 'AbstractSchemaManager::getSchemaSearchPaths() is deprecated.', - ); - - $database = $this->_conn->getDatabase(); - - if ($database !== null) { - return [$database]; - } - - return []; - } - - /** - * Given a table comment this method tries to extract a typehint for Doctrine Type, or returns - * the type given as default. - * - * @internal This method should be only used from within the AbstractSchemaManager class hierarchy. - * - * @param string|null $comment - * @param string $currentType - * - * @return string - */ - public function extractDoctrineTypeFromComment($comment, $currentType) - { - if ($this->_conn->getConfiguration()->getDisableTypeComments()) { - return $currentType; - } - - if ($comment !== null && preg_match('(\(DC2Type:(((?!\)).)+)\))', $comment, $match) === 1) { - return $match[1]; - } - - return $currentType; - } - - /** - * @internal This method should be only used from within the AbstractSchemaManager class hierarchy. - * - * @param string|null $comment - * @param string|null $type - * - * @return string|null - */ - public function removeDoctrineTypeFromComment($comment, $type) - { - if ($this->_conn->getConfiguration()->getDisableTypeComments()) { - return $comment; - } - - if ($comment === null) { - return null; - } - - return str_replace('(DC2Type:' . $type . ')', '', $comment); - } - - /** @throws Exception */ private function getDatabase(string $methodName): string { - $database = $this->_conn->getDatabase(); + $database = $this->connection->getDatabase(); if ($database === null) { throw DatabaseRequired::new($methodName); @@ -1784,25 +1460,28 @@ private function getDatabase(string $methodName): string return $database; } - public function createComparator(): Comparator + public function createComparator(/* ComparatorConfig $config = new ComparatorConfig() */): Comparator { - return new Comparator($this->_platform); + return new Comparator($this->platform, func_num_args() > 0 ? func_get_arg(0) : new ComparatorConfig()); } /** - * @return array>> + * Groups the rows representing database object elements by table they belong to. * - * @throws Exception + * @param list> $rows + * + * @return array>> */ - private function fetchAllAssociativeGrouped(Result $result): array + private function groupByTable(array $rows): array { $data = []; - foreach ($result->fetchAllAssociative() as $row) { + foreach ($rows as $row) { $tableName = $this->_getPortableTableDefinition($row); $data[$tableName][] = $row; } + /** @phpstan-ignore return.type */ return $data; } } diff --git a/doctrine/dbal/src/Schema/Collections/Exception.php b/doctrine/dbal/src/Schema/Collections/Exception.php new file mode 100644 index 000000000..31b5451c8 --- /dev/null +++ b/doctrine/dbal/src/Schema/Collections/Exception.php @@ -0,0 +1,12 @@ +objectName; + } + + public static function new(UnqualifiedName $objectName): self + { + return new self(sprintf('Object %s already exists.', $objectName->toString()), $objectName); + } +} diff --git a/doctrine/dbal/src/Schema/Collections/Exception/ObjectDoesNotExist.php b/doctrine/dbal/src/Schema/Collections/Exception/ObjectDoesNotExist.php new file mode 100644 index 000000000..37f6534f6 --- /dev/null +++ b/doctrine/dbal/src/Schema/Collections/Exception/ObjectDoesNotExist.php @@ -0,0 +1,30 @@ +objectName; + } + + public static function new(UnqualifiedName $objectName): self + { + return new self(sprintf('Object %s does not exist.', $objectName->toString()), $objectName); + } +} diff --git a/doctrine/dbal/src/Schema/Collections/ObjectSet.php b/doctrine/dbal/src/Schema/Collections/ObjectSet.php new file mode 100644 index 000000000..e78afa237 --- /dev/null +++ b/doctrine/dbal/src/Schema/Collections/ObjectSet.php @@ -0,0 +1,71 @@ + + */ +interface ObjectSet extends IteratorAggregate +{ + /** + * Checks if the set is empty. + */ + public function isEmpty(): bool; + + /** + * Returns the element with the given name. If no such element exists, null is returned. + * + * @phpstan-return E|null + */ + public function get(UnqualifiedName $elementName): ?object; + + /** + * Adds the given element to the set. + * + * @phpstan-param E $element + * + * @throws ObjectAlreadyExists If an element with the same name already exists. + */ + public function add(object $element): void; + + /** + * Removes the element with the given name from the set. + * + * @throws ObjectDoesNotExist If no element with the given name exists. + */ + public function remove(UnqualifiedName $elementName): void; + + /** + * Modifies the element with the given name using the provided callable. + * + * @param callable(E): E $modification + * + * @throws ObjectDoesNotExist If no element with the given name exists. + * @throws ObjectAlreadyExists If an element with the name after modification already exists. + */ + public function modify(UnqualifiedName $elementName, callable $modification): void; + + /** + * Clears the set, removing all elements. + */ + public function clear(): void; + + /** + * Returns the elements of the set represented as a list. + * + * @phpstan-return list + */ + public function toList(): array; +} diff --git a/doctrine/dbal/src/Schema/Collections/OptionallyUnqualifiedNamedObjectSet.php b/doctrine/dbal/src/Schema/Collections/OptionallyUnqualifiedNamedObjectSet.php new file mode 100644 index 000000000..8884456b6 --- /dev/null +++ b/doctrine/dbal/src/Schema/Collections/OptionallyUnqualifiedNamedObjectSet.php @@ -0,0 +1,169 @@ + + * @template-implements ObjectSet + */ +final class OptionallyUnqualifiedNamedObjectSet implements ObjectSet +{ + /** @var list */ + private array $elements = []; + + /** @var array */ + private array $elementPositionsByKey = []; + + /** @phpstan-param E ...$elements */ + public function __construct(OptionallyNamedObject ...$elements) + { + foreach ($elements as $element) { + $this->add($element); + } + } + + public function isEmpty(): bool + { + return count($this->elements) === 0; + } + + public function get(UnqualifiedName $elementName): ?OptionallyNamedObject + { + $key = $this->getKey($elementName); + + if (isset($this->elementPositionsByKey[$key])) { + return $this->elements[$this->elementPositionsByKey[$key]]; + } + + return null; + } + + public function add(object $element): void + { + $elementName = $element->getObjectName(); + + if ($elementName !== null) { + $key = $this->getKey($elementName); + + if (isset($this->elementPositionsByKey[$key])) { + throw ObjectAlreadyExists::new($elementName); + } + + $this->elementPositionsByKey[$key] = count($this->elements); + } + + $this->elements[] = $element; + } + + public function remove(UnqualifiedName $elementName): void + { + $key = $this->getKey($elementName); + + if (! isset($this->elementPositionsByKey[$key])) { + throw ObjectDoesNotExist::new($elementName); + } + + $this->removeByKey($key); + } + + public function modify(UnqualifiedName $elementName, callable $modification): void + { + $key = $this->getKey($elementName); + + if (! isset($this->elementPositionsByKey[$key])) { + throw ObjectDoesNotExist::new($elementName); + } + + $position = $this->elementPositionsByKey[$key]; + + $this->replace($key, $position, $modification($this->elements[$position])); + } + + public function clear(): void + { + $this->elements = $this->elementPositionsByKey = []; + } + + /** {@inheritDoc} */ + public function toList(): array + { + return $this->elements; + } + + /** {@inheritDoc} */ + public function getIterator(): Traversable + { + yield from $this->elements; + } + + /** + * Replaces the element corresponding to the old key with the provided element. + * + * @phpstan-param E $element + * + * @throws ObjectAlreadyExists If an element with the same name as the element name already exists. + */ + private function replace(string $oldKey, int $position, OptionallyNamedObject $element): void + { + $elementName = $element->getObjectName(); + + if ($elementName !== null) { + $newKey = $this->getKey($elementName); + + if ($newKey !== $oldKey) { + if (isset($this->elementPositionsByKey[$newKey])) { + throw ObjectAlreadyExists::new($elementName); + } + + unset($this->elementPositionsByKey[$oldKey]); + + $this->elementPositionsByKey[$newKey] = $position; + } + } else { + unset($this->elementPositionsByKey[$oldKey]); + } + + // @phpstan-ignore assign.propertyType + $this->elements[$position] = $element; + } + + private function removeByKey(string $key): void + { + $position = $this->elementPositionsByKey[$key]; + + array_splice($this->elements, $position, 1); + unset($this->elementPositionsByKey[$key]); + + foreach ($this->elementPositionsByKey as $elementKey => $elementPosition) { + if ($elementPosition > $position) { + $this->elementPositionsByKey[$elementKey]--; + } + } + } + + private function getKey(UnqualifiedName $name): string + { + return strtolower($name->getIdentifier()->getValue()); + } +} diff --git a/doctrine/dbal/src/Schema/Collections/UnqualifiedNamedObjectSet.php b/doctrine/dbal/src/Schema/Collections/UnqualifiedNamedObjectSet.php new file mode 100644 index 000000000..d37c420ef --- /dev/null +++ b/doctrine/dbal/src/Schema/Collections/UnqualifiedNamedObjectSet.php @@ -0,0 +1,148 @@ + + * @template-implements ObjectSet + */ +final class UnqualifiedNamedObjectSet implements ObjectSet +{ + /** @var array */ + private array $elements = []; + + /** @phpstan-param E ...$elements */ + public function __construct(NamedObject ...$elements) + { + foreach ($elements as $element) { + $this->add($element); + } + } + + public function isEmpty(): bool + { + return count($this->elements) === 0; + } + + public function get(UnqualifiedName $elementName): ?NamedObject + { + $key = $this->getKey($elementName); + + return $this->elements[$key] ?? null; + } + + public function add(object $element): void + { + $elementName = $element->getObjectName(); + $key = $this->getKey($elementName); + + if (isset($this->elements[$key])) { + throw ObjectAlreadyExists::new($elementName); + } + + $this->elements[$key] = $element; + } + + public function remove(UnqualifiedName $elementName): void + { + $key = $this->getKey($elementName); + + if (! isset($this->elements[$key])) { + throw ObjectDoesNotExist::new($elementName); + } + + unset($this->elements[$key]); + } + + public function modify(UnqualifiedName $elementName, callable $modification): void + { + $key = $this->getKey($elementName); + + if (! isset($this->elements[$key])) { + throw ObjectDoesNotExist::new($elementName); + } + + $this->replace($key, $modification($this->elements[$key])); + } + + public function clear(): void + { + $this->elements = []; + } + + /** {@inheritDoc} */ + public function toList(): array + { + return array_values($this->elements); + } + + /** {@inheritDoc} */ + public function getIterator(): Traversable + { + foreach ($this->elements as $element) { + yield $element; + } + } + + /** + * Replaces the element corresponding to the old key with the provided element. The position of the element in the + * set is preserved. + * + * @phpstan-param E $element + * + * @throws ObjectAlreadyExists If an element with the same name as the element name already exists. + */ + private function replace(string $oldKey, NamedObject $element): void + { + $elementName = $element->getObjectName(); + $newKey = $this->getKey($elementName); + + if ($newKey === $oldKey) { + $this->elements[$oldKey] = $element; + + return; + } + + if (isset($this->elements[$newKey])) { + throw ObjectAlreadyExists::new($elementName); + } + + $keys = array_keys($this->elements); + $values = array_values($this->elements); + + $position = array_search($oldKey, $keys, true); + assert($position !== false); + + $keys[$position] = $newKey; + $values[$position] = $element; + + $this->elements = array_combine($keys, $values); + } + + private function getKey(UnqualifiedName $name): string + { + return strtolower($name->getIdentifier()->getValue()); + } +} diff --git a/doctrine/dbal/src/Schema/Column.php b/doctrine/dbal/src/Schema/Column.php index e15801786..69035f7ba 100644 --- a/doctrine/dbal/src/Schema/Column.php +++ b/doctrine/dbal/src/Schema/Column.php @@ -1,86 +1,97 @@ + * @phpstan-type ColumnProperties = array{ + * name: string, + * type: Type, + * default: mixed, + * notnull?: bool, + * autoincrement: bool, + * columnDefinition: ?non-empty-string, + * comment: string, + * charset?: ?non-empty-string, + * collation?: ?non-empty-string, + * } + * @phpstan-type PlatformOptions = array{ + * charset?: ?non-empty-string, + * collation?: ?non-empty-string, + * default_constraint_name?: non-empty-string, + * enumType?: class-string, + * jsonb?: bool, + * version?: bool, + * } */ -class Column extends AbstractAsset +class Column extends AbstractNamedObject { - /** @var Type */ - protected $_type; + protected Type $_type; - /** @var int|null */ - protected $_length; + protected ?int $_length = null; - /** @var int */ - protected $_precision = 10; + protected ?int $_precision = null; - /** @var int */ - protected $_scale = 0; + protected int $_scale = 0; - /** @var bool */ - protected $_unsigned = false; + protected bool $_unsigned = false; - /** @var bool */ - protected $_fixed = false; + protected bool $_fixed = false; - /** @var bool */ - protected $_notnull = true; + protected bool $_notnull = true; - /** @var mixed */ - protected $_default; + protected mixed $_default = null; - /** @var bool */ - protected $_autoincrement = false; + protected bool $_autoincrement = false; - /** @var mixed[] */ - protected $_platformOptions = []; + /** @var list */ + protected array $_values = []; - /** @var string|null */ - protected $_columnDefinition; + /** @var PlatformOptions */ + protected array $_platformOptions = []; - /** @var string|null */ - protected $_comment; + /** @var ?non-empty-string */ + protected ?string $_columnDefinition = null; - /** - * @deprecated Use {@link $_platformOptions} instead - * - * @var mixed[] - */ - protected $_customSchemaOptions = []; + protected string $_comment = ''; /** - * Creates a new Column. + * @internal Use {@link Column::editor()} to instantiate an editor and {@link ColumnEditor::create()} to create a + * column. * - * @param string $name - * @param mixed[] $options - * - * @throws SchemaException + * @param array $options */ - public function __construct($name, Type $type, array $options = []) + public function __construct(string $name, Type $type, array $options = []) { - $this->_setName($name); + parent::__construct($name); + $this->setType($type); $this->setOptions($options); } - /** - * @param mixed[] $options - * - * @return Column - * - * @throws SchemaException - */ - public function setOptions(array $options) + protected function getNameParser(): UnqualifiedNameParser + { + return Parsers::getUnqualifiedNameParser(); + } + + /** @param array $options */ + public function setOptions(array $options): self { foreach ($options as $name => $value) { $method = 'set' . $name; @@ -95,372 +106,327 @@ public function setOptions(array $options) return $this; } - /** @return Column */ - public function setType(Type $type) + public function setType(Type $type): self { $this->_type = $type; return $this; } - /** - * @param int|null $length - * - * @return Column - */ - public function setLength($length) + public function setLength(?int $length): self { - if ($length !== null) { - $this->_length = (int) $length; - } else { - $this->_length = null; - } + $this->_length = $length; return $this; } - /** - * @param int $precision - * - * @return Column - */ - public function setPrecision($precision) + public function setPrecision(?int $precision): self { - if (! is_numeric($precision)) { - $precision = 10; // defaults to 10 when no valid precision is given. - } - - $this->_precision = (int) $precision; + $this->_precision = $precision; return $this; } - /** - * @param int $scale - * - * @return Column - */ - public function setScale($scale) + public function setScale(int $scale): self { - if (! is_numeric($scale)) { - $scale = 0; - } - - $this->_scale = (int) $scale; + $this->_scale = $scale; return $this; } - /** - * @param bool $unsigned - * - * @return Column - */ - public function setUnsigned($unsigned) + public function setUnsigned(bool $unsigned): self { - $this->_unsigned = (bool) $unsigned; + $this->_unsigned = $unsigned; return $this; } - /** - * @param bool $fixed - * - * @return Column - */ - public function setFixed($fixed) + public function setFixed(bool $fixed): self { - $this->_fixed = (bool) $fixed; + $this->_fixed = $fixed; return $this; } - /** - * @param bool $notnull - * - * @return Column - */ - public function setNotnull($notnull) + public function setNotnull(bool $notnull): self { - $this->_notnull = (bool) $notnull; + $this->_notnull = $notnull; return $this; } - /** - * @param mixed $default - * - * @return Column - */ - public function setDefault($default) + public function setDefault(mixed $default): self { $this->_default = $default; return $this; } - /** - * @param mixed[] $platformOptions - * - * @return Column - */ - public function setPlatformOptions(array $platformOptions) + /** @param PlatformOptions $platformOptions */ + public function setPlatformOptions(array $platformOptions): self { + if (isset($platformOptions['jsonb']) && $platformOptions['jsonb']) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6939', + 'The "jsonb" column platform option is deprecated. Use the "JSONB" type instead.', + ); + } + $this->_platformOptions = $platformOptions; return $this; } - /** - * @param string $name - * @param mixed $value - * - * @return Column - */ - public function setPlatformOption($name, $value) + /** @param key-of $name */ + public function setPlatformOption(string $name, mixed $value): self { + if ($name === 'jsonb' && (bool) $value === true) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6939', + 'The "jsonb" column platform option is deprecated. Use the "JSONB" type instead.', + ); + } + $this->_platformOptions[$name] = $value; return $this; } - /** - * @param string|null $value - * - * @return Column - */ - public function setColumnDefinition($value) + /** @param ?non-empty-string $value */ + public function setColumnDefinition(?string $value): self { $this->_columnDefinition = $value; return $this; } - /** @return Type */ - public function getType() + public function getType(): Type { return $this->_type; } - /** @return int|null */ - public function getLength() + public function getLength(): ?int { return $this->_length; } - /** @return int */ - public function getPrecision() + public function getPrecision(): ?int { return $this->_precision; } - /** @return int */ - public function getScale() + public function getScale(): int { return $this->_scale; } - /** @return bool */ - public function getUnsigned() + public function getUnsigned(): bool { return $this->_unsigned; } - /** @return bool */ - public function getFixed() + public function getFixed(): bool { return $this->_fixed; } - /** @return bool */ - public function getNotnull() + public function getNotnull(): bool { return $this->_notnull; } - /** @return mixed */ - public function getDefault() + public function getDefault(): mixed { return $this->_default; } - /** @return mixed[] */ - public function getPlatformOptions() - { - return $this->_platformOptions; - } - /** - * @param string $name + * Returns the name of the character set to use with the column. * - * @return bool + * @return ?non-empty-string */ - public function hasPlatformOption($name) + public function getCharset(): ?string { - return isset($this->_platformOptions[$name]); + return $this->_platformOptions['charset'] ?? null; } /** - * @param string $name + * Returns the name of the collation to use with the column. * - * @return mixed + * @return ?non-empty-string */ - public function getPlatformOption($name) + public function getCollation(): ?string { - return $this->_platformOptions[$name]; + return $this->_platformOptions['collation'] ?? null; } - /** @return string|null */ - public function getColumnDefinition() + /** + * Returns the minimum value to enforce on the column. + */ + public function getMinimumValue(): mixed { - return $this->_columnDefinition; + return $this->_platformOptions['min'] ?? null; } - /** @return bool */ - public function getAutoincrement() + /** + * Returns the maximum value to enforce on the column. + */ + public function getMaximumValue(): mixed { - return $this->_autoincrement; + return $this->_platformOptions['max'] ?? null; } /** - * @param bool $flag + * Returns the enum type used by the column. * - * @return Column + * @return ?class-string */ - public function setAutoincrement($flag) + public function getEnumType(): ?string { - $this->_autoincrement = $flag; - - return $this; + return $this->_platformOptions['enumType'] ?? null; } /** - * @param string|null $comment + * @internal Should be used only from within the {@see AbstractSchemaManager} class hierarchy. + * + * Returns the name of the DEFAULT constraint that implements the default value for the column on SQL Server. * - * @return Column + * @return ?non-empty-string */ - public function setComment($comment) - { - $this->_comment = $comment; - - return $this; - } - - /** @return string|null */ - public function getComment() + public function getDefaultConstraintName(): ?string { - return $this->_comment; + return $this->_platformOptions[SQLServerPlatform::OPTION_DEFAULT_CONSTRAINT_NAME] ?? null; } /** - * @deprecated Use {@link setPlatformOption()} instead - * - * @param string $name - * @param mixed $value + * @deprecated Use {@see getCharset()}, {@see getCollation()}, {@see getMinimumValue()}, {@see getMaximumValue()} + * or {@see getEnumType()} instead. * - * @return Column + * @return PlatformOptions */ - public function setCustomSchemaOption($name, $value) + public function getPlatformOptions(): array { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5476', - 'Column::setCustomSchemaOption() is deprecated. Use setPlatformOption() instead.', - ); - - $this->_customSchemaOptions[$name] = $value; - - return $this; + return $this->_platformOptions; } /** - * @deprecated Use {@link hasPlatformOption()} instead + * @deprecated Use {@see getCharset()}, {@see getCollation()}, {@see getMinimumValue()}, {@see getMaximumValue()} + * or {@see getEnumType()} instead. * - * @param string $name - * - * @return bool + * @param key-of $name */ - public function hasCustomSchemaOption($name) + public function hasPlatformOption(string $name): bool { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5476', - 'Column::hasCustomSchemaOption() is deprecated. Use hasPlatformOption() instead.', - ); - - return isset($this->_customSchemaOptions[$name]); + return isset($this->_platformOptions[$name]); } /** - * @deprecated Use {@link getPlatformOption()} instead + * @deprecated Use {@see getCharset()}, {@see getCollation()}, {@see getMinimumValue()}, {@see getMaximumValue()} + * or {@see getEnumType()} instead. * - * @param string $name - * - * @return mixed + * @param key-of $name */ - public function getCustomSchemaOption($name) + public function getPlatformOption(string $name): mixed { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5476', - 'Column::getCustomSchemaOption() is deprecated. Use getPlatformOption() instead.', - ); + /** @phpstan-ignore offsetAccess.notFound */ + return $this->_platformOptions[$name]; + } - return $this->_customSchemaOptions[$name]; + public function getColumnDefinition(): ?string + { + return $this->_columnDefinition; } - /** - * @deprecated Use {@link setPlatformOptions()} instead - * - * @param mixed[] $customSchemaOptions - * - * @return Column - */ - public function setCustomSchemaOptions(array $customSchemaOptions) + public function getAutoincrement(): bool { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5476', - 'Column::setCustomSchemaOptions() is deprecated. Use setPlatformOptions() instead.', - ); + return $this->_autoincrement; + } - $this->_customSchemaOptions = $customSchemaOptions; + public function setAutoincrement(bool $flag): self + { + $this->_autoincrement = $flag; return $this; } + public function setComment(string $comment): self + { + $this->_comment = $comment; + + return $this; + } + + public function getComment(): string + { + return $this->_comment; + } + /** - * @deprecated Use {@link getPlatformOptions()} instead + * @param list $values * - * @return mixed[] + * @return $this */ - public function getCustomSchemaOptions() + public function setValues(array $values): static { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5476', - 'Column::getCustomSchemaOptions() is deprecated. Use getPlatformOptions() instead.', - ); + $this->_values = $values; + + return $this; + } - return $this->_customSchemaOptions; + /** @return list */ + public function getValues(): array + { + return $this->_values; } - /** @return mixed[] */ - public function toArray() + /** @return ColumnProperties */ + public function toArray(): array { return array_merge([ - 'name' => $this->_name, - 'type' => $this->_type, - 'default' => $this->_default, - 'notnull' => $this->_notnull, - 'length' => $this->_length, - 'precision' => $this->_precision, - 'scale' => $this->_scale, - 'fixed' => $this->_fixed, - 'unsigned' => $this->_unsigned, - 'autoincrement' => $this->_autoincrement, + 'name' => $this->_name, + 'type' => $this->_type, + 'default' => $this->_default, + 'notnull' => $this->_notnull, + 'length' => $this->_length, + 'precision' => $this->_precision, + 'scale' => $this->_scale, + 'fixed' => $this->_fixed, + 'unsigned' => $this->_unsigned, + 'autoincrement' => $this->_autoincrement, 'columnDefinition' => $this->_columnDefinition, - 'comment' => $this->_comment, - ], $this->_platformOptions, $this->_customSchemaOptions); + 'comment' => $this->_comment, + 'values' => $this->_values, + ], $this->_platformOptions); + } + + public static function editor(): ColumnEditor + { + return new ColumnEditor(); + } + + public function edit(): ColumnEditor + { + return self::editor() + ->setName($this->getObjectName()) + ->setType($this->_type) + ->setLength($this->_length) + ->setPrecision($this->_precision) + ->setScale($this->_scale) + ->setUnsigned($this->_unsigned) + ->setFixed($this->_fixed) + ->setNotNull($this->_notnull) + ->setDefaultValue($this->_default) + ->setAutoincrement($this->_autoincrement) + ->setComment($this->_comment) + ->setValues($this->_values) + ->setColumnDefinition($this->_columnDefinition) + ->setCharset($this->getCharset()) + ->setCollation($this->getCollation()) + ->setMinimumValue($this->getMinimumValue()) + ->setMaximumValue($this->getMaximumValue()) + ->setEnumType($this->getEnumType()) + ->setDefaultConstraintName($this->getDefaultConstraintName()); } } diff --git a/doctrine/dbal/src/Schema/ColumnDiff.php b/doctrine/dbal/src/Schema/ColumnDiff.php index bd1b0eee0..6c86e9216 100644 --- a/doctrine/dbal/src/Schema/ColumnDiff.php +++ b/doctrine/dbal/src/Schema/ColumnDiff.php @@ -1,169 +1,141 @@ oldColumnName = $oldColumnName; - $this->column = $column; - $this->changedProperties = $changedProperties; - $this->fromColumn = $fromColumn; + public function countChangedProperties(): int + { + return (int) $this->hasUnsignedChanged() + + (int) $this->hasAutoIncrementChanged() + + (int) $this->hasDefaultChanged() + + (int) $this->hasFixedChanged() + + (int) $this->hasPrecisionChanged() + + (int) $this->hasScaleChanged() + + (int) $this->hasLengthChanged() + + (int) $this->hasNotNullChanged() + + (int) $this->hasNameChanged() + + (int) $this->hasTypeChanged() + + (int) $this->hasPlatformOptionsChanged() + + (int) $this->hasCommentChanged(); } - public function getOldColumn(): ?Column + public function getOldColumn(): Column { - return $this->fromColumn; + return $this->oldColumn; } public function getNewColumn(): Column { - return $this->column; + return $this->newColumn; + } + + public function hasNameChanged(): bool + { + $oldColumn = $this->getOldColumn(); + + // Column names are case insensitive + return strcasecmp($oldColumn->getName(), $this->getNewColumn()->getName()) !== 0; } public function hasTypeChanged(): bool { - return $this->hasChanged('type'); + return $this->newColumn->getType()::class !== $this->oldColumn->getType()::class; } public function hasLengthChanged(): bool { - return $this->hasChanged('length'); + return $this->hasPropertyChanged(static function (Column $column): ?int { + return $column->getLength(); + }); } public function hasPrecisionChanged(): bool { - return $this->hasChanged('precision'); + return $this->hasPropertyChanged(static function (Column $column): ?int { + return $column->getPrecision(); + }); } public function hasScaleChanged(): bool { - return $this->hasChanged('scale'); + return $this->hasPropertyChanged(static function (Column $column): int { + return $column->getScale(); + }); } public function hasUnsignedChanged(): bool { - return $this->hasChanged('unsigned'); + return $this->hasPropertyChanged(static function (Column $column): bool { + return $column->getUnsigned(); + }); } public function hasFixedChanged(): bool { - return $this->hasChanged('fixed'); + return $this->hasPropertyChanged(static function (Column $column): bool { + return $column->getFixed(); + }); } public function hasNotNullChanged(): bool { - return $this->hasChanged('notnull'); + return $this->hasPropertyChanged(static function (Column $column): bool { + return $column->getNotnull(); + }); } public function hasDefaultChanged(): bool { - return $this->hasChanged('default'); + $oldDefault = $this->oldColumn->getDefault(); + $newDefault = $this->newColumn->getDefault(); + + // Null values need to be checked additionally as they tell whether to create or drop a default value. + // null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation. + if (($newDefault === null) xor ($oldDefault === null)) { + return true; + } + + return $newDefault != $oldDefault; } public function hasAutoIncrementChanged(): bool { - return $this->hasChanged('autoincrement'); + return $this->hasPropertyChanged(static function (Column $column): bool { + return $column->getAutoincrement(); + }); } public function hasCommentChanged(): bool { - return $this->hasChanged('comment'); - } - - /** - * @deprecated Use {@see hasTypeChanged()}, {@see hasLengthChanged()}, {@see hasPrecisionChanged()}, - * {@see hasScaleChanged()}, {@see hasUnsignedChanged()}, {@see hasFixedChanged()}, {@see hasNotNullChanged()}, - * {@see hasDefaultChanged()}, {@see hasAutoIncrementChanged()} or {@see hasCommentChanged()} instead. - * - * @param string $propertyName - * - * @return bool - */ - public function hasChanged($propertyName) - { - return in_array($propertyName, $this->changedProperties, true); - } - - /** - * @deprecated Use {@see $fromColumn} instead. - * - * @return Identifier - */ - public function getOldColumnName() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5622', - '%s is deprecated. Use $fromColumn instead.', - __METHOD__, - ); - - if ($this->fromColumn !== null) { - $name = $this->fromColumn->getName(); - $quote = $this->fromColumn->isQuoted(); - } else { - $name = $this->oldColumnName; - $quote = false; - } + return $this->hasPropertyChanged(static function (Column $column): string { + return $column->getComment(); + }); + } - return new Identifier($name, $quote); + public function hasPlatformOptionsChanged(): bool + { + return $this->hasPropertyChanged(static function (Column $column): array { + return $column->getPlatformOptions(); + }); + } + + private function hasPropertyChanged(callable $property): bool + { + return $property($this->newColumn) !== $property($this->oldColumn); } } diff --git a/doctrine/dbal/src/Schema/ColumnEditor.php b/doctrine/dbal/src/Schema/ColumnEditor.php new file mode 100644 index 000000000..1b2cff2c3 --- /dev/null +++ b/doctrine/dbal/src/Schema/ColumnEditor.php @@ -0,0 +1,286 @@ + */ + private array $values = []; + + /** @var ?non-empty-string */ + private ?string $charset = null; + + /** @var ?non-empty-string */ + private ?string $collation = null; + + /** @var ?non-empty-string */ + private ?string $defaultConstraintName = null; + + /** @var ?non-empty-string */ + private ?string $columnDefinition = null; + + /** @internal Use {@link Column::editor()} or {@link Column::edit()} to create an instance */ + public function __construct() + { + } + + public function setName(UnqualifiedName $name): self + { + $this->name = $name; + + return $this; + } + + /** @param non-empty-string $name */ + public function setUnquotedName(string $name): self + { + $this->name = UnqualifiedName::unquoted($name); + + return $this; + } + + /** @param non-empty-string $name */ + public function setQuotedName(string $name): self + { + $this->name = UnqualifiedName::quoted($name); + + return $this; + } + + public function setType(Type $type): self + { + $this->type = $type; + + return $this; + } + + /** @throws TypesException */ + public function setTypeName(string $typeName): self + { + $this->type = Type::getType($typeName); + + return $this; + } + + public function setLength(?int $length): self + { + $this->length = $length; + + return $this; + } + + public function setPrecision(?int $precision): self + { + $this->precision = $precision; + + return $this; + } + + public function setScale(int $scale): self + { + $this->scale = $scale; + + return $this; + } + + public function setUnsigned(bool $unsigned): self + { + $this->unsigned = $unsigned; + + return $this; + } + + public function setFixed(bool $fixed): self + { + $this->fixed = $fixed; + + return $this; + } + + public function setNotNull(bool $notNull): self + { + $this->notNull = $notNull; + + return $this; + } + + public function setDefaultValue(mixed $defaultValue): self + { + $this->defaultValue = $defaultValue; + + return $this; + } + + public function setMinimumValue(mixed $minimumValue): self + { + $this->minimumValue = $minimumValue; + + return $this; + } + + public function setMaximumValue(mixed $maximumValue): self + { + $this->maximumValue = $maximumValue; + + return $this; + } + + /** @param ?class-string $enumType */ + public function setEnumType(?string $enumType): self + { + $this->enumType = $enumType; + + return $this; + } + + public function setAutoincrement(bool $flag): self + { + $this->autoincrement = $flag; + + return $this; + } + + public function setComment(string $comment): self + { + $this->comment = $comment; + + return $this; + } + + /** @param list $values */ + public function setValues(array $values): self + { + $this->values = $values; + + return $this; + } + + /** @param ?non-empty-string $charset */ + public function setCharset(?string $charset): self + { + $this->charset = $charset; + + return $this; + } + + /** @param ?non-empty-string $collation */ + public function setCollation(?string $collation): self + { + $this->collation = $collation; + + return $this; + } + + /** + * @internal Should be used only from within the {@see AbstractSchemaManager} class hierarchy. + * + * @param ?non-empty-string $defaultConstraintName + */ + public function setDefaultConstraintName(?string $defaultConstraintName): self + { + $this->defaultConstraintName = $defaultConstraintName; + + return $this; + } + + /** @param ?non-empty-string $columnDefinition */ + public function setColumnDefinition(?string $columnDefinition): self + { + $this->columnDefinition = $columnDefinition; + + return $this; + } + + public function create(): Column + { + if ($this->name === null) { + throw InvalidColumnDefinition::nameNotSpecified(); + } + + if ($this->type === null) { + throw InvalidColumnDefinition::dataTypeNotSpecified($this->name); + } + + $platformOptions = []; + + if ($this->charset !== null) { + $platformOptions['charset'] = $this->charset; + } + + if ($this->collation !== null) { + $platformOptions['collation'] = $this->collation; + } + + if ($this->minimumValue !== null) { + $platformOptions['min'] = $this->minimumValue; + } + + if ($this->maximumValue !== null) { + $platformOptions['max'] = $this->maximumValue; + } + + if ($this->enumType !== null) { + $platformOptions['enumType'] = $this->enumType; + } + + if ($this->defaultConstraintName !== null) { + $platformOptions[SQLServerPlatform::OPTION_DEFAULT_CONSTRAINT_NAME] = $this->defaultConstraintName; + } + + return new Column( + $this->name->toString(), + $this->type, + [ + 'length' => $this->length, + 'precision' => $this->precision, + 'scale' => $this->scale, + 'unsigned' => $this->unsigned, + 'fixed' => $this->fixed, + 'notnull' => $this->notNull, + 'default' => $this->defaultValue, + 'autoincrement' => $this->autoincrement, + 'comment' => $this->comment, + 'values' => $this->values, + 'platformOptions' => $platformOptions, + 'columnDefinition' => $this->columnDefinition, + ], + ); + } +} diff --git a/doctrine/dbal/src/Schema/Comparator.php b/doctrine/dbal/src/Schema/Comparator.php index 8114ec5e2..02cd6f81d 100644 --- a/doctrine/dbal/src/Schema/Comparator.php +++ b/doctrine/dbal/src/Schema/Comparator.php @@ -1,93 +1,34 @@ platform = $platform; - } - - /** @param list $args */ - public function __call(string $method, array $args): SchemaDiff - { - if ($method !== 'compareSchemas') { - throw new BadMethodCallException(sprintf('Unknown method "%s"', $method)); - } - - return $this->doCompareSchemas(...$args); - } - - /** @param list $args */ - public static function __callStatic(string $method, array $args): SchemaDiff - { - if ($method !== 'compareSchemas') { - throw new BadMethodCallException(sprintf('Unknown method "%s"', $method)); - } - - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4707', - 'Calling %s::%s() statically is deprecated.', - self::class, - $method, - ); - - $comparator = new self(); - - return $comparator->doCompareSchemas(...$args); + public function __construct( + private readonly AbstractPlatform $platform, + private readonly ComparatorConfig $config = new ComparatorConfig(), + ) { } /** - * Returns a SchemaDiff object containing the differences between the schemas $fromSchema and $toSchema. - * - * This method should be called non-statically since it will be declared as non-static in the next major release. - * - * @return SchemaDiff - * - * @throws SchemaException + * Returns the differences between the schemas. */ - private function doCompareSchemas( - Schema $fromSchema, - Schema $toSchema - ) { + public function compareSchemas(Schema $oldSchema, Schema $newSchema): SchemaDiff + { $createdSchemas = []; $droppedSchemas = []; $createdTables = []; @@ -97,164 +38,82 @@ private function doCompareSchemas( $alteredSequences = []; $droppedSequences = []; - $orphanedForeignKeys = []; - - $foreignKeysToTable = []; - - foreach ($toSchema->getNamespaces() as $namespace) { - if ($fromSchema->hasNamespace($namespace)) { - continue; + foreach ($newSchema->getNamespaces() as $newNamespace) { + if (! $oldSchema->hasNamespace($newNamespace)) { + $createdSchemas[] = $newNamespace; } - - $createdSchemas[$namespace] = $namespace; } - foreach ($fromSchema->getNamespaces() as $namespace) { - if ($toSchema->hasNamespace($namespace)) { - continue; + foreach ($oldSchema->getNamespaces() as $oldNamespace) { + if (! $newSchema->hasNamespace($oldNamespace)) { + $droppedSchemas[] = $oldNamespace; } - - $droppedSchemas[$namespace] = $namespace; } - foreach ($toSchema->getTables() as $table) { - $tableName = $table->getShortestName($toSchema->getName()); - if (! $fromSchema->hasTable($tableName)) { - $createdTables[$tableName] = $toSchema->getTable($tableName); + foreach ($newSchema->getTables() as $newTable) { + $newTableName = $newTable->getShortestName($newSchema->getName()); + if (! $oldSchema->hasTable($newTableName)) { + $createdTables[] = $newSchema->getTable($newTableName); } else { - $tableDifferences = $this->diffTable( - $fromSchema->getTable($tableName), - $toSchema->getTable($tableName), + $tableDiff = $this->compareTables( + $oldSchema->getTable($newTableName), + $newSchema->getTable($newTableName), ); - if ($tableDifferences !== false) { - $alteredTables[$tableName] = $tableDifferences; + if (! $tableDiff->isEmpty()) { + $alteredTables[] = $tableDiff; } } } - /* Check if there are tables removed */ - foreach ($fromSchema->getTables() as $table) { - $tableName = $table->getShortestName($fromSchema->getName()); - - $table = $fromSchema->getTable($tableName); - if (! $toSchema->hasTable($tableName)) { - $droppedTables[$tableName] = $table; - } - - // also remember all foreign keys that point to a specific table - foreach ($table->getForeignKeys() as $foreignKey) { - $foreignTable = strtolower($foreignKey->getForeignTableName()); - if (! isset($foreignKeysToTable[$foreignTable])) { - $foreignKeysToTable[$foreignTable] = []; - } - - $foreignKeysToTable[$foreignTable][] = $foreignKey; - } - } - - foreach ($droppedTables as $tableName => $table) { - if (! isset($foreignKeysToTable[$tableName])) { - continue; - } - - foreach ($foreignKeysToTable[$tableName] as $foreignKey) { - if (isset($droppedTables[strtolower($foreignKey->getLocalTableName())])) { - continue; - } - - $orphanedForeignKeys[] = $foreignKey; - } - - // deleting duplicated foreign keys present on both on the orphanedForeignKey - // and the removedForeignKeys from changedTables - foreach ($foreignKeysToTable[$tableName] as $foreignKey) { - // strtolower the table name to make if compatible with getShortestName - $localTableName = strtolower($foreignKey->getLocalTableName()); - if (! isset($alteredTables[$localTableName])) { - continue; - } - - foreach ($alteredTables[$localTableName]->getDroppedForeignKeys() as $droppedForeignKey) { - assert($droppedForeignKey instanceof ForeignKeyConstraint); - - // We check if the key is from the removed table if not we skip. - if ($tableName !== strtolower($droppedForeignKey->getForeignTableName())) { - continue; - } + // Check if there are tables removed + foreach ($oldSchema->getTables() as $oldTable) { + $oldTableName = $oldTable->getShortestName($oldSchema->getName()); - $alteredTables[$localTableName]->unsetDroppedForeignKey($droppedForeignKey); - } + $oldTable = $oldSchema->getTable($oldTableName); + if (! $newSchema->hasTable($oldTableName)) { + $droppedTables[] = $oldTable; } } - foreach ($toSchema->getSequences() as $sequence) { - $sequenceName = $sequence->getShortestName($toSchema->getName()); - if (! $fromSchema->hasSequence($sequenceName)) { - if (! $this->isAutoIncrementSequenceInSchema($fromSchema, $sequence)) { - $createdSequences[] = $sequence; + foreach ($newSchema->getSequences() as $newSequence) { + $newSequenceName = $newSequence->getShortestName($newSchema->getName()); + if (! $oldSchema->hasSequence($newSequenceName)) { + if (! $this->isAutoIncrementSequenceInSchema($oldSchema, $newSequence)) { + $createdSequences[] = $newSequence; } } else { - if ($this->diffSequence($sequence, $fromSchema->getSequence($sequenceName))) { - $alteredSequences[] = $toSchema->getSequence($sequenceName); + if ($this->diffSequence($newSequence, $oldSchema->getSequence($newSequenceName))) { + $alteredSequences[] = $newSchema->getSequence($newSequenceName); } } } - foreach ($fromSchema->getSequences() as $sequence) { - if ($this->isAutoIncrementSequenceInSchema($toSchema, $sequence)) { + foreach ($oldSchema->getSequences() as $oldSequence) { + if ($this->isAutoIncrementSequenceInSchema($newSchema, $oldSequence)) { continue; } - $sequenceName = $sequence->getShortestName($fromSchema->getName()); + $oldSequenceName = $oldSequence->getShortestName($oldSchema->getName()); - if ($toSchema->hasSequence($sequenceName)) { - continue; + if (! $newSchema->hasSequence($oldSequenceName)) { + $droppedSequences[] = $oldSequence; } - - $droppedSequences[] = $sequence; } - $diff = new SchemaDiff( + return new SchemaDiff( + $createdSchemas, + $droppedSchemas, $createdTables, $alteredTables, $droppedTables, - $fromSchema, - $createdSchemas, - $droppedSchemas, $createdSequences, $alteredSequences, $droppedSequences, ); - - $diff->orphanedForeignKeys = $orphanedForeignKeys; - - return $diff; - } - - /** - * @deprecated Use non-static call to {@see compareSchemas()} instead. - * - * @return SchemaDiff - * - * @throws SchemaException - */ - public function compare(Schema $fromSchema, Schema $toSchema) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4707', - 'Method compare() is deprecated. Use a non-static call to compareSchemas() instead.', - ); - - return $this->compareSchemas($fromSchema, $toSchema); } - /** - * @param Schema $schema - * @param Sequence $sequence - */ - private function isAutoIncrementSequenceInSchema($schema, $sequence): bool + private function isAutoIncrementSequenceInSchema(Schema $schema, Sequence $sequence): bool { foreach ($schema->getTables() as $table) { if ($sequence->isAutoIncrementsFor($table)) { @@ -265,8 +124,7 @@ private function isAutoIncrementSequenceInSchema($schema, $sequence): bool return false; } - /** @return bool */ - public function diffSequence(Sequence $sequence1, Sequence $sequence2) + public function diffSequence(Sequence $sequence1, Sequence $sequence2): bool { if ($sequence1->getAllocationSize() !== $sequence2->getAllocationSize()) { return true; @@ -276,171 +134,170 @@ public function diffSequence(Sequence $sequence1, Sequence $sequence2) } /** - * Returns the difference between the tables $fromTable and $toTable. - * - * If there are no differences this method returns the boolean false. - * - * @deprecated Use {@see compareTables()} and, optionally, {@see TableDiff::isEmpty()} instead. - * - * @return TableDiff|false - * - * @throws Exception + * Compares the tables and returns the difference between them. */ - public function diffTable(Table $fromTable, Table $toTable) + public function compareTables(Table $oldTable, Table $newTable): TableDiff { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5770', - '%s is deprecated. Use compareTables() instead.', - __METHOD__, - ); + $shouldReportModifiedIndexes = $this->config->getReportModifiedIndexes(); + if ($shouldReportModifiedIndexes) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6890', + 'Detection of modified indexes is deprecated. Please disable it by configuring the comparator' + . ' using ComparatorConfig::withReportModifiedIndexes(false).', + ); + } - $diff = $this->compareTables($fromTable, $toTable); + $addedColumns = []; + $modifiedColumns = []; + $droppedColumns = []; + $addedIndexes = []; + $modifiedIndexes = []; + $droppedIndexes = []; + $renamedIndexes = []; + $addedForeignKeys = []; + $droppedForeignKeys = []; - if ($diff->isEmpty()) { - return false; - } + $oldColumns = $oldTable->getColumns(); + $newColumns = $newTable->getColumns(); - return $diff; - } + // See if all the columns in the old table exist in the new table + foreach ($newColumns as $newColumn) { + $newColumnName = strtolower($newColumn->getName()); - /** - * Compares the tables and returns the difference between them. - * - * @throws Exception - */ - public function compareTables(Table $fromTable, Table $toTable): TableDiff - { - $addedColumns = []; - $modifiedColumns = []; - $droppedColumns = []; - $addedIndexes = []; - $modifiedIndexes = []; - $droppedIndexes = []; - $addedForeignKeys = []; - $modifiedForeignKeys = []; - $droppedForeignKeys = []; - - $fromTableColumns = $fromTable->getColumns(); - $toTableColumns = $toTable->getColumns(); - - /* See if all the columns in "from" table exist in "to" table */ - foreach ($toTableColumns as $columnName => $column) { - if ($fromTable->hasColumn($columnName)) { - continue; + if (! $oldTable->hasColumn($newColumnName)) { + $addedColumns[$newColumnName] = $newColumn; } - - $addedColumns[$columnName] = $column; } - /* See if there are any removed columns in "to" table */ - foreach ($fromTableColumns as $columnName => $column) { - // See if column is removed in "to" table. - if (! $toTable->hasColumn($columnName)) { - $droppedColumns[$columnName] = $column; + // See if there are any removed columns in the new table + foreach ($oldColumns as $oldColumn) { + $oldColumnName = strtolower($oldColumn->getName()); + + // See if column is removed in the new table. + if (! $newTable->hasColumn($oldColumnName)) { + $droppedColumns[$oldColumnName] = $oldColumn; continue; } - $toColumn = $toTable->getColumn($columnName); + $newColumn = $newTable->getColumn($oldColumnName); - // See if column has changed properties in "to" table. - $changedProperties = $this->diffColumn($column, $toColumn); + if (! $this->columnsEqual($oldColumn, $newColumn)) { + $modifiedColumns[$oldColumnName] = new ColumnDiff($oldColumn, $newColumn); + } + } - if ($this->platform !== null) { - if ($this->columnsEqual($column, $toColumn)) { - continue; - } - } elseif (count($changedProperties) === 0) { + $renamedColumnNames = $newTable->getRenamedColumns(); + + foreach ($addedColumns as $addedColumnName => $addedColumn) { + if (! isset($renamedColumnNames[$addedColumn->getName()])) { continue; } - $modifiedColumns[$column->getName()] = new ColumnDiff( - $column->getName(), - $toColumn, - $changedProperties, - $column, + $removedColumnName = strtolower($renamedColumnNames[$addedColumn->getName()]); + // Explicitly renamed columns need to be diffed, because their types can also have changed + $modifiedColumns[$removedColumnName] = new ColumnDiff( + $droppedColumns[$removedColumnName], + $addedColumn, + ); + + unset( + $addedColumns[$addedColumnName], + $droppedColumns[$removedColumnName], ); } - $renamedColumns = $this->detectRenamedColumns($addedColumns, $droppedColumns); + if ($this->config->getDetectRenamedColumns()) { + $this->detectRenamedColumns($modifiedColumns, $addedColumns, $droppedColumns); + } - $fromTableIndexes = $fromTable->getIndexes(); - $toTableIndexes = $toTable->getIndexes(); + $oldIndexes = $oldTable->getIndexes(); + $newIndexes = $newTable->getIndexes(); - /* See if all the indexes in "from" table exist in "to" table */ - foreach ($toTableIndexes as $indexName => $index) { - if (($index->isPrimary() && $fromTable->getPrimaryKey() !== null) || $fromTable->hasIndex($indexName)) { - continue; - } + // See if all the indexes from the old table exist in the new one + foreach ($newIndexes as $newIndex) { + $newIndexName = $newIndex->getName(); - $addedIndexes[$indexName] = $index; + if ( + (! $newIndex->isPrimary() || $oldTable->getPrimaryKey() === null) + && ! $oldTable->hasIndex($newIndexName) + ) { + $addedIndexes[] = $newIndex; + } } - /* See if there are any removed indexes in "to" table */ - foreach ($fromTableIndexes as $indexName => $index) { - // See if index is removed in "to" table. + // See if there are any removed indexes in the new table + foreach ($oldIndexes as $oldIndex) { + $oldIndexName = $oldIndex->getName(); + + // See if the index is removed in the new table. if ( - ($index->isPrimary() && $toTable->getPrimaryKey() === null) || - ! $index->isPrimary() && ! $toTable->hasIndex($indexName) + ($oldIndex->isPrimary() && $newTable->getPrimaryKey() === null) || + ! $oldIndex->isPrimary() && ! $newTable->hasIndex($oldIndexName) ) { - $droppedIndexes[$indexName] = $index; + $droppedIndexes[] = $oldIndex; continue; } - // See if index has changed in "to" table. - $toTableIndex = $index->isPrimary() ? $toTable->getPrimaryKey() : $toTable->getIndex($indexName); - assert($toTableIndex instanceof Index); + // See if index has changed in the new table. + $newIndex = $oldIndex->isPrimary() ? $newTable->getPrimaryKey() : $newTable->getIndex($oldIndexName); + assert($newIndex instanceof Index); - if (! $this->diffIndex($index, $toTableIndex)) { + if (! $this->diffIndex($oldIndex, $newIndex)) { continue; } - $modifiedIndexes[$indexName] = $toTableIndex; + if ($shouldReportModifiedIndexes) { + $modifiedIndexes[] = $newIndex; + } else { + $droppedIndexes[] = $oldIndex; + $addedIndexes[] = $newIndex; + } } - $renamedIndexes = $this->detectRenamedIndexes($addedIndexes, $droppedIndexes); + if ($this->config->getDetectRenamedIndexes()) { + $renamedIndexes = $this->detectRenamedIndexes($addedIndexes, $droppedIndexes); + } - $fromForeignKeys = $fromTable->getForeignKeys(); - $toForeignKeys = $toTable->getForeignKeys(); + $oldForeignKeys = $oldTable->getForeignKeys(); + $newForeignKeys = $newTable->getForeignKeys(); - foreach ($fromForeignKeys as $fromKey => $fromConstraint) { - foreach ($toForeignKeys as $toKey => $toConstraint) { - if ($this->diffForeignKey($fromConstraint, $toConstraint) === false) { - unset($fromForeignKeys[$fromKey], $toForeignKeys[$toKey]); + foreach ($oldForeignKeys as $oldKey => $oldForeignKey) { + foreach ($newForeignKeys as $newKey => $newForeignKey) { + if ($this->diffForeignKey($oldForeignKey, $newForeignKey) === false) { + unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]); } else { - if (strtolower($fromConstraint->getName()) === strtolower($toConstraint->getName())) { - $modifiedForeignKeys[] = $toConstraint; + if (strtolower($oldForeignKey->getName()) === strtolower($newForeignKey->getName())) { + $droppedForeignKeys[$oldKey] = $oldForeignKey; + $addedForeignKeys[$newKey] = $newForeignKey; - unset($fromForeignKeys[$fromKey], $toForeignKeys[$toKey]); + unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]); } } } } - foreach ($fromForeignKeys as $fromConstraint) { - $droppedForeignKeys[] = $fromConstraint; + foreach ($oldForeignKeys as $oldForeignKey) { + $droppedForeignKeys[] = $oldForeignKey; } - foreach ($toForeignKeys as $toConstraint) { - $addedForeignKeys[] = $toConstraint; + foreach ($newForeignKeys as $newForeignKey) { + $addedForeignKeys[] = $newForeignKey; } return new TableDiff( - $toTable->getName(), - $addedColumns, - $modifiedColumns, - $droppedColumns, - $addedIndexes, - $modifiedIndexes, - $droppedIndexes, - $fromTable, - $addedForeignKeys, - $modifiedForeignKeys, - $droppedForeignKeys, - $renamedColumns, - $renamedIndexes, + $oldTable, + addedColumns: $addedColumns, + changedColumns: $modifiedColumns, + droppedColumns: $droppedColumns, + addedIndexes: $addedIndexes, + modifiedIndexes: $modifiedIndexes, + droppedIndexes: $droppedIndexes, + renamedIndexes: $renamedIndexes, + addedForeignKeys: $addedForeignKeys, + droppedForeignKeys: $droppedForeignKeys, ); } @@ -448,73 +305,71 @@ public function compareTables(Table $fromTable, Table $toTable): TableDiff * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop * however ambiguities between different possibilities should not lead to renaming at all. * - * @param array $addedColumns - * @param array $removedColumns - * - * @return array - * - * @throws Exception + * @param array $modifiedColumns + * @param array $addedColumns + * @param array $removedColumns */ - private function detectRenamedColumns(array &$addedColumns, array &$removedColumns): array + private function detectRenamedColumns(array &$modifiedColumns, array &$addedColumns, array &$removedColumns): void { + /** @var array>> $candidatesByName */ $candidatesByName = []; foreach ($addedColumns as $addedColumnName => $addedColumn) { foreach ($removedColumns as $removedColumn) { - if (! $this->columnsEqual($addedColumn, $removedColumn)) { - continue; + if ($this->columnsEqual($addedColumn, $removedColumn)) { + $candidatesByName[$addedColumnName][] = [$removedColumn, $addedColumn]; } - - $candidatesByName[$addedColumn->getName()][] = [$removedColumn, $addedColumn, $addedColumnName]; } } - $renamedColumns = []; - - foreach ($candidatesByName as $candidates) { + foreach ($candidatesByName as $addedColumnName => $candidates) { if (count($candidates) !== 1) { continue; } - [$removedColumn, $addedColumn] = $candidates[0]; - $removedColumnName = $removedColumn->getName(); - $addedColumnName = strtolower($addedColumn->getName()); + [$oldColumn, $newColumn] = $candidates[0]; + $oldColumnName = strtolower($oldColumn->getName()); - if (isset($renamedColumns[$removedColumnName])) { + if (isset($modifiedColumns[$oldColumnName])) { continue; } - $renamedColumns[$removedColumnName] = $addedColumn; + $modifiedColumns[$oldColumnName] = new ColumnDiff( + $oldColumn, + $newColumn, + ); + unset( $addedColumns[$addedColumnName], - $removedColumns[strtolower($removedColumnName)], + $removedColumns[$oldColumnName], ); } - - return $renamedColumns; } /** * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop * however ambiguities between different possibilities should not lead to renaming at all. * - * @param array $addedIndexes - * @param array $removedIndexes + * @param array $addedIndexes + * @param array $removedIndexes * * @return array */ private function detectRenamedIndexes(array &$addedIndexes, array &$removedIndexes): array { - $candidatesByName = []; + $candidatesByName = []; + $removedIndexMatchCount = []; // Gather possible rename candidates by comparing each added and removed index based on semantics. - foreach ($addedIndexes as $addedIndexName => $addedIndex) { - foreach ($removedIndexes as $removedIndex) { + foreach ($addedIndexes as $addedIndexKey => $addedIndex) { + foreach ($removedIndexes as $removedIndexKey => $removedIndex) { if ($this->diffIndex($addedIndex, $removedIndex)) { continue; } - $candidatesByName[$addedIndex->getName()][] = [$removedIndex, $addedIndex, $addedIndexName]; + $candidatesByName[$addedIndex->getName()][] = [$removedIndexKey, $addedIndexKey]; + + $removedIndexMatchCount[$removedIndexKey] = ($removedIndexMatchCount[$removedIndexKey] ?? 0) + 1; } } @@ -529,31 +384,29 @@ private function detectRenamedIndexes(array &$addedIndexes, array &$removedIndex continue; } - [$removedIndex, $addedIndex] = $candidates[0]; - - $removedIndexName = strtolower($removedIndex->getName()); - $addedIndexName = strtolower($addedIndex->getName()); + [$removedIndexKey, $addedIndexKey] = $candidates[0]; - if (isset($renamedIndexes[$removedIndexName])) { + // Likewise, a removed index that matches more than one added index is ambiguous. + if ($removedIndexMatchCount[$removedIndexKey] > 1) { continue; } + $removedIndex = $removedIndexes[$removedIndexKey]; + $removedIndexName = strtolower($removedIndex->getName()); + + $addedIndex = $addedIndexes[$addedIndexKey]; + $renamedIndexes[$removedIndexName] = $addedIndex; unset( - $addedIndexes[$addedIndexName], - $removedIndexes[$removedIndexName], + $addedIndexes[$addedIndexKey], + $removedIndexes[$removedIndexKey], ); } return $renamedIndexes; } - /** - * @internal The method should be only used from within the {@see Comparator} class hierarchy. - * - * @return bool - */ - public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2) + protected function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2): bool { if ( array_map('strtolower', $key1->getUnquotedLocalColumns()) @@ -582,134 +435,19 @@ public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint /** * Compares the definitions of the given columns - * - * @internal The method should be only used from within the {@see Comparator} class hierarchy. - * - * @throws Exception */ - public function columnsEqual(Column $column1, Column $column2): bool + protected function columnsEqual(Column $column1, Column $column2): bool { - if ($this->platform === null) { - return $this->diffColumn($column1, $column2) === []; - } - return $this->platform->columnsEqual($column1, $column2); } - /** - * Returns the difference between the columns - * - * If there are differences this method returns the changed properties as a - * string array, otherwise an empty array gets returned. - * - * @deprecated Use {@see columnsEqual()} instead. - * - * @return string[] - */ - public function diffColumn(Column $column1, Column $column2) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5650', - '%s is deprecated. Use diffTable() instead.', - __METHOD__, - ); - - $properties1 = $column1->toArray(); - $properties2 = $column2->toArray(); - - $changedProperties = []; - - if (get_class($properties1['type']) !== get_class($properties2['type'])) { - $changedProperties[] = 'type'; - } - - foreach (['notnull', 'unsigned', 'autoincrement'] as $property) { - if ($properties1[$property] === $properties2[$property]) { - continue; - } - - $changedProperties[] = $property; - } - - // Null values need to be checked additionally as they tell whether to create or drop a default value. - // null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation. - if ( - ($properties1['default'] === null) !== ($properties2['default'] === null) - || $properties1['default'] != $properties2['default'] // @phpstan-ignore notEqual.notAllowed - ) { - $changedProperties[] = 'default'; - } - - if ( - ($properties1['type'] instanceof Types\StringType && ! $properties1['type'] instanceof Types\GuidType) || - $properties1['type'] instanceof Types\BinaryType - ) { - // check if value of length is set at all, default value assumed otherwise. - $length1 = $properties1['length'] ?? 255; - $length2 = $properties2['length'] ?? 255; - if ($length1 !== $length2) { - $changedProperties[] = 'length'; - } - - if ($properties1['fixed'] !== $properties2['fixed']) { - $changedProperties[] = 'fixed'; - } - } elseif ($properties1['type'] instanceof Types\DecimalType) { - if (($properties1['precision'] ?? 10) !== ($properties2['precision'] ?? 10)) { - $changedProperties[] = 'precision'; - } - - if ($properties1['scale'] !== $properties2['scale']) { - $changedProperties[] = 'scale'; - } - } - - // A null value and an empty string are actually equal for a comment so they should not trigger a change. - if ( - $properties1['comment'] !== $properties2['comment'] && - ! ($properties1['comment'] === null && $properties2['comment'] === '') && - ! ($properties2['comment'] === null && $properties1['comment'] === '') - ) { - $changedProperties[] = 'comment'; - } - - $customOptions1 = $column1->getCustomSchemaOptions(); - $customOptions2 = $column2->getCustomSchemaOptions(); - - foreach (array_merge(array_keys($customOptions1), array_keys($customOptions2)) as $key) { - if (! array_key_exists($key, $properties1) || ! array_key_exists($key, $properties2)) { - $changedProperties[] = $key; - } elseif ($properties1[$key] !== $properties2[$key]) { - $changedProperties[] = $key; - } - } - - $platformOptions1 = $column1->getPlatformOptions(); - $platformOptions2 = $column2->getPlatformOptions(); - - foreach (array_keys(array_intersect_key($platformOptions1, $platformOptions2)) as $key) { - if ($properties1[$key] === $properties2[$key]) { - continue; - } - - $changedProperties[] = $key; - } - - return array_unique($changedProperties); - } - /** * Finds the difference between the indexes $index1 and $index2. * * Compares $index1 with $index2 and returns true if there are any * differences or false in case there are no differences. - * - * @internal The method should be only used from within the {@see Comparator} class hierarchy. - * - * @return bool */ - public function diffIndex(Index $index1, Index $index2) + protected function diffIndex(Index $index1, Index $index2): bool { return ! ($index1->isFulfilledBy($index2) && $index2->isFulfilledBy($index1)); } diff --git a/doctrine/dbal/src/Schema/ComparatorConfig.php b/doctrine/dbal/src/Schema/ComparatorConfig.php new file mode 100644 index 000000000..c303cdc23 --- /dev/null +++ b/doctrine/dbal/src/Schema/ComparatorConfig.php @@ -0,0 +1,58 @@ +detectRenamedIndexes, + $this->reportModifiedIndexes, + ); + } + + public function getDetectRenamedColumns(): bool + { + return $this->detectRenamedColumns; + } + + public function withDetectRenamedIndexes(bool $detectRenamedIndexes): self + { + return new self( + $this->detectRenamedColumns, + $detectRenamedIndexes, + $this->reportModifiedIndexes, + ); + } + + public function getDetectRenamedIndexes(): bool + { + return $this->detectRenamedIndexes; + } + + public function withReportModifiedIndexes(bool $reportModifiedIndexes): self + { + return new self( + $this->detectRenamedColumns, + $this->detectRenamedIndexes, + $reportModifiedIndexes, + ); + } + + /** @internal This method is intended solely to provide an upgrade path to DBAL 5.0. */ + public function getReportModifiedIndexes(): bool + { + return $this->reportModifiedIndexes; + } +} diff --git a/doctrine/dbal/src/Schema/Constraint.php b/doctrine/dbal/src/Schema/Constraint.php deleted file mode 100644 index f47ee1fd1..000000000 --- a/doctrine/dbal/src/Schema/Constraint.php +++ /dev/null @@ -1,41 +0,0 @@ - */ class DB2SchemaManager extends AbstractSchemaManager @@ -30,75 +33,13 @@ class DB2SchemaManager extends AbstractSchemaManager /** * {@inheritDoc} */ - public function listTableNames() - { - return $this->doListTableNames(); - } - - /** - * {@inheritDoc} - */ - public function listTables() - { - return $this->doListTables(); - } - - /** - * {@inheritDoc} - * - * @deprecated Use {@see introspectTable()} instead. - */ - public function listTableDetails($name) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5595', - '%s is deprecated. Use introspectTable() instead.', - __METHOD__, - ); - - return $this->doListTableDetails($name); - } - - /** - * {@inheritDoc} - */ - public function listTableColumns($table, $database = null) - { - return $this->doListTableColumns($table, $database); - } - - /** - * {@inheritDoc} - */ - public function listTableIndexes($table) - { - return $this->doListTableIndexes($table); - } - - /** - * {@inheritDoc} - */ - public function listTableForeignKeys($table, $database = null) - { - return $this->doListTableForeignKeys($table, $database); - } - - /** - * {@inheritDoc} - * - * @throws Exception - */ - protected function _getPortableTableColumnDefinition($tableColumn) + protected function _getPortableTableColumnDefinition(array $tableColumn): Column { $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); - $length = null; - $fixed = null; - $scale = false; - $precision = false; - - $default = null; + $length = $precision = $default = null; + $scale = 0; + $fixed = false; if ($tableColumn['default'] !== null && $tableColumn['default'] !== 'NULL') { $default = $tableColumn['default']; @@ -108,12 +49,7 @@ protected function _getPortableTableColumnDefinition($tableColumn) } } - $type = $this->_platform->getDoctrineTypeMapping($tableColumn['typename']); - - if (isset($tableColumn['comment'])) { - $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type); - $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type); - } + $type = $this->platform->getDoctrineTypeMapping($tableColumn['typename']); switch (strtolower($tableColumn['typename'])) { case 'varchar': @@ -122,7 +58,6 @@ protected function _getPortableTableColumnDefinition($tableColumn) } $length = $tableColumn['length']; - $fixed = false; break; case 'character': @@ -147,16 +82,17 @@ protected function _getPortableTableColumnDefinition($tableColumn) } $options = [ - 'length' => $length, - 'fixed' => (bool) $fixed, - 'default' => $default, - 'autoincrement' => (bool) $tableColumn['autoincrement'], - 'notnull' => $tableColumn['nulls'] === 'N', - 'comment' => isset($tableColumn['comment']) && $tableColumn['comment'] !== '' - ? $tableColumn['comment'] - : null, + 'length' => $length, + 'fixed' => $fixed, + 'default' => $default, + 'autoincrement' => (bool) $tableColumn['autoincrement'], + 'notnull' => $tableColumn['nulls'] === 'N', ]; + if ($tableColumn['comment'] !== null) { + $options['comment'] = $tableColumn['comment']; + } + if ($scale !== null && $precision !== null) { $options['scale'] = $scale; $options['precision'] = $precision; @@ -166,9 +102,11 @@ protected function _getPortableTableColumnDefinition($tableColumn) } /** + * @deprecated Use the schema name and the unqualified table name separately instead. + * * {@inheritDoc} */ - protected function _getPortableTableDefinition($table) + protected function _getPortableTableDefinition(array $table): string { $table = array_change_key_case($table, CASE_LOWER); @@ -178,20 +116,20 @@ protected function _getPortableTableDefinition($table) /** * {@inheritDoc} */ - protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) + protected function _getPortableTableIndexesList(array $rows, string $tableName): array { - foreach ($tableIndexes as &$tableIndexRow) { - $tableIndexRow = array_change_key_case($tableIndexRow, CASE_LOWER); - $tableIndexRow['primary'] = (bool) $tableIndexRow['primary']; + foreach ($rows as &$row) { + $row = array_change_key_case($row, CASE_LOWER); + $row['primary'] = (bool) $row['primary']; } - return parent::_getPortableTableIndexesList($tableIndexes, $tableName); + return parent::_getPortableTableIndexesList($rows, $tableName); } /** * {@inheritDoc} */ - protected function _getPortableTableForeignKeyDefinition($tableForeignKey) + protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint { return new ForeignKeyConstraint( $tableForeignKey['local_columns'], @@ -205,11 +143,11 @@ protected function _getPortableTableForeignKeyDefinition($tableForeignKey) /** * {@inheritDoc} */ - protected function _getPortableTableForeignKeysList($tableForeignKeys) + protected function _getPortableTableForeignKeysList(array $rows): array { $foreignKeys = []; - foreach ($tableForeignKeys as $tableForeignKey) { + foreach ($rows as $tableForeignKey) { $tableForeignKey = array_change_key_case($tableForeignKey, CASE_LOWER); if (! isset($foreignKeys[$tableForeignKey['index_name']])) { @@ -232,28 +170,10 @@ protected function _getPortableTableForeignKeysList($tableForeignKeys) return parent::_getPortableTableForeignKeysList($foreignKeys); } - /** - * @param string $def - * - * @return string|null - */ - protected function _getPortableForeignKeyRuleDef($def) - { - if ($def === 'C') { - return 'CASCADE'; - } - - if ($def === 'N') { - return 'SET NULL'; - } - - return null; - } - /** * {@inheritDoc} */ - protected function _getPortableViewDefinition($view) + protected function _getPortableViewDefinition(array $view): View { $view = array_change_key_case($view, CASE_LOWER); @@ -267,6 +187,7 @@ protected function _getPortableViewDefinition($view) return new View($view['name'], $sql); } + /** @deprecated Use {@see Identifier::toNormalizedValue()} instead. */ protected function normalizeName(string $name): string { $identifier = new Identifier($name); @@ -277,24 +198,29 @@ protected function normalizeName(string $name): string protected function selectTableNames(string $databaseName): Result { $sql = <<<'SQL' -SELECT NAME -FROM SYSIBM.SYSTABLES +SELECT TABNAME AS NAME +FROM SYSCAT.TABLES WHERE TYPE = 'T' - AND CREATOR = ? + AND TABSCHEMA = ? SQL; - return $this->_conn->executeQuery($sql, [$databaseName]); + return $this->connection->executeQuery($sql, [$databaseName]); } protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; + $conditions = ['C.TABSCHEMA = ?']; + $params = [$databaseName]; - if ($tableName === null) { - $sql .= ' C.TABNAME AS NAME,'; + if ($tableName !== null) { + $conditions[] = 'C.TABNAME = ?'; + $params[] = $tableName; } - $sql .= <<<'SQL' + $sql = sprintf( + <<<'SQL' +SELECT + C.TABNAME AS NAME, C.COLNAME, C.TYPENAME, C.CODEPAGE, @@ -311,30 +237,30 @@ protected function selectTableColumns(string $databaseName, ?string $tableName = JOIN SYSCAT.TABLES AS T ON T.TABSCHEMA = C.TABSCHEMA AND T.TABNAME = C.TABNAME -SQL; - - $conditions = ['C.TABSCHEMA = ?', "T.TYPE = 'T'"]; - $params = [$databaseName]; - - if ($tableName !== null) { - $conditions[] = 'C.TABNAME = ?'; - $params[] = $tableName; - } - - $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY C.TABNAME, C.COLNO'; + WHERE %s + AND T.TYPE = 'T' +ORDER BY C.TABNAME, C.COLNO +SQL, + implode(' AND ', $conditions), + ); - return $this->_conn->executeQuery($sql, $params); + return $this->connection->executeQuery($sql, $params); } protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; + $conditions = ['IDX.TABSCHEMA = ?']; + $params = [$databaseName]; - if ($tableName === null) { - $sql .= ' IDX.TABNAME AS NAME,'; + if ($tableName !== null) { + $conditions[] = 'IDX.TABNAME = ?'; + $params[] = $tableName; } - $sql .= <<<'SQL' + $sql = sprintf( + <<<'SQL' + SELECT + IDX.TABNAME AS NAME, IDX.INDNAME AS KEY_NAME, IDXCOL.COLNAME AS COLUMN_NAME, CASE @@ -350,30 +276,32 @@ protected function selectIndexColumns(string $databaseName, ?string $tableName = ON IDX.TABSCHEMA = T.TABSCHEMA AND IDX.TABNAME = T.TABNAME JOIN SYSCAT.INDEXCOLUSE AS IDXCOL ON IDX.INDSCHEMA = IDXCOL.INDSCHEMA AND IDX.INDNAME = IDXCOL.INDNAME -SQL; - - $conditions = ['IDX.TABSCHEMA = ?', "T.TYPE = 'T'"]; - $params = [$databaseName]; - - if ($tableName !== null) { - $conditions[] = 'IDX.TABNAME = ?'; - $params[] = $tableName; - } - - $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY IDX.INDNAME, IDXCOL.COLSEQ'; + WHERE %s + AND T.TYPE = 'T' + ORDER BY IDX.TABNAME, + IDX.INDNAME, + IDXCOL.COLSEQ +SQL, + implode(' AND ', $conditions), + ); - return $this->_conn->executeQuery($sql, $params); + return $this->connection->executeQuery($sql, $params); } protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; + $conditions = ['R.TABSCHEMA = ?']; + $params = [$databaseName]; - if ($tableName === null) { - $sql .= ' R.TABNAME AS NAME,'; + if ($tableName !== null) { + $conditions[] = 'R.TABNAME = ?'; + $params[] = $tableName; } - $sql .= <<<'SQL' + $sql = sprintf( + <<<'SQL' + SELECT + R.TABNAME AS NAME, FKCOL.COLNAME AS LOCAL_COLUMN, R.REFTABNAME AS FOREIGN_TABLE, PKCOL.COLNAME AS FOREIGN_COLUMN, @@ -399,19 +327,16 @@ protected function selectForeignKeyColumns(string $databaseName, ?string $tableN AND PKCOL.TABSCHEMA = R.REFTABSCHEMA AND PKCOL.TABNAME = R.REFTABNAME AND PKCOL.COLSEQ = FKCOL.COLSEQ -SQL; - - $conditions = ['R.TABSCHEMA = ?', "T.TYPE = 'T'"]; - $params = [$databaseName]; - - if ($tableName !== null) { - $conditions[] = 'R.TABNAME = ?'; - $params[] = $tableName; - } - - $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY R.CONSTNAME, FKCOL.COLSEQ'; + WHERE %s + AND T.TYPE = 'T' + ORDER BY R.TABNAME, + R.CONSTNAME, + FKCOL.COLSEQ +SQL, + implode(' AND ', $conditions), + ); - return $this->_conn->executeQuery($sql, $params); + return $this->connection->executeQuery($sql, $params); } /** @@ -419,31 +344,29 @@ protected function selectForeignKeyColumns(string $databaseName, ?string $tableN */ protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array { - $sql = 'SELECT NAME, REMARKS'; - - $conditions = []; - $params = []; + $conditions = ['TABSCHEMA = ?']; + $params = [$databaseName]; if ($tableName !== null) { - $conditions[] = 'NAME = ?'; + $conditions[] = 'TABNAME = ?'; $params[] = $tableName; } - $sql .= ' FROM SYSIBM.SYSTABLES'; - - if ($conditions !== []) { - $sql .= ' WHERE ' . implode(' AND ', $conditions); - } - - /** @var array> $metadata */ - $metadata = $this->_conn->executeQuery($sql, $params) - ->fetchAllAssociativeIndexed(); + $sql = sprintf( + <<<'SQL' + SELECT TABNAME, + REMARKS + FROM SYSCAT.TABLES + WHERE %s + AND TYPE = 'T' + ORDER BY TABNAME +SQL, + implode(' AND ', $conditions), + ); $tableOptions = []; - foreach ($metadata as $table => $data) { - $data = array_change_key_case($data, CASE_LOWER); - - $tableOptions[$table] = ['comment' => $data['remarks']]; + foreach ($this->connection->iterateKeyValue($sql, $params) as $table => $remarks) { + $tableOptions[$table] = ['comment' => $remarks]; } return $tableOptions; diff --git a/doctrine/dbal/src/Schema/DefaultExpression.php b/doctrine/dbal/src/Schema/DefaultExpression.php new file mode 100644 index 000000000..af1f70112 --- /dev/null +++ b/doctrine/dbal/src/Schema/DefaultExpression.php @@ -0,0 +1,18 @@ +getCurrentDateSQL(); + } +} diff --git a/doctrine/dbal/src/Schema/DefaultExpression/CurrentTime.php b/doctrine/dbal/src/Schema/DefaultExpression/CurrentTime.php new file mode 100644 index 000000000..3d55a8f4c --- /dev/null +++ b/doctrine/dbal/src/Schema/DefaultExpression/CurrentTime.php @@ -0,0 +1,19 @@ +getCurrentTimeSQL(); + } +} diff --git a/doctrine/dbal/src/Schema/DefaultExpression/CurrentTimestamp.php b/doctrine/dbal/src/Schema/DefaultExpression/CurrentTimestamp.php new file mode 100644 index 000000000..4d80949bd --- /dev/null +++ b/doctrine/dbal/src/Schema/DefaultExpression/CurrentTimestamp.php @@ -0,0 +1,19 @@ +getCurrentTimestampSQL(); + } +} diff --git a/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php b/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php index a0d62d20d..c9bc82b1e 100644 --- a/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php +++ b/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php @@ -5,16 +5,14 @@ namespace Doctrine\DBAL\Schema\Exception; use Doctrine\DBAL\Schema\SchemaException; +use LogicException; use function sprintf; -final class ColumnAlreadyExists extends SchemaException +final class ColumnAlreadyExists extends LogicException implements SchemaException { public static function new(string $tableName, string $columnName): self { - return new self( - sprintf('The column "%s" on table "%s" already exists.', $columnName, $tableName), - self::COLUMN_ALREADY_EXISTS, - ); + return new self(sprintf('The column "%s" on table "%s" already exists.', $columnName, $tableName)); } } diff --git a/doctrine/dbal/src/Schema/Exception/ColumnDoesNotExist.php b/doctrine/dbal/src/Schema/Exception/ColumnDoesNotExist.php index 7c4bb633c..28ade2bd6 100644 --- a/doctrine/dbal/src/Schema/Exception/ColumnDoesNotExist.php +++ b/doctrine/dbal/src/Schema/Exception/ColumnDoesNotExist.php @@ -5,16 +5,14 @@ namespace Doctrine\DBAL\Schema\Exception; use Doctrine\DBAL\Schema\SchemaException; +use LogicException; use function sprintf; -final class ColumnDoesNotExist extends SchemaException +final class ColumnDoesNotExist extends LogicException implements SchemaException { public static function new(string $columnName, string $table): self { - return new self( - sprintf('There is no column with name "%s" on table "%s".', $columnName, $table), - self::COLUMN_DOESNT_EXIST, - ); + return new self(sprintf('There is no column with name "%s" on table "%s".', $columnName, $table)); } } diff --git a/doctrine/dbal/src/Schema/Exception/ForeignKeyDoesNotExist.php b/doctrine/dbal/src/Schema/Exception/ForeignKeyDoesNotExist.php index 4833de5c0..dbd8a7e13 100644 --- a/doctrine/dbal/src/Schema/Exception/ForeignKeyDoesNotExist.php +++ b/doctrine/dbal/src/Schema/Exception/ForeignKeyDoesNotExist.php @@ -5,16 +5,16 @@ namespace Doctrine\DBAL\Schema\Exception; use Doctrine\DBAL\Schema\SchemaException; +use LogicException; use function sprintf; -final class ForeignKeyDoesNotExist extends SchemaException +final class ForeignKeyDoesNotExist extends LogicException implements SchemaException { public static function new(string $foreignKeyName, string $table): self { return new self( sprintf('There exists no foreign key with the name "%s" on table "%s".', $foreignKeyName, $table), - self::FOREIGNKEY_DOESNT_EXIST, ); } } diff --git a/doctrine/dbal/src/Schema/Exception/IncomparableNames.php b/doctrine/dbal/src/Schema/Exception/IncomparableNames.php new file mode 100644 index 000000000..4aca27d1b --- /dev/null +++ b/doctrine/dbal/src/Schema/Exception/IncomparableNames.php @@ -0,0 +1,25 @@ +toString(), + $name2->toString(), + )); + } +} diff --git a/doctrine/dbal/src/Schema/Exception/IndexAlreadyExists.php b/doctrine/dbal/src/Schema/Exception/IndexAlreadyExists.php index 292bf81e4..6baec5441 100644 --- a/doctrine/dbal/src/Schema/Exception/IndexAlreadyExists.php +++ b/doctrine/dbal/src/Schema/Exception/IndexAlreadyExists.php @@ -5,16 +5,16 @@ namespace Doctrine\DBAL\Schema\Exception; use Doctrine\DBAL\Schema\SchemaException; +use LogicException; use function sprintf; -final class IndexAlreadyExists extends SchemaException +final class IndexAlreadyExists extends LogicException implements SchemaException { public static function new(string $indexName, string $table): self { return new self( sprintf('An index with name "%s" was already defined on table "%s".', $indexName, $table), - self::INDEX_ALREADY_EXISTS, ); } } diff --git a/doctrine/dbal/src/Schema/Exception/IndexDoesNotExist.php b/doctrine/dbal/src/Schema/Exception/IndexDoesNotExist.php index 526819726..e147bd588 100644 --- a/doctrine/dbal/src/Schema/Exception/IndexDoesNotExist.php +++ b/doctrine/dbal/src/Schema/Exception/IndexDoesNotExist.php @@ -5,16 +5,14 @@ namespace Doctrine\DBAL\Schema\Exception; use Doctrine\DBAL\Schema\SchemaException; +use LogicException; use function sprintf; -final class IndexDoesNotExist extends SchemaException +final class IndexDoesNotExist extends LogicException implements SchemaException { public static function new(string $indexName, string $table): self { - return new self( - sprintf('Index "%s" does not exist on table "%s".', $indexName, $table), - self::INDEX_DOESNT_EXIST, - ); + return new self(sprintf('Index "%s" does not exist on table "%s".', $indexName, $table)); } } diff --git a/doctrine/dbal/src/Schema/Exception/IndexNameInvalid.php b/doctrine/dbal/src/Schema/Exception/IndexNameInvalid.php index d69b7a930..9c7c56412 100644 --- a/doctrine/dbal/src/Schema/Exception/IndexNameInvalid.php +++ b/doctrine/dbal/src/Schema/Exception/IndexNameInvalid.php @@ -5,16 +5,14 @@ namespace Doctrine\DBAL\Schema\Exception; use Doctrine\DBAL\Schema\SchemaException; +use InvalidArgumentException; use function sprintf; -final class IndexNameInvalid extends SchemaException +final class IndexNameInvalid extends InvalidArgumentException implements SchemaException { public static function new(string $indexName): self { - return new self( - sprintf('Invalid index name "%s" given, has to be [a-zA-Z0-9_].', $indexName), - self::INDEX_INVALID_NAME, - ); + return new self(sprintf('Invalid index name "%s" given, has to be [a-zA-Z0-9_].', $indexName)); } } diff --git a/doctrine/dbal/src/Schema/Exception/InvalidColumnDefinition.php b/doctrine/dbal/src/Schema/Exception/InvalidColumnDefinition.php new file mode 100644 index 000000000..61fc224fd --- /dev/null +++ b/doctrine/dbal/src/Schema/Exception/InvalidColumnDefinition.php @@ -0,0 +1,25 @@ +toString())); + } +} diff --git a/doctrine/dbal/src/Schema/Exception/InvalidForeignKeyConstraintDefinition.php b/doctrine/dbal/src/Schema/Exception/InvalidForeignKeyConstraintDefinition.php new file mode 100644 index 000000000..427cbdf87 --- /dev/null +++ b/doctrine/dbal/src/Schema/Exception/InvalidForeignKeyConstraintDefinition.php @@ -0,0 +1,43 @@ +' : $constraintName->toString(); + } +} diff --git a/doctrine/dbal/src/Schema/Exception/InvalidIdentifier.php b/doctrine/dbal/src/Schema/Exception/InvalidIdentifier.php new file mode 100644 index 000000000..dc10cbcc0 --- /dev/null +++ b/doctrine/dbal/src/Schema/Exception/InvalidIdentifier.php @@ -0,0 +1,16 @@ +toString())); + } + + public static function fromNonPositiveColumnLength(UnqualifiedName $columnName, int $length): self + { + return new self(sprintf( + 'Indexed column length must be a positive integer, %d given for column %s.', + $length, + $columnName->toString(), + )); + } +} diff --git a/doctrine/dbal/src/Schema/Exception/InvalidName.php b/doctrine/dbal/src/Schema/Exception/InvalidName.php new file mode 100644 index 000000000..5053766c9 --- /dev/null +++ b/doctrine/dbal/src/Schema/Exception/InvalidName.php @@ -0,0 +1,16 @@ +toString())); + } +} diff --git a/doctrine/dbal/src/Schema/Exception/InvalidTableModification.php b/doctrine/dbal/src/Schema/Exception/InvalidTableModification.php new file mode 100644 index 000000000..0465ac4ef --- /dev/null +++ b/doctrine/dbal/src/Schema/Exception/InvalidTableModification.php @@ -0,0 +1,184 @@ +getObjectName()->toString(), + self::formatTableName($tableName), + ), previous: $previous); + } + + public static function columnDoesNotExist( + ?OptionallyQualifiedName $tableName, + ObjectDoesNotExist $previous, + ): self { + return new self(sprintf( + 'Column %s does not exist on table %s.', + $previous->getObjectName()->toString(), + self::formatTableName($tableName), + ), previous: $previous); + } + + public static function indexAlreadyExists( + ?OptionallyQualifiedName $tableName, + ObjectAlreadyExists $previous, + ): self { + return new self(sprintf( + 'Index %s already exists on table %s.', + $previous->getObjectName()->toString(), + self::formatTableName($tableName), + ), previous: $previous); + } + + public static function indexDoesNotExist( + ?OptionallyQualifiedName $tableName, + ObjectDoesNotExist $previous, + ): self { + return new self(sprintf( + 'Index %s does not exist on table %s.', + $previous->getObjectName()->toString(), + self::formatTableName($tableName), + ), previous: $previous); + } + + public static function primaryKeyConstraintAlreadyExists(?OptionallyQualifiedName $tableName): self + { + return new self(sprintf( + 'Primary key constraint already exists on table %s.', + self::formatTableName($tableName), + )); + } + + public static function primaryKeyConstraintDoesNotExist(?OptionallyQualifiedName $tableName): self + { + return new self(sprintf( + 'Primary key constraint does not exist on table %s.', + self::formatTableName($tableName), + )); + } + + public static function uniqueConstraintAlreadyExists( + ?OptionallyQualifiedName $tableName, + ObjectAlreadyExists $previous, + ): self { + return new self(sprintf( + 'Unique constraint %s already exists on table %s.', + $previous->getObjectName()->toString(), + self::formatTableName($tableName), + ), previous: $previous); + } + + public static function uniqueConstraintDoesNotExist( + ?OptionallyQualifiedName $tableName, + ObjectDoesNotExist $previous, + ): self { + return new self(sprintf( + 'Unique constraint %s does not exist on table %s.', + $previous->getObjectName()->toString(), + self::formatTableName($tableName), + ), previous: $previous); + } + + public static function foreignKeyConstraintAlreadyExists( + ?OptionallyQualifiedName $tableName, + ObjectAlreadyExists $previous, + ): self { + return new self(sprintf( + 'Foreign key constraint %s already exists on table %s.', + $previous->getObjectName()->toString(), + self::formatTableName($tableName), + ), previous: $previous); + } + + public static function foreignKeyConstraintDoesNotExist( + ?OptionallyQualifiedName $tableName, + ObjectDoesNotExist $previous, + ): self { + return new self(sprintf( + 'Foreign key constraint %s does not exist on table %s.', + $previous->getObjectName()->toString(), + self::formatTableName($tableName), + ), previous: $previous); + } + + public static function indexedColumnDoesNotExist( + ?OptionallyQualifiedName $tableName, + UnqualifiedName $indexName, + UnqualifiedName $columnName, + ): self { + return new self(sprintf( + 'Column %s referenced by index %s does not exist on table %s.', + $columnName->toString(), + $indexName->toString(), + self::formatTableName($tableName), + )); + } + + public static function primaryKeyConstraintColumnDoesNotExist( + ?OptionallyQualifiedName $tableName, + ?UnqualifiedName $constraintName, + UnqualifiedName $columnName, + ): self { + return new self(sprintf( + 'Column %s referenced by primary key constraint %s does not exist on table %s.', + $columnName->toString(), + self::formatConstraintName($constraintName), + self::formatTableName($tableName), + )); + } + + public static function uniqueConstraintColumnDoesNotExist( + ?OptionallyQualifiedName $tableName, + ?UnqualifiedName $constraintName, + UnqualifiedName $columnName, + ): self { + return new self(sprintf( + 'Column %s referenced by unique constraint %s does not exist on table %s.', + $columnName->toString(), + self::formatConstraintName($constraintName), + self::formatTableName($tableName), + )); + } + + public static function foreignKeyConstraintReferencingColumnDoesNotExist( + ?OptionallyQualifiedName $tableName, + ?UnqualifiedName $constraintName, + UnqualifiedName $columnName, + ): self { + return new self(sprintf( + 'Referencing column %s of foreign key constraint %s does not exist on table %s.', + $columnName->toString(), + self::formatConstraintName($constraintName), + self::formatTableName($tableName), + )); + } + + private static function formatTableName(?OptionallyQualifiedName $tableName): string + { + return $tableName === null ? '' : $tableName->toString(); + } + + private static function formatConstraintName(?UnqualifiedName $constraintName): string + { + return $constraintName === null ? '' : $constraintName->toString(); + } +} diff --git a/doctrine/dbal/src/Schema/Exception/InvalidTableName.php b/doctrine/dbal/src/Schema/Exception/InvalidTableName.php index 763e920f6..0433b0cda 100644 --- a/doctrine/dbal/src/Schema/Exception/InvalidTableName.php +++ b/doctrine/dbal/src/Schema/Exception/InvalidTableName.php @@ -5,10 +5,11 @@ namespace Doctrine\DBAL\Schema\Exception; use Doctrine\DBAL\Schema\SchemaException; +use InvalidArgumentException; use function sprintf; -final class InvalidTableName extends SchemaException +final class InvalidTableName extends InvalidArgumentException implements SchemaException { public static function new(string $tableName): self { diff --git a/doctrine/dbal/src/Schema/Exception/InvalidUniqueConstraintDefinition.php b/doctrine/dbal/src/Schema/Exception/InvalidUniqueConstraintDefinition.php new file mode 100644 index 000000000..0807ab8fa --- /dev/null +++ b/doctrine/dbal/src/Schema/Exception/InvalidUniqueConstraintDefinition.php @@ -0,0 +1,22 @@ +' : $constraintName->toString(), + )); + } +} diff --git a/doctrine/dbal/src/Schema/Exception/InvalidViewDefinition.php b/doctrine/dbal/src/Schema/Exception/InvalidViewDefinition.php new file mode 100644 index 000000000..43c70a049 --- /dev/null +++ b/doctrine/dbal/src/Schema/Exception/InvalidViewDefinition.php @@ -0,0 +1,24 @@ +toString())); + } +} diff --git a/doctrine/dbal/src/Schema/Exception/NamedForeignKeyRequired.php b/doctrine/dbal/src/Schema/Exception/NamedForeignKeyRequired.php deleted file mode 100644 index 453286136..000000000 --- a/doctrine/dbal/src/Schema/Exception/NamedForeignKeyRequired.php +++ /dev/null @@ -1,29 +0,0 @@ -getName(), - implode(', ', $foreignKey->getColumns()), - $foreignKey->getForeignTableName(), - implode(', ', $foreignKey->getForeignColumns()), - ), - ); - } -} diff --git a/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php b/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php index 527e196ef..2bad73528 100644 --- a/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php +++ b/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php @@ -5,16 +5,14 @@ namespace Doctrine\DBAL\Schema\Exception; use Doctrine\DBAL\Schema\SchemaException; +use LogicException; use function sprintf; -final class NamespaceAlreadyExists extends SchemaException +final class NamespaceAlreadyExists extends LogicException implements SchemaException { public static function new(string $namespaceName): self { - return new self( - sprintf('The namespace with name "%s" already exists.', $namespaceName), - self::NAMESPACE_ALREADY_EXISTS, - ); + return new self(sprintf('The namespace with name "%s" already exists.', $namespaceName)); } } diff --git a/doctrine/dbal/src/Schema/Exception/NotImplemented.php b/doctrine/dbal/src/Schema/Exception/NotImplemented.php new file mode 100644 index 000000000..c7ccbd2bf --- /dev/null +++ b/doctrine/dbal/src/Schema/Exception/NotImplemented.php @@ -0,0 +1,18 @@ +'; + } + + return new self(sprintf( + 'Unable to introspect foreign key constraint %s on table "%s" because the referenced column names' + . ' are omitted, and the referenced table "%s" does not exist or does not have a primary key.', + $constraintReference, + $referencingTableName, + $referencedTableName, + )); + } +} diff --git a/doctrine/dbal/src/Schema/ForeignKeyConstraint.php b/doctrine/dbal/src/Schema/ForeignKeyConstraint.php index dc69c629a..dd042ba75 100644 --- a/doctrine/dbal/src/Schema/ForeignKeyConstraint.php +++ b/doctrine/dbal/src/Schema/ForeignKeyConstraint.php @@ -1,11 +1,25 @@ + * @final This class will be made final in DBAL 5.0. */ -class ForeignKeyConstraint extends AbstractAsset implements Constraint +class ForeignKeyConstraint extends AbstractOptionallyNamedObject { - /** - * Instance of the referencing table the foreign key constraint is associated with. - * - * @var Table - */ - protected $_localTable; - /** * Asset identifier instances of the referencing table column names the foreign key constraint is associated with. - * array($columnName => Identifier) * - * @var Identifier[] + * @deprecated + * + * @var non-empty-array */ - protected $_localColumnNames; + protected array $_localColumnNames; /** * Table or asset identifier instance of the referenced table name the foreign key constraint is associated with. * - * @var Table|Identifier + * @deprecated */ - protected $_foreignTableName; + protected Identifier $_foreignTableName; /** * Asset identifier instances of the referenced table column names the foreign key constraint is associated with. - * array($columnName => Identifier) * - * @var Identifier[] + * @deprecated + * + * @var non-empty-array */ - protected $_foreignColumnNames; + protected array $_foreignColumnNames; /** * Options associated with the foreign key constraint. * - * @var mixed[] + * @deprecated + * + * @var array + */ + protected array $options; + + /** + * Referencing table column names the foreign key constraint is associated with. + * + * An empty list indicates that an attempt to parse column names failed. + * + * @var list */ - protected $_options; + private readonly array $referencingColumnNames; /** - * Initializes the foreign key constraint. + * Referenced table name the foreign key constraint is associated with. * - * @param string[] $localColumnNames Names of the referencing table columns. - * @param Table|string $foreignTableName Referenced table. - * @param string[] $foreignColumnNames Names of the referenced table columns. - * @param string|null $name Name of the foreign key constraint. - * @param mixed[] $options Options associated with the foreign key constraint. + * A null value indicates that an attempt to parse the table name failed. + */ + private readonly ?OptionallyQualifiedName $referencedTableName; + + /** + * Referenced table column names the foreign key constraint is associated with. + * + * An empty list indicates that an attempt to parse column names failed. + * + * @var list + */ + private readonly array $referencedColumnNames; + + /** + * The match type of the foreign key constraint. + * + * A null value indicates that an attempt to parse the match type failed. + */ + private readonly ?MatchType $matchType; + + /** + * The referential action for UPDATE operations. + * + * A null value indicates that an attempt to parse the referential action failed. + */ + private readonly ?ReferentialAction $onUpdateAction; + + /** + * The referential action for DELETE operations. + * + * A null value indicates that an attempt to parse the referential action failed. + */ + private readonly ?ReferentialAction $onDeleteAction; + + /** + * Indicates whether the constraint is or can be deferred. + * + * A null value indicates that the combination of the options that defined deferrability was invalid. + */ + private readonly ?Deferrability $deferrability; + + /** + * @internal Use {@link ForeignKeyConstraint::editor()} to instantiate an editor and + * {@link ForeignKeyConstraintEditor::create()} to create a foreign key constraint. + * + * @param non-empty-list $localColumnNames Names of the referencing table columns. + * @param string $foreignTableName Referenced table. + * @param non-empty-list $foreignColumnNames Names of the referenced table columns. + * @param string $name Name of the foreign key constraint. + * @param array $options Options associated with the foreign key constraint. */ public function __construct( array $localColumnNames, - $foreignTableName, + string $foreignTableName, array $foreignColumnNames, - $name = null, - array $options = [] + string $name = '', + array $options = [], ) { - if ($name !== null) { - $this->_setName($name); + $this->options = $options; + + if (count($localColumnNames) < 1) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + 'Instantiation of a foreign key constraint without local column names is deprecated.', + ); } - $this->_localColumnNames = $this->createIdentifierMap($localColumnNames); + if (count($foreignColumnNames) < 1) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + 'Instantiation of a foreign key constraint without foreign column names is deprecated.', + ); + } - if ($foreignTableName instanceof Table) { - $this->_foreignTableName = $foreignTableName; - } else { - $this->_foreignTableName = new Identifier($foreignTableName); + if (count($foreignColumnNames) !== count($localColumnNames)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + 'Instantiation of a foreign key constraint with a different number of local and foreign' + . ' column names is deprecated.', + ); } + parent::__construct($name); + + $this->_localColumnNames = $this->createIdentifierMap($localColumnNames); + $this->_foreignTableName = new Identifier($foreignTableName); + $this->_foreignColumnNames = $this->createIdentifierMap($foreignColumnNames); - $this->_options = $options; + + $this->referencingColumnNames = $this->parseColumnNames($localColumnNames); + $this->referencedTableName = $this->parseReferencedTableName($foreignTableName); + $this->referencedColumnNames = $this->parseColumnNames($foreignColumnNames); + + $this->matchType = $this->parseMatchType($options); + $this->onUpdateAction = $this->parseReferentialAction($options, 'onUpdate'); + $this->onDeleteAction = $this->parseReferentialAction($options, 'onDelete'); + + $this->deferrability = $this->parseDeferrability($options); + } + + protected function getNameParser(): UnqualifiedNameParser + { + return Parsers::getUnqualifiedNameParser(); } /** - * @param string[] $names + * Returns the names of the referencing table columns the foreign key constraint is associated with. * - * @return Identifier[] + * @return non-empty-list */ - private function createIdentifierMap(array $names): array + public function getReferencingColumnNames(): array { - $identifiers = []; + if (count($this->referencingColumnNames) < 1) { + throw InvalidState::foreignKeyConstraintHasInvalidReferencingColumnNames($this->getName()); + } - foreach ($names as $name) { - $identifiers[$name] = new Identifier($name); + return $this->referencingColumnNames; + } + + /** + * Returns the names of the referenced table columns the foreign key constraint is associated with. + */ + public function getReferencedTableName(): OptionallyQualifiedName + { + if ($this->referencedTableName === null) { + throw InvalidState::foreignKeyConstraintHasInvalidReferencedTableName($this->getName()); } - return $identifiers; + return $this->referencedTableName; } /** - * Returns the name of the referencing table - * the foreign key constraint is associated with. - * - * @deprecated Use the table that contains the foreign key as part of its {@see Table::$_fkConstraints} instead. + * Returns the names of the referenced table columns the foreign key constraint is associated with. * - * @return string + * @return non-empty-list */ - public function getLocalTableName() + public function getReferencedColumnNames(): array { - return $this->_localTable->getName(); + if (count($this->referencedColumnNames) < 1) { + throw InvalidState::foreignKeyConstraintHasInvalidReferencedColumnNames($this->getName()); + } + + return $this->referencedColumnNames; } /** - * Sets the Table instance of the referencing table - * the foreign key constraint is associated with. - * - * @deprecated Use the table that contains the foreign key as part of its {@see Table::$_fkConstraints} instead. - * - * @param Table $table Instance of the referencing table. - * - * @return void + * Returns the match type of the foreign key constraint. + */ + public function getMatchType(): MatchType + { + if ($this->matchType === null) { + throw InvalidState::foreignKeyConstraintHasInvalidMatchType($this->getName()); + } + + return $this->matchType; + } + + /** + * Returns the referential action for UPDATE operations. + */ + public function getOnUpdateAction(): ReferentialAction + { + if ($this->onUpdateAction === null) { + throw InvalidState::foreignKeyConstraintHasInvalidOnUpdateAction($this->getName()); + } + + return $this->onUpdateAction; + } + + /** + * Returns the referential action for DELETE operations. + */ + public function getOnDeleteAction(): ReferentialAction + { + if ($this->onDeleteAction === null) { + throw InvalidState::foreignKeyConstraintHasInvalidOnDeleteAction($this->getName()); + } + + return $this->onDeleteAction; + } + + /** + * Returns whether the constraint is or can be deferred. */ - public function setLocalTable(Table $table) + public function getDeferrability(): Deferrability { - $this->_localTable = $table; + if ($this->deferrability === null) { + throw InvalidState::foreignKeyConstraintHasInvalidDeferrability($this->getName()); + } + + return $this->deferrability; } /** - * @deprecated Use the table that contains the foreign key as part of its {@see Table::$_fkConstraints} instead. + * @param non-empty-array $names * - * @return Table + * @return non-empty-array */ - public function getLocalTable() + private function createIdentifierMap(array $names): array { - return $this->_localTable; + $identifiers = []; + + foreach ($names as $name) { + $identifiers[$name] = new Identifier($name); + } + + return $identifiers; } /** * Returns the names of the referencing table columns * the foreign key constraint is associated with. * - * @return string[] + * @deprecated Use {@see getReferencingColumnNames()} instead. + * + * @return non-empty-list */ - public function getLocalColumns() + public function getLocalColumns(): array { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated. Use getReferencingColumnNames() instead.', + __METHOD__, + ); + return array_keys($this->_localColumnNames); } /** + * @deprecated Use {@see getReferencingColumnNames()} and {@see UnqualifiedName::toSQL()} instead. + * * Returns the quoted representation of the referencing table column names * the foreign key constraint is associated with. * @@ -160,10 +323,17 @@ public function getLocalColumns() * * @param AbstractPlatform $platform The platform to use for quotation. * - * @return string[] + * @return non-empty-array */ - public function getQuotedLocalColumns(AbstractPlatform $platform) + public function getQuotedLocalColumns(AbstractPlatform $platform): array { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated. Use getReferencingColumnNames() and UnqualifiedName::toSQL() instead.', + __METHOD__, + ); + $columns = []; foreach ($this->_localColumnNames as $column) { @@ -174,76 +344,75 @@ public function getQuotedLocalColumns(AbstractPlatform $platform) } /** - * Returns unquoted representation of local table column names for comparison with other FK + * @deprecated Use {@see getReferencingColumnNames()} instead. * - * @return string[] - */ - public function getUnquotedLocalColumns() - { - return array_map([$this, 'trimQuotes'], $this->getLocalColumns()); - } - - /** - * Returns unquoted representation of foreign table column names for comparison with other FK + * Returns unquoted representation of local table column names for comparison with other FK * - * @return string[] + * @return non-empty-array */ - public function getUnquotedForeignColumns() + public function getUnquotedLocalColumns(): array { - return array_map([$this, 'trimQuotes'], $this->getForeignColumns()); + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated. Use getReferencingColumnNames() instead.', + __METHOD__, + ); + + return array_map($this->trimQuotes(...), $this->getLocalColumns()); } /** - * {@inheritDoc} + * @deprecated Use {@see getReferencedColumnNames()} instead. * - * @deprecated Use {@see getLocalColumns()} instead. + * Returns unquoted representation of foreign table column names for comparison with other FK * - * @see getLocalColumns + * @return non-empty-array */ - public function getColumns() + public function getUnquotedForeignColumns(): array { - return $this->getLocalColumns(); + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated. Use getReferencedColumnNames() instead.', + __METHOD__, + ); + + return array_map($this->trimQuotes(...), $this->getForeignColumns()); } /** - * Returns the quoted representation of the referencing table column names - * the foreign key constraint is associated with. - * - * But only if they were defined with one or the referencing table column name - * is a keyword reserved by the platform. - * Otherwise the plain unquoted value as inserted is returned. - * - * @deprecated Use {@see getQuotedLocalColumns()} instead. - * - * @see getQuotedLocalColumns + * @deprecated Use {@see getReferencedTableName()} instead. * - * @param AbstractPlatform $platform The platform to use for quotation. - * - * @return string[] - */ - public function getQuotedColumns(AbstractPlatform $platform) - { - return $this->getQuotedLocalColumns($platform); - } - - /** * Returns the name of the referenced table * the foreign key constraint is associated with. - * - * @return string */ - public function getForeignTableName() + public function getForeignTableName(): string { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated. Use getReferencedTableName() instead.', + __METHOD__, + ); + return $this->_foreignTableName->getName(); } /** - * Returns the non-schema qualified foreign table name. + * @deprecated Use {@see getReferencedTableName()} instead. * - * @return string + * Returns the non-schema qualified foreign table name. */ - public function getUnqualifiedForeignTableName() + public function getUnqualifiedForeignTableName(): string { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated. Use getReferencedTableName() instead.', + __METHOD__, + ); + $name = $this->_foreignTableName->getName(); $position = strrpos($name, '.'); @@ -259,6 +428,8 @@ public function getUnqualifiedForeignTableName() } /** + * @deprecated Use {@see getReferencedTableName()} and {@see OptionallyQualifiedName::toSQL()} instead. + * * Returns the quoted representation of the referenced table name * the foreign key constraint is associated with. * @@ -267,26 +438,42 @@ public function getUnqualifiedForeignTableName() * Otherwise the plain unquoted value as inserted is returned. * * @param AbstractPlatform $platform The platform to use for quotation. - * - * @return string */ - public function getQuotedForeignTableName(AbstractPlatform $platform) + public function getQuotedForeignTableName(AbstractPlatform $platform): string { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated. Use getReferencedTableName() and OptionallyQualifiedName::toSQL() instead.', + __METHOD__, + ); + return $this->_foreignTableName->getQuotedName($platform); } /** + * @deprecated Use {@see getReferencedColumnNames()} instead. + * * Returns the names of the referenced table columns * the foreign key constraint is associated with. * - * @return string[] + * @return non-empty-array */ - public function getForeignColumns() + public function getForeignColumns(): array { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated. Use getReferencedColumnNames() instead.', + __METHOD__, + ); + return array_keys($this->_foreignColumnNames); } /** + * @deprecated Use {@see getReferencedColumnNames()} and {@see UnqualifiedName::toSQL()} instead. + * * Returns the quoted representation of the referenced table column names * the foreign key constraint is associated with. * @@ -296,10 +483,17 @@ public function getForeignColumns() * * @param AbstractPlatform $platform The platform to use for quotation. * - * @return string[] + * @return non-empty-array */ - public function getQuotedForeignColumns(AbstractPlatform $platform) + public function getQuotedForeignColumns(AbstractPlatform $platform): array { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated. Use getReferencedColumnNames() and UnqualifiedName::toSQL() instead.', + __METHOD__, + ); + $columns = []; foreach ($this->_foreignColumnNames as $column) { @@ -310,72 +504,236 @@ public function getQuotedForeignColumns(AbstractPlatform $platform) } /** + * @deprecated Use {@see getMatchType()}, {@see getOnDeleteAction()}, {@see getOnUpdateAction()} or + * {@see getDeferrability()} instead. + * * Returns whether or not a given option * is associated with the foreign key constraint. - * - * @param string $name Name of the option to check. - * - * @return bool */ - public function hasOption($name) + public function hasOption(string $name): bool { - return isset($this->_options[$name]); + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated. Use getMatchType(), getOnDeleteAction(), getOnUpdateAction() or' + . ' getDeferrability() instead.', + __METHOD__, + ); + + return isset($this->options[$name]); } /** - * Returns an option associated with the foreign key constraint. - * - * @param string $name Name of the option the foreign key constraint is associated with. + * @deprecated Use {@see getMatchType()}, {@see getOnDeleteAction()}, {@see getOnUpdateAction()} or + * {@see getDeferrability()} instead. * - * @return mixed + * Returns an option associated with the foreign key constraint. */ - public function getOption($name) + public function getOption(string $name): mixed { - return $this->_options[$name]; + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated. Use getMatchType(), getOnDeleteAction(), getOnUpdateAction() or' + . ' getDeferrability() instead.', + __METHOD__, + ); + + return $this->options[$name]; } /** + * @deprecated Use {@see getMatchType()}, {@see getOnDeleteAction()}, {@see getOnUpdateAction()} or + * {@see getDeferrability()} instead. + * * Returns the options associated with the foreign key constraint. * - * @return mixed[] + * @return array */ - public function getOptions() + public function getOptions(): array { - return $this->_options; + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated. Use getMatchType(), getOnDeleteAction(), getOnUpdateAction() or' + . ' getDeferrability() instead.', + __METHOD__, + ); + + return $this->options; } /** + * @deprecated Use {@see getOnUpdateAction()} instead. + * * Returns the referential action for UPDATE operations * on the referenced table the foreign key constraint is associated with. - * - * @return string|null */ - public function onUpdate() + public function onUpdate(): ?string { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated. Use getOnUpdateAction() instead.', + __METHOD__, + ); + return $this->onEvent('onUpdate'); } /** + * @deprecated Use {@see getOnDeleteAction()} instead. + * * Returns the referential action for DELETE operations * on the referenced table the foreign key constraint is associated with. - * - * @return string|null */ - public function onDelete() + public function onDelete(): ?string { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated. Use getOnDeleteAction() instead.', + __METHOD__, + ); + return $this->onEvent('onDelete'); } + private function parseReferencedTableName(string $referencedTableName): ?OptionallyQualifiedName + { + $parser = Parsers::getOptionallyQualifiedNameParser(); + + try { + return $parser->parse($referencedTableName); + } catch (Throwable $e) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + 'Unable to parse referenced table name: %s.', + $e->getMessage(), + ); + + return null; + } + } + + /** + * @param list $columnNames + * + * @return list + */ + private function parseColumnNames(array $columnNames): array + { + $parser = Parsers::getUnqualifiedNameParser(); + + try { + return array_map( + static fn (string $columnName) => $parser->parse($columnName), + $columnNames, + ); + } catch (Throwable $e) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + 'Unable to parse column name: %s.', + $e->getMessage(), + ); + + return []; + } + } + + /** @param array $options */ + private function parseMatchType(array $options): ?MatchType + { + if (isset($options['match'])) { + try { + /** + * This looks like a PHPStan bug. + * + * @phpstan-ignore missingType.checkedException + */ + return MatchType::from(strtoupper($options['match'])); + } catch (ValueError $e) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + 'Unable to parse match type: %s.', + $e->getMessage(), + ); + + return null; + } + } + + return MatchType::SIMPLE; + } + + /** @param array $options */ + private function parseReferentialAction(array $options, string $option): ?ReferentialAction + { + if (isset($options[$option])) { + try { + /** + * This looks like a PHPStan bug. + * + * @phpstan-ignore missingType.checkedException + */ + return ReferentialAction::from(strtoupper($options[$option])); + } catch (ValueError $e) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + 'Unable to parse referential action: %s.', + $e->getMessage(), + ); + + return null; + } + } + + return ReferentialAction::NO_ACTION; + } + + /** @param array $options */ + private function parseDeferrability(array $options): ?Deferrability + { + // a constraint is INITIALLY IMMEDIATE unless explicitly declared as INITIALLY DEFERRED + $isDeferred = isset($options['deferred']) && $options['deferred'] !== false; + + // a constraint is NOT DEFERRABLE unless explicitly declared as DEFERRABLE or is explicitly or implicitly + // INITIALLY DEFERRED + $isDeferrable = isset($options['deferrable']) + ? $options['deferrable'] !== false + : $isDeferred; + + if ($isDeferred) { + if (! $isDeferrable) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + 'Declaring a constraint as NOT DEFERRABLE INITIALLY DEFERRED is deprecated', + ); + + return null; + } + + return Deferrability::DEFERRED; + } + + return $isDeferrable ? Deferrability::DEFERRABLE : Deferrability::NOT_DEFERRABLE; + } + /** * Returns the referential action for a given database operation * on the referenced table the foreign key constraint is associated with. * * @param string $event Name of the database operation/event to return the referential action for. */ - private function onEvent($event): ?string + private function onEvent(string $event): ?string { - if (isset($this->_options[$event])) { - $onEvent = strtoupper($this->_options[$event]); + if (isset($this->options[$event])) { + $onEvent = strtoupper($this->options[$event]); if ($onEvent !== 'NO ACTION' && $onEvent !== 'RESTRICT') { return $onEvent; @@ -386,17 +744,24 @@ private function onEvent($event): ?string } /** + * @deprecated + * * Checks whether this foreign key constraint intersects the given index columns. * * Returns `true` if at least one of this foreign key's local columns * matches one of the given index's columns, `false` otherwise. * * @param Index $index The index to be checked against. - * - * @return bool */ - public function intersectsIndexColumns(Index $index) + public function intersectsIndexColumns(Index $index): bool { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6728', + '%s is deprecated.', + __METHOD__, + ); + foreach ($index->getColumns() as $indexColumn) { foreach ($this->_localColumnNames as $localColumn) { if (strtolower($indexColumn) === strtolower($localColumn->getName())) { @@ -407,4 +772,28 @@ public function intersectsIndexColumns(Index $index) return false; } + + /** + * Instantiates a new foreign key constraint editor. + */ + public static function editor(): ForeignKeyConstraintEditor + { + return new ForeignKeyConstraintEditor(); + } + + /** + * Instantiates a new foreign key constraint editor and initializes it with the constraint's properties. + */ + public function edit(): ForeignKeyConstraintEditor + { + return self::editor() + ->setName($this->getObjectName()) + ->setReferencedTableName($this->getReferencedTableName()) + ->setReferencingColumnNames(...$this->getReferencingColumnNames()) + ->setReferencedColumnNames(...$this->getReferencedColumnNames()) + ->setMatchType($this->getMatchType()) + ->setOnDeleteAction($this->getOnDeleteAction()) + ->setOnUpdateAction($this->getOnUpdateAction()) + ->setDeferrability($this->getDeferrability()); + } } diff --git a/doctrine/dbal/src/Schema/ForeignKeyConstraint/Deferrability.php b/doctrine/dbal/src/Schema/ForeignKeyConstraint/Deferrability.php new file mode 100644 index 000000000..4bd09b908 --- /dev/null +++ b/doctrine/dbal/src/Schema/ForeignKeyConstraint/Deferrability.php @@ -0,0 +1,23 @@ +value; + } +} diff --git a/doctrine/dbal/src/Schema/ForeignKeyConstraint/MatchType.php b/doctrine/dbal/src/Schema/ForeignKeyConstraint/MatchType.php new file mode 100644 index 000000000..be67fdabc --- /dev/null +++ b/doctrine/dbal/src/Schema/ForeignKeyConstraint/MatchType.php @@ -0,0 +1,33 @@ +" + * @link https://dev.mysql.com/doc/refman/8.4/en/constraint-foreign-key.html + * @link https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-REFERENCES + * @link https://www.sqlite.org/foreignkeys.html + */ +enum MatchType: string +{ + case FULL = 'FULL'; + case PARTIAL = 'PARTIAL'; + + /** + * The SIMPLE match type is not part of the SQL-92 standard but is supported by and is the default + * for MySQL, PostgreSQL and SQLite. + */ + case SIMPLE = 'SIMPLE'; + + /** + * Returns the SQL representation of the match type. + */ + public function toSQL(): string + { + return $this->value; + } +} diff --git a/doctrine/dbal/src/Schema/ForeignKeyConstraint/ReferentialAction.php b/doctrine/dbal/src/Schema/ForeignKeyConstraint/ReferentialAction.php new file mode 100644 index 000000000..b56580706 --- /dev/null +++ b/doctrine/dbal/src/Schema/ForeignKeyConstraint/ReferentialAction.php @@ -0,0 +1,38 @@ +" + * @link https://dev.mysql.com/doc/refman/8.4/en/constraint-foreign-key.html + * @link https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-REFERENCES + * @link https://learn.microsoft.com/en-us/sql/relational-databases/tables/primary-and-foreign-key-constraints#cascading-referential-integrity + * @link https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/constraint.html + * @link https://www.ibm.com/docs/en/db2/11.5?topic=constraints-foreign-key-referential + * @link https://www.sqlite.org/foreignkeys.html + */ +enum ReferentialAction: string +{ + case CASCADE = 'CASCADE'; + case NO_ACTION = 'NO ACTION'; + case SET_DEFAULT = 'SET DEFAULT'; + case SET_NULL = 'SET NULL'; + + /** + * The RESTRICT referential action is not part of the SQL-92 standard but is supported by MySQL, + * PostgreSQL, IBM DB2 and SQLite. + */ + case RESTRICT = 'RESTRICT'; + + /** + * Returns the SQL representation of the referential action. + */ + public function toSQL(): string + { + return $this->value; + } +} diff --git a/doctrine/dbal/src/Schema/ForeignKeyConstraintEditor.php b/doctrine/dbal/src/Schema/ForeignKeyConstraintEditor.php new file mode 100644 index 000000000..d8378be61 --- /dev/null +++ b/doctrine/dbal/src/Schema/ForeignKeyConstraintEditor.php @@ -0,0 +1,275 @@ + */ + private array $referencingColumnNames = []; + + private ?OptionallyQualifiedName $referencedTableName = null; + + /** @var list */ + private array $referencedColumnNames = []; + + private MatchType $matchType = MatchType::SIMPLE; + + private ReferentialAction $onUpdateAction = ReferentialAction::NO_ACTION; + + private ReferentialAction $onDeleteAction = ReferentialAction::NO_ACTION; + + private Deferrability $deferrability = Deferrability::NOT_DEFERRABLE; + + /** + * @internal Use {@link ForeignKeyConstraint::editor()} or {@link ForeignKeyConstraint::edit()} to create + * an instance. + */ + public function __construct() + { + } + + public function setName(?UnqualifiedName $name): self + { + $this->name = $name; + + return $this; + } + + /** @param non-empty-string $name */ + public function setUnquotedName(string $name): self + { + $this->name = UnqualifiedName::unquoted($name); + + return $this; + } + + /** @param non-empty-string $name */ + public function setQuotedName(string $name): self + { + $this->name = UnqualifiedName::quoted($name); + + return $this; + } + + public function setReferencingColumnNames( + UnqualifiedName $firstColumnName, + UnqualifiedName ...$otherColumnNames, + ): self { + $this->referencingColumnNames = [$firstColumnName, ...array_values($otherColumnNames)]; + + return $this; + } + + /** + * @param non-empty-string $firstColumnName + * @param non-empty-string ...$otherColumnNames + */ + public function setUnquotedReferencingColumnNames( + string $firstColumnName, + string ...$otherColumnNames, + ): self { + $this->referencingColumnNames = array_map( + static fn (string $name): UnqualifiedName => UnqualifiedName::unquoted($name), + [$firstColumnName, ...array_values($otherColumnNames)], + ); + + return $this; + } + + /** + * @param non-empty-string $firstColumnName + * @param non-empty-string ...$otherColumnNames + */ + public function setQuotedReferencingColumnNames( + string $firstColumnName, + string ...$otherColumnNames, + ): self { + $this->referencingColumnNames = array_map( + static fn (string $name): UnqualifiedName => UnqualifiedName::quoted($name), + [$firstColumnName, ...array_values($otherColumnNames)], + ); + + return $this; + } + + public function addReferencingColumnName(UnqualifiedName $columName): self + { + $this->referencingColumnNames[] = $columName; + + return $this; + } + + public function setReferencedTableName(OptionallyQualifiedName $referencedTableName): self + { + $this->referencedTableName = $referencedTableName; + + return $this; + } + + /** + * @param non-empty-string $unqualifiedReferencedTableName + * @param ?non-empty-string $referencedTableNameQualifier + */ + public function setUnquotedReferencedTableName( + string $unqualifiedReferencedTableName, + ?string $referencedTableNameQualifier = null, + ): self { + $this->referencedTableName = + OptionallyQualifiedName::unquoted($unqualifiedReferencedTableName, $referencedTableNameQualifier); + + return $this; + } + + /** + * @param non-empty-string $unqualifiedReferencedTableName + * @param ?non-empty-string $referencedTableNameQualifier + */ + public function setQuotedReferencedTableName( + string $unqualifiedReferencedTableName, + ?string $referencedTableNameQualifier = null, + ): self { + $this->referencedTableName = + OptionallyQualifiedName::quoted($unqualifiedReferencedTableName, $referencedTableNameQualifier); + + return $this; + } + + public function setReferencedColumnNames( + UnqualifiedName $firstColumnName, + UnqualifiedName ...$otherColumnNames, + ): self { + $this->referencedColumnNames = [$firstColumnName, ...array_values($otherColumnNames)]; + + return $this; + } + + /** + * @param non-empty-string $firstColumnName + * @param non-empty-string ...$otherColumnNames + */ + public function setUnquotedReferencedColumnNames( + string $firstColumnName, + string ...$otherColumnNames, + ): self { + $this->referencedColumnNames = array_map( + static fn (string $name): UnqualifiedName => UnqualifiedName::unquoted($name), + [$firstColumnName, ...array_values($otherColumnNames)], + ); + + return $this; + } + + /** + * @param non-empty-string $firstColumnName + * @param non-empty-string ...$otherColumnNames + */ + public function setQuotedReferencedColumnNames( + string $firstColumnName, + string ...$otherColumnNames, + ): self { + $this->referencedColumnNames = array_map( + static fn (string $name): UnqualifiedName => UnqualifiedName::quoted($name), + [$firstColumnName, ...array_values($otherColumnNames)], + ); + + return $this; + } + + public function addReferencedColumnName(UnqualifiedName $columName): self + { + $this->referencedColumnNames[] = $columName; + + return $this; + } + + public function setMatchType(MatchType $matchType): self + { + $this->matchType = $matchType; + + return $this; + } + + public function setOnUpdateAction(ReferentialAction $action): self + { + $this->onUpdateAction = $action; + + return $this; + } + + public function setOnDeleteAction(ReferentialAction $action): self + { + $this->onDeleteAction = $action; + + return $this; + } + + public function setDeferrability(Deferrability $deferrability): self + { + $this->deferrability = $deferrability; + + return $this; + } + + public function create(): ForeignKeyConstraint + { + if (count($this->referencingColumnNames) < 1) { + throw InvalidForeignKeyConstraintDefinition::referencingColumnNamesNotSet($this->name); + } + + if ($this->referencedTableName === null) { + throw InvalidForeignKeyConstraintDefinition::referencedTableNameNotSet($this->name); + } + + if (count($this->referencedColumnNames) < 1) { + throw InvalidForeignKeyConstraintDefinition::referencedColumnNamesNotSet($this->name); + } + + $options = []; + + if ($this->matchType !== MatchType::SIMPLE) { + $options['match'] = $this->matchType->value; + } + + if ($this->onUpdateAction !== ReferentialAction::NO_ACTION) { + $options['onUpdate'] = $this->onUpdateAction->value; + } + + if ($this->onDeleteAction !== ReferentialAction::NO_ACTION) { + $options['onDelete'] = $this->onDeleteAction->value; + } + + return new ForeignKeyConstraint( + array_map( + static fn (UnqualifiedName $columnName) => $columnName->toString(), + $this->referencingColumnNames, + ), + $this->referencedTableName->toString(), + array_map( + static fn (UnqualifiedName $columnName) => $columnName->toString(), + $this->referencedColumnNames, + ), + $this->name?->toString() ?? '', + array_merge($options, match ($this->deferrability) { + Deferrability::NOT_DEFERRABLE => [], + Deferrability::DEFERRABLE => ['deferrable' => true], + Deferrability::DEFERRED => ['deferrable' => true, 'deferred' => true], + }), + ); + } +} diff --git a/doctrine/dbal/src/Schema/Identifier.php b/doctrine/dbal/src/Schema/Identifier.php index f34465e9e..bf27dc83f 100644 --- a/doctrine/dbal/src/Schema/Identifier.php +++ b/doctrine/dbal/src/Schema/Identifier.php @@ -1,27 +1,40 @@ */ -class Identifier extends AbstractAsset +class Identifier extends AbstractNamedObject { /** * @param string $identifier Identifier name to wrap. * @param bool $quote Whether to force quoting the given identifier. */ - public function __construct($identifier, $quote = false) + public function __construct(string $identifier, bool $quote = false) { - $this->_setName($identifier); + parent::__construct($identifier); - if (! $quote || $this->_quoted) { - return; + if ($quote && ! $this->_quoted) { + $this->_setName('"' . $this->getName() . '"'); } + } - $this->_setName('"' . $this->getName() . '"'); + protected function getNameParser(): GenericNameParser + { + return Parsers::getGenericNameParser(); } } diff --git a/doctrine/dbal/src/Schema/Index.php b/doctrine/dbal/src/Schema/Index.php index 84fac4146..777348fab 100644 --- a/doctrine/dbal/src/Schema/Index.php +++ b/doctrine/dbal/src/Schema/Index.php @@ -1,9 +1,18 @@ + */ +class Index extends AbstractNamedObject { /** * Asset identifier instances of the column names the index is associated with. - * array($columnName => Identifier) * - * @var Identifier[] + * @deprecated Use {@see getIndexedColumns()} instead. + * + * @var array */ - protected $_columns = []; + protected array $_columns = []; - /** @var bool */ - protected $_isUnique = false; + /** @deprecated Use {@see getType()} and compare with {@see IndexType::UNIQUE} instead. */ + protected bool $_isUnique = false; - /** @var bool */ - protected $_isPrimary = false; + /** @deprecated Use {@see PrimaryKeyConstraint} instead. */ + protected bool $_isPrimary = false; /** * Platform specific flags for indexes. - * array($flagName => true) * - * @var true[] + * @deprecated + * + * @var array + */ + protected array $_flags = []; + + /** + * Column the index is associated with. + * + * An empty list indicates that an attempt to parse indexed columns failed. + * + * @var list */ - protected $_flags = []; + private readonly array $columns; /** - * Platform specific options + * Index type. * - * @todo $_flags should eventually be refactored into options - * @var mixed[] + * A null value indicates that an attempt to parse the index type failed. */ - private array $options = []; + private ?IndexType $type = null; + + private ?string $predicate = null; + + private bool $failedToParsePredicate = false; /** - * @param string $name - * @param string[] $columns - * @param bool $isUnique - * @param bool $isPrimary - * @param string[] $flags - * @param mixed[] $options + * @internal Use {@link Index::editor()} to instantiate an editor and {@link IndexEditor::create()} to create an + * index. + * + * @param non-empty-list $columns + * @param array $flags + * @param array $options */ public function __construct( - $name, + ?string $name, array $columns, - $isUnique = false, - $isPrimary = false, + bool $isUnique = false, + bool $isPrimary = false, array $flags = [], - array $options = [] + private readonly array $options = [], ) { - $isUnique = $isUnique || $isPrimary; + parent::__construct($name ?? ''); + + if (count($columns) < 1) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6787', + 'Instantiation of an index without column names is deprecated.', + ); + } - $this->_setName($name); - $this->_isUnique = $isUnique; + if ($isPrimary) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6867', + 'Declaring an index as primary is deprecated. Use PrimaryKeyConstraint instead.', + ); + } + + $this->_isUnique = $isUnique || $isPrimary; $this->_isPrimary = $isPrimary; - $this->options = $options; foreach ($columns as $column) { $this->_addColumn($column); } + if (isset($options['where'])) { + $predicate = $options['where']; + + if (strlen($predicate) === 0) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + 'Passing an empty string as index predicate is deprecated.', + ); + + $this->failedToParsePredicate = true; + } else { + $this->predicate = $predicate; + } + } + foreach ($flags as $flag) { $this->addFlag($flag); } + + if (count($flags) === 0) { + $this->type = $this->inferType(); + } + + $this->columns = $this->parseColumns($isPrimary, $columns, $options['lengths'] ?? []); + } + + protected function getNameParser(): UnqualifiedNameParser + { + return Parsers::getUnqualifiedNameParser(); + } + + public function getType(): IndexType + { + if ($this->type === null) { + throw InvalidState::indexHasInvalidType($this->getName()); + } + + return $this->type; + } + + /** + * Returns the indexed columns. + * + * @return non-empty-list + */ + public function getIndexedColumns(): array + { + if (count($this->columns) < 1) { + throw InvalidState::indexHasInvalidColumns($this->getName()); + } + + return $this->columns; + } + + /** + * Returns whether the index is clustered. + */ + public function isClustered(): bool + { + return $this->hasFlag('clustered'); + } + + /** + * Returns the index predicate. + * + * @return ?non-empty-string + */ + public function getPredicate(): ?string + { + if ($this->failedToParsePredicate) { + throw InvalidState::indexHasInvalidPredicate($this->getName()); + } + + return $this->hasOption('where') + ? $this->getOption('where') + : null; } - /** @throws InvalidArgumentException */ protected function _addColumn(string $column): void { $this->_columns[$column] = new Identifier($column); } /** - * {@inheritDoc} + * Returns the names of the referencing table columns the constraint is associated with. + * + * @deprecated Use {@see getIndexedColumns()} instead. + * + * @return non-empty-list */ - public function getColumns() + public function getColumns(): array { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated. Use Index::getIndexedColumns() instead.', + __METHOD__, + ); + + /** @phpstan-ignore return.type */ return array_keys($this->_columns); } /** - * {@inheritDoc} + * Returns the quoted representation of the column names the constraint is associated with. + * + * But only if they were defined with one or a column name + * is a keyword reserved by the platform. + * Otherwise, the plain unquoted value as inserted is returned. + * + * @deprecated Use {@see getIndexedColumns()} instead. + * + * @param AbstractPlatform $platform The platform to use for quotation. + * + * @return list */ - public function getQuotedColumns(AbstractPlatform $platform) + public function getQuotedColumns(AbstractPlatform $platform): array { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated. Use Index::getIndexedColumns() instead.', + __METHOD__, + ); + $subParts = $platform->supportsColumnLengthIndexes() && $this->hasOption('lengths') ? $this->getOption('lengths') : []; @@ -116,42 +264,75 @@ public function getQuotedColumns(AbstractPlatform $platform) return $columns; } - /** @return string[] */ - public function getUnquotedColumns() + /** + * @deprecated Use {@see getIndexedColumns()} instead. + * + * @return non-empty-list + */ + public function getUnquotedColumns(): array { - return array_map([$this, 'trimQuotes'], $this->getColumns()); + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated. Use Index::getIndexedColumns() instead.', + __METHOD__, + ); + + return array_map($this->trimQuotes(...), $this->getColumns()); } /** * Is the index neither unique nor primary key? * - * @return bool + * @deprecated Use {@see getType()} and compare with {@see IndexType::REGULAR} instead. */ - public function isSimpleIndex() + public function isSimpleIndex(): bool { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated. Use Index::getType() and compare with IndexType::REGULAR instead.', + __METHOD__, + ); + return ! $this->_isPrimary && ! $this->_isUnique; } - /** @return bool */ - public function isUnique() + /** @deprecated Use {@see getType()} and compare with {@see IndexType::UNIQUE} instead. */ + public function isUnique(): bool { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated. Use Index::getType() and compare with IndexType::UNIQUE instead.', + __METHOD__, + ); + return $this->_isUnique; } - /** @return bool */ - public function isPrimary() + /** @deprecated Use {@see PrimaryKeyConstraint} instead. */ + public function isPrimary(): bool { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6867', + 'Checking whether an index is primary is deprecated. Use PrimaryKeyConstraint instead.', + ); + return $this->_isPrimary; } - /** - * @param string $name - * @param int $pos - * - * @return bool - */ - public function hasColumnAtPosition($name, $pos = 0) + /** @deprecated Use {@see getIndexedColumns()} instead. */ + public function hasColumnAtPosition(string $name, int $pos = 0): bool { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated. Use Index::getIndexedColumns() instead.', + __METHOD__, + ); + $name = $this->trimQuotes(strtolower($name)); $indexColumns = array_map('strtolower', $this->getUnquotedColumns()); @@ -161,11 +342,11 @@ public function hasColumnAtPosition($name, $pos = 0) /** * Checks if this index exactly spans the given column names in the correct order. * - * @param string[] $columnNames + * @internal * - * @return bool + * @param array $columnNames */ - public function spansColumns(array $columnNames) + public function spansColumns(array $columnNames): bool { $columns = $this->getColumns(); $numberOfColumns = count($columns); @@ -185,18 +366,6 @@ public function spansColumns(array $columnNames) return $sameColumns; } - /** - * Keeping misspelled function name for backwards compatibility - * - * @deprecated Use {@see isFulfilledBy()} instead. - * - * @return bool - */ - public function isFullfilledBy(Index $other) - { - return $this->isFulfilledBy($other); - } - /** * Checks if the other index already fulfills all the indexing and constraint needs of the current one. */ @@ -241,10 +410,17 @@ public function isFulfilledBy(Index $other): bool /** * Detects if the other index is a non-unique, non primary index that can be overwritten by this one. * - * @return bool + * @deprecated */ - public function overrules(Index $other) + public function overrules(Index $other): bool { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated.', + __METHOD__, + ); + if ($other->isPrimary()) { return false; } @@ -261,77 +437,276 @@ public function overrules(Index $other) /** * Returns platform specific flags for indexes. * - * @return string[] + * @deprecated Use {@see getType()} and {@see isClustered()} instead. + * + * @return array */ - public function getFlags() + public function getFlags(): array { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated. Use Index::getType() and Index::isClustered() instead.', + __METHOD__, + ); + return array_keys($this->_flags); } /** * Adds Flag for an index that translates to platform specific handling. * - * @param string $flag - * - * @return Index + * @deprecated Use {@see edit()}, {@see IndexEditor::setType()} and {@see IndexEditor::setIsClustered()} instead. * * @example $index->addFlag('CLUSTERED') */ - public function addFlag($flag) + public function addFlag(string $flag): self { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated. Use Index::edit(), IndexEditor::setType() and IndexEditor::setIsClustered()' + . ' instead.', + __METHOD__, + ); + $this->_flags[strtolower($flag)] = true; + $this->validateFlags(); + + $this->type = $this->inferType(); + return $this; } /** * Does this index have a specific flag? * - * @param string $flag - * - * @return bool + * @deprecated Use {@see getType()} and {@see isClustered()} instead. */ - public function hasFlag($flag) + public function hasFlag(string $flag): bool { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated. Use Index::getType() and Index::isClustered() instead.', + __METHOD__, + ); + return isset($this->_flags[strtolower($flag)]); } /** - * Removes a flag. - * - * @param string $flag - * - * @return void + * @deprecated Use {@see edit()}, {@see IndexEditor::setType()} and {@see IndexEditor::setIsClustered()} + * instead. */ - public function removeFlag($flag) + public function removeFlag(string $flag): void { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated. Use Index::edit(), IndexEditor::setType() and IndexEditor::setIsClustered()' + . ' instead.', + __METHOD__, + ); + unset($this->_flags[strtolower($flag)]); + + $this->type = $this->inferType(); } - /** - * @param string $name - * - * @return bool - */ - public function hasOption($name) + /** @deprecated Use {@see getIndexedColumns()} and {@see getPredicate()} instead. */ + public function hasOption(string $name): bool { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated. Use Index::getIndexedColumns() and Index::getPredicate() instead.', + __METHOD__, + ); + return isset($this->options[strtolower($name)]); } + /** @deprecated Use {@see getIndexedColumns()} and {@see getPredicate()} instead. */ + public function getOption(string $name): mixed + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated. Use Index::getIndexedColumns() and Index::getPredicate() instead.', + __METHOD__, + ); + + return $this->options[strtolower($name)]; + } + /** - * @param string $name + * @deprecated Use {@see getIndexedColumns()} and {@see getPredicate()} instead. * - * @return mixed + * @return array */ - public function getOption($name) + public function getOptions(): array { - return $this->options[strtolower($name)]; + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + '%s is deprecated. Use Index::getIndexedColumns() and Index::getPredicate() instead.', + __METHOD__, + ); + + return $this->options; } - /** @return mixed[] */ - public function getOptions() + private function validateFlags(): void { - return $this->options; + $unsupportedFlags = $this->_flags; + unset( + $unsupportedFlags['fulltext'], + $unsupportedFlags['spatial'], + $unsupportedFlags['clustered'], + $unsupportedFlags['nonclustered'], + ); + + if (count($unsupportedFlags) > 0) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + 'Configuring an index with non-standard flags is deprecated: %s', + implode(', ', array_keys($unsupportedFlags)), + ); + } + + if ( + $this->hasFlag('clustered') && ( + $this->hasFlag('nonclustered') + || $this->hasFlag('fulltext') + || $this->hasFlag('spatial') + ) + ) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + 'A fulltext, spatial or non-clustered index cannot be clustered.', + ); + } + + if ( + $this->predicate === null + || (! $this->hasFlag('fulltext') + && ! $this->hasFlag('spatial') + && ! $this->hasFlag('clustered')) + ) { + return; + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + 'A fulltext, spatial or clustered index cannot be partial.', + ); + } + + private function inferType(): ?IndexType + { + $type = IndexType::REGULAR; + $matches = []; + + if ($this->_isUnique) { + $type = IndexType::UNIQUE; + $matches[] = 'unique'; + } + + if ($this->hasFlag('fulltext')) { + $type = IndexType::FULLTEXT; + $matches[] = 'fulltext'; + } + + if ($this->hasFlag('spatial')) { + $type = IndexType::SPATIAL; + $matches[] = 'spatial'; + } + + if (count($matches) > 1) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6886', + 'Configuring an index with mutually exclusive properties is deprecated: %s', + implode(', ', $matches), + ); + + return null; + } + + return $type; + } + + /** + * @param non-empty-array $columnNames + * @param array $lengths + * + * @return list + */ + private function parseColumns(bool $isPrimary, array $columnNames, array $lengths): array + { + $columns = []; + + $parser = Parsers::getUnqualifiedNameParser(); + + foreach ($columnNames as $columnName) { + try { + $parsedName = $parser->parse($columnName); + } catch (Throwable $e) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6787', + 'Unable to parse column name: %s.', + $e->getMessage(), + ); + + return []; + } + + $length = array_shift($lengths); + + if ($length !== null) { + if ($isPrimary) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6787', + 'Declaring column length for primary key indexes is deprecated.', + ); + + return []; + } + + if (! is_int($length)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6787', + 'Indexed column length should be an integer, %s given.', + is_object($length) ? $length::class : gettype($length), + ); + + $length = (int) $length; + } + + if ($length < 1) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6787', + 'Indexed column length should be a positive integer, %d given.', + $length, + ); + + return []; + } + } + + $columns[] = new IndexedColumn($parsedName, $length); + } + + return $columns; } /** @@ -362,4 +737,25 @@ private function hasSameColumnLengths(self $other): bool return array_filter($this->options['lengths'] ?? [], $filter) === array_filter($other->options['lengths'] ?? [], $filter); } + + /** + * Instantiates a new index editor. + */ + public static function editor(): IndexEditor + { + return new IndexEditor(); + } + + /** + * Instantiates a new index editor and initializes it with the properties of the current index. + */ + public function edit(): IndexEditor + { + return self::editor() + ->setName($this->getObjectName()) + ->setType($this->getType()) + ->setColumns(...$this->getIndexedColumns()) + ->setIsClustered($this->isClustered()) + ->setPredicate($this->getPredicate()); + } } diff --git a/doctrine/dbal/src/Schema/Index/IndexType.php b/doctrine/dbal/src/Schema/Index/IndexType.php new file mode 100644 index 000000000..646668853 --- /dev/null +++ b/doctrine/dbal/src/Schema/Index/IndexType.php @@ -0,0 +1,13 @@ +columnName; + } + + /** @return ?positive-int */ + public function getLength(): ?int + { + return $this->length; + } +} diff --git a/doctrine/dbal/src/Schema/IndexEditor.php b/doctrine/dbal/src/Schema/IndexEditor.php new file mode 100644 index 000000000..eb9b2da56 --- /dev/null +++ b/doctrine/dbal/src/Schema/IndexEditor.php @@ -0,0 +1,187 @@ + */ + private array $columns = []; + + private bool $isClustered = false; + + /** @var ?non-empty-string */ + private ?string $predicate = null; + + /** @internal Use {@link Index::editor()} or {@link Index::edit()} to create an instance */ + public function __construct() + { + } + + public function setName(?UnqualifiedName $name): self + { + $this->name = $name; + + return $this; + } + + /** @param non-empty-string $name */ + public function setUnquotedName(string $name): self + { + $this->name = UnqualifiedName::unquoted($name); + + return $this; + } + + /** @param non-empty-string $name */ + public function setQuotedName(string $name): self + { + $this->name = UnqualifiedName::quoted($name); + + return $this; + } + + public function setType(IndexType $type): self + { + $this->type = $type; + + return $this; + } + + public function setColumns(IndexedColumn $firstColumn, IndexedColumn ...$otherColumns): self + { + $this->columns = [$firstColumn, ...array_values($otherColumns)]; + + return $this; + } + + public function addColumn(IndexedColumn $column): self + { + $this->columns[] = $column; + + return $this; + } + + public function setColumnNames(UnqualifiedName $firstColumnName, UnqualifiedName ...$otherColumnNames): self + { + $this->columns = array_map( + static fn (UnqualifiedName $name) => new IndexedColumn($name, null), + [$firstColumnName, ...array_values($otherColumnNames)], + ); + + return $this; + } + + /** + * @param non-empty-string $firstColumnName + * @param non-empty-string ...$otherColumnNames + */ + public function setUnquotedColumnNames( + string $firstColumnName, + string ...$otherColumnNames, + ): self { + $this->columns = array_map( + static fn (string $name): IndexedColumn => new IndexedColumn(UnqualifiedName::unquoted($name), null), + [$firstColumnName, ...array_values($otherColumnNames)], + ); + + return $this; + } + + /** + * @param non-empty-string $firstColumnName + * @param non-empty-string ...$otherColumnNames + */ + public function setQuotedColumnNames( + string $firstColumnName, + string ...$otherColumnNames, + ): self { + $this->columns = array_map( + static fn (string $name): IndexedColumn => new IndexedColumn(UnqualifiedName::quoted($name), null), + [$firstColumnName, ...array_values($otherColumnNames)], + ); + + return $this; + } + + public function setIsClustered(bool $isClustered): self + { + $this->isClustered = $isClustered; + + return $this; + } + + /** @param ?non-empty-string $predicate */ + public function setPredicate(?string $predicate): self + { + $this->predicate = $predicate; + + return $this; + } + + public function create(): Index + { + if ($this->name === null) { + throw InvalidIndexDefinition::nameNotSet(); + } + + if (count($this->columns) < 1) { + throw InvalidIndexDefinition::columnsNotSet($this->name); + } + + $columnNames = $lengths = $options = $flags = []; + $hasLength = false; + + foreach ($this->columns as $column) { + $columnNames[] = $column->getColumnName()->toString(); + + $length = $column->getLength(); + $lengths[] = $length; + + if ($length !== null) { + $hasLength = true; + } + } + + if ($hasLength) { + $options['lengths'] = $lengths; + } + + if ($this->type === IndexType::FULLTEXT) { + $flags[] = 'fulltext'; + } elseif ($this->type === IndexType::SPATIAL) { + $flags[] = 'spatial'; + } + + if ($this->isClustered) { + $flags[] = 'clustered'; + } + + if ($this->predicate !== null) { + $options['where'] = $this->predicate; + } + + return new Index( + $this->name->toString(), + $columnNames, + $this->type === IndexType::UNIQUE, + false, + $flags, + $options, + ); + } +} diff --git a/doctrine/dbal/src/Schema/Introspection/IntrospectingSchemaProvider.php b/doctrine/dbal/src/Schema/Introspection/IntrospectingSchemaProvider.php new file mode 100644 index 000000000..a46fe79e3 --- /dev/null +++ b/doctrine/dbal/src/Schema/Introspection/IntrospectingSchemaProvider.php @@ -0,0 +1,413 @@ +NULL schema key in results grouped by schema name. + * + * The value should be a valid array key but, ideally, not a valid schema name, so an empty string looks like a + * perfect fit. + */ + private const NULL_SCHEMA_KEY = ''; + + /** @param ?non-empty-string $currentSchemaName */ + public function __construct( + private MetadataProvider $metadataProvider, + private ?string $currentSchemaName, + private TableConfiguration $tableConfiguration, + ) { + } + + /** {@inheritDoc} */ + public function getAllDatabaseNames(): array + { + $databaseNames = []; + + foreach ($this->metadataProvider->getAllDatabaseNames() as $row) { + $databaseNames[] = UnqualifiedName::quoted($row->getDatabaseName()); + } + + return $databaseNames; + } + + /** {@inheritDoc} */ + public function getAllSchemaNames(): array + { + $schemaNames = []; + + foreach ($this->metadataProvider->getAllSchemaNames() as $row) { + $schemaNames[] = UnqualifiedName::quoted($row->getSchemaName()); + } + + return $schemaNames; + } + + /** {@inheritDoc} */ + public function getAllTables(): array + { + $tableColumnsByTable = $this->getColumnsForAllTables(); + $indexesByTable = $this->getIndexesForAllTables(); + $primaryKeyConstraintsByTable = $this->getPrimaryKeyConstraintsForAllTables(); + $foreignKeyConstraintsByTable = $this->getForeignKeyConstraintsForAllTables(); + $tableOptionsByTable = $this->getOptionsForAllTables(); + + $tables = []; + + foreach ($tableColumnsByTable as $schemaNameKey => $schemaTables) { + if ($schemaNameKey !== self::NULL_SCHEMA_KEY && $schemaNameKey !== $this->currentSchemaName) { + $schemaName = $schemaNameKey; + } else { + $schemaName = null; + } + + foreach ($schemaTables as $unqualifiedName => $tableColumns) { + $editor = Table::editor() + ->setName( + OptionallyQualifiedName::quoted($unqualifiedName, $schemaName), + ) + ->setColumns(...$tableColumns) + ->setIndexes( + ...$indexesByTable[$schemaNameKey][$unqualifiedName] ?? [], + ); + + if (isset($primaryKeyConstraintsByTable[$schemaNameKey][$unqualifiedName])) { + $editor->setPrimaryKeyConstraint( + $primaryKeyConstraintsByTable[$schemaNameKey][$unqualifiedName], + ); + } + + if (isset($foreignKeyConstraintsByTable[$schemaNameKey][$unqualifiedName])) { + $editor->setForeignKeyConstraints( + ...$foreignKeyConstraintsByTable[$schemaNameKey][$unqualifiedName], + ); + } + + if (isset($tableOptionsByTable[$schemaNameKey][$unqualifiedName])) { + $editor->setOptions($tableOptionsByTable[$schemaNameKey][$unqualifiedName]); + } + + $tables[] = $editor + ->setConfiguration($this->tableConfiguration) + ->create(); + } + } + + return $tables; + } + + /** {@inheritDoc} */ + public function getAllTableNames(): array + { + $tableNames = []; + + foreach ($this->metadataProvider->getAllTableNames() as $row) { + $schemaName = $row->getSchemaName(); + $tableName = $row->getTableName(); + + if ($schemaName === $this->currentSchemaName) { + $schemaName = null; + } + + $tableNames[] = OptionallyQualifiedName::quoted($tableName, $schemaName); + } + + return $tableNames; + } + + /** {@inheritDoc} */ + public function getColumnsForTable(?string $schemaName, string $tableName): array + { + $columns = []; + + foreach ($this->metadataProvider->getTableColumnsForTable($schemaName, $tableName) as $row) { + $columns[] = $row->getColumn(); + } + + return $columns; + } + + /** + * Returns columns of all tables, grouped by schema and table. + * + * If the underlying database does not support schemas, the schema key will be {@link NULL_SCHEMA_KEY}. + * + * @return array>> + * + * @throws Exception + */ + private function getColumnsForAllTables(): array + { + $columns = []; + + foreach ($this->metadataProvider->getTableColumnsForAllTables() as $row) { + $schemaName = $row->getSchemaName() ?? self::NULL_SCHEMA_KEY; + $tableName = $row->getTableName(); + + $columns[$schemaName][$tableName][] = $row->getColumn(); + } + + return $columns; + } + + /** {@inheritDoc} */ + public function getIndexesForTable(?string $schemaName, string $tableName): array + { + $editors = []; + $processor = new IndexColumnMetadataProcessor(); + + foreach ($this->metadataProvider->getIndexColumnsForTable($schemaName, $tableName) as $row) { + $indexName = $row->getIndexName(); + + if (! isset($editors[$indexName])) { + $editors[$indexName] = $processor->initializeEditor($row); + } + + $processor->applyRow($editors[$indexName], $row); + } + + return array_map( + static fn (IndexEditor $e): Index => $e->create(), + array_values($editors), + ); + } + + /** + * Returns indexes for all tables, grouped by schema and table. + * + * If the underlying database does not support schemas, the schema key will be {@link NULL_SCHEMA_KEY}. + * + * @return array>> + * + * @throws Exception + */ + private function getIndexesForAllTables(): array + { + $editors = []; + $processor = new IndexColumnMetadataProcessor(); + + foreach ($this->metadataProvider->getIndexColumnsForAllTables() as $row) { + $schemaName = $row->getSchemaName() ?? self::NULL_SCHEMA_KEY; + $tableName = $row->getTableName(); + $indexName = $row->getIndexName(); + + if (! isset($editors[$schemaName][$tableName][$indexName])) { + $editors[$schemaName][$tableName][$indexName] = $processor->initializeEditor($row); + } + + $processor->applyRow($editors[$schemaName][$tableName][$indexName], $row); + } + + return array_map( + static fn (array $editors): array => array_map( + static fn (array $editors): array => array_map( + static fn (IndexEditor $editor): Index => $editor->create(), + array_values($editors), + ), + $editors, + ), + $editors, + ); + } + + public function getPrimaryKeyConstraintForTable(?string $schemaName, string $tableName): ?PrimaryKeyConstraint + { + $editor = null; + $processor = new PrimaryKeyConstraintColumnMetadataProcessor(); + + foreach ($this->metadataProvider->getPrimaryKeyConstraintColumnsForTable($schemaName, $tableName) as $row) { + $editor ??= $processor->initializeEditor($row); + + $processor->applyRow($editor, $row); + } + + return $editor?->create(); + } + + /** + * Returns the primary key constraints for all tables, grouped by schema and table. + * + * If the underlying database does not support schemas, the schema key will be {@link NULL_SCHEMA_KEY}. + * + * @return array> + * + * @throws Exception + */ + private function getPrimaryKeyConstraintsForAllTables(): array + { + $editors = []; + $processor = new PrimaryKeyConstraintColumnMetadataProcessor(); + + foreach ($this->metadataProvider->getPrimaryKeyConstraintColumnsForAllTables() as $row) { + $schemaName = $row->getSchemaName() ?? self::NULL_SCHEMA_KEY; + $tableName = $row->getTableName(); + + if (! isset($editors[$schemaName][$tableName])) { + $editors[$schemaName][$tableName] = $processor->initializeEditor($row); + } + + $processor->applyRow($editors[$schemaName][$tableName], $row); + } + + return array_map( + static fn (array $editors): array => array_map( + static fn (PrimaryKeyConstraintEditor $editor): PrimaryKeyConstraint => $editor->create(), + $editors, + ), + $editors, + ); + } + + /** {@inheritDoc} */ + public function getForeignKeyConstraintsForTable(?string $schemaName, string $tableName): array + { + $editors = []; + $processor = new ForeignKeyConstraintColumnMetadataProcessor($this->currentSchemaName); + + foreach ($this->metadataProvider->getForeignKeyConstraintColumnsForTable($schemaName, $tableName) as $row) { + $id = $row->getId(); + + if (! isset($editors[$id])) { + $editors[$id] = $processor->initializeEditor($row); + } + + $processor->applyRow($editors[$id], $row); + } + + return array_map( + static fn (ForeignKeyConstraintEditor $e): ForeignKeyConstraint => $e->create(), + array_values($editors), + ); + } + + /** + * Returns the foreign key constraints, grouped by schema and table. + * + * If the underlying database does not support schemas, the schema key will be {@link NULL_SCHEMA_KEY}. + * + * @return array>> + * + * @throws Exception + */ + private function getForeignKeyConstraintsForAllTables(): array + { + $editors = []; + $processor = new ForeignKeyConstraintColumnMetadataProcessor($this->currentSchemaName); + + foreach ($this->metadataProvider->getForeignKeyConstraintColumnsForAllTables() as $row) { + $schemaName = $row->getSchemaName() ?? self::NULL_SCHEMA_KEY; + $tableName = $row->getTableName(); + $id = $row->getId(); + + if (! isset($editors[$schemaName][$tableName][$id])) { + $editors[$schemaName][$tableName][$id] = $processor->initializeEditor($row); + } + + $processor->applyRow($editors[$schemaName][$tableName][$id], $row); + } + + return array_map( + static fn (array $editors): array => array_map( + static fn (array $editors): array => array_map( + static fn (ForeignKeyConstraintEditor $editor): ForeignKeyConstraint => $editor->create(), + array_values($editors), + ), + $editors, + ), + $editors, + ); + } + + /** {@inheritDoc} */ + public function getOptionsForTable(?string $schemaName, string $tableName): ?array + { + foreach ($this->metadataProvider->getTableOptionsForTable($schemaName, $tableName) as $row) { + return $row->getOptions(); + } + + return null; + } + + /** + * Returns options for all tables, grouped by schema and table. + * + * If the underlying database does not support schemas, the schema key will be {@link NULL_SCHEMA_KEY}. + * + * @return array>> + * + * @throws Exception + */ + private function getOptionsForAllTables(): array + { + $options = []; + + foreach ($this->metadataProvider->getTableOptionsForAllTables() as $row) { + $schemaName = $row->getSchemaName() ?? self::NULL_SCHEMA_KEY; + $tableName = $row->getTableName(); + + $options[$schemaName][$tableName] = $row->getOptions(); + } + + return $options; + } + + /** {@inheritDoc} */ + public function getAllViews(): array + { + $processor = new ViewMetadataProcessor(); + $views = []; + + foreach ($this->metadataProvider->getAllViews() as $row) { + $views[] = $processor->createObject($row); + } + + return $views; + } + + /** {@inheritDoc} */ + public function getAllSequences(): array + { + $processor = new SequenceMetadataProcessor(); + $sequences = []; + + foreach ($this->metadataProvider->getAllSequences() as $row) { + $sequences[] = $processor->createObject($row); + } + + return $sequences; + } +} diff --git a/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/ForeignKeyConstraintColumnMetadataProcessor.php b/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/ForeignKeyConstraintColumnMetadataProcessor.php new file mode 100644 index 000000000..020bd39a0 --- /dev/null +++ b/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/ForeignKeyConstraintColumnMetadataProcessor.php @@ -0,0 +1,71 @@ +getName(); + if ($constraintName !== null) { + $editor->setName( + UnqualifiedName::quoted($constraintName), + ); + } + + $referencedSchemaName = $row->getReferencedSchemaName(); + if ($referencedSchemaName === $this->currentSchemaName) { + $referencedSchemaName = null; + } + + $editor + ->setReferencedTableName( + OptionallyQualifiedName::quoted( + $row->getReferencedTableName(), + $referencedSchemaName, + ), + ) + ->setMatchType($row->getMatchType()) + ->setOnUpdateAction($row->getOnUpdateAction()) + ->setOnDeleteAction($row->getOnDeleteAction()); + + if ($row->isDeferred()) { + $editor->setDeferrability(Deferrability::DEFERRED); + } elseif ($row->isDeferrable()) { + $editor->setDeferrability(Deferrability::DEFERRABLE); + } + + return $editor; + } + + public function applyRow(ForeignKeyConstraintEditor $editor, ForeignKeyConstraintColumnMetadataRow $row): void + { + $editor + ->addReferencingColumnName( + UnqualifiedName::quoted($row->getReferencingColumnName()), + ) + ->addReferencedColumnName( + UnqualifiedName::quoted($row->getReferencedColumnName()), + ); + } +} diff --git a/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/IndexColumnMetadataProcessor.php b/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/IndexColumnMetadataProcessor.php new file mode 100644 index 000000000..215b53979 --- /dev/null +++ b/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/IndexColumnMetadataProcessor.php @@ -0,0 +1,40 @@ +setName( + UnqualifiedName::quoted($row->getIndexName()), + ) + ->setType($row->getType()) + ->setIsClustered($row->isClustered()) + ->setPredicate($row->getPredicate()); + } + + public function applyRow(IndexEditor $editor, IndexColumnMetadataRow $row): void + { + $editor->addColumn( + new IndexedColumn( + UnqualifiedName::quoted($row->getColumnName()), + $row->getColumnLength(), + ), + ); + } +} diff --git a/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/PrimaryKeyConstraintColumnMetadataProcessor.php b/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/PrimaryKeyConstraintColumnMetadataProcessor.php new file mode 100644 index 000000000..027bf5a19 --- /dev/null +++ b/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/PrimaryKeyConstraintColumnMetadataProcessor.php @@ -0,0 +1,33 @@ +setIsClustered($row->isClustered()); + } + + public function applyRow(PrimaryKeyConstraintEditor $editor, PrimaryKeyConstraintColumnRow $row): void + { + $editor->addColumnName( + UnqualifiedName::quoted($row->getColumnName()), + ); + } +} diff --git a/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/SequenceMetadataProcessor.php b/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/SequenceMetadataProcessor.php new file mode 100644 index 000000000..b499112ce --- /dev/null +++ b/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/SequenceMetadataProcessor.php @@ -0,0 +1,26 @@ +setQuotedName($row->getSequenceName(), $row->getSchemaName()) + ->setAllocationSize($row->getAllocationSize()) + ->setInitialValue($row->getInitialValue()) + ->setCacheSize($row->getCacheSize()) + ->create(); + } +} diff --git a/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/ViewMetadataProcessor.php b/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/ViewMetadataProcessor.php new file mode 100644 index 000000000..68788e2c0 --- /dev/null +++ b/doctrine/dbal/src/Schema/Introspection/MetadataProcessor/ViewMetadataProcessor.php @@ -0,0 +1,24 @@ +setQuotedName($row->getViewName(), $row->getSchemaName()) + ->setSQL($row->getDefinition()) + ->create(); + } +} diff --git a/doctrine/dbal/src/Schema/LegacySchemaManagerFactory.php b/doctrine/dbal/src/Schema/LegacySchemaManagerFactory.php deleted file mode 100644 index 01c856b6e..000000000 --- a/doctrine/dbal/src/Schema/LegacySchemaManagerFactory.php +++ /dev/null @@ -1,19 +0,0 @@ -getDriver()->getSchemaManager( - $connection, - $connection->getDatabasePlatform(), - ); - } -} diff --git a/doctrine/dbal/src/Schema/Metadata/DatabaseMetadataRow.php b/doctrine/dbal/src/Schema/Metadata/DatabaseMetadataRow.php new file mode 100644 index 000000000..bbe3e9191 --- /dev/null +++ b/doctrine/dbal/src/Schema/Metadata/DatabaseMetadataRow.php @@ -0,0 +1,22 @@ +databaseName; + } +} diff --git a/doctrine/dbal/src/Schema/Metadata/ForeignKeyConstraintColumnMetadataRow.php b/doctrine/dbal/src/Schema/Metadata/ForeignKeyConstraintColumnMetadataRow.php new file mode 100644 index 000000000..f56e52202 --- /dev/null +++ b/doctrine/dbal/src/Schema/Metadata/ForeignKeyConstraintColumnMetadataRow.php @@ -0,0 +1,131 @@ +id = $id; + } elseif ($name !== null) { + $this->id = $name; + } else { + throw new InvalidArgumentException( + 'Either the id or name must be set to a non-null value.', + ); + } + } + + /** @return ?non-empty-string */ + public function getSchemaName(): ?string + { + return $this->referencingSchemaName; + } + + /** @return non-empty-string */ + public function getTableName(): string + { + return $this->referencingTableName; + } + + /** @return int|non-empty-string */ + public function getId(): int|string + { + return $this->id; + } + + /** @return ?non-empty-string */ + public function getName(): ?string + { + return $this->name; + } + + /** @return ?non-empty-string */ + public function getReferencedSchemaName(): ?string + { + return $this->referencedSchemaName; + } + + /** @return non-empty-string */ + public function getReferencedTableName(): string + { + return $this->referencedTableName; + } + + public function getMatchType(): MatchType + { + return $this->matchType; + } + + public function getOnUpdateAction(): ReferentialAction + { + return $this->onUpdateAction; + } + + public function getOnDeleteAction(): ReferentialAction + { + return $this->onDeleteAction; + } + + public function isDeferrable(): bool + { + return $this->isDeferrable; + } + + public function isDeferred(): bool + { + return $this->isDeferred; + } + + /** @return non-empty-string */ + public function getReferencingColumnName(): string + { + return $this->referencingColumnName; + } + + /** @return non-empty-string */ + public function getReferencedColumnName(): string + { + return $this->referencedColumnName; + } +} diff --git a/doctrine/dbal/src/Schema/Metadata/IndexColumnMetadataRow.php b/doctrine/dbal/src/Schema/Metadata/IndexColumnMetadataRow.php new file mode 100644 index 000000000..669f9d4b1 --- /dev/null +++ b/doctrine/dbal/src/Schema/Metadata/IndexColumnMetadataRow.php @@ -0,0 +1,79 @@ +schemaName; + } + + /** @return non-empty-string */ + public function getTableName(): string + { + return $this->tableName; + } + + /** @return non-empty-string */ + public function getIndexName(): string + { + return $this->indexName; + } + + public function getType(): IndexType + { + return $this->type; + } + + public function isClustered(): bool + { + return $this->isClustered; + } + + /** @return ?non-empty-string */ + public function getPredicate(): ?string + { + return $this->predicate; + } + + /** @return non-empty-string */ + public function getColumnName(): string + { + return $this->columnName; + } + + /** @return ?positive-int */ + public function getColumnLength(): ?int + { + return $this->columnLength; + } +} diff --git a/doctrine/dbal/src/Schema/Metadata/MetadataProvider.php b/doctrine/dbal/src/Schema/Metadata/MetadataProvider.php new file mode 100644 index 000000000..ccd2d071b --- /dev/null +++ b/doctrine/dbal/src/Schema/Metadata/MetadataProvider.php @@ -0,0 +1,241 @@ + + * + * @throws Exception + */ + public function getAllDatabaseNames(): iterable; + + /** + * Returns names of all schemas available within the current database. + * + * The resulting list is ordered by schema name. + * + * @return iterable + * + * @throws Exception + */ + public function getAllSchemaNames(): iterable; + + /** + * Returns names of all tables within the current database. + * + * The results are ordered by schema name (if the underlying database platform supports schemas) and table name. + * + * @return iterable + * + * @throws Exception + */ + public function getAllTableNames(): iterable; + + /** + * Returns the columns of all tables within the current database. + * + * The results are ordered by schema name (if the underlying database platform supports schemas), table name, and + * column position within the table. + * + * @return iterable + * + * @throws Exception + */ + public function getTableColumnsForAllTables(): iterable; + + /** + * Returns the columns of the given table. + * + * If the underlying database platform supports schemas, the schema name must be specified. Otherwise, null must be + * passed as the schema name. + * + * The results are ordered by column position within the table. If the table doesn't exist, or is not accessible to + * the connection, an empty value is returned. + * + * @param ?non-empty-string $schemaName + * @param non-empty-string $tableName + * + * @return iterable + * + * @throws Exception + */ + public function getTableColumnsForTable(?string $schemaName, string $tableName): iterable; + + /** + * Returns the index columns of all tables within the current database. + * + * The results are ordered by schema name (if the underlying database platform supports schemas), table name, index + * name, and column position within the index. + * + * @return iterable + * + * @throws Exception + */ + public function getIndexColumnsForAllTables(): iterable; + + /** + * Returns the index columns of the given table. + * + * If the underlying database platform supports schemas, the schema name must be specified. Otherwise, null must be + * passed as the schema name. + * + * The results are ordered by index name and column position within the index. If the table doesn't exist, or is not + * accessible to the connection, an empty value is returned. + * + * @param ?non-empty-string $schemaName + * @param non-empty-string $tableName + * + * @return iterable + * + * @throws Exception + */ + public function getIndexColumnsForTable(?string $schemaName, string $tableName): iterable; + + /** + * Returns the primary key constraint columns of all tables within the current database. + * + * The results are ordered by schema name (if the underlying database platform supports schemas), table name, and + * column position within the primary key constraint. If a table does not have a primary key constraint, it will not + * be represented in the results. + * + * @return iterable + * + * @throws Exception + */ + public function getPrimaryKeyConstraintColumnsForAllTables(): iterable; + + /** + * Returns the primary key constraint columns of the given table. + * + * If the underlying database platform supports schemas, the schema name must be specified. Otherwise, null must be + * passed as the schema name. + * + * The results are ordered by column position within the primary key constraint. If the table doesn't exist, + * is not accessible to the connection or doesn't have a primary key constraint, an empty value is returned. + * + * @param ?non-empty-string $schemaName + * @param non-empty-string $tableName + * + * @return iterable + * + * @throws Exception + */ + public function getPrimaryKeyConstraintColumnsForTable(?string $schemaName, string $tableName): iterable; + + /** + * Returns the foreign key constraint columns of all tables within the current database. + * + * The results are ordered by schema name (if the underlying database platform supports schemas), table name, + * foreign key constraint name, and column position within the foreign key constraint. If the underlying database + * platform supports unnamed foreign key constraints, instead of ordering by name, it may provide another stable + * order of the results. + * + * @return iterable + * + * @throws Exception + */ + public function getForeignKeyConstraintColumnsForAllTables(): iterable; + + /** + * Returns the foreign key constraint columns of the given table. + * + * If the underlying database platform supports schemas, the schema name must be specified. Otherwise, null must be + * passed as the schema name. + * + * The results are ordered by foreign key constraint name and column position within the foreign key constraint. If + * the underlying database platform supports unnamed foreign key constraints, instead of ordering by name, it may + * provide another stable order of the results. If the table doesn't exist, or is not accessible to the connection, + * an empty value is returned. + * + * @param ?non-empty-string $schemaName + * @param non-empty-string $tableName + * + * @return iterable + * + * @throws Exception + */ + public function getForeignKeyConstraintColumnsForTable( + ?string $schemaName, + string $tableName, + ): iterable; + + /** + * Returns the options of all tables within the current database. + * + * The results are ordered by schema name (if the underlying database platform supports schemas) and table name. The + * order of the options within each array is not significant. + * + * Implementations must return an element for each table, even if their options are not explicitly represented in + * the underlying database. + * + * @return iterable + * + * @throws Exception + */ + public function getTableOptionsForAllTables(): iterable; + + /** + * Returns the options of the given table. + * + * If the underlying database platform supports schemas, the schema name must be specified. Otherwise, null must be + * passed as the schema name. + * + * If the table doesn't exist or is not accessible to the connection, an empty value is returned. Implementations + * must return a non-empty array as long as the table exists, even if its options are not explicitly represented + * in the underlying database. + * + * @param ?non-empty-string $schemaName + * @param non-empty-string $tableName + * + * @return iterable + * + * @throws Exception + */ + public function getTableOptionsForTable(?string $schemaName, string $tableName): iterable; + + /** + * Returns the definitions of all views within the current database. + * + * If the underlying database platform supports schemas, the schema name must be specified. Otherwise, null must be + * passed as the schema name. + * + * The results are ordered by schema name (if the underlying database platform supports schemas) and view name. + * + * @return iterable + * + * @throws Exception + */ + public function getAllViews(): iterable; + + /** + * Returns the definitions of all sequences within the current database. + * + * If the underlying database platform supports schemas, the schema name must be specified. Otherwise, null must be + * passed as the schema name. + * + * The results are ordered by schema name (if the underlying database platform supports schemas) and sequence name. + * + * @return iterable + * + * @throws Exception + */ + public function getAllSequences(): iterable; +} diff --git a/doctrine/dbal/src/Schema/Metadata/PrimaryKeyConstraintColumnRow.php b/doctrine/dbal/src/Schema/Metadata/PrimaryKeyConstraintColumnRow.php new file mode 100644 index 000000000..1ce0b74c5 --- /dev/null +++ b/doctrine/dbal/src/Schema/Metadata/PrimaryKeyConstraintColumnRow.php @@ -0,0 +1,55 @@ +schemaName; + } + + /** @return non-empty-string */ + public function getTableName(): string + { + return $this->tableName; + } + + /** @return ?non-empty-string */ + public function getConstraintName(): ?string + { + return $this->constraintName; + } + + public function isClustered(): bool + { + return $this->isClustered; + } + + /** @return non-empty-string */ + public function getColumnName(): string + { + return $this->columnName; + } +} diff --git a/doctrine/dbal/src/Schema/Metadata/SchemaMetadataRow.php b/doctrine/dbal/src/Schema/Metadata/SchemaMetadataRow.php new file mode 100644 index 000000000..31a5e82f6 --- /dev/null +++ b/doctrine/dbal/src/Schema/Metadata/SchemaMetadataRow.php @@ -0,0 +1,22 @@ +schemaName; + } +} diff --git a/doctrine/dbal/src/Schema/Metadata/SequenceMetadataRow.php b/doctrine/dbal/src/Schema/Metadata/SequenceMetadataRow.php new file mode 100644 index 000000000..b6182f48c --- /dev/null +++ b/doctrine/dbal/src/Schema/Metadata/SequenceMetadataRow.php @@ -0,0 +1,53 @@ +schemaName; + } + + /** @return non-empty-string */ + public function getSequenceName(): string + { + return $this->sequenceName; + } + + public function getAllocationSize(): int + { + return $this->allocationSize; + } + + public function getInitialValue(): int + { + return $this->initialValue; + } + + /** @return ?non-negative-int */ + public function getCacheSize(): ?int + { + return $this->cacheSize; + } +} diff --git a/doctrine/dbal/src/Schema/Metadata/TableColumnMetadataRow.php b/doctrine/dbal/src/Schema/Metadata/TableColumnMetadataRow.php new file mode 100644 index 000000000..b4cdf3b68 --- /dev/null +++ b/doctrine/dbal/src/Schema/Metadata/TableColumnMetadataRow.php @@ -0,0 +1,46 @@ +schemaName; + } + + /** @return non-empty-string */ + public function getTableName(): string + { + return $this->tableName; + } + + public function getColumn(): Column + { + return $this->column; + } +} diff --git a/doctrine/dbal/src/Schema/Metadata/TableMetadataRow.php b/doctrine/dbal/src/Schema/Metadata/TableMetadataRow.php new file mode 100644 index 000000000..63745c1db --- /dev/null +++ b/doctrine/dbal/src/Schema/Metadata/TableMetadataRow.php @@ -0,0 +1,41 @@ + $options + */ + public function __construct( + private ?string $schemaName, + private string $tableName, + private array $options, + ) { + } + + /** @return ?non-empty-string */ + public function getSchemaName(): ?string + { + return $this->schemaName; + } + + /** @return non-empty-string */ + public function getTableName(): string + { + return $this->tableName; + } + + /** @return array */ + public function getOptions(): array + { + return $this->options; + } +} diff --git a/doctrine/dbal/src/Schema/Metadata/ViewMetadataRow.php b/doctrine/dbal/src/Schema/Metadata/ViewMetadataRow.php new file mode 100644 index 000000000..e8a29f29c --- /dev/null +++ b/doctrine/dbal/src/Schema/Metadata/ViewMetadataRow.php @@ -0,0 +1,39 @@ +schemaName; + } + + /** @return non-empty-string */ + public function getViewName(): string + { + return $this->viewName; + } + + public function getDefinition(): string + { + return $this->definition; + } +} diff --git a/doctrine/dbal/src/Schema/MySQLSchemaManager.php b/doctrine/dbal/src/Schema/MySQLSchemaManager.php index 6e444d213..78359c877 100644 --- a/doctrine/dbal/src/Schema/MySQLSchemaManager.php +++ b/doctrine/dbal/src/Schema/MySQLSchemaManager.php @@ -1,26 +1,35 @@ "'", ]; - /** - * {@inheritDoc} - */ - public function listTableNames() - { - return $this->doListTableNames(); - } + private ?DefaultTableOptions $defaultTableOptions = null; /** - * {@inheritDoc} - */ - public function listTables() - { - return $this->doListTables(); - } - - /** - * {@inheritDoc} + * @deprecated Use the schema name and the unqualified table name separately instead. * - * @deprecated Use {@see introspectTable()} instead. - */ - public function listTableDetails($name) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5595', - '%s is deprecated. Use introspectTable() instead.', - __METHOD__, - ); - - return $this->doListTableDetails($name); - } - - /** * {@inheritDoc} */ - public function listTableColumns($table, $database = null) + protected function _getPortableTableDefinition(array $table): string { - return $this->doListTableColumns($table, $database); + return $table['TABLE_NAME']; } /** * {@inheritDoc} */ - public function listTableIndexes($table) - { - return $this->doListTableIndexes($table); - } - - /** - * {@inheritDoc} - */ - public function listTableForeignKeys($table, $database = null) - { - return $this->doListTableForeignKeys($table, $database); - } - - /** - * {@inheritDoc} - */ - protected function _getPortableViewDefinition($view) + protected function _getPortableViewDefinition(array $view): View { return new View($view['TABLE_NAME'], $view['VIEW_DEFINITION']); } @@ -118,45 +82,34 @@ protected function _getPortableViewDefinition($view) /** * {@inheritDoc} */ - protected function _getPortableTableDefinition($table) + protected function _getPortableTableIndexesList(array $rows, string $tableName): array { - return array_shift($table); - } + foreach ($rows as $i => $row) { + $row = array_change_key_case($row, CASE_LOWER); - /** - * {@inheritDoc} - */ - protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) - { - foreach ($tableIndexes as $k => $v) { - $v = array_change_key_case($v, CASE_LOWER); - if ($v['key_name'] === 'PRIMARY') { - $v['primary'] = true; - } else { - $v['primary'] = false; - } + $row['primary'] = $row['key_name'] === 'PRIMARY'; - if (strpos($v['index_type'], 'FULLTEXT') !== false) { - $v['flags'] = ['FULLTEXT']; - } elseif (strpos($v['index_type'], 'SPATIAL') !== false) { - $v['flags'] = ['SPATIAL']; + if (str_contains($row['index_type'], 'FULLTEXT')) { + $row['flags'] = ['FULLTEXT']; + } elseif (str_contains($row['index_type'], 'SPATIAL')) { + $row['flags'] = ['SPATIAL']; } // Ignore prohibited prefix `length` for spatial index - if (strpos($v['index_type'], 'SPATIAL') === false) { - $v['length'] = isset($v['sub_part']) ? (int) $v['sub_part'] : null; + if (! str_contains($row['index_type'], 'SPATIAL')) { + $row['length'] = isset($row['sub_part']) ? (int) $row['sub_part'] : null; } - $tableIndexes[$k] = $v; + $rows[$i] = $row; } - return parent::_getPortableTableIndexesList($tableIndexes, $tableName); + return parent::_getPortableTableIndexesList($rows, $tableName); } /** * {@inheritDoc} */ - protected function _getPortableDatabaseDefinition($database) + protected function _getPortableDatabaseDefinition(array $database): string { return $database['Database']; } @@ -164,56 +117,28 @@ protected function _getPortableDatabaseDefinition($database) /** * {@inheritDoc} */ - protected function _getPortableTableColumnDefinition($tableColumn) + protected function _getPortableTableColumnDefinition(array $tableColumn): Column { $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); - $dbType = strtolower($tableColumn['type']); - $dbType = strtok($dbType, '(), '); - assert(is_string($dbType)); - - $length = $tableColumn['length'] ?? strtok('(), '); - - $fixed = null; - - if (! isset($tableColumn['name'])) { - $tableColumn['name'] = ''; - } - - $scale = null; + $dbType = $tableColumn['type']; + $length = null; + $scale = 0; $precision = null; + $fixed = false; + $values = []; - $type = $origType = $this->_platform->getDoctrineTypeMapping($dbType); - - // In cases where not connected to a database DESCRIBE $table does not return 'Comment' - if (isset($tableColumn['comment'])) { - $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type); - $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type); - } + $type = $this->platform->getDoctrineTypeMapping($dbType); switch ($dbType) { case 'char': - case 'binary': - $fixed = true; + case 'varchar': + $length = (int) $tableColumn['character_maximum_length']; break; - case 'float': - case 'double': - case 'real': - case 'numeric': - case 'decimal': - if ( - preg_match( - '([A-Za-z]+\(([0-9]+),([0-9]+)\))', - $tableColumn['type'], - $match, - ) === 1 - ) { - $precision = $match[1]; - $scale = $match[2]; - $length = null; - } - + case 'binary': + case 'varbinary': + $length = (int) $tableColumn['character_octet_length']; break; case 'tinytext': @@ -240,85 +165,85 @@ protected function _getPortableTableColumnDefinition($tableColumn) $length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMBLOB; break; - case 'tinyint': - case 'smallint': - case 'mediumint': - case 'int': - case 'integer': - case 'bigint': - case 'year': - $length = null; + case 'float': + case 'double': + case 'real': + case 'numeric': + case 'decimal': + $precision = (int) $tableColumn['numeric_precision']; + + if (isset($tableColumn['numeric_scale'])) { + $scale = (int) $tableColumn['numeric_scale']; + } + + break; + } + + switch ($dbType) { + case 'char': + case 'binary': + $fixed = true; + break; + + case 'enum': + $values = $this->parseEnumExpression($tableColumn['column_type']); break; } - if ($this->_platform instanceof MariaDb1027Platform) { - $columnDefault = $this->getMariaDb1027ColumnDefault($this->_platform, $tableColumn['default']); + if ($tableColumn['default'] === null) { + $columnDefault = null; + } elseif ($this->platform instanceof MariaDBPlatform) { + $columnDefault = $this->parseMariaDBColumnDefault($tableColumn['default']); } else { - $columnDefault = $tableColumn['default']; + $columnDefault = $this->parseMySQLColumnDefault($dbType, $tableColumn['default']); } $options = [ - 'length' => $length !== null ? (int) $length : null, - 'unsigned' => strpos($tableColumn['type'], 'unsigned') !== false, - 'fixed' => (bool) $fixed, + 'length' => $length, + 'unsigned' => str_contains($tableColumn['column_type'], 'unsigned'), + 'fixed' => $fixed, 'default' => $columnDefault, 'notnull' => $tableColumn['null'] !== 'YES', - 'scale' => null, - 'precision' => null, - 'autoincrement' => strpos($tableColumn['extra'], 'auto_increment') !== false, - 'comment' => isset($tableColumn['comment']) && $tableColumn['comment'] !== '' - ? $tableColumn['comment'] - : null, + 'scale' => $scale, + 'precision' => $precision, + 'autoincrement' => str_contains($tableColumn['extra'], 'auto_increment'), + 'values' => $values, ]; - if ($scale !== null && $precision !== null) { - $options['scale'] = (int) $scale; - $options['precision'] = (int) $precision; + if ($tableColumn['comment'] !== null) { + $options['comment'] = $tableColumn['comment']; } $column = new Column($tableColumn['field'], Type::getType($type), $options); + $column->setPlatformOption('charset', $tableColumn['characterset']); + $column->setPlatformOption('collation', $tableColumn['collation']); - if (isset($tableColumn['characterset'])) { - $column->setPlatformOption('charset', $tableColumn['characterset']); - } - - if (isset($tableColumn['collation'])) { - $column->setPlatformOption('collation', $tableColumn['collation']); - } - - if (isset($tableColumn['declarationMismatch'])) { - $column->setPlatformOption('declarationMismatch', $tableColumn['declarationMismatch']); - } + return $column; + } - // Check underlying database type where doctrine type is inferred from DC2Type comment - // and set a flag if it is not as expected. - if ($type === 'json' && $origType !== $type && $this->expectedDbType($type, $options) !== $dbType) { - $column->setPlatformOption('declarationMismatch', true); - } + /** @return list */ + private function parseEnumExpression(string $expression): array + { + $result = preg_match_all("/'([^']*(?:''[^']*)*)'/", $expression, $matches); + assert($result !== false); - return $column; + return array_map( + static fn (string $match): string => strtr($match, ["''" => "'"]), + $matches[1], + ); } - /** - * Returns the database data type for a given doctrine type and column - * - * Note that for data types that depend on length where length is not part of the column definition - * and therefore the $tableColumn['length'] will not be set, for example TEXT (which could be LONGTEXT, - * MEDIUMTEXT) or BLOB (LONGBLOB or TINYBLOB), the expectedDbType cannot be inferred exactly, merely - * the default type. - * - * This method is intended to be used to determine underlying database type where doctrine type is - * inferred from a DC2Type comment. - * - * @param mixed[] $tableColumn - */ - private function expectedDbType(string $type, array $tableColumn): string + /** @link https://dev.mysql.com/doc/refman/8.4/en/timestamp-initialization.html */ + private function parseMySQLColumnDefault(string $type, string $default): string|DefaultExpression { - $_type = Type::getType($type); - $expectedDbType = strtolower($_type->getSQLDeclaration($tableColumn, $this->_platform)); - $expectedDbType = strtok($expectedDbType, '(), '); + // There is no way to tell whether the "CURRENT_TIMESTAMP" value represents a string or an expression in MySQL + // schema introspection results. We rely on the fact that the "CURRENT_TIMESTAMP" expression is only supported + // for the "DATETIME" and "TIMESTAMP" data types and represent this value as an expression only in these cases. + if (($type === 'datetime' || $type === 'timestamp') && $default === 'CURRENT_TIMESTAMP') { + return new CurrentTimestamp(); + } - return $expectedDbType === false ? '' : $expectedDbType; + return $default; } /** @@ -335,11 +260,11 @@ private function expectedDbType(string $type, array $tableColumn): string * @link https://mariadb.com/kb/en/library/information-schema-columns-table/ * @link https://jira.mariadb.org/browse/MDEV-13132 * - * @param string|null $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7 + * @param string $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7 */ - private function getMariaDb1027ColumnDefault(MariaDb1027Platform $platform, ?string $columnDefault): ?string + private function parseMariaDBColumnDefault(string $columnDefault): string|DefaultExpression|null { - if ($columnDefault === 'NULL' || $columnDefault === null) { + if ($columnDefault === 'NULL') { return null; } @@ -347,49 +272,43 @@ private function getMariaDb1027ColumnDefault(MariaDb1027Platform $platform, ?str return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES); } - switch ($columnDefault) { - case 'current_timestamp()': - return $platform->getCurrentTimestampSQL(); - - case 'curdate()': - return $platform->getCurrentDateSQL(); - - case 'curtime()': - return $platform->getCurrentTimeSQL(); - } - - return $columnDefault; + return match ($columnDefault) { + 'current_timestamp()' => new CurrentTimestamp(), + 'curdate()' => new CurrentDate(), + 'curtime()' => new CurrentTime(), + default => $columnDefault, + }; } /** * {@inheritDoc} */ - protected function _getPortableTableForeignKeysList($tableForeignKeys) + protected function _getPortableTableForeignKeysList(array $rows): array { $list = []; - foreach ($tableForeignKeys as $value) { - $value = array_change_key_case($value, CASE_LOWER); - if (! isset($list[$value['constraint_name']])) { - if (! isset($value['delete_rule']) || $value['delete_rule'] === 'RESTRICT') { - $value['delete_rule'] = null; + foreach ($rows as $row) { + $row = array_change_key_case($row, CASE_LOWER); + if (! isset($list[$row['constraint_name']])) { + if (! isset($row['delete_rule']) || $row['delete_rule'] === 'RESTRICT') { + $row['delete_rule'] = null; } - if (! isset($value['update_rule']) || $value['update_rule'] === 'RESTRICT') { - $value['update_rule'] = null; + if (! isset($row['update_rule']) || $row['update_rule'] === 'RESTRICT') { + $row['update_rule'] = null; } - $list[$value['constraint_name']] = [ - 'name' => $value['constraint_name'], + $list[$row['constraint_name']] = [ + 'name' => $this->getQuotedIdentifierName($row['constraint_name']), 'local' => [], 'foreign' => [], - 'foreignTable' => $value['referenced_table_name'], - 'onDelete' => $value['delete_rule'], - 'onUpdate' => $value['update_rule'], + 'foreignTable' => $row['referenced_table_name'], + 'onDelete' => $row['delete_rule'], + 'onUpdate' => $row['update_rule'], ]; } - $list[$value['constraint_name']]['local'][] = $value['column_name']; - $list[$value['constraint_name']]['foreign'][] = $value['referenced_column_name']; + $list[$row['constraint_name']]['local'][] = $row['column_name']; + $list[$row['constraint_name']]['foreign'][] = $row['referenced_column_name']; } return parent::_getPortableTableForeignKeysList($list); @@ -398,7 +317,7 @@ protected function _getPortableTableForeignKeysList($tableForeignKeys) /** * {@inheritDoc} */ - protected function _getPortableTableForeignKeyDefinition($tableForeignKey): ForeignKeyConstraint + protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint { return new ForeignKeyConstraint( $tableForeignKey['local'], @@ -412,13 +331,19 @@ protected function _getPortableTableForeignKeyDefinition($tableForeignKey): Fore ); } - public function createComparator(): Comparator + /** @throws Exception */ + public function createComparator(/* ComparatorConfig $config = new ComparatorConfig() */): Comparator { return new MySQL\Comparator( - $this->_platform, + $this->platform, + new CachingCharsetMetadataProvider( + new ConnectionCharsetMetadataProvider($this->connection), + ), new CachingCollationMetadataProvider( - new ConnectionCollationMetadataProvider($this->_conn), + new ConnectionCollationMetadataProvider($this->connection), ), + $this->getDefaultTableOptions(), + func_num_args() > 0 ? func_get_arg(0) : new ComparatorConfig(), ); } @@ -432,23 +357,33 @@ protected function selectTableNames(string $databaseName): Result ORDER BY TABLE_NAME SQL; - return $this->_conn->executeQuery($sql, [$databaseName]); + return $this->connection->executeQuery($sql, [$databaseName]); } protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result { - // @todo 4.0 - call getColumnTypeSQLSnippet() instead - [$columnTypeSQL, $joinCheckConstraintSQL] = $this->_platform->getColumnTypeSQLSnippets('c', $databaseName); - - $sql = 'SELECT'; + // The schema name is passed multiple times as a literal in the WHERE clause instead of using a JOIN condition + // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions + // caused by https://bugs.mysql.com/bug.php?id=81347 + $conditions = ['c.TABLE_SCHEMA = ?', 't.TABLE_SCHEMA = ?']; + $params = [$databaseName, $databaseName]; - if ($tableName === null) { - $sql .= ' c.TABLE_NAME,'; + if ($tableName !== null) { + $conditions[] = 't.TABLE_NAME = ?'; + $params[] = $tableName; } - $sql .= <<platform->getColumnTypeSQLSnippet('c', $databaseName), + implode(' AND ', $conditions), + ); - return $this->_conn->executeQuery($sql, $params); + return $this->connection->executeQuery($sql, $params); } protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; + $conditions = ['TABLE_SCHEMA = ?']; + $params = [$databaseName]; - if ($tableName === null) { - $sql .= ' TABLE_NAME,'; + if ($tableName !== null) { + $conditions[] = 'TABLE_NAME = ?'; + $params[] = $tableName; } - $sql .= <<<'SQL' + $sql = sprintf( + <<<'SQL' +SELECT + TABLE_NAME, NON_UNIQUE AS Non_Unique, INDEX_NAME AS Key_name, COLUMN_NAME AS Column_Name, SUB_PART AS Sub_Part, INDEX_TYPE AS Index_Type FROM information_schema.STATISTICS -SQL; - - $conditions = ['TABLE_SCHEMA = ?']; - $params = [$databaseName]; - - if ($tableName !== null) { - $conditions[] = 'TABLE_NAME = ?'; - $params[] = $tableName; - } - - $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY SEQ_IN_INDEX'; +WHERE %s +ORDER BY TABLE_NAME, + SEQ_IN_INDEX +SQL, + implode(' AND ', $conditions), + ); - return $this->_conn->executeQuery($sql, $params); + return $this->connection->executeQuery($sql, $params); } protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT DISTINCT'; + // The schema name is passed multiple times in the WHERE clause instead of using a JOIN condition + // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions + // caused by https://bugs.mysql.com/bug.php?id=81347 + $conditions = ['k.TABLE_SCHEMA = ?', 'c.CONSTRAINT_SCHEMA = ?']; + $params = [$databaseName, $databaseName]; - if ($tableName === null) { - $sql .= ' k.TABLE_NAME,'; + if ($tableName !== null) { + $conditions[] = 'k.TABLE_NAME = ?'; + $params[] = $tableName; } - $sql .= <<<'SQL' + $sql = sprintf( + <<<'SQL' +SELECT + k.TABLE_NAME, k.CONSTRAINT_NAME, k.COLUMN_NAME, k.REFERENCED_TABLE_NAME, k.REFERENCED_COLUMN_NAME, - k.ORDINAL_POSITION /*!50116, + k.ORDINAL_POSITION, c.UPDATE_RULE, - c.DELETE_RULE */ -FROM information_schema.key_column_usage k /*!50116 + c.DELETE_RULE +FROM information_schema.key_column_usage k INNER JOIN information_schema.referential_constraints c ON c.CONSTRAINT_NAME = k.CONSTRAINT_NAME -AND c.TABLE_NAME = k.TABLE_NAME */ -SQL; - - $conditions = ['k.TABLE_SCHEMA = ?']; - $params = [$databaseName]; - - if ($tableName !== null) { - $conditions[] = 'k.TABLE_NAME = ?'; - $params[] = $tableName; - } - - $conditions[] = 'k.REFERENCED_COLUMN_NAME IS NOT NULL'; - - $sql .= ' WHERE ' . implode(' AND ', $conditions) - // The schema name is passed multiple times in the WHERE clause instead of using a JOIN condition - // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions - // caused by https://bugs.mysql.com/bug.php?id=81347. - // Use a string literal for the database name since the internal PDO SQL parser - // cannot recognize parameter placeholders inside conditional comments - . ' /*!50116 AND c.CONSTRAINT_SCHEMA = ' . $this->_conn->quote($databaseName) . ' */' - . ' ORDER BY k.ORDINAL_POSITION'; +AND c.TABLE_NAME = k.TABLE_NAME +WHERE %s +AND k.REFERENCED_COLUMN_NAME IS NOT NULL +ORDER BY k.TABLE_NAME, + k.CONSTRAINT_NAME, + k.ORDINAL_POSITION +SQL, + implode(' AND ', $conditions), + ); - return $this->_conn->executeQuery($sql, $params); + return $this->connection->executeQuery($sql, $params); } /** @@ -557,15 +481,15 @@ protected function selectForeignKeyColumns(string $databaseName, ?string $tableN */ protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array { - $sql = $this->_platform->fetchTableOptionsByTable($tableName !== null); + $sql = $this->platform->fetchTableOptionsByTable($tableName !== null); $params = [$databaseName]; if ($tableName !== null) { $params[] = $tableName; } - /** @var array> $metadata */ - $metadata = $this->_conn->executeQuery($sql, $params) + /** @var array> $metadata */ + $metadata = $this->connection->executeQuery($sql, $params) ->fetchAllAssociativeIndexed(); $tableOptions = []; @@ -585,7 +509,7 @@ protected function fetchTableOptionsByTable(string $databaseName, ?string $table return $tableOptions; } - /** @return string[]|true[] */ + /** @return array|array */ private function parseCreateOptions(?string $string): array { $options = []; @@ -602,4 +526,30 @@ private function parseCreateOptions(?string $string): array return $options; } + + /** @throws Exception */ + private function getDefaultTableOptions(): DefaultTableOptions + { + if ($this->defaultTableOptions === null) { + $row = $this->connection->fetchNumeric( + 'SELECT @@character_set_database, @@collation_database', + ); + + assert($row !== false); + + $this->defaultTableOptions = new DefaultTableOptions(...$row); + } + + return $this->defaultTableOptions; + } + + /** Returns the quoted representation of the given identifier name. */ + private function getQuotedIdentifierName(?string $identifier): ?string + { + if ($identifier === null) { + return null; + } + + return $this->platform->quoteSingleIdentifier($identifier); + } } diff --git a/doctrine/dbal/src/Schema/Name.php b/doctrine/dbal/src/Schema/Name.php new file mode 100644 index 000000000..d5169775b --- /dev/null +++ b/doctrine/dbal/src/Schema/Name.php @@ -0,0 +1,25 @@ + $identifiers */ + private array $identifiers; + + public function __construct(Identifier $firstIdentifier, Identifier ...$otherIdentifiers) + { + $this->identifiers = array_merge([$firstIdentifier], array_values($otherIdentifiers)); + } + + /** @return non-empty-list */ + public function getIdentifiers(): array + { + return $this->identifiers; + } + + public function toSQL(AbstractPlatform $platform): string + { + return $this->joinIdentifiers(static fn (Identifier $identifier): string => $identifier->toSQL($platform)); + } + + public function toString(): string + { + return $this->joinIdentifiers(static fn (Identifier $identifier): string => $identifier->toString()); + } + + /** @param callable(Identifier): string $mapper */ + private function joinIdentifiers(callable $mapper): string + { + return implode('.', array_map($mapper, $this->identifiers)); + } +} diff --git a/doctrine/dbal/src/Schema/Name/Identifier.php b/doctrine/dbal/src/Schema/Name/Identifier.php new file mode 100644 index 000000000..8ce33196e --- /dev/null +++ b/doctrine/dbal/src/Schema/Name/Identifier.php @@ -0,0 +1,103 @@ +value) === 0) { + throw InvalidIdentifier::fromEmpty(); + } + } + + /** @return non-empty-string */ + public function getValue(): string + { + return $this->value; + } + + public function isQuoted(): bool + { + return $this->isQuoted; + } + + /** + * Returns whether this identifier is equal to the other. + */ + public function equals(self $other, UnquotedIdentifierFolding $folding): bool + { + if ($this === $other) { + return true; + } + + return $this->toNormalizedValue($folding) === $other->toNormalizedValue($folding); + } + + public function toSQL(AbstractPlatform $platform): string + { + return $platform->quoteSingleIdentifier( + $this->toNormalizedValue($platform->getUnquotedIdentifierFolding()), + ); + } + + /** + * Returns the literal value of the identifier normalized according to the rules of the given database platform. + * + * Consumers should use the normalized value for schema comparison and referencing the objects to be introspected. + * + * @return non-empty-string + */ + public function toNormalizedValue(UnquotedIdentifierFolding $folding): string + { + if (! $this->isQuoted) { + return $folding->foldUnquotedIdentifier($this->value); + } + + return $this->value; + } + + public function toString(): string + { + if (! $this->isQuoted) { + return $this->value; + } + + return sprintf('"%s"', str_replace('"', '""', $this->value)); + } + + /** + * Creates a quoted identifier. + * + * @param non-empty-string $value + */ + public static function quoted(string $value): self + { + return new self($value, true); + } + + /** + * Creates an unquoted identifier. + * + * @param non-empty-string $value + */ + public static function unquoted(string $value): self + { + return new self($value, false); + } +} diff --git a/doctrine/dbal/src/Schema/Name/OptionallyQualifiedName.php b/doctrine/dbal/src/Schema/Name/OptionallyQualifiedName.php new file mode 100644 index 000000000..e82a2475a --- /dev/null +++ b/doctrine/dbal/src/Schema/Name/OptionallyQualifiedName.php @@ -0,0 +1,103 @@ +unqualifiedName; + } + + public function getQualifier(): ?Identifier + { + return $this->qualifier; + } + + public function toSQL(AbstractPlatform $platform): string + { + $unqualifiedName = $this->unqualifiedName->toSQL($platform); + + if ($this->qualifier === null) { + return $unqualifiedName; + } + + return $this->qualifier->toSQL($platform) . '.' . $unqualifiedName; + } + + public function toString(): string + { + $unqualifiedName = $this->unqualifiedName->toString(); + + if ($this->qualifier === null) { + return $unqualifiedName; + } + + return $this->qualifier->toString() . '.' . $unqualifiedName; + } + + /** + * Returns whether this optionally qualified name is equal to the other. + * + * To be comparable, both names must either have a qualifier or have no qualifier. + */ + public function equals(self $other, UnquotedIdentifierFolding $folding): bool + { + if ($this === $other) { + return true; + } + + if (($this->qualifier === null) !== ($other->qualifier === null)) { + throw IncomparableNames::fromOptionallyQualifiedNames($this, $other); + } + + if (! $this->unqualifiedName->equals($other->getUnqualifiedName(), $folding)) { + return false; + } + + return $this->qualifier === null + || $other->qualifier === null + || $this->qualifier->equals($other->qualifier, $folding); + } + + /** + * Creates an optionally qualified name with all identifiers quoted. + * + * @param non-empty-string $unqualifiedName + * @param ?non-empty-string $qualifier + */ + public static function quoted(string $unqualifiedName, ?string $qualifier = null): self + { + return new self( + Identifier::quoted($unqualifiedName), + $qualifier !== null ? Identifier::quoted($qualifier) : null, + ); + } + + /** + * Creates an optionally qualified name with all identifiers unquoted. + * + * @param non-empty-string $unqualifiedName + * @param ?non-empty-string $qualifier + */ + public static function unquoted(string $unqualifiedName, ?string $qualifier = null): self + { + return new self( + Identifier::unquoted($unqualifiedName), + $qualifier !== null ? Identifier::unquoted($qualifier) : null, + ); + } +} diff --git a/doctrine/dbal/src/Schema/Name/Parser.php b/doctrine/dbal/src/Schema/Name/Parser.php new file mode 100644 index 000000000..90254ba8e --- /dev/null +++ b/doctrine/dbal/src/Schema/Name/Parser.php @@ -0,0 +1,25 @@ + + */ +final class GenericNameParser implements Parser +{ + private const IDENTIFIER_PATTERN = <<<'PATTERN' + /\G + (?: + "(?[^"]*(?:""[^"]*)*)" # ANSI SQL double-quoted + | `(?[^`]*(?:``[^`]*)*)` # MySQL-style backtick-quoted + | \[(?[^]]*(?:]][^]]*)*)] # SQL Server-style square-bracket-quoted + | (?[^\s."`\[\]]+) # Unquoted + ) + /x + PATTERN; + + public function parse(string $input): GenericName + { + $offset = 0; + $identifiers = []; + $length = strlen($input); + + while (true) { + if ($offset >= $length) { + throw ExpectedNextIdentifier::new(); + } + + if (preg_match(self::IDENTIFIER_PATTERN, $input, $matches, 0, $offset) === 0) { + throw UnableToParseIdentifier::new($offset); + } + + if (isset($matches['ansi']) && strlen($matches['ansi']) > 0) { + $identifier = Identifier::quoted(str_replace('""', '"', $matches['ansi'])); + } elseif (isset($matches['mysql']) && strlen($matches['mysql']) > 0) { + $identifier = Identifier::quoted(str_replace('``', '`', $matches['mysql'])); + } elseif (isset($matches['sqlserver']) && strlen($matches['sqlserver']) > 0) { + $identifier = Identifier::quoted(str_replace(']]', ']', $matches['sqlserver'])); + } else { + assert(isset($matches['unquoted']) && strlen($matches['unquoted']) > 0); + $identifier = Identifier::unquoted($matches['unquoted']); + } + + $identifiers[] = $identifier; + + $offset += strlen($matches[0]); + + if ($offset >= $length) { + break; + } + + $character = $input[$offset]; + + if ($character !== '.') { + throw ExpectedDot::new($offset, $character); + } + + $offset++; + } + + assert(count($identifiers) > 0); + + return new GenericName(...$identifiers); + } +} diff --git a/doctrine/dbal/src/Schema/Name/Parser/OptionallyQualifiedNameParser.php b/doctrine/dbal/src/Schema/Name/Parser/OptionallyQualifiedNameParser.php new file mode 100644 index 000000000..92b868a21 --- /dev/null +++ b/doctrine/dbal/src/Schema/Name/Parser/OptionallyQualifiedNameParser.php @@ -0,0 +1,35 @@ + + */ +final readonly class OptionallyQualifiedNameParser implements Parser +{ + public function __construct(private GenericNameParser $genericNameParser) + { + } + + public function parse(string $input): OptionallyQualifiedName + { + $identifiers = $this->genericNameParser->parse($input) + ->getIdentifiers(); + + return match (count($identifiers)) { + 1 => new OptionallyQualifiedName($identifiers[0], null), + 2 => new OptionallyQualifiedName($identifiers[1], $identifiers[0]), + default => throw InvalidName::forOptionallyQualifiedName(count($identifiers)), + }; + } +} diff --git a/doctrine/dbal/src/Schema/Name/Parser/UnqualifiedNameParser.php b/doctrine/dbal/src/Schema/Name/Parser/UnqualifiedNameParser.php new file mode 100644 index 000000000..f91dc1d78 --- /dev/null +++ b/doctrine/dbal/src/Schema/Name/Parser/UnqualifiedNameParser.php @@ -0,0 +1,35 @@ + + */ +final readonly class UnqualifiedNameParser implements Parser +{ + public function __construct(private GenericNameParser $genericNameParser) + { + } + + public function parse(string $input): UnqualifiedName + { + $identifiers = $this->genericNameParser->parse($input) + ->getIdentifiers(); + + if (count($identifiers) > 1) { + throw InvalidName::forUnqualifiedName(count($identifiers)); + } + + return new UnqualifiedName($identifiers[0]); + } +} diff --git a/doctrine/dbal/src/Schema/Name/Parsers.php b/doctrine/dbal/src/Schema/Name/Parsers.php new file mode 100644 index 000000000..58611d1df --- /dev/null +++ b/doctrine/dbal/src/Schema/Name/Parsers.php @@ -0,0 +1,43 @@ +identifier; + } + + public function toSQL(AbstractPlatform $platform): string + { + return $this->identifier->toSQL($platform); + } + + public function toString(): string + { + return $this->identifier->toString(); + } + + /** + * Returns whether this unqualified name is equal to the other. + */ + public function equals(self $other, UnquotedIdentifierFolding $folding): bool + { + if ($this === $other) { + return true; + } + + return $this->identifier->equals($other->getIdentifier(), $folding); + } + + /** + * Creates a quoted unqualified name. + * + * @param non-empty-string $value + */ + public static function quoted(string $value): self + { + return new self(Identifier::quoted($value)); + } + + /** + * Creates an unquoted unqualified name. + * + * @param non-empty-string $value + */ + public static function unquoted(string $value): self + { + return new self(Identifier::unquoted($value)); + } +} diff --git a/doctrine/dbal/src/Schema/Name/UnquotedIdentifierFolding.php b/doctrine/dbal/src/Schema/Name/UnquotedIdentifierFolding.php new file mode 100644 index 000000000..4b1ed833d --- /dev/null +++ b/doctrine/dbal/src/Schema/Name/UnquotedIdentifierFolding.php @@ -0,0 +1,45 @@ + strtoupper($value), + self::LOWER => strtolower($value), + self::NONE => $value, + }; + } +} diff --git a/doctrine/dbal/src/Schema/NamedObject.php b/doctrine/dbal/src/Schema/NamedObject.php new file mode 100644 index 000000000..c17310f6b --- /dev/null +++ b/doctrine/dbal/src/Schema/NamedObject.php @@ -0,0 +1,22 @@ +null, if the name is not set. + * + * @return ?N + */ + public function getObjectName(): ?Name; +} diff --git a/doctrine/dbal/src/Schema/OracleSchemaManager.php b/doctrine/dbal/src/Schema/OracleSchemaManager.php index 3608e0566..47d87400f 100644 --- a/doctrine/dbal/src/Schema/OracleSchemaManager.php +++ b/doctrine/dbal/src/Schema/OracleSchemaManager.php @@ -1,20 +1,25 @@ doListTableNames(); - } - - /** - * {@inheritDoc} - */ - public function listTables() - { - return $this->doListTables(); - } - - /** - * {@inheritDoc} - * - * @deprecated Use {@see introspectTable()} instead. - */ - public function listTableDetails($name) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5595', - '%s is deprecated. Use introspectTable() instead.', - __METHOD__, - ); - - return $this->doListTableDetails($name); - } - - /** - * {@inheritDoc} - */ - public function listTableColumns($table, $database = null) - { - return $this->doListTableColumns($table, $database); - } - - /** - * {@inheritDoc} - */ - public function listTableIndexes($table) - { - return $this->doListTableIndexes($table); - } - - /** - * {@inheritDoc} - */ - public function listTableForeignKeys($table, $database = null) - { - return $this->doListTableForeignKeys($table, $database); - } - - /** - * {@inheritDoc} - */ - protected function _getPortableViewDefinition($view) + protected function _getPortableViewDefinition(array $view): View { $view = array_change_key_case($view, CASE_LOWER); @@ -96,38 +44,40 @@ protected function _getPortableViewDefinition($view) } /** + * @deprecated Use the schema name and the unqualified table name separately instead. + * * {@inheritDoc} */ - protected function _getPortableTableDefinition($table) + protected function _getPortableTableDefinition(array $table): string { $table = array_change_key_case($table, CASE_LOWER); + /** @phpstan-ignore return.type */ return $this->getQuotedIdentifierName($table['table_name']); } /** * {@inheritDoc} */ - protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) + protected function _getPortableTableIndexesList(array $rows, string $tableName): array { $indexBuffer = []; - foreach ($tableIndexes as $tableIndex) { - $tableIndex = array_change_key_case($tableIndex, CASE_LOWER); + foreach ($rows as $row) { + $row = array_change_key_case($row, CASE_LOWER); - $keyName = strtolower($tableIndex['name']); - $buffer = []; + $buffer = []; - if ($tableIndex['is_primary'] === 'P') { - $keyName = 'primary'; + if ($row['is_primary'] === 'P') { + $buffer['key_name'] = 'primary'; $buffer['primary'] = true; $buffer['non_unique'] = false; } else { + $buffer['key_name'] = strtolower($row['name']); $buffer['primary'] = false; - $buffer['non_unique'] = ! $tableIndex['is_unique']; + $buffer['non_unique'] = ! $row['is_unique']; } - $buffer['key_name'] = $keyName; - $buffer['column_name'] = $this->getQuotedIdentifierName($tableIndex['column_name']); + $buffer['column_name'] = $this->getQuotedIdentifierName($row['column_name']); $indexBuffer[] = $buffer; } @@ -137,24 +87,24 @@ protected function _getPortableTableIndexesList($tableIndexes, $tableName = null /** * {@inheritDoc} */ - protected function _getPortableTableColumnDefinition($tableColumn) + protected function _getPortableTableColumnDefinition(array $tableColumn): Column { $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); $dbType = strtolower($tableColumn['data_type']); - if (strpos($dbType, 'timestamp(') === 0) { - if (strpos($dbType, 'with time zone') !== false) { + if (str_starts_with($dbType, 'timestamp(')) { + if (str_contains($dbType, 'with time zone')) { $dbType = 'timestamptz'; } else { $dbType = 'timestamp'; } } - $unsigned = $fixed = $precision = $scale = $length = null; + $length = $precision = null; + $scale = 0; + $fixed = false; - if (! isset($tableColumn['column_name'])) { - $tableColumn['column_name'] = ''; - } + assert(array_key_exists('data_default', $tableColumn)); // Default values returned from database sometimes have trailing spaces. if (is_string($tableColumn['data_default'])) { @@ -180,9 +130,7 @@ protected function _getPortableTableColumnDefinition($tableColumn) $scale = (int) $tableColumn['data_scale']; } - $type = $this->_platform->getDoctrineTypeMapping($dbType); - $type = $this->extractDoctrineTypeFromComment($tableColumn['comments'], $type); - $tableColumn['comments'] = $this->removeDoctrineTypeFromComment($tableColumn['comments'], $type); + $type = $this->platform->getDoctrineTypeMapping($dbType); switch ($dbType) { case 'number': @@ -198,68 +146,76 @@ protected function _getPortableTableColumnDefinition($tableColumn) break; + case 'float': + if ($precision === 63) { + $type = 'smallfloat'; + } + + break; + case 'varchar': case 'varchar2': case 'nvarchar2': - $length = $tableColumn['char_length']; - $fixed = false; + $length = (int) $tableColumn['char_length']; break; case 'raw': - $length = $tableColumn['data_length']; + $length = (int) $tableColumn['data_length']; $fixed = true; break; case 'char': case 'nchar': - $length = $tableColumn['char_length']; + $length = (int) $tableColumn['char_length']; $fixed = true; break; } $options = [ 'notnull' => $tableColumn['nullable'] === 'N', - 'fixed' => (bool) $fixed, - 'unsigned' => (bool) $unsigned, + 'fixed' => $fixed, 'default' => $tableColumn['data_default'], 'length' => $length, 'precision' => $precision, 'scale' => $scale, - 'comment' => isset($tableColumn['comments']) && $tableColumn['comments'] !== '' - ? $tableColumn['comments'] - : null, ]; + if ($tableColumn['comments'] !== null) { + $options['comment'] = $tableColumn['comments']; + } + return new Column($this->getQuotedIdentifierName($tableColumn['column_name']), Type::getType($type), $options); } /** * {@inheritDoc} */ - protected function _getPortableTableForeignKeysList($tableForeignKeys) + protected function _getPortableTableForeignKeysList(array $rows): array { $list = []; - foreach ($tableForeignKeys as $value) { - $value = array_change_key_case($value, CASE_LOWER); - if (! isset($list[$value['constraint_name']])) { - if ($value['delete_rule'] === 'NO ACTION') { - $value['delete_rule'] = null; + foreach ($rows as $row) { + $row = array_change_key_case($row, CASE_LOWER); + if (! isset($list[$row['constraint_name']])) { + if ($row['delete_rule'] === 'NO ACTION') { + $row['delete_rule'] = null; } - $list[$value['constraint_name']] = [ - 'name' => $this->getQuotedIdentifierName($value['constraint_name']), + $list[$row['constraint_name']] = [ + 'name' => $this->getQuotedIdentifierName($row['constraint_name']), 'local' => [], 'foreign' => [], - 'foreignTable' => $value['references_table'], - 'onDelete' => $value['delete_rule'], + 'foreignTable' => $row['references_table'], + 'onDelete' => $row['delete_rule'], + 'deferrable' => $row['deferrable'] === 'DEFERRABLE', + 'deferred' => $row['deferred'] === 'DEFERRED', ]; } - $localColumn = $this->getQuotedIdentifierName($value['local_column']); - $foreignColumn = $this->getQuotedIdentifierName($value['foreign_column']); + $localColumn = $this->getQuotedIdentifierName($row['local_column']); + $foreignColumn = $this->getQuotedIdentifierName($row['foreign_column']); - $list[$value['constraint_name']]['local'][$value['position']] = $localColumn; - $list[$value['constraint_name']]['foreign'][$value['position']] = $foreignColumn; + $list[$row['constraint_name']]['local'][] = $localColumn; + $list[$row['constraint_name']]['foreign'][] = $foreignColumn; } return parent::_getPortableTableForeignKeysList($list); @@ -268,21 +224,25 @@ protected function _getPortableTableForeignKeysList($tableForeignKeys) /** * {@inheritDoc} */ - protected function _getPortableTableForeignKeyDefinition($tableForeignKey): ForeignKeyConstraint + protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint { return new ForeignKeyConstraint( - array_values($tableForeignKey['local']), + $tableForeignKey['local'], $this->getQuotedIdentifierName($tableForeignKey['foreignTable']), - array_values($tableForeignKey['foreign']), + $tableForeignKey['foreign'], $this->getQuotedIdentifierName($tableForeignKey['name']), - ['onDelete' => $tableForeignKey['onDelete']], + [ + 'onDelete' => $tableForeignKey['onDelete'], + 'deferrable' => $tableForeignKey['deferrable'], + 'deferred' => $tableForeignKey['deferred'], + ], ); } /** * {@inheritDoc} */ - protected function _getPortableSequenceDefinition($sequence) + protected function _getPortableSequenceDefinition(array $sequence): Sequence { $sequence = array_change_key_case($sequence, CASE_LOWER); @@ -296,57 +256,50 @@ protected function _getPortableSequenceDefinition($sequence) /** * {@inheritDoc} */ - protected function _getPortableDatabaseDefinition($database) + protected function _getPortableDatabaseDefinition(array $database): string { $database = array_change_key_case($database, CASE_LOWER); return $database['username']; } - /** - * {@inheritDoc} - */ - public function createDatabase($database) + public function createDatabase(string $database): void { - $statement = $this->_platform->getCreateDatabaseSQL($database); + $statement = $this->platform->getCreateDatabaseSQL($database); - $params = $this->_conn->getParams(); + $params = $this->connection->getParams(); if (isset($params['password'])) { - $statement .= ' IDENTIFIED BY ' . $params['password']; + $statement .= ' IDENTIFIED BY ' . $this->connection->quoteSingleIdentifier($params['password']); } - $this->_conn->executeStatement($statement); + $this->connection->executeStatement($statement); $statement = 'GRANT DBA TO ' . $database; - $this->_conn->executeStatement($statement); + $this->connection->executeStatement($statement); } /** - * @internal The method should be only used from within the OracleSchemaManager class hierarchy. - * - * @param string $table - * - * @return bool + * @internal The method should be only used by the {@see OracleSchemaManager} class. * * @throws Exception */ - public function dropAutoincrement($table) + protected function dropAutoincrement(string $table): bool { - $sql = $this->_platform->getDropAutoincrementSql($table); + $sql = $this->platform->getDropAutoincrementSql($table); foreach ($sql as $query) { - $this->_conn->executeStatement($query); + $this->connection->executeStatement($query); } return true; } - /** - * {@inheritDoc} - */ - public function dropTable($name) + public function dropTable(string $name): void { - $this->tryMethod('dropAutoincrement', $name); + try { + $this->dropAutoincrement($name); + } catch (DatabaseObjectNotFoundException) { + } parent::dropTable($name); } @@ -356,13 +309,11 @@ public function dropTable($name) * * Quotes non-uppercase identifiers explicitly to preserve case * and thus make references to the particular identifier work. - * - * @param string $identifier The identifier to quote. */ - private function getQuotedIdentifierName($identifier): string + private function getQuotedIdentifierName(string $identifier): string { if (preg_match('/[a-z]/', $identifier) === 1) { - return $this->_platform->quoteIdentifier($identifier); + return $this->platform->quoteSingleIdentifier($identifier); } return $identifier; @@ -377,18 +328,23 @@ protected function selectTableNames(string $databaseName): Result ORDER BY TABLE_NAME SQL; - return $this->_conn->executeQuery($sql, ['OWNER' => $databaseName]); + return $this->connection->executeQuery($sql, ['OWNER' => $databaseName]); } protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; + $conditions = ['C.OWNER = :OWNER']; + $params = ['OWNER' => $databaseName]; - if ($tableName === null) { - $sql .= ' C.TABLE_NAME,'; + if ($tableName !== null) { + $conditions[] = 'C.TABLE_NAME = :TABLE_NAME'; + $params['TABLE_NAME'] = $tableName; } - $sql .= <<<'SQL' + $sql = sprintf( + <<<'SQL' + SELECT + C.TABLE_NAME, C.COLUMN_NAME, C.DATA_TYPE, C.DATA_DEFAULT, @@ -406,30 +362,29 @@ protected function selectTableColumns(string $databaseName, ?string $tableName = ON D.OWNER = C.OWNER AND D.TABLE_NAME = C.TABLE_NAME AND D.COLUMN_NAME = C.COLUMN_NAME -SQL; - - $conditions = ['C.OWNER = :OWNER']; - $params = ['OWNER' => $databaseName]; - - if ($tableName !== null) { - $conditions[] = 'C.TABLE_NAME = :TABLE_NAME'; - $params['TABLE_NAME'] = $tableName; - } - - $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY C.COLUMN_ID'; + WHERE %s + ORDER BY C.TABLE_NAME, C.COLUMN_ID +SQL, + implode(' AND ', $conditions), + ); - return $this->_conn->executeQuery($sql, $params); + return $this->connection->executeQuery($sql, $params); } protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; + $conditions = ['IND_COL.INDEX_OWNER = :OWNER']; + $params = ['OWNER' => $databaseName]; - if ($tableName === null) { - $sql .= ' IND_COL.TABLE_NAME,'; + if ($tableName !== null) { + $conditions[] = 'IND_COL.TABLE_NAME = :TABLE_NAME'; + $params['TABLE_NAME'] = $tableName; } - $sql .= <<<'SQL' + $sql = sprintf( + <<<'SQL' + SELECT + IND_COL.TABLE_NAME, IND_COL.INDEX_NAME AS NAME, IND.INDEX_TYPE AS TYPE, DECODE(IND.UNIQUENESS, 'NONUNIQUE', 0, 'UNIQUE', 1) AS IS_UNIQUE, @@ -443,33 +398,35 @@ protected function selectIndexColumns(string $databaseName, ?string $tableName = LEFT JOIN ALL_CONSTRAINTS CON ON CON.OWNER = IND_COL.INDEX_OWNER AND CON.INDEX_NAME = IND_COL.INDEX_NAME -SQL; - - $conditions = ['IND_COL.INDEX_OWNER = :OWNER']; - $params = ['OWNER' => $databaseName]; - - if ($tableName !== null) { - $conditions[] = 'IND_COL.TABLE_NAME = :TABLE_NAME'; - $params['TABLE_NAME'] = $tableName; - } - - $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY IND_COL.TABLE_NAME, IND_COL.INDEX_NAME' - . ', IND_COL.COLUMN_POSITION'; + WHERE %s + ORDER BY IND_COL.TABLE_NAME, + IND_COL.INDEX_NAME, + IND_COL.COLUMN_POSITION +SQL, + implode(' AND ', $conditions), + ); - return $this->_conn->executeQuery($sql, $params); + return $this->connection->executeQuery($sql, $params); } protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; + $conditions = ["ALC.CONSTRAINT_TYPE = 'R'", 'COLS.OWNER = :OWNER']; + $params = ['OWNER' => $databaseName]; - if ($tableName === null) { - $sql .= ' COLS.TABLE_NAME,'; + if ($tableName !== null) { + $conditions[] = 'COLS.TABLE_NAME = :TABLE_NAME'; + $params['TABLE_NAME'] = $tableName; } - $sql .= <<<'SQL' + $sql = sprintf( + <<<'SQL' + SELECT + COLS.TABLE_NAME, ALC.CONSTRAINT_NAME, ALC.DELETE_RULE, + ALC.DEFERRABLE, + ALC.DEFERRED, COLS.COLUMN_NAME LOCAL_COLUMN, COLS.POSITION, R_COLS.TABLE_NAME REFERENCES_TABLE, @@ -479,20 +436,15 @@ protected function selectForeignKeyColumns(string $databaseName, ?string $tableN LEFT JOIN ALL_CONS_COLUMNS R_COLS ON R_COLS.OWNER = ALC.R_OWNER AND R_COLS.CONSTRAINT_NAME = ALC.R_CONSTRAINT_NAME AND R_COLS.POSITION = COLS.POSITION -SQL; - - $conditions = ["ALC.CONSTRAINT_TYPE = 'R'", 'COLS.OWNER = :OWNER']; - $params = ['OWNER' => $databaseName]; - - if ($tableName !== null) { - $conditions[] = 'COLS.TABLE_NAME = :TABLE_NAME'; - $params['TABLE_NAME'] = $tableName; - } - - $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY COLS.TABLE_NAME, COLS.CONSTRAINT_NAME' - . ', COLS.POSITION'; + WHERE %s + ORDER BY COLS.TABLE_NAME, + COLS.CONSTRAINT_NAME, + COLS.POSITION +SQL, + implode(' AND ', $conditions), + ); - return $this->_conn->executeQuery($sql, $params); + return $this->connection->executeQuery($sql, $params); } /** @@ -500,8 +452,6 @@ protected function selectForeignKeyColumns(string $databaseName, ?string $tableN */ protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array { - $sql = 'SELECT TABLE_NAME, COMMENTS'; - $conditions = ['OWNER = :OWNER']; $params = ['OWNER' => $databaseName]; @@ -510,24 +460,26 @@ protected function fetchTableOptionsByTable(string $databaseName, ?string $table $params['TABLE_NAME'] = $tableName; } - $sql .= ' FROM ALL_TAB_COMMENTS WHERE ' . implode(' AND ', $conditions); - - /** @var array> $metadata */ - $metadata = $this->_conn->executeQuery($sql, $params) - ->fetchAllAssociativeIndexed(); + $sql = sprintf( + <<<'SQL' + SELECT TABLE_NAME, + COMMENTS + FROM ALL_TAB_COMMENTS + WHERE %s + ORDER BY TABLE_NAME +SQL, + implode(' AND ', $conditions), + ); $tableOptions = []; - foreach ($metadata as $table => $data) { - $data = array_change_key_case($data, CASE_LOWER); - - $tableOptions[$table] = [ - 'comment' => $data['comments'], - ]; + foreach ($this->connection->iterateKeyValue($sql, $params) as $table => $comments) { + $tableOptions[$table] = ['comment' => $comments]; } return $tableOptions; } + /** @deprecated Use {@see Identifier::toNormalizedValue()} instead. */ protected function normalizeName(string $name): string { $identifier = new Identifier($name); diff --git a/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php b/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php index 0510d1a82..10072969f 100644 --- a/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php +++ b/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php @@ -1,5 +1,7 @@ doListTableNames(); - } - - /** - * {@inheritDoc} - */ - public function listTables() - { - return $this->doListTables(); - } - - /** - * {@inheritDoc} - * - * @deprecated Use {@see introspectTable()} instead. - */ - public function listTableDetails($name) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5595', - '%s is deprecated. Use introspectTable() instead.', - __METHOD__, - ); - - return $this->doListTableDetails($name); - } - - /** - * {@inheritDoc} - */ - public function listTableColumns($table, $database = null) - { - return $this->doListTableColumns($table, $database); - } - - /** - * {@inheritDoc} - */ - public function listTableIndexes($table) - { - return $this->doListTableIndexes($table); - } - - /** - * {@inheritDoc} - */ - public function listTableForeignKeys($table, $database = null) - { - return $this->doListTableForeignKeys($table, $database); - } - - /** - * Gets all the existing schema names. - * - * @deprecated Use {@see listSchemaNames()} instead. - * - * @return string[] - * - * @throws Exception - */ - public function getSchemaNames() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4503', - 'PostgreSQLSchemaManager::getSchemaNames() is deprecated,' - . ' use PostgreSQLSchemaManager::listSchemaNames() instead.', - ); - - return $this->listNamespaceNames(); - } - /** * {@inheritDoc} */ public function listSchemaNames(): array { - return $this->_conn->fetchFirstColumn( + return $this->connection->fetchFirstColumn( <<<'SQL' SELECT schema_name FROM information_schema.schemata @@ -134,93 +49,44 @@ public function listSchemaNames(): array } /** - * {@inheritDoc} - * - * @deprecated - */ - public function getSchemaSearchPaths() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4821', - 'PostgreSQLSchemaManager::getSchemaSearchPaths() is deprecated.', - ); - - $params = $this->_conn->getParams(); - - $searchPaths = $this->_conn->fetchOne('SHOW search_path'); - assert($searchPaths !== false); - - $schema = explode(',', $searchPaths); - - if (isset($params['user'])) { - $schema = str_replace('"$user"', $params['user'], $schema); - } - - return array_map('trim', $schema); - } - - /** - * Gets names of all existing schemas in the current users search path. - * - * This is a PostgreSQL only function. - * - * @internal The method should be only used from within the PostgreSQLSchemaManager class hierarchy. + * Returns the name of the current schema. * - * @return string[] + * @deprecated Use {@link getCurrentSchemaName()} instead * * @throws Exception */ - public function getExistingSchemaSearchPaths() + protected function getCurrentSchema(): ?string { - if ($this->existingSchemaPaths === null) { - $this->determineExistingSchemaSearchPaths(); - } - - assert($this->existingSchemaPaths !== null); - - return $this->existingSchemaPaths; + return $this->getCurrentSchemaName(); } /** - * Returns the name of the current schema. + * Determines the name of the current schema. + * + * @deprecated Use {@link determineCurrentSchemaName()} instead * - * @return string|null + * @return non-empty-string * * @throws Exception */ - protected function getCurrentSchema() + protected function determineCurrentSchema(): string { - $schemas = $this->getExistingSchemaSearchPaths(); + $currentSchema = $this->connection->fetchOne('SELECT current_schema()'); + assert(is_string($currentSchema)); + assert(strlen($currentSchema) > 0); - return array_shift($schemas); + return $currentSchema; } - /** - * Sets or resets the order of the existing schemas in the current search path of the user. - * - * This is a PostgreSQL only function. - * - * @internal The method should be only used from within the PostgreSQLSchemaManager class hierarchy. - * - * @return void - * - * @throws Exception - */ - public function determineExistingSchemaSearchPaths() + protected function determineCurrentSchemaName(): ?string { - $names = $this->listSchemaNames(); - $paths = $this->getSchemaSearchPaths(); - - $this->existingSchemaPaths = array_filter($paths, static function ($v) use ($names): bool { - return in_array($v, $names, true); - }); + return $this->determineCurrentSchema(); } /** * {@inheritDoc} */ - protected function _getPortableTableForeignKeyDefinition($tableForeignKey) + protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint { $onUpdate = null; $onDelete = null; @@ -259,23 +125,31 @@ protected function _getPortableTableForeignKeyDefinition($tableForeignKey) $foreignTable, $foreignColumns, $tableForeignKey['conname'], - ['onUpdate' => $onUpdate, 'onDelete' => $onDelete], + [ + 'onUpdate' => $onUpdate, + 'onDelete' => $onDelete, + 'deferrable' => (bool) $tableForeignKey['condeferrable'], + 'deferred' => (bool) $tableForeignKey['condeferred'], + ], ); } /** * {@inheritDoc} */ - protected function _getPortableViewDefinition($view) + protected function _getPortableViewDefinition(array $view): View { return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']); } /** + * @deprecated Use the schema name and the unqualified table name separately instead. + * * {@inheritDoc} */ - protected function _getPortableTableDefinition($table) + protected function _getPortableTableDefinition(array $table): string { + // @phpstan-ignore missingType.checkedException $currentSchema = $this->getCurrentSchema(); if ($table['schema_name'] === $currentSchema) { @@ -288,76 +162,35 @@ protected function _getPortableTableDefinition($table) /** * {@inheritDoc} */ - protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) + protected function _getPortableTableIndexesList(array $rows, string $tableName): array { - $buffer = []; - foreach ($tableIndexes as $row) { - $colNumbers = array_map('intval', explode(' ', $row['indkey'])); - $columnNameSql = sprintf( - <<<'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), - ); - - $indexColumns = $this->_conn->fetchAllAssociative($columnNameSql); - - // required for getting the order of the columns right. - foreach ($colNumbers as $colNum) { - foreach ($indexColumns as $colRow) { - if ($colNum !== $colRow['attnum']) { - continue; - } - - $buffer[] = [ - 'key_name' => $row['relname'], - 'column_name' => trim($colRow['attname']), - 'non_unique' => ! $row['indisunique'], - 'primary' => $row['indisprimary'], - 'where' => $row['where'], - ]; - } - } - } - - return parent::_getPortableTableIndexesList($buffer, $tableName); + return parent::_getPortableTableIndexesList(array_map( + /** @param array $row */ + static function (array $row): array { + return [ + 'key_name' => $row['relname'], + 'non_unique' => ! $row['indisunique'], + 'primary' => (bool) $row['indisprimary'], + 'where' => $row['where'], + 'column_name' => $row['attname'], + ]; + }, + $rows, + ), $tableName); } /** * {@inheritDoc} */ - protected function _getPortableDatabaseDefinition($database) + protected function _getPortableDatabaseDefinition(array $database): string { return $database['datname']; } - /** - * {@inheritDoc} - * - * @deprecated Use {@see listSchemaNames()} instead. - */ - protected function getPortableNamespaceDefinition(array $namespace) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4503', - 'PostgreSQLSchemaManager::getPortableNamespaceDefinition() is deprecated,' - . ' use PostgreSQLSchemaManager::listSchemaNames() instead.', - ); - - return $namespace['nspname']; - } - /** * {@inheritDoc} */ - protected function _getPortableSequenceDefinition($sequence) + protected function _getPortableSequenceDefinition(array $sequence): Sequence { if ($sequence['schemaname'] !== 'public') { $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname']; @@ -371,198 +204,83 @@ protected function _getPortableSequenceDefinition($sequence) /** * {@inheritDoc} */ - protected function _getPortableTableColumnDefinition($tableColumn) + protected function _getPortableTableColumnDefinition(array $tableColumn): Column { $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); - if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') { - // get length from varchar definition - $length = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']); - $tableColumn['length'] = $length; - } - - $matches = []; - - $autoincrement = false; - - if ( - $tableColumn['default'] !== null - && preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches) === 1 - ) { - $tableColumn['sequence'] = $matches[1]; - $tableColumn['default'] = null; - $autoincrement = true; - } - - if ($tableColumn['default'] !== null) { - if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches) === 1) { - $tableColumn['default'] = $matches[1]; - } elseif (preg_match('/^NULL::/', $tableColumn['default']) === 1) { - $tableColumn['default'] = null; - } - } - - $length = $tableColumn['length'] ?? null; - if ($length === '-1' && isset($tableColumn['atttypmod'])) { - $length = $tableColumn['atttypmod'] - 4; - } - - if ((int) $length <= 0) { - $length = null; - } - - $fixed = null; - - if (! isset($tableColumn['name'])) { - $tableColumn['name'] = ''; - } - + $length = null; $precision = null; - $scale = null; - $jsonb = null; + $scale = 0; + $fixed = false; + $jsonb = false; + + $dbType = $tableColumn['type']; - $dbType = strtolower($tableColumn['type']); if ( $tableColumn['domain_type'] !== null - && $tableColumn['domain_type'] !== '' - && ! $this->_platform->hasDoctrineTypeMappingFor($tableColumn['type']) + && ! $this->platform->hasDoctrineTypeMappingFor($dbType) ) { - $dbType = strtolower($tableColumn['domain_type']); - $tableColumn['complete_type'] = $tableColumn['domain_complete_type']; + $dbType = $tableColumn['domain_type']; + $completeType = $tableColumn['domain_complete_type']; + } else { + $completeType = $tableColumn['complete_type']; } - $type = $this->_platform->getDoctrineTypeMapping($dbType); - $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type); - $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type); + $type = $this->platform->getDoctrineTypeMapping($dbType); switch ($dbType) { - case 'smallint': - case 'int2': - $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']); - $length = null; - break; - - case 'int': - case 'int4': - case 'integer': - $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']); - $length = null; - break; - - case 'bigint': - case 'int8': - $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']); - $length = null; - break; - - case 'bool': - case 'boolean': - if ($tableColumn['default'] === 'true') { - $tableColumn['default'] = true; - } - - if ($tableColumn['default'] === 'false') { - $tableColumn['default'] = false; - } - - $length = null; - break; - - case 'json': - case 'text': - case '_varchar': + case 'bpchar': case 'varchar': - $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']); - $fixed = false; - break; - case 'interval': - $fixed = false; - break; + $parameters = $this->parseColumnTypeParameters($completeType); + if (count($parameters) > 0) { + $length = $parameters[0]; + } - case 'char': - case 'bpchar': - $fixed = true; break; - case 'float': - case 'float4': - case 'float8': case 'double': - case 'double precision': - case 'real': case 'decimal': case 'money': case 'numeric': - $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']); - - if ( - preg_match( - '([A-Za-z]+\(([0-9]+),([0-9]+)\))', - $tableColumn['complete_type'], - $match, - ) === 1 - ) { - $precision = $match[1]; - $scale = $match[2]; - $length = null; + $parameters = $this->parseColumnTypeParameters($completeType); + if (count($parameters) > 0) { + $precision = $parameters[0]; } - break; - - case 'year': - $length = null; - break; + if (count($parameters) > 1) { + $scale = $parameters[1]; + } - // PostgreSQL 9.4+ only - case 'jsonb': - $jsonb = true; break; } - if ( - $tableColumn['default'] !== null && preg_match( - "('([^']+)'::)", - $tableColumn['default'], - $match, - ) === 1 - ) { - $tableColumn['default'] = $match[1]; + if ($dbType === 'bpchar') { + $fixed = true; + } elseif ($dbType === 'jsonb') { + $jsonb = true; } $options = [ 'length' => $length, 'notnull' => (bool) $tableColumn['isnotnull'], - 'default' => $tableColumn['default'], + 'default' => $this->parseDefaultExpression($tableColumn['default']), 'precision' => $precision, 'scale' => $scale, 'fixed' => $fixed, - 'autoincrement' => $autoincrement, - 'comment' => isset($tableColumn['comment']) && $tableColumn['comment'] !== '' - ? $tableColumn['comment'] - : null, + 'autoincrement' => $tableColumn['attidentity'] === 'd', ]; + if ($tableColumn['comment'] !== null) { + $options['comment'] = $tableColumn['comment']; + } + $column = new Column($tableColumn['field'], Type::getType($type), $options); if (! empty($tableColumn['collation'])) { $column->setPlatformOption('collation', $tableColumn['collation']); } - if ($column->getType()->getName() === Types::JSON) { - if (! $column->getType() instanceof JsonType) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5049', - <<<'DEPRECATION' - %s not extending %s while being named %s is deprecated, - and will lead to jsonb never to being used in 4.0., - DEPRECATION, - get_class($column->getType()), - JsonType::class, - Types::JSON, - ); - } - + if ($column->getType() instanceof JsonType) { $column->setPlatformOption('jsonb', $jsonb); } @@ -570,31 +288,47 @@ protected function _getPortableTableColumnDefinition($tableColumn) } /** - * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually. - * - * @param mixed $defaultValue + * Parses the parameters between parenthesis in the data type. * - * @return mixed + * @return list */ - private function fixVersion94NegativeNumericDefaultValue($defaultValue) + private function parseColumnTypeParameters(string $type): array { - if ($defaultValue !== null && strpos($defaultValue, '(') === 0) { - return trim($defaultValue, '()'); + if (preg_match('/\((\d+)(?:,(\d+))?\)/', $type, $matches) !== 1) { + return []; } - return $defaultValue; + $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 $default): ?string + private function parseDefaultExpression(?string $expression): mixed { - if ($default === null) { - return $default; + if ($expression === null || str_starts_with($expression, 'NULL::')) { + return null; + } + + if ($expression === 'true') { + return true; + } + + if ($expression === 'false') { + return false; } - return str_replace("''", "'", $default); + if (preg_match("/^'(.*)'::/s", $expression, $matches) === 1) { + return str_replace("''", "'", $matches[1]); + } + + return $expression; } protected function selectTableNames(string $databaseName): Result @@ -613,120 +347,138 @@ protected function selectTableNames(string $databaseName): Result quote_ident(table_name) SQL; - return $this->_conn->executeQuery($sql, [$databaseName]); + return $this->connection->executeQuery($sql, [$databaseName]); } protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; + $params = []; - if ($tableName === null) { - $sql .= ' quote_ident(c.relname) AS table_name, quote_ident(n.nspname) AS schema_name,'; - } - - $sql .= sprintf(<<<'SQL' - a.attnum, - quote_ident(a.attname) AS field, - t.typname AS type, - format_type(a.atttypid, a.atttypmod) AS complete_type, - (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation, - (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type, - (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM - pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type, - a.attnotnull AS isnotnull, - (SELECT 't' - FROM pg_index - WHERE c.oid = pg_index.indrelid - AND pg_index.indkey[0] = a.attnum - AND pg_index.indisprimary = 't' - ) AS pri, - (%s) AS default, - (SELECT pg_description.description - FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid - ) AS comment + $sql = sprintf( + <<<'SQL' + SELECT quote_ident(n.nspname) AS schema_name, + quote_ident(c.relname) AS table_name, + quote_ident(a.attname) AS field, + t.typname AS type, + format_type(a.atttypid, a.atttypmod) AS complete_type, + bt.typname AS domain_type, + format_type(bt.oid, t.typtypmod) AS domain_complete_type, + a.attnotnull AS isnotnull, + a.attidentity, + (%s) AS "default", + dsc.description AS comment, + CASE + WHEN coll.collprovider = 'c' + THEN coll.collcollate + WHEN coll.collprovider = 'd' + THEN NULL + ELSE coll.collname + END AS collation FROM pg_attribute a - INNER JOIN pg_class c - ON c.oid = a.attrelid - INNER JOIN pg_type t - ON t.oid = a.atttypid - INNER JOIN pg_namespace n - ON n.oid = c.relnamespace - LEFT JOIN pg_depend d - ON d.objid = c.oid - AND d.deptype = 'e' - AND d.classid = (SELECT oid FROM pg_class WHERE relname = 'pg_class') -SQL, $this->_platform->getDefaultColumnValueSQLSnippet()); - - $conditions = array_merge([ - 'a.attnum > 0', - "c.relkind = 'r'", - 'd.refobjid IS NULL', - ], $this->buildQueryConditions($tableName)); - - $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY a.attnum'; - - return $this->_conn->executeQuery($sql); + 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 + -- 'r' for regular tables - 'p' for partitioned tables + AND c.relkind IN ('r', 'p') + 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(), + implode(' AND ', $this->buildQueryConditions($tableName, $params)), + ); + + return $this->connection->executeQuery($sql, $params); } protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; - - if ($tableName === null) { - $sql .= ' quote_ident(tc.relname) AS table_name, quote_ident(tn.nspname) AS schema_name,'; - } + $params = []; - $sql .= <<<'SQL' + $sql = sprintf( + <<<'SQL' + SELECT + quote_ident(n.nspname) AS schema_name, + quote_ident(c.relname) AS table_name, quote_ident(ic.relname) AS relname, i.indisunique, i.indisprimary, i.indkey, i.indrelid, - pg_get_expr(indpred, indrelid) AS "where" + pg_get_expr(indpred, indrelid) AS "where", + quote_ident(attname) AS attname FROM pg_index i - JOIN pg_class AS tc ON tc.oid = i.indrelid - JOIN pg_namespace tn ON tn.oid = tc.relnamespace + 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 - WHERE ic.oid IN ( - SELECT indexrelid - FROM pg_index i, pg_class c, pg_namespace n -SQL; - - $conditions = array_merge([ - 'c.oid = i.indrelid', - 'c.relnamespace = n.oid', - ], $this->buildQueryConditions($tableName)); - - $sql .= ' WHERE ' . implode(' AND ', $conditions) . ') ORDER BY quote_ident(ic.relname)'; + 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 + ORDER BY 1, 2, keys.ord; + SQL, + implode(' AND ', $this->buildQueryConditions($tableName, $params)), + ); - return $this->_conn->executeQuery($sql); + return $this->connection->executeQuery($sql, $params); } protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; - - if ($tableName === null) { - $sql .= ' quote_ident(tc.relname) AS table_name, quote_ident(tn.nspname) AS schema_name,'; - } + $params = []; - $sql .= <<<'SQL' + $sql = sprintf( + <<<'SQL' + SELECT + quote_ident(tn.nspname) AS schema_name, + quote_ident(tc.relname) AS table_name, quote_ident(r.conname) as conname, - pg_get_constraintdef(r.oid, true) as condef + pg_get_constraintdef(r.oid, true) as condef, + r.condeferrable, + r.condeferred FROM pg_constraint r JOIN pg_class AS tc ON tc.oid = r.conrelid JOIN pg_namespace tn ON tn.oid = tc.relnamespace WHERE r.conrelid IN ( SELECT c.oid - FROM pg_class c, pg_namespace n -SQL; - - $conditions = array_merge(['n.oid = c.relnamespace'], $this->buildQueryConditions($tableName)); - - $sql .= ' WHERE ' . implode(' AND ', $conditions) . ") AND r.contype = 'f' ORDER BY quote_ident(r.conname)"; + FROM pg_class c + JOIN pg_namespace n + ON n.oid = c.relnamespace + WHERE %s) + AND r.contype = 'f' + ORDER BY 1, 2 + SQL, + implode(' AND ', $this->buildQueryConditions($tableName, $params)), + ); - return $this->_conn->executeQuery($sql); + return $this->connection->executeQuery($sql, $params); } /** @@ -734,22 +486,26 @@ protected function selectForeignKeyColumns(string $databaseName, ?string $tableN */ protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array { - $sql = <<<'SQL' -SELECT n.nspname AS schema_name, - 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 - INNER JOIN pg_namespace n - ON n.oid = c.relnamespace -SQL; - - $conditions = array_merge(["c.relkind = 'r'"], $this->buildQueryConditions($tableName)); + $params = []; - $sql .= ' WHERE ' . implode(' AND ', $conditions); + $sql = sprintf( + <<<'SQL' + 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 + INNER JOIN pg_namespace n + ON n.oid = c.relnamespace + WHERE + c.relkind = 'r' + AND %s + SQL, + implode(' AND ', $this->buildQueryConditions($tableName, $params)), + ); $tableOptions = []; - foreach ($this->_conn->iterateAssociative($sql) as $row) { + foreach ($this->connection->iterateAssociative($sql, $params) as $row) { $tableOptions[$this->_getPortableTableDefinition($row)] = $row; } @@ -757,24 +513,26 @@ protected function fetchTableOptionsByTable(string $databaseName, ?string $table } /** - * @param string|null $tableName + * @param list $params * - * @return list + * @return non-empty-list */ - private function buildQueryConditions($tableName): array + private function buildQueryConditions(?string $tableName, array &$params): array { $conditions = []; if ($tableName !== null) { - if (strpos($tableName, '.') !== false) { + if (str_contains($tableName, '.')) { [$schemaName, $tableName] = explode('.', $tableName); - $conditions[] = 'n.nspname = ' . $this->_platform->quoteStringLiteral($schemaName); + + $conditions[] = 'n.nspname = ?'; + $params[] = $schemaName; } else { $conditions[] = 'n.nspname = ANY(current_schemas(false))'; } - $identifier = new Identifier($tableName); - $conditions[] = 'c.relname = ' . $this->_platform->quoteStringLiteral($identifier->getName()); + $conditions[] = 'c.relname = ?'; + $params[] = $tableName; } $conditions[] = "n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')"; diff --git a/doctrine/dbal/src/Schema/PrimaryKeyConstraint.php b/doctrine/dbal/src/Schema/PrimaryKeyConstraint.php new file mode 100644 index 000000000..700a5bcd1 --- /dev/null +++ b/doctrine/dbal/src/Schema/PrimaryKeyConstraint.php @@ -0,0 +1,76 @@ + */ +final readonly class PrimaryKeyConstraint implements OptionallyNamedObject +{ + /** + * @internal Use {@link PrimaryKeyConstraint::editor()} to instantiate an editor and + * {@link PrimaryKeyConstraintEditor::create()} to create a primary key constraint. + * + * @param ?UnqualifiedName $name Name of the primary key constraint. If omitted in the schema + * defined by the application, it is considered that the name is + * not essential and may be generated by the underlying database + * platform. + * @param non-empty-list $columnNames + */ + public function __construct( + private ?UnqualifiedName $name, + private array $columnNames, + private bool $isClustered, + ) { + if (count($this->columnNames) < 1) { + throw InvalidPrimaryKeyConstraintDefinition::columnNamesNotSet(); + } + } + + public function getObjectName(): ?UnqualifiedName + { + return $this->name; + } + + /** + * Returns the names of the columns. + * + * @return non-empty-list + */ + public function getColumnNames(): array + { + return $this->columnNames; + } + + /** + * Returns whether the primary key constraint is clustered. + */ + public function isClustered(): bool + { + return $this->isClustered; + } + + /** + * Instantiates a new primary key constraint editor. + */ + public static function editor(): PrimaryKeyConstraintEditor + { + return new PrimaryKeyConstraintEditor(); + } + + /** + * Instantiates a new foreign key constraint editor and initializes it with the constraint's properties. + */ + public function edit(): PrimaryKeyConstraintEditor + { + return self::editor() + ->setName($this->name) + ->setColumnNames(...$this->columnNames) + ->setIsClustered($this->isClustered); + } +} diff --git a/doctrine/dbal/src/Schema/PrimaryKeyConstraintEditor.php b/doctrine/dbal/src/Schema/PrimaryKeyConstraintEditor.php new file mode 100644 index 000000000..9efc28609 --- /dev/null +++ b/doctrine/dbal/src/Schema/PrimaryKeyConstraintEditor.php @@ -0,0 +1,115 @@ + */ + private array $columnNames = []; + + private bool $isClustered = true; + + /** + * @internal Use {@link PrimaryKeyConstraint::editor()} or {@link PrimaryKeyConstraint::edit()} to create + * an instance. + */ + public function __construct() + { + } + + public function setName(?UnqualifiedName $name): self + { + $this->name = $name; + + return $this; + } + + /** @param non-empty-string $name */ + public function setUnquotedName(string $name): self + { + $this->name = UnqualifiedName::unquoted($name); + + return $this; + } + + /** @param non-empty-string $name */ + public function setQuotedName(string $name): self + { + $this->name = UnqualifiedName::quoted($name); + + return $this; + } + + public function setColumnNames(UnqualifiedName $firstColumnName, UnqualifiedName ...$otherColumnNames): self + { + $this->columnNames = [$firstColumnName, ...array_values($otherColumnNames)]; + + return $this; + } + + /** + * @param non-empty-string $firstColumnName + * @param non-empty-string ...$otherColumnNames + */ + public function setUnquotedColumnNames( + string $firstColumnName, + string ...$otherColumnNames, + ): self { + $this->columnNames = array_map( + static fn (string $name): UnqualifiedName => UnqualifiedName::unquoted($name), + [$firstColumnName, ...array_values($otherColumnNames)], + ); + + return $this; + } + + /** + * @param non-empty-string $firstColumnName + * @param non-empty-string ...$otherColumnNames + */ + public function setQuotedColumnNames( + string $firstColumnName, + string ...$otherColumnNames, + ): self { + $this->columnNames = array_map( + static fn (string $name): UnqualifiedName => UnqualifiedName::quoted($name), + [$firstColumnName, ...array_values($otherColumnNames)], + ); + + return $this; + } + + public function addColumnName(UnqualifiedName $columName): self + { + $this->columnNames[] = $columName; + + return $this; + } + + public function setIsClustered(bool $isClustered): self + { + $this->isClustered = $isClustered; + + return $this; + } + + public function create(): PrimaryKeyConstraint + { + if (count($this->columnNames) < 1) { + throw InvalidPrimaryKeyConstraintDefinition::columnNamesNotSet(); + } + + return new PrimaryKeyConstraint($this->name, $this->columnNames, $this->isClustered); + } +} diff --git a/doctrine/dbal/src/Schema/SQLServerSchemaManager.php b/doctrine/dbal/src/Schema/SQLServerSchemaManager.php index e82ccded1..86c1c6585 100644 --- a/doctrine/dbal/src/Schema/SQLServerSchemaManager.php +++ b/doctrine/dbal/src/Schema/SQLServerSchemaManager.php @@ -1,25 +1,27 @@ doListTableNames(); - } - - /** - * {@inheritDoc} - */ - public function listTables() - { - return $this->doListTables(); - } - - /** - * {@inheritDoc} - * - * @deprecated Use {@see introspectTable()} instead. - */ - public function listTableDetails($name) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5595', - '%s is deprecated. Use introspectTable() instead.', - __METHOD__, - ); - - return $this->doListTableDetails($name); - } - - /** - * {@inheritDoc} - */ - public function listTableColumns($table, $database = null) - { - return $this->doListTableColumns($table, $database); - } - - /** - * {@inheritDoc} - */ - public function listTableIndexes($table) - { - return $this->doListTableIndexes($table); - } - - /** - * {@inheritDoc} - */ - public function listTableForeignKeys($table, $database = null) - { - return $this->doListTableForeignKeys($table, $database); - } - /** * {@inheritDoc} */ public function listSchemaNames(): array { - return $this->_conn->fetchFirstColumn( + return $this->connection->fetchFirstColumn( <<<'SQL' SELECT name FROM sys.schemas @@ -106,7 +51,7 @@ public function listSchemaNames(): array /** * {@inheritDoc} */ - protected function _getPortableSequenceDefinition($sequence) + protected function _getPortableSequenceDefinition(array $sequence): Sequence { return new Sequence($sequence['name'], (int) $sequence['increment'], (int) $sequence['start_value']); } @@ -114,21 +59,23 @@ protected function _getPortableSequenceDefinition($sequence) /** * {@inheritDoc} */ - protected function _getPortableTableColumnDefinition($tableColumn) + protected function _getPortableTableColumnDefinition(array $tableColumn): Column { - $dbType = strtok($tableColumn['type'], '(), '); - assert(is_string($dbType)); + $dbType = $tableColumn['type']; - $fixed = null; - $length = (int) $tableColumn['length']; - $default = $tableColumn['default']; + $length = (int) $tableColumn['length']; - if (! isset($tableColumn['name'])) { - $tableColumn['name'] = ''; + $precision = null; + + $scale = 0; + $fixed = false; + + if ($tableColumn['scale'] !== null) { + $scale = (int) $tableColumn['scale']; } - if ($default !== null) { - $default = $this->parseDefaultExpression($default); + if ($tableColumn['precision'] !== null) { + $precision = (int) $tableColumn['precision']; } switch ($dbType) { @@ -167,35 +114,42 @@ protected function _getPortableTableColumnDefinition($tableColumn) $fixed = true; } - $type = $this->_platform->getDoctrineTypeMapping($dbType); - $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type); - $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type); + $type = $this->platform->getDoctrineTypeMapping($dbType); $options = [ - 'unsigned' => false, - 'fixed' => (bool) $fixed, - 'default' => $default, + 'fixed' => $fixed, 'notnull' => (bool) $tableColumn['notnull'], - 'scale' => $tableColumn['scale'], - 'precision' => $tableColumn['precision'], + 'scale' => $scale, + 'precision' => $precision, 'autoincrement' => (bool) $tableColumn['autoincrement'], - 'comment' => $tableColumn['comment'] !== '' ? $tableColumn['comment'] : null, ]; + if ($tableColumn['comment'] !== null) { + $options['comment'] = $tableColumn['comment']; + } + if ($length !== 0 && ($type === 'text' || $type === 'string' || $type === 'binary')) { $options['length'] = $length; } $column = new Column($tableColumn['name'], Type::getType($type), $options); - if (isset($tableColumn['collation']) && $tableColumn['collation'] !== 'NULL') { - $column->setPlatformOption('collation', $tableColumn['collation']); + if ($tableColumn['default'] !== null) { + $default = $this->parseDefaultExpression($tableColumn['default']); + + $column->setDefault($default); + $column->setPlatformOption( + SQLServerPlatform::OPTION_DEFAULT_CONSTRAINT_NAME, + $tableColumn['df_name'], + ); } + $column->setPlatformOption('collation', $tableColumn['collation']); + return $column; } - private function parseDefaultExpression(string $value): ?string + private function parseDefaultExpression(string $value): string|DefaultExpression|null { while (preg_match('/^\((.*)\)$/s', $value, $matches) === 1) { $value = $matches[1]; @@ -210,7 +164,7 @@ private function parseDefaultExpression(string $value): ?string } if ($value === 'getdate()') { - return $this->_platform->getCurrentTimestampSQL(); + return new CurrentTimestamp(); } return $value; @@ -219,33 +173,34 @@ private function parseDefaultExpression(string $value): ?string /** * {@inheritDoc} */ - protected function _getPortableTableForeignKeysList($tableForeignKeys) + protected function _getPortableTableForeignKeysList(array $rows): array { $foreignKeys = []; - foreach ($tableForeignKeys as $tableForeignKey) { - $name = $tableForeignKey['ForeignKey']; + foreach ($rows as $row) { + $name = $row['ForeignKey']; if (! isset($foreignKeys[$name])) { - $referencedTableName = $tableForeignKey['ReferenceTableName']; + $referencedTableName = $row['ReferenceTableName']; - if ($tableForeignKey['ReferenceSchemaName'] !== 'dbo') { - $referencedTableName = $tableForeignKey['ReferenceSchemaName'] . '.' . $referencedTableName; + // @phpstan-ignore missingType.checkedException + if ($row['ReferenceSchemaName'] !== $this->getCurrentSchemaName()) { + $referencedTableName = $row['ReferenceSchemaName'] . '.' . $referencedTableName; } $foreignKeys[$name] = [ - 'local_columns' => [$tableForeignKey['ColumnName']], + 'local_columns' => [$row['ColumnName']], 'foreign_table' => $referencedTableName, - 'foreign_columns' => [$tableForeignKey['ReferenceColumnName']], + 'foreign_columns' => [$row['ReferenceColumnName']], 'name' => $name, 'options' => [ - 'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']), - 'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']), + 'onUpdate' => str_replace('_', ' ', $row['update_referential_action_desc']), + 'onDelete' => str_replace('_', ' ', $row['delete_referential_action_desc']), ], ]; } else { - $foreignKeys[$name]['local_columns'][] = $tableForeignKey['ColumnName']; - $foreignKeys[$name]['foreign_columns'][] = $tableForeignKey['ReferenceColumnName']; + $foreignKeys[$name]['local_columns'][] = $row['ColumnName']; + $foreignKeys[$name]['foreign_columns'][] = $row['ReferenceColumnName']; } } @@ -255,21 +210,21 @@ protected function _getPortableTableForeignKeysList($tableForeignKeys) /** * {@inheritDoc} */ - protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) + protected function _getPortableTableIndexesList(array $rows, string $tableName): array { - foreach ($tableIndexes as &$tableIndex) { - $tableIndex['non_unique'] = (bool) $tableIndex['non_unique']; - $tableIndex['primary'] = (bool) $tableIndex['primary']; - $tableIndex['flags'] = $tableIndex['flags'] ? [$tableIndex['flags']] : null; + foreach ($rows as &$row) { + $row['non_unique'] = (bool) $row['non_unique']; + $row['primary'] = (bool) $row['primary']; + $row['flags'] = $row['flags'] ? [$row['flags']] : null; } - return parent::_getPortableTableIndexesList($tableIndexes, $tableName); + return parent::_getPortableTableIndexesList($rows, $tableName); } /** * {@inheritDoc} */ - protected function _getPortableTableForeignKeyDefinition($tableForeignKey) + protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint { return new ForeignKeyConstraint( $tableForeignKey['local_columns'], @@ -281,11 +236,14 @@ protected function _getPortableTableForeignKeyDefinition($tableForeignKey) } /** + * @deprecated Use the schema name and the unqualified table name separately instead. + * * {@inheritDoc} */ - protected function _getPortableTableDefinition($table) + protected function _getPortableTableDefinition(array $table): string { - if ($table['schema_name'] !== 'dbo') { + // @phpstan-ignore missingType.checkedException + if ($table['schema_name'] !== $this->getCurrentSchemaName()) { return $table['schema_name'] . '.' . $table['table_name']; } @@ -295,105 +253,36 @@ protected function _getPortableTableDefinition($table) /** * {@inheritDoc} */ - protected function _getPortableDatabaseDefinition($database) + protected function _getPortableDatabaseDefinition(array $database): string { return $database['name']; } /** * {@inheritDoc} - * - * @deprecated Use {@see listSchemaNames()} instead. */ - protected function getPortableNamespaceDefinition(array $namespace) + protected function _getPortableViewDefinition(array $view): View { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4503', - 'SQLServerSchemaManager::getPortableNamespaceDefinition() is deprecated,' - . ' use SQLServerSchemaManager::listSchemaNames() instead.', - ); - - return $namespace['name']; - } - - /** - * {@inheritDoc} - */ - protected function _getPortableViewDefinition($view) - { - // @todo return new View($view['name'], $view['definition']); } - /** - * {@inheritDoc} - */ - public function alterTable(TableDiff $tableDiff) - { - $droppedColumns = $tableDiff->getDroppedColumns(); - - if (count($droppedColumns) > 0) { - $tableName = ($tableDiff->getOldTable() ?? $tableDiff->getName($this->_platform))->getName(); - - foreach ($droppedColumns as $col) { - foreach ($this->getColumnConstraints($tableName, $col->getName()) as $constraint) { - $this->_conn->executeStatement( - sprintf( - 'ALTER TABLE %s DROP CONSTRAINT %s', - $tableName, - $constraint, - ), - ); - } - } - } - - parent::alterTable($tableDiff); - } - - /** - * Returns the names of the constraints for a given column. - * - * @return iterable - * - * @throws Exception - */ - private function getColumnConstraints(string $table, string $column): iterable - { - return $this->_conn->iterateColumn( - <<<'SQL' -SELECT o.name -FROM sys.objects o - INNER JOIN sys.objects t - ON t.object_id = o.parent_object_id - AND t.type = 'U' - INNER JOIN sys.default_constraints dc - ON dc.object_id = o.object_id - INNER JOIN sys.columns c - ON c.column_id = dc.parent_column_id - AND c.object_id = t.object_id -WHERE t.name = ? - AND c.name = ? -SQL - , - [$table, $column], - ); - } - /** @throws Exception */ - public function createComparator(): Comparator + public function createComparator(/* ComparatorConfig $config = new ComparatorConfig() */): Comparator { - return new SQLServer\Comparator($this->_platform, $this->getDatabaseCollation()); + return new SQLServer\Comparator( + $this->platform, + $this->getDatabaseCollation(), + func_num_args() > 0 ? func_get_arg(0) : new ComparatorConfig(), + ); } /** @throws Exception */ private function getDatabaseCollation(): string { if ($this->databaseCollation === null) { - $databaseCollation = $this->_conn->fetchOne( + $databaseCollation = $this->connection->fetchOne( 'SELECT collation_name FROM sys.databases WHERE name = ' - . $this->_platform->getCurrentDatabaseExpression(), + . $this->platform->getCurrentDatabaseExpression(), ); // a database is always selected, even if omitted in the connection parameters @@ -405,35 +294,43 @@ private function getDatabaseCollation(): string return $this->databaseCollation; } + protected function determineCurrentSchemaName(): ?string + { + $schemaName = $this->connection->fetchOne('SELECT SCHEMA_NAME()'); + assert($schemaName !== false); + + return $schemaName; + } + protected function selectTableNames(string $databaseName): Result { // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams $sql = <<<'SQL' -SELECT name AS table_name, - SCHEMA_NAME(schema_id) AS schema_name -FROM sys.objects -WHERE type = 'U' - AND name != 'sysdiagrams' +SELECT SCHEMA_NAME(schema_id) AS schema_name, + name AS table_name +FROM sys.tables +WHERE name != 'sysdiagrams' ORDER BY name SQL; - return $this->_conn->executeQuery($sql); + return $this->connection->executeQuery($sql); } protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; + $params = []; - if ($tableName === null) { - $sql .= ' obj.name AS table_name, scm.name AS schema_name,'; - } - - $sql .= <<<'SQL' + $sql = sprintf( + <<<'SQL' + SELECT + scm.name AS schema_name, + tbl.name AS table_name, col.name, type.name AS type, col.max_length AS length, ~col.is_nullable AS notnull, def.definition AS [default], + def.name AS df_name, col.scale, col.precision, col.is_identity AS autoincrement, @@ -443,41 +340,37 @@ protected function selectTableColumns(string $databaseName, ?string $tableName = FROM sys.columns AS col JOIN sys.types AS type ON col.user_type_id = type.user_type_id - JOIN sys.objects AS obj - ON col.object_id = obj.object_id + JOIN sys.tables AS tbl + ON col.object_id = tbl.object_id JOIN sys.schemas AS scm - ON obj.schema_id = scm.schema_id + ON tbl.schema_id = scm.schema_id LEFT JOIN sys.default_constraints def ON col.default_object_id = def.object_id AND col.object_id = def.parent_object_id LEFT JOIN sys.extended_properties AS prop - ON obj.object_id = prop.major_id + ON tbl.object_id = prop.major_id AND col.column_id = prop.minor_id AND prop.name = 'MS_Description' -SQL; - - // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams - $conditions = ["obj.type = 'U'", "obj.name != 'sysdiagrams'"]; - $params = []; - - if ($tableName !== null) { - $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'obj.name'); - } - - $sql .= ' WHERE ' . implode(' AND ', $conditions); + WHERE %s + ORDER BY scm.name, + tbl.name, + col.column_id +SQL, + $this->getWhereClause($tableName, 'scm.name', 'tbl.name', $params), + ); - return $this->_conn->executeQuery($sql, $params); + return $this->connection->executeQuery($sql, $params); } protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; + $params = []; - if ($tableName === null) { - $sql .= ' tbl.name AS table_name, scm.name AS schema_name,'; - } - - $sql .= <<<'SQL' + $sql = sprintf( + <<<'SQL' + SELECT + scm.name AS schema_name, + tbl.name AS table_name, idx.name AS key_name, col.name AS column_name, ~idx.is_unique AS non_unique, @@ -498,61 +391,54 @@ protected function selectIndexColumns(string $databaseName, ?string $tableName = JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id -SQL; - - $conditions = []; - $params = []; - - if ($tableName !== null) { - $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'tbl.name'); - $sql .= ' WHERE ' . implode(' AND ', $conditions); - } - - $sql .= ' ORDER BY idx.index_id, idxcol.key_ordinal'; + WHERE %s + ORDER BY scm.name, + tbl.name, + idx.index_id, + idxcol.key_ordinal +SQL, + $this->getWhereClause($tableName, 'scm.name', 'tbl.name', $params), + ); - return $this->_conn->executeQuery($sql, $params); + return $this->connection->executeQuery($sql, $params); } protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; + $params = []; - if ($tableName === null) { - $sql .= ' OBJECT_NAME (f.parent_object_id) AS table_name, SCHEMA_NAME(f.schema_id) AS schema_name,'; - } - - $sql .= <<<'SQL' + $sql = sprintf( + <<<'SQL' + SELECT + SCHEMA_NAME(f.schema_id) AS schema_name, + OBJECT_NAME(f.parent_object_id) AS table_name, f.name AS ForeignKey, - SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName, - OBJECT_NAME (f.parent_object_id) AS TableName, - COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName, - SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName, - OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName, - COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName, + COL_NAME(fc.parent_object_id, fc.parent_column_id) AS ColumnName, + SCHEMA_NAME(t.schema_id) ReferenceSchemaName, + OBJECT_NAME(f.referenced_object_id) AS ReferenceTableName, + COL_NAME(fc.referenced_object_id, fc.referenced_column_id) AS ReferenceColumnName, f.delete_referential_action_desc, f.update_referential_action_desc FROM sys.foreign_keys AS f INNER JOIN sys.foreign_key_columns AS fc - INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id - ON f.OBJECT_ID = fc.constraint_object_id -SQL; - - $conditions = []; - $params = []; - - if ($tableName !== null) { - $conditions[] = $this->getTableWhereClause( + ON f.object_id = fc.constraint_object_id + INNER JOIN sys.tables AS t + ON t.object_id = fc.referenced_object_id + WHERE %s + ORDER BY 1, + 2, + 3, + fc.constraint_column_id +SQL, + $this->getWhereClause( $tableName, 'SCHEMA_NAME(f.schema_id)', 'OBJECT_NAME(f.parent_object_id)', - ); - - $sql .= ' WHERE ' . implode(' AND ', $conditions); - } - - $sql .= ' ORDER BY fc.constraint_column_id'; + $params, + ), + ); - return $this->_conn->executeQuery($sql, $params); + return $this->connection->executeQuery($sql, $params); } /** @@ -560,32 +446,32 @@ protected function selectForeignKeyColumns(string $databaseName, ?string $tableN */ protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array { - $sql = <<<'SQL' + $params = []; + + $sql = sprintf( + <<<'SQL' SELECT scm.name AS schema_name, tbl.name AS table_name, - p.value AS [table_comment] + p.value 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 = ["p.name = N'MS_Description'"]; - - if ($tableName !== null) { - $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'tbl.name'); - } - - $sql .= ' WHERE ' . implode(' AND ', $conditions); + WHERE + p.name = N'MS_Description' + AND %s +SQL, + $this->getWhereClause($tableName, 'scm.name', 'tbl.name', $params), + ); $tableOptions = []; - foreach ($this->_conn->iterateAssociative($sql) as $data) { + foreach ($this->connection->iterateAssociative($sql, $params) as $data) { $data = array_change_key_case($data, CASE_LOWER); $tableOptions[$this->_getPortableTableDefinition($data)] = [ - 'comment' => $data['table_comment'], + 'comment' => $data['value'], ]; } @@ -595,21 +481,36 @@ protected function fetchTableOptionsByTable(string $databaseName, ?string $table /** * Returns the where clause to filter schema and table name in a query. * - * @param string $table The full qualified name of the table. - * @param string $schemaColumn The name of the column to compare the schema to in the where clause. - * @param string $tableColumn The name of the column to compare the table to in the where clause. + * @param ?string $tableName The full qualified name of the table. + * @param string $schemaColumn The name of the column to compare the schema to in the where clause. + * @param string $tableColumn The name of the column to compare the table to in the where clause. + * @param list $params */ - private function getTableWhereClause($table, $schemaColumn, $tableColumn): string - { - if (strpos($table, '.') !== false) { - [$schema, $table] = explode('.', $table); - $schema = $this->_platform->quoteStringLiteral($schema); - $table = $this->_platform->quoteStringLiteral($table); - } else { - $schema = 'SCHEMA_NAME()'; - $table = $this->_platform->quoteStringLiteral($table); + private function getWhereClause( + ?string $tableName, + string $schemaColumn, + string $tableColumn, + array &$params, + ): string { + $conditions = []; + + if ($tableName !== null) { + if (str_contains($tableName, '.')) { + [$schemaName, $tableName] = explode('.', $tableName); + + $conditions = [sprintf('%s = ?', $schemaColumn)]; + $params[] = $schemaName; + } else { + $conditions = [sprintf('%s = SCHEMA_NAME()', $schemaColumn)]; + } + + $conditions[] = sprintf('%s = ?', $tableColumn); + $params[] = $tableName; } - return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema); + // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams + $conditions[] = sprintf("%s != 'sysdiagrams'", $tableColumn); + + return implode(' AND ', $conditions); } } diff --git a/doctrine/dbal/src/Schema/SQLiteSchemaManager.php b/doctrine/dbal/src/Schema/SQLiteSchemaManager.php new file mode 100644 index 000000000..7b23fe0e3 --- /dev/null +++ b/doctrine/dbal/src/Schema/SQLiteSchemaManager.php @@ -0,0 +1,616 @@ + + */ +class SQLiteSchemaManager extends AbstractSchemaManager +{ + public function createForeignKey(ForeignKeyConstraint $foreignKey, string $table): void + { + $table = $this->introspectTable($table); + + $this->alterTable(new TableDiff($table, addedForeignKeys: [$foreignKey])); + } + + public function dropForeignKey(string $name, string $table): void + { + $table = $this->introspectTable($table); + + $foreignKey = $table->getForeignKey($name); + + $this->alterTable(new TableDiff($table, droppedForeignKeys: [$foreignKey])); + } + + /** + * @deprecated Use the schema name and the unqualified table name separately instead. + * + * {@inheritDoc} + */ + protected function _getPortableTableDefinition(array $table): string + { + return $table['table_name']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableColumnDefinition(array $tableColumn): Column + { + $matchResult = preg_match('/^([A-Z\s]+?)(?:\s*\((\d+)(?:,\s*(\d+))?\))?$/i', $tableColumn['type'], $matches); + assert($matchResult === 1); + + $dbType = strtolower($matches[1]); + + $length = $precision = null; + $fixed = $unsigned = false; + $scale = 0; + + if (isset($matches[2])) { + if (isset($matches[3])) { + $precision = (int) $matches[2]; + $scale = (int) $matches[3]; + } else { + $length = (int) $matches[2]; + } + } + + if (str_contains($dbType, ' unsigned')) { + $dbType = str_replace(' unsigned', '', $dbType); + $unsigned = true; + } + + $type = $this->platform->getDoctrineTypeMapping($dbType); + $default = $tableColumn['dflt_value']; + if ($default === 'NULL') { + $default = null; + } + + if ($default !== null) { + // SQLite returns the default value as a literal expression, so we need to parse it + if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) { + $default = str_replace("''", "'", $matches[1]); + } + } + + $notnull = (bool) $tableColumn['notnull']; + + if ($dbType === 'char') { + $fixed = true; + } + + $options = [ + 'autoincrement' => $tableColumn['autoincrement'], + 'comment' => $tableColumn['comment'], + 'length' => $length, + 'unsigned' => $unsigned, + 'fixed' => $fixed, + 'notnull' => $notnull, + 'default' => $default, + 'precision' => $precision, + 'scale' => $scale, + ]; + + $column = new Column($tableColumn['name'], Type::getType($type), $options); + + if ($type === Types::STRING || $type === Types::TEXT) { + $column->setPlatformOption('collation', $tableColumn['collation'] ?? 'BINARY'); + } + + return $column; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableViewDefinition(array $view): View + { + return new View($view['name'], $view['sql']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeysList(array $rows): array + { + $list = []; + foreach ($rows as $row) { + $row = array_change_key_case($row, CASE_LOWER); + $id = $row['id']; + if (! isset($list[$id])) { + if (! isset($row['on_delete']) || $row['on_delete'] === 'RESTRICT') { + $row['on_delete'] = null; + } + + if (! isset($row['on_update']) || $row['on_update'] === 'RESTRICT') { + $row['on_update'] = null; + } + + $list[$id] = [ + 'name' => $row['constraint_name'], + 'local' => [], + 'foreign' => [], + 'foreignTable' => $row['table'], + 'onDelete' => $row['on_delete'], + 'onUpdate' => $row['on_update'], + 'deferrable' => $row['deferrable'], + 'deferred' => $row['deferred'], + ]; + } + + $list[$id]['local'][] = $row['from']; + + if ($row['to'] !== null) { + $list[$id]['foreign'][] = $row['to']; + } + } + + foreach ($list as $id => $value) { + if (count($value['foreign']) !== 0) { + continue; + } + + // Inferring a shorthand form for the foreign key constraint, where the "to" field is empty. + // @see https://www.sqlite.org/foreignkeys.html#fk_indexes. + // @phpstan-ignore missingType.checkedException + $foreignTablePrimaryKeyColumnRows = $this->fetchPrimaryKeyColumns($value['foreignTable']); + + if (count($foreignTablePrimaryKeyColumnRows) < 1) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6701', + 'Introspection of SQLite foreign key constraints with omitted referenced column names' + . ' in an incomplete schema is deprecated.', + ); + + continue; + } + + $list[$id]['foreign'] = array_column($foreignTablePrimaryKeyColumnRows, 'name'); + } + + return parent::_getPortableTableForeignKeysList($list); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint + { + return new ForeignKeyConstraint( + $tableForeignKey['local'], + $tableForeignKey['foreignTable'], + $tableForeignKey['foreign'], + $tableForeignKey['name'], + [ + 'onDelete' => $tableForeignKey['onDelete'], + 'onUpdate' => $tableForeignKey['onUpdate'], + 'deferrable' => $tableForeignKey['deferrable'], + 'deferred' => $tableForeignKey['deferred'], + ], + ); + } + + private function parseColumnCollationFromSQL(string $column, string $sql): ?string + { + $pattern = '{' . $this->buildIdentifierPattern($column) + . '[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is'; + + if (preg_match($pattern, $sql, $match) !== 1) { + return null; + } + + return $match[1]; + } + + private function parseTableCommentFromSQL(string $table, string $sql): ?string + { + $pattern = '/\s* # Allow whitespace characters at start of line +CREATE\sTABLE' . $this->buildIdentifierPattern($table) . ' +( # Start capture + (?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s) +)/ix'; + + if (preg_match($pattern, $sql, $match) !== 1) { + return null; + } + + $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n")); + + return $comment === '' ? null : $comment; + } + + private function parseColumnCommentFromSQL(string $column, string $sql): string + { + $pattern = '{[\s(,]' . $this->buildIdentifierPattern($column) + . '(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i'; + + if (preg_match($pattern, $sql, $match) !== 1) { + return ''; + } + + $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n")); + assert(is_string($comment)); + + return $comment; + } + + /** + * Returns a regular expression pattern that matches the given unquoted or quoted identifier. + */ + private function buildIdentifierPattern(string $identifier): string + { + return '(?:' . implode('|', array_map( + static function (string $sql): string { + return '\W' . preg_quote($sql, '/') . '\W'; + }, + [ + $identifier, + $this->platform->quoteSingleIdentifier($identifier), + ], + )) . ')'; + } + + /** @throws Exception */ + private function getCreateTableSQL(string $table): string + { + $sql = $this->connection->fetchOne( + <<<'SQL' +SELECT sql + FROM ( + SELECT * + FROM sqlite_master + UNION ALL + SELECT * + FROM sqlite_temp_master + ) +WHERE type = 'table' +AND name = ? +SQL + , + [$table], + ); + + if ($sql !== false) { + return $sql; + } + + return ''; + } + + /** + * @return list> + * + * @throws Exception + */ + private function getForeignKeyDetails(string $table): array + { + $createSql = $this->getCreateTableSQL($table); + + if ( + preg_match_all( + '# + (?:CONSTRAINT\s+(\S+)\s+)? + (?:FOREIGN\s+KEY[^)]+\)\s*)? + REFERENCES\s+\S+\s*(?:\([^)]+\))? + (?: + [^,]*? + (NOT\s+DEFERRABLE|DEFERRABLE) + (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))? + )?#isx', + $createSql, + $match, + ) === 0 + ) { + return []; + } + + $names = $match[1]; + $deferrable = $match[2]; + $deferred = $match[3]; + $details = []; + + for ($i = 0, $count = count($match[0]); $i < $count; $i++) { + $details[] = [ + 'constraint_name' => $names[$i] ?? '', + 'deferrable' => isset($deferrable[$i]) && strcasecmp($deferrable[$i], 'deferrable') === 0, + 'deferred' => isset($deferred[$i]) && strcasecmp($deferred[$i], 'deferred') === 0, + ]; + } + + return $details; + } + + public function createComparator(/* ComparatorConfig $config = new ComparatorConfig() */): Comparator + { + return new SQLite\Comparator($this->platform, func_num_args() > 0 ? func_get_arg(0) : new ComparatorConfig()); + } + + protected function selectTableNames(string $databaseName): Result + { + $sql = <<<'SQL' +SELECT name AS table_name +FROM sqlite_master +WHERE type = 'table' + AND name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence') +UNION ALL +SELECT name +FROM sqlite_temp_master +WHERE type = 'table' +ORDER BY name +SQL; + + return $this->connection->executeQuery($sql); + } + + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT t.name AS table_name, + c.* + FROM sqlite_master t + JOIN pragma_table_info(t.name) c + WHERE %s + ORDER BY t.name, + c.cid +SQL, + $this->getWhereClause($tableName, $params), + ); + + return $this->connection->executeQuery($sql, $params); + } + + /** + * {@inheritDoc} + * + * @link https://www.sqlite.org/pragma.html#pragma_index_info + * @link https://www.sqlite.org/pragma.html#pragma_table_info + * @link https://www.sqlite.org/fileformat2.html#internal_schema_objects + */ + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT t.name AS table_name, + i.name, + i."unique", + c.name AS column_name + FROM sqlite_master t + JOIN pragma_index_list(t.name) i + JOIN pragma_index_info(i.name) c + WHERE %s + AND i.name NOT LIKE 'sqlite_%%' + ORDER BY t.name, i.seq, c.seqno +SQL, + $this->getWhereClause($tableName, $params), + ); + + return $this->connection->executeQuery($sql, $params); + } + + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT t.name AS table_name, + p.* + FROM sqlite_master t + JOIN pragma_foreign_key_list(t.name) p + ON p.seq != '-1' + WHERE %s + ORDER BY t.name, + p.id DESC, + p.seq +SQL, + $this->getWhereClause($tableName, $params), + ); + + return $this->connection->executeQuery($sql, $params); + } + + /** + * {@inheritDoc} + */ + protected function fetchTableColumns(string $databaseName, ?string $tableName = null): array + { + $rows = parent::fetchTableColumns($databaseName, $tableName); + + $sqlByTable = $pkColumnNamesByTable = $result = []; + + foreach ($rows as $row) { + $tableName = $row['table_name']; + + $sqlByTable[$tableName] ??= $this->getCreateTableSQL($tableName); + + if ($row['pk'] !== 0 && $row['pk'] !== '0' && $row['type'] === 'INTEGER') { + $pkColumnNamesByTable[$tableName][] = $row['name']; + } + } + + foreach ($rows as $row) { + $tableName = $row['table_name']; + $columnName = $row['name']; + $tableSQL = $sqlByTable[$row['table_name']]; + + $result[] = array_merge($row, [ + 'autoincrement' => isset($pkColumnNamesByTable[$tableName]) + && $pkColumnNamesByTable[$tableName] === [$columnName], + 'collation' => $this->parseColumnCollationFromSQL($columnName, $tableSQL), + 'comment' => $this->parseColumnCommentFromSQL($columnName, $tableSQL), + ]); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + protected function fetchIndexColumns(string $databaseName, ?string $tableName = null): array + { + $result = []; + + $pkColumnNameRows = $this->fetchPrimaryKeyColumns($tableName); + + foreach ($pkColumnNameRows as $pkColumnNameRow) { + $result[] = [ + 'table_name' => $pkColumnNameRow['table_name'], + 'key_name' => 'primary', + 'primary' => true, + 'non_unique' => false, + 'column_name' => $pkColumnNameRow['name'], + ]; + } + + $indexColumnRows = parent::fetchIndexColumns($databaseName, $tableName); + + foreach ($indexColumnRows as $indexColumnRow) { + $result[] = [ + 'table_name' => $indexColumnRow['table_name'], + 'key_name' => $indexColumnRow['name'], + 'primary' => false, + 'non_unique' => ! $indexColumnRow['unique'], + 'column_name' => $indexColumnRow['column_name'], + ]; + } + + return $result; + } + + /** + * Fetches names of primary key columns. If the table name is specified, narrows down the selection to this table. + * + * @link https://www.sqlite.org/pragma.html#pragma_table_info + * + * @return list> + * + * @throws Exception + */ + private function fetchPrimaryKeyColumns(?string $tableName = null): array + { + $params = []; + + $sql = sprintf( + <<<'SQL' + SELECT t.name AS table_name, + p.name + FROM sqlite_master t + JOIN pragma_table_info(t.name) p + WHERE %s + AND p.pk > 0 + ORDER BY t.name, + p.pk + SQL, + $this->getWhereClause($tableName, $params), + ); + + return $this->connection->fetchAllAssociative($sql, $params); + } + + /** + * {@inheritDoc} + */ + protected function fetchForeignKeyColumns(string $databaseName, ?string $tableName = null): array + { + $columnsByTable = []; + foreach (parent::fetchForeignKeyColumns($databaseName, $tableName) as $column) { + $columnsByTable[$column['table_name']][] = $column; + } + + $columns = []; + foreach ($columnsByTable as $table => $tableColumns) { + $foreignKeyDetails = $this->getForeignKeyDetails($table); + $foreignKeyCount = count($foreignKeyDetails); + + foreach ($tableColumns as $column) { + // SQLite identifies foreign keys in reverse order of appearance in SQL + $columns[] = array_merge($column, $foreignKeyDetails[$foreignKeyCount - $column['id'] - 1]); + } + } + + return $columns; + } + + /** + * {@inheritDoc} + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + if ($tableName === null) { + $tables = $this->listTableNames(); + } else { + $tables = [$tableName]; + } + + $tableOptions = []; + foreach ($tables as $table) { + $comment = $this->parseTableCommentFromSQL($table, $this->getCreateTableSQL($table)); + + if ($comment !== null) { + $tableOptions[$table]['comment'] = $comment; + } + } + + /** @phpstan-ignore return.type */ + return $tableOptions; + } + + /** @param list $params */ + private function getWhereClause(?string $tableName, array &$params): string + { + $conditions = [ + "t.type = 'table'", + "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')", + ]; + + if ($tableName !== null) { + $conditions[] = 't.name = ?'; + $params[] = $tableName; + } + + return implode(' AND ', $conditions); + } +} diff --git a/doctrine/dbal/src/Schema/Schema.php b/doctrine/dbal/src/Schema/Schema.php index 703c83c59..f92ee0f7d 100644 --- a/doctrine/dbal/src/Schema/Schema.php +++ b/doctrine/dbal/src/Schema/Schema.php @@ -1,17 +1,25 @@ */ class Schema extends AbstractAsset { /** * The namespaces in this schema. * - * @var string[] + * @var array */ private array $namespaces = []; - /** @var Table[] */ - protected $_tables = []; + /** @var array */ + protected array $_tables = []; - /** @var Sequence[] */ - protected $_sequences = []; + /** @var array */ + protected array $_sequences = []; - /** @var SchemaConfig */ - protected $_schemaConfig; + protected SchemaConfig $_schemaConfig; /** - * @param Table[] $tables - * @param Sequence[] $sequences - * @param string[] $namespaces - * - * @throws SchemaException + * Indicates whether the schema uses unqualified names for its objects. Once this flag is set to true, it won't be + * unset even after the objects with unqualified names have been dropped from the schema. + */ + private bool $usesUnqualifiedNames = false; + + /** + * @param array
$tables + * @param array $sequences + * @param array $namespaces */ public function __construct( array $tables = [], array $sequences = [], ?SchemaConfig $schemaConfig = null, - array $namespaces = [] + array $namespaces = [], ) { + if (count($namespaces) > 0) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/7186', + 'Passing the $namespaces argument to the Schema constructor is deprecated.', + ); + } + $schemaConfig ??= new SchemaConfig(); $this->_schemaConfig = $schemaConfig; - $this->_setName($schemaConfig->getName() ?? 'public'); + + $name = $schemaConfig->getName(); + + parent::__construct($name ?? ''); foreach ($namespaces as $namespace) { $this->createNamespace($namespace); } foreach ($tables as $table) { + $table->setSchemaConfig($this->_schemaConfig); $this->_addTable($table); } @@ -87,133 +119,193 @@ public function __construct( } } - /** - * @deprecated - * - * @return bool - */ - public function hasExplicitForeignKeyIndexes() + /** @deprecated */ + public function getName(): string { - Deprecation::trigger( + Deprecation::triggerIfCalledFromOutside( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4822', - 'Schema::hasExplicitForeignKeyIndexes() is deprecated.', + 'https://github.com/doctrine/dbal/pull/6734', + 'Using Schema as AbstractAsset, including %s, is deprecated.', + __METHOD__, ); - return $this->_schemaConfig->hasExplicitForeignKeyIndexes(); + return parent::getName(); + } + + protected function getNameParser(): UnqualifiedNameParser + { + return Parsers::getUnqualifiedNameParser(); } /** - * @return void + * The object representation of the name isn't used because {@see Schema} is not an {@see AbstractAsset}. * - * @throws SchemaException + * This method implements the abstract method in the parent class and will be removed once {@see Schema} stops + * extending {@see AbstractAsset}. */ - protected function _addTable(Table $table) + protected function setName(?Name $name): void + { + } + + protected function _addTable(Table $table): void { - $namespaceName = $table->getNamespaceName(); - $tableName = $this->normalizeName($table); + $resolvedName = $this->resolveName($table); - if (isset($this->_tables[$tableName])) { - throw SchemaException::tableAlreadyExists($tableName); + $key = $this->getKeyFromResolvedName($resolvedName); + + if (isset($this->_tables[$key])) { + throw TableAlreadyExists::new($resolvedName->getName()); } - if ( - $namespaceName !== null - && ! $table->isInDefaultNamespace($this->getName()) - && ! $this->hasNamespace($namespaceName) - ) { - $this->createNamespace($namespaceName); + $namespaceName = $resolvedName->getNamespaceName(); + + if ($namespaceName !== null) { + if ( + ! $table->isInDefaultNamespace($this->getName()) + && ! $this->hasNamespace($namespaceName) + ) { + $this->createNamespace($namespaceName); + } + } else { + $this->usesUnqualifiedNames = true; } - $this->_tables[$tableName] = $table; - $table->setSchemaConfig($this->_schemaConfig); + $this->_tables[$key] = $table; } - /** - * @return void - * - * @throws SchemaException - */ - protected function _addSequence(Sequence $sequence) + protected function _addSequence(Sequence $sequence): void { - $namespaceName = $sequence->getNamespaceName(); - $seqName = $this->normalizeName($sequence); + $resolvedName = $this->resolveName($sequence); - if (isset($this->_sequences[$seqName])) { - throw SchemaException::sequenceAlreadyExists($seqName); + $key = $this->getKeyFromResolvedName($resolvedName); + + if (isset($this->_sequences[$key])) { + throw SequenceAlreadyExists::new($resolvedName->getName()); } - if ( - $namespaceName !== null - && ! $sequence->isInDefaultNamespace($this->getName()) - && ! $this->hasNamespace($namespaceName) - ) { - $this->createNamespace($namespaceName); + $namespaceName = $resolvedName->getNamespaceName(); + + if ($namespaceName !== null) { + if ( + ! $sequence->isInDefaultNamespace($this->getName()) + && ! $this->hasNamespace($namespaceName) + ) { + $this->createNamespace($namespaceName); + } + } else { + $this->usesUnqualifiedNames = true; } - $this->_sequences[$seqName] = $sequence; + $this->_sequences[$key] = $sequence; } /** * Returns the namespaces of this schema. * - * @return string[] A list of namespace names. + * @return list A list of namespace names. */ - public function getNamespaces() + public function getNamespaces(): array { - return $this->namespaces; + return array_values($this->namespaces); } /** * Gets all tables of this schema. * - * @return Table[] + * @return list
*/ - public function getTables() + public function getTables(): array + { + return array_values($this->_tables); + } + + public function getTable(string $name): Table { - return $this->_tables; + $key = $this->getKeyFromName($name); + if (! isset($this->_tables[$key])) { + throw TableDoesNotExist::new($name); + } + + return $this->_tables[$key]; } /** - * @param string $name + * Returns the key that will be used to store the given object in a collection of such objects based on its name. + * + * If the schema uses unqualified names, the object name must be unqualified. If the schema uses qualified names, + * the object name must be qualified. * - * @return Table + * The resulting key is the lower-cased full object name. Lower-casing is + * actually wrong, but we have to do it to keep our sanity. If you are + * using database objects that only differentiate in the casing (FOO vs + * Foo) then you will NOT be able to use Doctrine Schema abstraction. * - * @throws SchemaException + * @param AbstractAsset $asset + * + * @template N of Name */ - public function getTable($name) + private function getKeyFromResolvedName(AbstractAsset $asset): string { - $name = $this->getFullQualifiedAssetName($name); - if (! isset($this->_tables[$name])) { - throw SchemaException::tableDoesNotExist($name); + $key = $asset->getName(); + + if ($asset->getNamespaceName() !== null) { + if ($this->usesUnqualifiedNames) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6677#user-content-qualified-names', + 'Using qualified names to create or reference objects in a schema that uses unqualified ' + . 'names is deprecated.', + ); + } + + $key = $this->getName() . '.' . $key; + } elseif (count($this->namespaces) > 0) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6677#user-content-unqualified-names', + 'Using unqualified names to create or reference objects in a schema that uses qualified ' + . 'names and lacks a default namespace configuration is deprecated.', + ); } - return $this->_tables[$name]; + return strtolower($key); } - /** @param string $name */ - private function getFullQualifiedAssetName($name): string + /** + * Returns the key that will be used to store the given object with the given name in a collection of such objects. + * + * If the schema configuration has the default namespace, an unqualified name will be resolved to qualified against + * that namespace. + */ + private function getKeyFromName(string $name): string { - $name = $this->getUnquotedAssetName($name); - - if (strpos($name, '.') === false) { - $name = $this->getName() . '.' . $name; - } - - return strtolower($name); + return $this->getKeyFromResolvedName($this->resolveName(new Identifier($name))); } - private function normalizeName(AbstractAsset $asset): string + /** + * Resolves the qualified or unqualified name against the current schema name and returns a qualified name. + * + * @param AbstractAsset $asset A database object with optionally qualified name. + * + * @template N of Name + */ + private function resolveName(AbstractAsset $asset): AbstractAsset { - return $asset->getFullQualifiedName($this->getName()); + if ($asset->getNamespaceName() === null) { + $defaultNamespaceName = $this->getName(); + + if ($defaultNamespaceName !== '') { + return new Identifier($defaultNamespaceName . '.' . $asset->getName()); + } + } + + return $asset; } /** * Returns the unquoted representation of a given asset name. - * - * @param string $assetName Quoted or unquoted representation of an asset name. */ - private function getUnquotedAssetName($assetName): string + private function getUnquotedAssetName(string $assetName): string { if ($this->isIdentifierQuoted($assetName)) { return $this->trimQuotes($assetName); @@ -224,12 +316,8 @@ private function getUnquotedAssetName($assetName): string /** * Does this schema have a namespace with the given name? - * - * @param string $name - * - * @return bool */ - public function hasNamespace($name) + public function hasNamespace(string $name): bool { $name = strtolower($this->getUnquotedAssetName($name)); @@ -238,88 +326,59 @@ public function hasNamespace($name) /** * Does this schema have a table with the given name? - * - * @param string $name - * - * @return bool */ - public function hasTable($name) + public function hasTable(string $name): bool { - $name = $this->getFullQualifiedAssetName($name); - - return isset($this->_tables[$name]); - } - - /** - * Gets all table names, prefixed with a schema name, even the default one if present. - * - * @deprecated Use {@see getTables()} and {@see Table::getName()} instead. - * - * @return string[] - */ - public function getTableNames() - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4800', - 'Schema::getTableNames() is deprecated.' - . ' Use Schema::getTables() and Table::getName() instead.', - __METHOD__, - ); + $key = $this->getKeyFromName($name); - return array_keys($this->_tables); + return isset($this->_tables[$key]); } - /** - * @param string $name - * - * @return bool - */ - public function hasSequence($name) + public function hasSequence(string $name): bool { - $name = $this->getFullQualifiedAssetName($name); + $key = $this->getKeyFromName($name); - return isset($this->_sequences[$name]); + return isset($this->_sequences[$key]); } - /** - * @param string $name - * - * @return Sequence - * - * @throws SchemaException - */ - public function getSequence($name) + public function getSequence(string $name): Sequence { - $name = $this->getFullQualifiedAssetName($name); - if (! $this->hasSequence($name)) { - throw SchemaException::sequenceDoesNotExist($name); + $key = $this->getKeyFromName($name); + if (! isset($this->_sequences[$key])) { + throw SequenceDoesNotExist::new($name); } - return $this->_sequences[$name]; + return $this->_sequences[$key]; } - /** @return Sequence[] */ - public function getSequences() + /** @return list */ + public function getSequences(): array { - return $this->_sequences; + return array_values($this->_sequences); } /** * Creates a new namespace. * - * @param string $name The name of the namespace to create. + * @deprecated The schema automatically derives namespaces from the names of its tables and sequences. + * Creating empty namespaces is deprecated. * - * @return Schema This schema instance. - * - * @throws SchemaException + * @return $this */ - public function createNamespace($name) + public function createNamespace(string $name): self { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/7186', + '%s is deprecated. The schema automatically derives namespaces from the names of its tables and' + . ' sequences. Creating empty namespaces is deprecated.', + __METHOD__, + ); + $unquotedName = strtolower($this->getUnquotedAssetName($name)); if (isset($this->namespaces[$unquotedName])) { - throw SchemaException::namespaceAlreadyExists($unquotedName); + throw NamespaceAlreadyExists::new($unquotedName); } $this->namespaces[$unquotedName] = $name; @@ -329,16 +388,10 @@ public function createNamespace($name) /** * Creates a new table. - * - * @param string $name - * - * @return Table - * - * @throws SchemaException */ - public function createTable($name) + public function createTable(string $name): Table { - $table = new Table($name); + $table = new Table($name, [], [], [], [], [], $this->_schemaConfig->toTableConfiguration()); $this->_addTable($table); foreach ($this->_schemaConfig->getDefaultTableOptions() as $option => $value) { @@ -351,17 +404,17 @@ public function createTable($name) /** * Renames a table. * - * @param string $oldName - * @param string $newName - * - * @return Schema - * - * @throws SchemaException + * @return $this */ - public function renameTable($oldName, $newName) + public function renameTable(string $oldName, string $newName): self { $table = $this->getTable($oldName); - $table->_setName($newName); + + $identifier = new Identifier($newName); + + $table->_name = $identifier->_name; + $table->_namespace = $identifier->_namespace; + $table->_quoted = $identifier->_quoted; $this->dropTable($oldName); $this->_addTable($table); @@ -372,33 +425,24 @@ public function renameTable($oldName, $newName) /** * Drops a table from the schema. * - * @param string $name - * - * @return Schema - * - * @throws SchemaException + * @return $this */ - public function dropTable($name) + public function dropTable(string $name): self { - $name = $this->getFullQualifiedAssetName($name); - $this->getTable($name); - unset($this->_tables[$name]); + $key = $this->getKeyFromName($name); + if (! isset($this->_tables[$key])) { + throw TableDoesNotExist::new($name); + } + + unset($this->_tables[$key]); return $this; } /** * Creates a new sequence. - * - * @param string $name - * @param int $allocationSize - * @param int $initialValue - * - * @return Sequence - * - * @throws SchemaException */ - public function createSequence($name, $allocationSize = 1, $initialValue = 1) + public function createSequence(string $name, int $allocationSize = 1, int $initialValue = 1): Sequence { $seq = new Sequence($name, $allocationSize, $initialValue); $this->_addSequence($seq); @@ -406,15 +450,11 @@ public function createSequence($name, $allocationSize = 1, $initialValue = 1) return $seq; } - /** - * @param string $name - * - * @return Schema - */ - public function dropSequence($name) + /** @return $this */ + public function dropSequence(string $name): self { - $name = $this->getFullQualifiedAssetName($name); - unset($this->_sequences[$name]); + $key = $this->getKeyFromName($name); + unset($this->_sequences[$key]); return $this; } @@ -426,7 +466,7 @@ public function dropSequence($name) * * @throws Exception */ - public function toSql(AbstractPlatform $platform) + public function toSql(AbstractPlatform $platform): array { $builder = new CreateSchemaObjectsSQLBuilder($platform); @@ -437,78 +477,16 @@ public function toSql(AbstractPlatform $platform) * Return an array of necessary SQL queries to drop the schema on the given platform. * * @return list - * - * @throws Exception */ - public function toDropSql(AbstractPlatform $platform) + public function toDropSql(AbstractPlatform $platform): array { $builder = new DropSchemaObjectsSQLBuilder($platform); return $builder->buildSQL($this); } - /** - * @deprecated - * - * @return string[] - * - * @throws SchemaException - */ - public function getMigrateToSql(Schema $toSchema, AbstractPlatform $platform) - { - $schemaDiff = (new Comparator())->compareSchemas($this, $toSchema); - - return $schemaDiff->toSql($platform); - } - - /** - * @deprecated - * - * @return string[] - * - * @throws SchemaException - */ - public function getMigrateFromSql(Schema $fromSchema, AbstractPlatform $platform) - { - $schemaDiff = (new Comparator())->compareSchemas($fromSchema, $this); - - return $schemaDiff->toSql($platform); - } - - /** - * @deprecated - * - * @return void - */ - public function visit(Visitor $visitor) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5435', - 'Schema::visit() is deprecated.', - ); - - $visitor->acceptSchema($this); - - if ($visitor instanceof NamespaceVisitor) { - foreach ($this->namespaces as $namespace) { - $visitor->acceptNamespace($namespace); - } - } - - foreach ($this->_tables as $table) { - $table->visit($visitor); - } - - foreach ($this->_sequences as $sequence) { - $sequence->visit($visitor); - } - } - /** * Cloning a Schema triggers a deep clone of all related assets. - * - * @return void */ public function __clone() { diff --git a/doctrine/dbal/src/Schema/SchemaConfig.php b/doctrine/dbal/src/Schema/SchemaConfig.php index 5e394514b..557ed1e70 100644 --- a/doctrine/dbal/src/Schema/SchemaConfig.php +++ b/doctrine/dbal/src/Schema/SchemaConfig.php @@ -1,76 +1,31 @@ */ + protected array $defaultTableOptions = []; - /** - * @deprecated - * - * @return bool - */ - public function hasExplicitForeignKeyIndexes() + /** @param positive-int $length */ + public function setMaxIdentifierLength(int $length): void { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4822', - 'SchemaConfig::hasExplicitForeignKeyIndexes() is deprecated.', - ); - - return $this->hasExplicitForeignKeyIndexes; + $this->maxIdentifierLength = $length; } - /** - * @deprecated - * - * @param bool $flag - * - * @return void - */ - public function setExplicitForeignKeyIndexes($flag) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4822', - 'SchemaConfig::setExplicitForeignKeyIndexes() is deprecated.', - ); - - $this->hasExplicitForeignKeyIndexes = (bool) $flag; - } - - /** - * @param int $length - * - * @return void - */ - public function setMaxIdentifierLength($length) - { - $this->maxIdentifierLength = (int) $length; - } - - /** @return int */ - public function getMaxIdentifierLength() + /** @return positive-int */ + public function getMaxIdentifierLength(): int { return $this->maxIdentifierLength; } @@ -78,9 +33,9 @@ public function getMaxIdentifierLength() /** * Gets the default namespace of schema objects. * - * @return string|null + * @return ?non-empty-string */ - public function getName() + public function getName(): ?string { return $this->name; } @@ -88,11 +43,9 @@ public function getName() /** * Sets the default namespace name of schema objects. * - * @param string $name The value to set. - * - * @return void + * @param ?non-empty-string $name */ - public function setName($name) + public function setName(?string $name): void { $this->name = $name; } @@ -101,20 +54,21 @@ public function setName($name) * Gets the default options that are passed to Table instances created with * Schema#createTable(). * - * @return mixed[] + * @return array */ - public function getDefaultTableOptions() + public function getDefaultTableOptions(): array { return $this->defaultTableOptions; } - /** - * @param mixed[] $defaultTableOptions - * - * @return void - */ - public function setDefaultTableOptions(array $defaultTableOptions) + /** @param array $defaultTableOptions */ + public function setDefaultTableOptions(array $defaultTableOptions): void { $this->defaultTableOptions = $defaultTableOptions; } + + public function toTableConfiguration(): TableConfiguration + { + return new TableConfiguration($this->maxIdentifierLength); + } } diff --git a/doctrine/dbal/src/Schema/SchemaDiff.php b/doctrine/dbal/src/Schema/SchemaDiff.php index a6a866579..185c1b5eb 100644 --- a/doctrine/dbal/src/Schema/SchemaDiff.php +++ b/doctrine/dbal/src/Schema/SchemaDiff.php @@ -1,188 +1,97 @@ */ + private readonly array $alteredTables; /** * Constructs an SchemaDiff object. * * @internal The diff can be only instantiated by a {@see Comparator}. * - * @param Table[] $newTables - * @param TableDiff[] $changedTables - * @param Table[] $removedTables - * @param array $createdSchemas - * @param array $droppedSchemas - * @param array $createdSequences - * @param array $alteredSequences - * @param array $droppedSequences + * @param array $createdSchemas + * @param array $droppedSchemas + * @param array
$createdTables + * @param array $alteredTables + * @param array
$droppedTables + * @param array $createdSequences + * @param array $alteredSequences + * @param array $droppedSequences */ public function __construct( - $newTables = [], - $changedTables = [], - $removedTables = [], - ?Schema $fromSchema = null, - $createdSchemas = [], - $droppedSchemas = [], - $createdSequences = [], - $alteredSequences = [], - $droppedSequences = [] + private readonly array $createdSchemas, + private readonly array $droppedSchemas, + private readonly array $createdTables, + array $alteredTables, + private readonly array $droppedTables, + private readonly array $createdSequences, + private readonly array $alteredSequences, + private readonly array $droppedSequences, ) { - $this->newTables = $newTables; - - $this->changedTables = array_filter($changedTables, static function (TableDiff $diff): bool { + $this->alteredTables = array_filter($alteredTables, static function (TableDiff $diff): bool { return ! $diff->isEmpty(); }); - - $this->removedTables = $removedTables; - $this->fromSchema = $fromSchema; - $this->newNamespaces = $createdSchemas; - $this->removedNamespaces = $droppedSchemas; - $this->newSequences = $createdSequences; - $this->changedSequences = $alteredSequences; - $this->removedSequences = $droppedSequences; } /** @return array */ public function getCreatedSchemas(): array { - return $this->newNamespaces; + return $this->createdSchemas; } /** @return array */ public function getDroppedSchemas(): array { - return $this->removedNamespaces; + return $this->droppedSchemas; } /** @return array
*/ public function getCreatedTables(): array { - return $this->newTables; + return $this->createdTables; } /** @return array */ public function getAlteredTables(): array { - return $this->changedTables; + return $this->alteredTables; } /** @return array
*/ public function getDroppedTables(): array { - return $this->removedTables; + return $this->droppedTables; } /** @return array */ public function getCreatedSequences(): array { - return $this->newSequences; + return $this->createdSequences; } /** @return array */ public function getAlteredSequences(): array { - return $this->changedSequences; + return $this->alteredSequences; } /** @return array */ public function getDroppedSequences(): array { - return $this->removedSequences; + return $this->droppedSequences; } /** @@ -190,105 +99,13 @@ public function getDroppedSequences(): array */ public function isEmpty(): bool { - return count($this->newNamespaces) === 0 - && count($this->removedNamespaces) === 0 - && count($this->newTables) === 0 - && count($this->changedTables) === 0 - && count($this->removedTables) === 0 - && count($this->newSequences) === 0 - && count($this->changedSequences) === 0 - && count($this->removedSequences) === 0; - } - - /** - * The to save sql mode ensures that the following things don't happen: - * - * 1. Tables are deleted - * 2. Sequences are deleted - * 3. Foreign Keys which reference tables that would otherwise be deleted. - * - * This way it is ensured that assets are deleted which might not be relevant to the metadata schema at all. - * - * @deprecated - * - * @return list - */ - public function toSaveSql(AbstractPlatform $platform) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5766', - '%s is deprecated.', - __METHOD__, - ); - - return $this->_toSql($platform, true); - } - - /** - * @deprecated Use {@link AbstractPlatform::getAlterSchemaSQL()} instead. - * - * @return list - */ - public function toSql(AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5766', - '%s is deprecated. Use AbstractPlatform::getAlterSchemaSQL() instead.', - __METHOD__, - ); - - return $this->_toSql($platform, false); - } - - /** - * @param bool $saveMode - * - * @return list - */ - protected function _toSql(AbstractPlatform $platform, $saveMode = false) - { - $sql = []; - - if ($platform->supportsSchemas()) { - foreach ($this->getCreatedSchemas() as $schema) { - $sql[] = $platform->getCreateSchemaSQL($schema); - } - } - - if ($platform->supportsForeignKeyConstraints() && $saveMode === false) { - foreach ($this->orphanedForeignKeys as $orphanedForeignKey) { - $sql[] = $platform->getDropForeignKeySQL($orphanedForeignKey, $orphanedForeignKey->getLocalTable()); - } - } - - if ($platform->supportsSequences() === true) { - foreach ($this->getAlteredSequences() as $sequence) { - $sql[] = $platform->getAlterSequenceSQL($sequence); - } - - if ($saveMode === false) { - foreach ($this->getDroppedSequences() as $sequence) { - $sql[] = $platform->getDropSequenceSQL($sequence); - } - } - - foreach ($this->getCreatedSequences() as $sequence) { - $sql[] = $platform->getCreateSequenceSQL($sequence); - } - } - - $sql = array_merge($sql, $platform->getCreateTablesSQL($this->getCreatedTables())); - - if ($saveMode === false) { - $sql = array_merge($sql, $platform->getDropTablesSQL($this->getDroppedTables())); - } - - foreach ($this->getAlteredTables() as $tableDiff) { - $sql = array_merge($sql, $platform->getAlterTableSQL($tableDiff)); - } - - return $sql; + return count($this->createdSchemas) === 0 + && count($this->droppedSchemas) === 0 + && count($this->createdTables) === 0 + && count($this->alteredTables) === 0 + && count($this->droppedTables) === 0 + && count($this->createdSequences) === 0 + && count($this->alteredSequences) === 0 + && count($this->droppedSequences) === 0; } } diff --git a/doctrine/dbal/src/Schema/SchemaException.php b/doctrine/dbal/src/Schema/SchemaException.php index 24a9dd346..42771392d 100644 --- a/doctrine/dbal/src/Schema/SchemaException.php +++ b/doctrine/dbal/src/Schema/SchemaException.php @@ -1,203 +1,11 @@ + * + * @throws Exception + */ + public function getAllDatabaseNames(): array; + + /** + * Returns names of all available schemas. + * + * @return list + * + * @throws Exception + */ + public function getAllSchemaNames(): array; + + /** + * Returns names of all tables. + * + * @return list + * + * @throws Exception + */ + public function getAllTableNames(): array; + + /** + * Returns all tables. + * + * @return list
+ * + * @throws Exception + */ + public function getAllTables(): array; + + /** + * Returns columns for the given table. + * + * If the current database platform supports schemas, the schema name must be specified. Otherwise, null must be + * passed as the schema name. + * + * Returns an empty value if the table doesn't exist or is not accessible to the connection. + * + * @param ?non-empty-string $schemaName + * @param non-empty-string $tableName + * + * @return list + * + * @throws Exception + */ + public function getColumnsForTable(?string $schemaName, string $tableName): array; + + /** + * Returns indexes for the given table. + * + * If the current database platform supports schemas, the schema name must be specified. Otherwise, null must be + * passed as the schema name. + * + * Returns an empty value if the table doesn't exist or is not accessible to the connection. + * + * @param ?non-empty-string $schemaName + * @param non-empty-string $tableName + * + * @return list + * + * @throws Exception + */ + public function getIndexesForTable(?string $schemaName, string $tableName): array; + + /** + * Returns the primary key constraint for the given table. + * + * If the current database platform supports schemas, the schema name must be specified. Otherwise, null must be + * passed as the schema name. + * + * Returns null if the table doesn't exist, doesn't have a primary key constraint or is not accessible + * to the connection. + * + * @param ?non-empty-string $schemaName + * @param non-empty-string $tableName + * + * @throws Exception + */ + public function getPrimaryKeyConstraintForTable(?string $schemaName, string $tableName): ?PrimaryKeyConstraint; + + /** + * Returns the foreign key constraints in the given table. + * + * If the current database platform supports schemas, the schema name must be specified. Otherwise, null must be + * passed as the schema name. + * + * Returns an empty value if the table doesn't exist or is not accessible to the connection. + * + * @param ?non-empty-string $schemaName + * @param non-empty-string $tableName + * + * @return list + * + * @throws Exception + */ + public function getForeignKeyConstraintsForTable(?string $schemaName, string $tableName): array; + + /** + * Returns options of the given table. + * + * If the current database platform supports schemas, the schema name must be specified. Otherwise, null must be + * passed as the schema name. + * + * Returns null if the table doesn't exist or is not accessible to the connection. + * + * @param ?non-empty-string $schemaName + * @param non-empty-string $tableName + * + * @return array|null + * + * @throws Exception + */ + public function getOptionsForTable(?string $schemaName, string $tableName): ?array; + + /** + * @return list + * + * @throws Exception + */ + public function getAllViews(): array; + + /** + * @return list + * + * @throws Exception + */ + public function getAllSequences(): array; +} diff --git a/doctrine/dbal/src/Schema/Sequence.php b/doctrine/dbal/src/Schema/Sequence.php index 527c63c35..71c63a627 100644 --- a/doctrine/dbal/src/Schema/Sequence.php +++ b/doctrine/dbal/src/Schema/Sequence.php @@ -1,8 +1,12 @@ */ -class Sequence extends AbstractAsset +class Sequence extends AbstractNamedObject { - /** @var int */ - protected $allocationSize = 1; - - /** @var int */ - protected $initialValue = 1; + protected int $allocationSize = 1; - /** @var int|null */ - protected $cache; + protected int $initialValue = 1; /** - * @param string $name - * @param int $allocationSize - * @param int $initialValue - * @param int|null $cache + * @internal Use {@link Sequence::editor()} to instantiate an editor and {@link SequenceEditor::create()} to create + * a sequence. + * + * @param ?non-negative-int $cache */ - public function __construct($name, $allocationSize = 1, $initialValue = 1, $cache = null) - { - $this->_setName($name); + public function __construct( + string $name, + int $allocationSize = 1, + int $initialValue = 1, + protected ?int $cache = null, + ) { + parent::__construct($name); + + if ($cache < 0) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/7108', + 'Passing a negative value as sequence cache size is deprecated.', + ); + } + $this->setAllocationSize($allocationSize); $this->setInitialValue($initialValue); - $this->cache = $cache; } - /** @return int */ - public function getAllocationSize() + protected function getNameParser(): OptionallyQualifiedNameParser { - return $this->allocationSize; + return Parsers::getOptionallyQualifiedNameParser(); } - /** @return int */ - public function getInitialValue() + public function getAllocationSize(): int { - return $this->initialValue; + return $this->allocationSize; } - /** @return int|null */ - public function getCache() + public function getInitialValue(): int { - return $this->cache; + return $this->initialValue; } /** - * @param int $allocationSize + * @deprecated Use {@see getCacheSize()} instead. * - * @return Sequence + * @return ?non-negative-int */ - public function setAllocationSize($allocationSize) + public function getCache(): ?int { - if ($allocationSize > 0) { - $this->allocationSize = $allocationSize; - } else { - $this->allocationSize = 1; - } + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/7108', + '%s is deprecated, use `getCacheSize()` instead.', + __METHOD__, + ); + + return $this->cache; + } + + /** @return ?non-negative-int */ + public function getCacheSize(): ?int + { + return $this->getCache(); + } + + /** @deprecated Use {@see edit()} and {@see SequenceEditor::setAllocationSize()} instead. */ + public function setAllocationSize(int $allocationSize): self + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/7115', + '%s is deprecated. Use Sequence::edit() and SequenceEditor::setAllocationSize() instead.', + __METHOD__, + ); + + $this->allocationSize = $allocationSize; return $this; } - /** - * @param int $initialValue - * - * @return Sequence - */ - public function setInitialValue($initialValue) + /** @deprecated Use {@see edit()} and {@see SequenceEditor::setInitialValue()} instead. */ + public function setInitialValue(int $initialValue): self { - if ($initialValue > 0) { - $this->initialValue = $initialValue; - } else { - $this->initialValue = 1; - } + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/7115', + '%s is deprecated. Use Sequence::edit() and SequenceEditor::setInitialValue() instead.', + __METHOD__, + ); + + $this->initialValue = $initialValue; return $this; } /** - * @param int $cache + * @deprecated Use {@see edit()} and {@see SequenceEditor::setCacheSize()} instead. * - * @return Sequence + * @param non-negative-int $cache */ - public function setCache($cache) + public function setCache(int $cache): self { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/7115', + '%s is deprecated. Use Sequence::edit() and SequenceEditor::setCacheSize() instead.', + __METHOD__, + ); + $this->cache = $cache; return $this; @@ -104,10 +143,17 @@ public function setCache($cache) * This is used inside the comparator to not report sequences as missing, * when the "from" schema implicitly creates the sequences. * - * @return bool + * @deprecated */ - public function isAutoIncrementsFor(Table $table) + public function isAutoIncrementsFor(Table $table): bool { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6654', + '%s is deprecated and will be removed in 5.0.', + __METHOD__, + ); + $primaryKey = $table->getPrimaryKey(); if ($primaryKey === null) { @@ -134,18 +180,22 @@ public function isAutoIncrementsFor(Table $table) } /** - * @deprecated - * - * @return void + * Instantiates a new sequence editor. */ - public function visit(Visitor $visitor) + public static function editor(): SequenceEditor { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5435', - 'Sequence::visit() is deprecated.', - ); + return new SequenceEditor(); + } - $visitor->acceptSequence($this); + /** + * Instantiates a new sequence editor and initializes it with the sequence's properties. + */ + public function edit(): SequenceEditor + { + return self::editor() + ->setName($this->getObjectName()) + ->setAllocationSize($this->getAllocationSize()) + ->setInitialValue($this->getInitialValue()) + ->setCacheSize($this->getCacheSize()); } } diff --git a/doctrine/dbal/src/Schema/SequenceEditor.php b/doctrine/dbal/src/Schema/SequenceEditor.php new file mode 100644 index 000000000..119ff5084 --- /dev/null +++ b/doctrine/dbal/src/Schema/SequenceEditor.php @@ -0,0 +1,94 @@ +name = $name; + + return $this; + } + + /** + * @param non-empty-string $unqualifiedName + * @param ?non-empty-string $qualifier + */ + public function setUnquotedName(string $unqualifiedName, ?string $qualifier = null): self + { + $this->name = OptionallyQualifiedName::unquoted($unqualifiedName, $qualifier); + + return $this; + } + + /** + * @param non-empty-string $unqualifiedName + * @param ?non-empty-string $qualifier + */ + public function setQuotedName(string $unqualifiedName, ?string $qualifier = null): self + { + $this->name = OptionallyQualifiedName::quoted($unqualifiedName, $qualifier); + + return $this; + } + + public function setAllocationSize(int $allocationSize): self + { + $this->allocationSize = $allocationSize; + + return $this; + } + + public function setInitialValue(int $initialValue): self + { + $this->initialValue = $initialValue; + + return $this; + } + + /** @param ?non-negative-int $cacheSize */ + public function setCacheSize(?int $cacheSize): self + { + if ($cacheSize < 0) { + throw InvalidSequenceDefinition::fromNegativeCacheSize($cacheSize); + } + + $this->cacheSize = $cacheSize; + + return $this; + } + + public function create(): Sequence + { + if ($this->name === null) { + throw InvalidSequenceDefinition::nameNotSet(); + } + + return new Sequence( + $this->name->toString(), + $this->allocationSize, + $this->initialValue, + $this->cacheSize, + ); + } +} diff --git a/doctrine/dbal/src/Schema/SqliteSchemaManager.php b/doctrine/dbal/src/Schema/SqliteSchemaManager.php deleted file mode 100644 index 4b96739e1..000000000 --- a/doctrine/dbal/src/Schema/SqliteSchemaManager.php +++ /dev/null @@ -1,807 +0,0 @@ - - */ -class SqliteSchemaManager extends AbstractSchemaManager -{ - /** - * {@inheritDoc} - */ - public function listTableNames() - { - return $this->doListTableNames(); - } - - /** - * {@inheritDoc} - */ - public function listTables() - { - return $this->doListTables(); - } - - /** - * {@inheritDoc} - * - * @deprecated Use {@see introspectTable()} instead. - */ - public function listTableDetails($name) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5595', - '%s is deprecated. Use introspectTable() instead.', - __METHOD__, - ); - - return $this->doListTableDetails($name); - } - - /** - * {@inheritDoc} - */ - public function listTableColumns($table, $database = null) - { - return $this->doListTableColumns($table, $database); - } - - /** - * {@inheritDoc} - */ - public function listTableIndexes($table) - { - return $this->doListTableIndexes($table); - } - - /** - * {@inheritDoc} - */ - protected function fetchForeignKeyColumnsByTable(string $databaseName): array - { - $columnsByTable = parent::fetchForeignKeyColumnsByTable($databaseName); - - if (count($columnsByTable) > 0) { - foreach ($columnsByTable as $table => $columns) { - $columnsByTable[$table] = $this->addDetailsToTableForeignKeyColumns($table, $columns); - } - } - - return $columnsByTable; - } - - /** - * {@inheritDoc} - * - * @deprecated Delete the database file using the filesystem. - */ - public function dropDatabase($database) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4963', - 'SqliteSchemaManager::dropDatabase() is deprecated. Delete the database file using the filesystem.', - ); - - if (! file_exists($database)) { - return; - } - - unlink($database); - } - - /** - * {@inheritDoc} - * - * @deprecated The engine will create the database file automatically. - */ - public function createDatabase($database) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4963', - 'SqliteSchemaManager::createDatabase() is deprecated.' - . ' The engine will create the database file automatically.', - ); - - $params = $this->_conn->getParams(); - - $params['path'] = $database; - unset($params['memory']); - - $conn = DriverManager::getConnection($params); - $conn->connect(); - $conn->close(); - } - - /** - * {@inheritDoc} - */ - public function createForeignKey(ForeignKeyConstraint $foreignKey, $table) - { - if (! $table instanceof Table) { - $table = $this->listTableDetails($table); - } - - $this->alterTable(new TableDiff($table->getName(), [], [], [], [], [], [], $table, [$foreignKey])); - } - - /** - * {@inheritDoc} - * - * @deprecated Use {@see dropForeignKey()} and {@see createForeignKey()} instead. - */ - public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4897', - 'SqliteSchemaManager::dropAndCreateForeignKey() is deprecated.' - . ' Use SqliteSchemaManager::dropForeignKey() and SqliteSchemaManager::createForeignKey() instead.', - ); - - if (! $table instanceof Table) { - $table = $this->listTableDetails($table); - } - - $this->alterTable(new TableDiff($table->getName(), [], [], [], [], [], [], $table, [], [$foreignKey])); - } - - /** - * {@inheritDoc} - */ - public function dropForeignKey($foreignKey, $table) - { - if (! $table instanceof Table) { - $table = $this->listTableDetails($table); - } - - $this->alterTable(new TableDiff($table->getName(), [], [], [], [], [], [], $table, [], [], [$foreignKey])); - } - - /** - * {@inheritDoc} - */ - public function listTableForeignKeys($table, $database = null) - { - $table = $this->normalizeName($table); - - $columns = $this->selectForeignKeyColumns($database ?? 'main', $table) - ->fetchAllAssociative(); - - if (count($columns) > 0) { - $columns = $this->addDetailsToTableForeignKeyColumns($table, $columns); - } - - return $this->_getPortableTableForeignKeysList($columns); - } - - /** - * {@inheritDoc} - */ - protected function _getPortableTableDefinition($table) - { - return $table['table_name']; - } - - /** - * {@inheritDoc} - */ - protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) - { - $indexBuffer = []; - - // fetch primary - $indexArray = $this->_conn->fetchAllAssociative('SELECT * FROM PRAGMA_TABLE_INFO (?)', [$tableName]); - - usort( - $indexArray, - /** - * @param array $a - * @param array $b - */ - static function (array $a, array $b): int { - if ($a['pk'] === $b['pk']) { - return $a['cid'] - $b['cid']; - } - - return $a['pk'] - $b['pk']; - }, - ); - - foreach ($indexArray as $indexColumnRow) { - if ($indexColumnRow['pk'] === 0 || $indexColumnRow['pk'] === '0') { - continue; - } - - $indexBuffer[] = [ - 'key_name' => 'primary', - 'primary' => true, - 'non_unique' => false, - 'column_name' => $indexColumnRow['name'], - ]; - } - - // fetch regular indexes - foreach ($tableIndexes as $tableIndex) { - // Ignore indexes with reserved names, e.g. autoindexes - if (strpos($tableIndex['name'], 'sqlite_') === 0) { - continue; - } - - $keyName = $tableIndex['name']; - $idx = []; - $idx['key_name'] = $keyName; - $idx['primary'] = false; - $idx['non_unique'] = ! $tableIndex['unique']; - - $indexArray = $this->_conn->fetchAllAssociative('SELECT * FROM PRAGMA_INDEX_INFO (?)', [$keyName]); - - foreach ($indexArray as $indexColumnRow) { - $idx['column_name'] = $indexColumnRow['name']; - $indexBuffer[] = $idx; - } - } - - return parent::_getPortableTableIndexesList($indexBuffer, $tableName); - } - - /** - * {@inheritDoc} - */ - protected function _getPortableTableColumnList($table, $database, $tableColumns) - { - $list = parent::_getPortableTableColumnList($table, $database, $tableColumns); - - // find column with autoincrement - $autoincrementColumn = null; - $autoincrementCount = 0; - - foreach ($tableColumns as $tableColumn) { - if ($tableColumn['pk'] === 0 || $tableColumn['pk'] === '0') { - continue; - } - - $autoincrementCount++; - if ($autoincrementColumn !== null || strtolower($tableColumn['type']) !== 'integer') { - continue; - } - - $autoincrementColumn = $tableColumn['name']; - } - - if ($autoincrementCount === 1 && $autoincrementColumn !== null) { - foreach ($list as $column) { - if ($autoincrementColumn !== $column->getName()) { - continue; - } - - $column->setAutoincrement(true); - } - } - - // inspect column collation and comments - $createSql = $this->getCreateTableSQL($table); - - foreach ($list as $columnName => $column) { - $type = $column->getType(); - - if ($type instanceof StringType || $type instanceof TextType) { - $column->setPlatformOption( - 'collation', - $this->parseColumnCollationFromSQL($columnName, $createSql) ?? 'BINARY', - ); - } - - $comment = $this->parseColumnCommentFromSQL($columnName, $createSql); - - if ($comment === null) { - continue; - } - - $type = $this->extractDoctrineTypeFromComment($comment, ''); - - if ($type !== '') { - $column->setType(Type::getType($type)); - - $comment = $this->removeDoctrineTypeFromComment($comment, $type); - } - - $column->setComment($comment); - } - - return $list; - } - - /** - * {@inheritDoc} - */ - protected function _getPortableTableColumnDefinition($tableColumn) - { - $parts = explode('(', $tableColumn['type']); - $tableColumn['type'] = trim($parts[0]); - if (isset($parts[1])) { - $length = trim($parts[1], ')'); - $tableColumn['length'] = $length; - } - - $dbType = strtolower($tableColumn['type']); - $length = $tableColumn['length'] ?? null; - $unsigned = false; - - if (strpos($dbType, ' unsigned') !== false) { - $dbType = str_replace(' unsigned', '', $dbType); - $unsigned = true; - } - - $fixed = false; - $type = $this->_platform->getDoctrineTypeMapping($dbType); - $default = $tableColumn['dflt_value']; - if ($default === 'NULL') { - $default = null; - } - - if ($default !== null) { - // SQLite returns the default value as a literal expression, so we need to parse it - if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) { - $default = str_replace("''", "'", $matches[1]); - } - } - - $notnull = (bool) $tableColumn['notnull']; - - if (! isset($tableColumn['name'])) { - $tableColumn['name'] = ''; - } - - $precision = null; - $scale = null; - - switch ($dbType) { - case 'char': - $fixed = true; - break; - case 'float': - case 'double': - case 'real': - case 'decimal': - case 'numeric': - if (isset($tableColumn['length'])) { - if (strpos($tableColumn['length'], ',') === false) { - $tableColumn['length'] .= ',0'; - } - - [$precision, $scale] = array_map('trim', explode(',', $tableColumn['length'])); - } - - $length = null; - break; - } - - $options = [ - 'length' => $length, - 'unsigned' => $unsigned, - 'fixed' => $fixed, - 'notnull' => $notnull, - 'default' => $default, - 'precision' => $precision, - 'scale' => $scale, - ]; - - return new Column($tableColumn['name'], Type::getType($type), $options); - } - - /** - * {@inheritDoc} - */ - protected function _getPortableViewDefinition($view) - { - return new View($view['name'], $view['sql']); - } - - /** - * {@inheritDoc} - */ - protected function _getPortableTableForeignKeysList($tableForeignKeys) - { - $list = []; - foreach ($tableForeignKeys as $value) { - $value = array_change_key_case($value, CASE_LOWER); - $id = $value['id']; - if (! isset($list[$id])) { - if (! isset($value['on_delete']) || $value['on_delete'] === 'RESTRICT') { - $value['on_delete'] = null; - } - - if (! isset($value['on_update']) || $value['on_update'] === 'RESTRICT') { - $value['on_update'] = null; - } - - $list[$id] = [ - 'name' => $value['constraint_name'], - 'local' => [], - 'foreign' => [], - 'foreignTable' => $value['table'], - 'onDelete' => $value['on_delete'], - 'onUpdate' => $value['on_update'], - 'deferrable' => $value['deferrable'], - 'deferred' => $value['deferred'], - ]; - } - - $list[$id]['local'][] = $value['from']; - - if ($value['to'] === null) { - // Inferring a shorthand form for the foreign key constraint, where the "to" field is empty. - // @see https://www.sqlite.org/foreignkeys.html#fk_indexes. - $foreignTableIndexes = $this->_getPortableTableIndexesList([], $value['table']); - - if (! isset($foreignTableIndexes['primary'])) { - continue; - } - - $list[$id]['foreign'] = [...$list[$id]['foreign'], ...$foreignTableIndexes['primary']->getColumns()]; - - continue; - } - - $list[$id]['foreign'][] = $value['to']; - } - - return parent::_getPortableTableForeignKeysList($list); - } - - /** - * {@inheritDoc} - */ - protected function _getPortableTableForeignKeyDefinition($tableForeignKey): ForeignKeyConstraint - { - return new ForeignKeyConstraint( - $tableForeignKey['local'], - $tableForeignKey['foreignTable'], - $tableForeignKey['foreign'], - $tableForeignKey['name'], - [ - 'onDelete' => $tableForeignKey['onDelete'], - 'onUpdate' => $tableForeignKey['onUpdate'], - 'deferrable' => $tableForeignKey['deferrable'], - 'deferred' => $tableForeignKey['deferred'], - ], - ); - } - - private function parseColumnCollationFromSQL(string $column, string $sql): ?string - { - $pattern = '{' . $this->buildIdentifierPattern($column) - . '[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is'; - - if (preg_match($pattern, $sql, $match) !== 1) { - return null; - } - - return $match[1]; - } - - private function parseTableCommentFromSQL(string $table, string $sql): ?string - { - $pattern = '/\s* # Allow whitespace characters at start of line -CREATE\sTABLE' . $this->buildIdentifierPattern($table) . ' -( # Start capture - (?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s) -)/ix'; - - if (preg_match($pattern, $sql, $match) !== 1) { - return null; - } - - $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n")); - - return $comment === '' ? null : $comment; - } - - private function parseColumnCommentFromSQL(string $column, string $sql): ?string - { - $pattern = '{[\s(,]' . $this->buildIdentifierPattern($column) - . '(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i'; - - if (preg_match($pattern, $sql, $match) !== 1) { - return null; - } - - $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n")); - - return $comment === '' ? null : $comment; - } - - /** - * Returns a regular expression pattern that matches the given unquoted or quoted identifier. - */ - private function buildIdentifierPattern(string $identifier): string - { - return '(?:' . implode('|', array_map( - static function (string $sql): string { - return '\W' . preg_quote($sql, '/') . '\W'; - }, - [ - $identifier, - $this->_platform->quoteSingleIdentifier($identifier), - ], - )) . ')'; - } - - /** @throws Exception */ - private function getCreateTableSQL(string $table): string - { - $sql = $this->_conn->fetchOne( - <<<'SQL' -SELECT sql - FROM ( - SELECT * - FROM sqlite_master - UNION ALL - SELECT * - FROM sqlite_temp_master - ) -WHERE type = 'table' -AND name = ? -SQL - , - [$table], - ); - - if ($sql !== false) { - return $sql; - } - - return ''; - } - - /** - * @param list> $columns - * - * @return list> - * - * @throws Exception - */ - private function addDetailsToTableForeignKeyColumns(string $table, array $columns): array - { - $foreignKeyDetails = $this->getForeignKeyDetails($table); - $foreignKeyCount = count($foreignKeyDetails); - - foreach ($columns as $i => $column) { - // SQLite identifies foreign keys in reverse order of appearance in SQL - $columns[$i] = array_merge($column, $foreignKeyDetails[$foreignKeyCount - $column['id'] - 1]); - } - - return $columns; - } - - /** - * @param string $table - * - * @return list> - * - * @throws Exception - */ - private function getForeignKeyDetails($table) - { - $createSql = $this->getCreateTableSQL($table); - - if ( - preg_match_all( - '# - (?:CONSTRAINT\s+(\S+)\s+)? - (?:FOREIGN\s+KEY[^)]+\)\s*)? - REFERENCES\s+\S+\s*(?:\([^)]+\))? - (?: - [^,]*? - (NOT\s+DEFERRABLE|DEFERRABLE) - (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))? - )?#isx', - $createSql, - $match, - ) === 0 - ) { - return []; - } - - $names = $match[1]; - $deferrable = $match[2]; - $deferred = $match[3]; - $details = []; - - for ($i = 0, $count = count($match[0]); $i < $count; $i++) { - $details[] = [ - 'constraint_name' => isset($names[$i]) && $names[$i] !== '' ? $names[$i] : null, - 'deferrable' => isset($deferrable[$i]) && strcasecmp($deferrable[$i], 'deferrable') === 0, - 'deferred' => isset($deferred[$i]) && strcasecmp($deferred[$i], 'deferred') === 0, - ]; - } - - return $details; - } - - public function createComparator(): Comparator - { - return new SQLite\Comparator($this->_platform); - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function getSchemaSearchPaths() - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4821', - 'SqliteSchemaManager::getSchemaSearchPaths() is deprecated.', - ); - - // SQLite does not support schemas or databases - return []; - } - - protected function selectTableNames(string $databaseName): Result - { - $sql = <<<'SQL' -SELECT name AS table_name -FROM sqlite_master -WHERE type = 'table' - AND name != 'sqlite_sequence' - AND name != 'geometry_columns' - AND name != 'spatial_ref_sys' -UNION ALL -SELECT name -FROM sqlite_temp_master -WHERE type = 'table' -ORDER BY name -SQL; - - return $this->_conn->executeQuery($sql); - } - - protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result - { - $sql = <<<'SQL' - SELECT t.name AS table_name, - c.* - FROM sqlite_master t - JOIN pragma_table_info(t.name) c -SQL; - - $conditions = [ - "t.type = 'table'", - "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')", - ]; - $params = []; - - if ($tableName !== null) { - $conditions[] = 't.name = ?'; - $params[] = $this->_platform->canEmulateSchemas() - ? str_replace('.', '__', $tableName) - : $tableName; - } - - $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, c.cid'; - - return $this->_conn->executeQuery($sql, $params); - } - - protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result - { - $sql = <<<'SQL' - SELECT t.name AS table_name, - i.* - FROM sqlite_master t - JOIN pragma_index_list(t.name) i -SQL; - - $conditions = [ - "t.type = 'table'", - "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')", - ]; - $params = []; - - if ($tableName !== null) { - $conditions[] = 't.name = ?'; - $params[] = $this->_platform->canEmulateSchemas() - ? str_replace('.', '__', $tableName) - : $tableName; - } - - $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, i.seq'; - - return $this->_conn->executeQuery($sql, $params); - } - - protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result - { - $sql = <<<'SQL' - SELECT t.name AS table_name, - p.* - FROM sqlite_master t - JOIN pragma_foreign_key_list(t.name) p - ON p."seq" != '-1' -SQL; - - $conditions = [ - "t.type = 'table'", - "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')", - ]; - $params = []; - - if ($tableName !== null) { - $conditions[] = 't.name = ?'; - $params[] = $this->_platform->canEmulateSchemas() - ? str_replace('.', '__', $tableName) - : $tableName; - } - - $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, p.id DESC, p.seq'; - - return $this->_conn->executeQuery($sql, $params); - } - - /** - * {@inheritDoc} - */ - protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array - { - if ($tableName === null) { - $tables = $this->listTableNames(); - } else { - $tables = [$tableName]; - } - - $tableOptions = []; - foreach ($tables as $table) { - $comment = $this->parseTableCommentFromSQL($table, $this->getCreateTableSQL($table)); - - if ($comment === null) { - continue; - } - - $tableOptions[$table]['comment'] = $comment; - } - - return $tableOptions; - } -} diff --git a/doctrine/dbal/src/Schema/Table.php b/doctrine/dbal/src/Schema/Table.php index ce4cc3260..de0d3d6be 100644 --- a/doctrine/dbal/src/Schema/Table.php +++ b/doctrine/dbal/src/Schema/Table.php @@ -1,63 +1,95 @@ */ -class Table extends AbstractAsset +class Table extends AbstractNamedObject { /** @var Column[] */ - protected $_columns = []; + protected array $_columns = []; + + /** @var array keys are new names, values are old names */ + protected array $renamedColumns = []; /** @var Index[] */ - protected $_indexes = []; + protected array $_indexes = []; + + /** + * The keys of this array are the names of the indexes that were implicitly created as backing for foreign key + * constraints. The values are not used but must be non-null for {@link isset()} to work correctly. + * + * @var array + */ + private array $implicitIndexNames = []; - /** @var string|null */ - protected $_primaryKeyName; + /** @deprecated Use {@see $primaryKeyConstraint} instead. */ + protected ?string $_primaryKeyName = null; /** @var UniqueConstraint[] */ - protected $uniqueConstraints = []; + protected array $uniqueConstraints = []; /** @var ForeignKeyConstraint[] */ - protected $_fkConstraints = []; + protected array $_fkConstraints = []; /** @var mixed[] */ - protected $_options = [ + protected array $_options = [ 'create_options' => [], ]; - /** @var SchemaConfig|null */ - protected $_schemaConfig; + /** @deprecated Pass a {@link TableConfiguration} instance to the constructor instead. */ + protected ?SchemaConfig $_schemaConfig = null; - /** @var Index[] */ - private array $implicitIndexes = []; + /** @var positive-int */ + private int $maxIdentifierLength; + + private ?PrimaryKeyConstraint $primaryKeyConstraint = null; + + private bool $failedToParsePrimaryKeyConstraint = false; /** - * @param Column[] $columns - * @param Index[] $indexes - * @param UniqueConstraint[] $uniqueConstraints - * @param ForeignKeyConstraint[] $fkConstraints - * @param mixed[] $options - * - * @throws SchemaException - * @throws Exception + * @param array $columns + * @param array $indexes + * @param array $uniqueConstraints + * @param array $fkConstraints + * @param array $options */ public function __construct( string $name, @@ -65,13 +97,19 @@ public function __construct( array $indexes = [], array $uniqueConstraints = [], array $fkConstraints = [], - array $options = [] + array $options = [], + ?TableConfiguration $configuration = null, + ?PrimaryKeyConstraint $primaryKeyConstraint = null, ) { if ($name === '') { throw InvalidTableName::new($name); } - $this->_setName($name); + parent::__construct($name); + + $configuration ??= (new SchemaConfig())->toTableConfiguration(); + + $this->maxIdentifierLength = $configuration->getMaxIdentifierLength(); foreach ($columns as $column) { $this->_addColumn($column); @@ -81,46 +119,58 @@ public function __construct( $this->_addIndex($idx); } + if ($primaryKeyConstraint !== null) { + $this->addPrimaryKeyConstraint($primaryKeyConstraint); + } + foreach ($uniqueConstraints as $uniqueConstraint) { $this->_addUniqueConstraint($uniqueConstraint); } - foreach ($fkConstraints as $constraint) { - $this->_addForeignKeyConstraint($constraint); + foreach ($fkConstraints as $fkConstraint) { + $this->_addForeignKeyConstraint($fkConstraint); } $this->_options = array_merge($this->_options, $options); } - /** @return void */ - public function setSchemaConfig(SchemaConfig $schemaConfig) + protected function getNameParser(): OptionallyQualifiedNameParser { - $this->_schemaConfig = $schemaConfig; + return Parsers::getOptionallyQualifiedNameParser(); } - /** @return int */ - protected function _getMaxIdentifierLength() + /** @deprecated Pass a {@link TableConfiguration} instance to the constructor instead. */ + public function setSchemaConfig(SchemaConfig $schemaConfig): void { - if ($this->_schemaConfig instanceof SchemaConfig) { - return $this->_schemaConfig->getMaxIdentifierLength(); - } + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6635', + '%s is deprecated. Pass TableConfiguration to the constructor instead.', + __METHOD__, + ); + + $this->_schemaConfig = $schemaConfig; - return 63; + $this->maxIdentifierLength = $schemaConfig->getMaxIdentifierLength(); } /** * Sets the Primary Key. * - * @param string[] $columnNames - * @param string|false $indexName + * @deprecated Use {@see addPrimaryKeyConstraint()} instead. * - * @return self - * - * @throws SchemaException + * @param non-empty-list $columnNames */ - public function setPrimaryKey(array $columnNames, $indexName = false) + public function setPrimaryKey(array $columnNames, ?string $indexName = null): self { - if ($indexName === false) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6867', + '%s() is deprecated. Use Table::addPrimaryKeyConstraint() instead.', + __METHOD__, + ); + + if ($indexName === null) { $indexName = 'primary'; } @@ -128,63 +178,93 @@ public function setPrimaryKey(array $columnNames, $indexName = false) foreach ($columnNames as $columnName) { $column = $this->getColumn($columnName); + + if (! $column->getNotnull()) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6787', + 'Using nullable columns (%s.%s) in a primary key index is deprecated.', + $this->getName(), + $columnName, + ); + } + $column->setNotnull(true); } return $this; } + public function addPrimaryKeyConstraint(PrimaryKeyConstraint $primaryKeyConstraint): self + { + $this->setPrimaryKey( + array_map( + static fn (UnqualifiedName $columnName): string => $columnName->toString(), + $primaryKeyConstraint->getColumnNames(), + ), + $primaryKeyConstraint->getObjectName()?->toString(), + ); + + // there is no way to set a primary index with flags. we have to set it and then add the flag + if (! $primaryKeyConstraint->isClustered()) { + $index = $this->getPrimaryKey(); + assert($index !== null); + $index->addFlag('nonclustered'); + } + + $this->primaryKeyConstraint = $primaryKeyConstraint; + + return $this; + } + /** - * @param string[] $columnNames - * @param string[] $flags - * @param mixed[] $options - * - * @return self - * - * @throws SchemaException + * @param non-empty-list $columnNames + * @param array $flags + * @param array $options */ - public function addIndex(array $columnNames, ?string $indexName = null, array $flags = [], array $options = []) - { + public function addUniqueConstraint( + array $columnNames, + ?string $indexName = null, + array $flags = [], + array $options = [], + ): self { $indexName ??= $this->_generateIdentifierName( array_merge([$this->getName()], $columnNames), - 'idx', + 'uniq', $this->_getMaxIdentifierLength(), ); - return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options)); + return $this->_addUniqueConstraint($this->_createUniqueConstraint($columnNames, $indexName, $flags, $options)); } /** - * @param string[] $columnNames - * @param string[] $flags - * @param mixed[] $options - * - * @return self + * @param non-empty-list $columnNames + * @param array $flags + * @param array $options */ - public function addUniqueConstraint( + public function addIndex( array $columnNames, ?string $indexName = null, array $flags = [], - array $options = [] - ): Table { + array $options = [], + ): self { $indexName ??= $this->_generateIdentifierName( array_merge([$this->getName()], $columnNames), - 'uniq', + 'idx', $this->_getMaxIdentifierLength(), ); - return $this->_addUniqueConstraint($this->_createUniqueConstraint($columnNames, $indexName, $flags, $options)); + return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options)); } /** * Drops the primary key from this table. - * - * @return void - * - * @throws SchemaException */ - public function dropPrimaryKey() + public function dropPrimaryKey(): void { + $this->primaryKeyConstraint = null; + $this->failedToParsePrimaryKeyConstraint = false; + if ($this->_primaryKeyName === null) { return; } @@ -195,34 +275,23 @@ public function dropPrimaryKey() /** * Drops an index from this table. - * - * @param string $name The index name. - * - * @return void - * - * @throws SchemaException If the index does not exist. */ - public function dropIndex($name) + public function dropIndex(string $name): void { $name = $this->normalizeIdentifier($name); if (! $this->hasIndex($name)) { - throw SchemaException::indexDoesNotExist($name, $this->_name); + throw IndexDoesNotExist::new($name, $this->_name); } unset($this->_indexes[$name]); } /** - * @param string[] $columnNames - * @param string|null $indexName - * @param mixed[] $options - * - * @return self - * - * @throws SchemaException + * @param non-empty-list $columnNames + * @param array $options */ - public function addUniqueIndex(array $columnNames, $indexName = null, array $options = []) + public function addUniqueIndex(array $columnNames, ?string $indexName = null, array $options = []): self { $indexName ??= $this->_generateIdentifierName( array_merge([$this->getName()], $columnNames), @@ -237,40 +306,46 @@ public function addUniqueIndex(array $columnNames, $indexName = null, array $opt * Renames an index. * * @param string $oldName The name of the index to rename from. - * @param string|null $newName The name of the index to rename to. - * If null is given, the index name will be auto-generated. - * - * @return self This table instance. - * - * @throws SchemaException If no index exists for the given current name - * or if an index with the given new name already exists on this table. + * @param string|null $newName The name of the index to rename to. If null is given, the index name + * will be auto-generated. */ - public function renameIndex($oldName, $newName = null) + public function renameIndex(string $oldName, ?string $newName = null): self { - $oldName = $this->normalizeIdentifier($oldName); - $normalizedNewName = $this->normalizeIdentifier($newName); - - if ($oldName === $normalizedNewName) { - return $this; - } - if (! $this->hasIndex($oldName)) { - throw SchemaException::indexDoesNotExist($oldName, $this->_name); + throw IndexDoesNotExist::new($oldName, $this->_name); } - if ($this->hasIndex($normalizedNewName)) { - throw SchemaException::indexAlreadyExists($normalizedNewName, $this->_name); + $normalizedOldName = $this->normalizeIdentifier($oldName); + + if ($newName !== null) { + $normalizedNewName = $this->normalizeIdentifier($newName); + + if ($normalizedOldName === $normalizedNewName) { + return $this; + } + + if ($this->hasIndex($newName)) { + throw IndexAlreadyExists::new($newName, $this->_name); + } } - $oldIndex = $this->_indexes[$oldName]; + $oldIndex = $this->_indexes[$normalizedOldName]; if ($oldIndex->isPrimary()) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6867', + 'Renaming primary key constraint via %s() is deprecated. Use Table::dropPrimaryKey() and ' + . ' Table::addPrimaryKeyConstraint() instead.', + __METHOD__, + ); + $this->dropPrimaryKey(); - return $this->setPrimaryKey($oldIndex->getColumns(), $newName ?? false); + return $this->setPrimaryKey($oldIndex->getColumns(), $newName ?? null); } - unset($this->_indexes[$oldName]); + unset($this->_indexes[$normalizedOldName]); if ($oldIndex->isUnique()) { return $this->addUniqueIndex($oldIndex->getColumns(), $newName, $oldIndex->getOptions()); @@ -282,12 +357,19 @@ public function renameIndex($oldName, $newName = null) /** * Checks if an index begins in the order of the given columns. * - * @param string[] $columnNames + * @deprecated * - * @return bool + * @param array $columnNames */ - public function columnsAreIndexed(array $columnNames) + public function columnsAreIndexed(array $columnNames): bool { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6710', + '%s is deprecated.', + __METHOD__, + ); + foreach ($this->getIndexes() as $index) { if ($index->spansColumns($columnNames)) { return true; @@ -298,46 +380,11 @@ public function columnsAreIndexed(array $columnNames) } /** - * @param string[] $columnNames - * @param string $indexName - * @param bool $isUnique - * @param bool $isPrimary - * @param string[] $flags - * @param mixed[] $options - * - * @throws SchemaException - */ - private function _createIndex( - array $columnNames, - $indexName, - $isUnique, - $isPrimary, - array $flags = [], - array $options = [] - ): Index { - if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) { - throw SchemaException::indexNameInvalid($indexName); - } - - foreach ($columnNames as $columnName) { - if (! $this->hasColumn($columnName)) { - throw SchemaException::columnDoesNotExist($columnName, $this->_name); - } - } - - return new Index($indexName, $columnNames, $isUnique, $isPrimary, $flags, $options); - } - - /** - * @param string $name - * @param string $typeName - * @param mixed[] $options - * - * @return Column + * @param array $options * - * @throws SchemaException + * @throws TypesException */ - public function addColumn($name, $typeName, array $options = []) + public function addColumn(string $name, string $typeName, array $options = []): Column { $column = new Column($name, Type::getType($typeName), $options); @@ -346,39 +393,57 @@ public function addColumn($name, $typeName, array $options = []) return $column; } + /** @return array */ + final public function getRenamedColumns(): array + { + return $this->renamedColumns; + } + /** - * Change Column Details. - * - * @deprecated Use {@link modifyColumn()} instead. - * - * @param string $name - * @param mixed[] $options - * - * @return self + * @param non-empty-string $oldName + * @param non-empty-string $newName * - * @throws SchemaException + * @throws LogicException */ - public function changeColumn($name, array $options) + final public function renameColumn(string $oldName, string $newName): Column { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5747', - '%s is deprecated. Use modifyColumn() instead.', - __METHOD__, - ); + $oldName = $this->normalizeIdentifier($oldName); + $newName = $this->normalizeIdentifier($newName); + + if ($oldName === $newName) { + throw new LogicException(sprintf( + 'Attempt to rename column "%s.%s" to the same name.', + $this->name->toString(), + $oldName, + )); + } + + $column = $this->getColumn($oldName); + + $column->_name = $newName; + unset($this->_columns[$oldName]); + $this->_addColumn($column); - return $this->modifyColumn($name, $options); + $this->renameColumnInIndexes($oldName, $newName); + $this->renameColumnInForeignKeyConstraints($oldName, $newName); + $this->renameColumnInUniqueConstraints($oldName, $newName); + + // If a column is renamed multiple times, we only want to know the original and last new name + if (isset($this->renamedColumns[$oldName])) { + $toRemove = $oldName; + $oldName = $this->renamedColumns[$oldName]; + unset($this->renamedColumns[$toRemove]); + } + + if ($newName !== $oldName) { + $this->renamedColumns[$newName] = $oldName; + } + + return $column; } - /** - * @param string $name - * @param mixed[] $options - * - * @return self - * - * @throws SchemaException - */ - public function modifyColumn($name, array $options) + /** @param array $options */ + public function modifyColumn(string $name, array $options): self { $column = $this->getColumn($name); $column->setOptions($options); @@ -388,15 +453,35 @@ public function modifyColumn($name, array $options) /** * Drops a Column from the Table. - * - * @param string $name - * - * @return self */ - public function dropColumn($name) + public function dropColumn(string $name): self { $name = $this->normalizeIdentifier($name); + $foreignKeyConstraintNames = $this->getForeignKeyConstraintNamesByLocalColumnName($name); + $uniqueConstraintNames = $this->getUniqueConstraintNamesByColumnName($name); + + if (count($foreignKeyConstraintNames) > 0 || count($uniqueConstraintNames) > 0) { + $constraints = []; + + if (count($foreignKeyConstraintNames) > 0) { + $constraints[] = 'foreign key constraints: ' . implode(', ', $foreignKeyConstraintNames); + } + + if (count($uniqueConstraintNames) > 0) { + $constraints[] = 'unique constraints: ' . implode(', ', $uniqueConstraintNames); + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6559', + 'Dropping columns referenced by constraints is deprecated.' + . ' Column %s is used by the following constraints: %s ', + $name, + implode('; ', $constraints), + ); + } + unset($this->_columns[$name]); return $this; @@ -407,46 +492,32 @@ public function dropColumn($name) * * Name is inferred from the local columns. * - * @param Table|string $foreignTable Table schema instance or table name - * @param string[] $localColumnNames - * @param string[] $foreignColumnNames - * @param mixed[] $options - * @param string|null $name - * - * @return self - * - * @throws SchemaException + * @param non-empty-list $localColumnNames + * @param non-empty-list $foreignColumnNames + * @param array $options */ public function addForeignKeyConstraint( - $foreignTable, + string $foreignTableName, array $localColumnNames, array $foreignColumnNames, array $options = [], - $name = null - ) { + ?string $name = null, + ): self { $name ??= $this->_generateIdentifierName( array_merge([$this->getName()], $localColumnNames), 'fk', $this->_getMaxIdentifierLength(), ); - if ($foreignTable instanceof Table) { - foreach ($foreignColumnNames as $columnName) { - if (! $foreignTable->hasColumn($columnName)) { - throw SchemaException::columnDoesNotExist($columnName, $foreignTable->getName()); - } - } - } - foreach ($localColumnNames as $columnName) { if (! $this->hasColumn($columnName)) { - throw SchemaException::columnDoesNotExist($columnName, $this->_name); + throw ColumnDoesNotExist::new($columnName, $this->_name); } } $constraint = new ForeignKeyConstraint( $localColumnNames, - $foreignTable, + $foreignTableName, $foreignColumnNames, $name, $options, @@ -455,161 +526,17 @@ public function addForeignKeyConstraint( return $this->_addForeignKeyConstraint($constraint); } - /** - * @param string $name - * @param mixed $value - * - * @return self - */ - public function addOption($name, $value) + public function addOption(string $name, mixed $value): self { $this->_options[$name] = $value; return $this; } - /** - * @return void - * - * @throws SchemaException - */ - protected function _addColumn(Column $column) - { - $columnName = $column->getName(); - $columnName = $this->normalizeIdentifier($columnName); - - if (isset($this->_columns[$columnName])) { - throw SchemaException::columnAlreadyExists($this->getName(), $columnName); - } - - $this->_columns[$columnName] = $column; - } - - /** - * Adds an index to the table. - * - * @return self - * - * @throws SchemaException - */ - protected function _addIndex(Index $indexCandidate) - { - $indexName = $indexCandidate->getName(); - $indexName = $this->normalizeIdentifier($indexName); - $replacedImplicitIndexes = []; - - foreach ($this->implicitIndexes as $name => $implicitIndex) { - if (! $implicitIndex->isFulfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) { - continue; - } - - $replacedImplicitIndexes[] = $name; - } - - if ( - (isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) || - ($this->_primaryKeyName !== null && $indexCandidate->isPrimary()) - ) { - throw SchemaException::indexAlreadyExists($indexName, $this->_name); - } - - foreach ($replacedImplicitIndexes as $name) { - unset($this->_indexes[$name], $this->implicitIndexes[$name]); - } - - if ($indexCandidate->isPrimary()) { - $this->_primaryKeyName = $indexName; - } - - $this->_indexes[$indexName] = $indexCandidate; - - return $this; - } - - /** @return self */ - protected function _addUniqueConstraint(UniqueConstraint $constraint): Table - { - $mergedNames = array_merge([$this->getName()], $constraint->getColumns()); - $name = strlen($constraint->getName()) > 0 - ? $constraint->getName() - : $this->_generateIdentifierName($mergedNames, 'fk', $this->_getMaxIdentifierLength()); - - $name = $this->normalizeIdentifier($name); - - $this->uniqueConstraints[$name] = $constraint; - - // If there is already an index that fulfills this requirements drop the request. In the case of __construct - // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates. - // This creates computation overhead in this case, however no duplicate indexes are ever added (column based). - $indexName = $this->_generateIdentifierName($mergedNames, 'idx', $this->_getMaxIdentifierLength()); - - $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false); - - foreach ($this->_indexes as $existingIndex) { - if ($indexCandidate->isFulfilledBy($existingIndex)) { - return $this; - } - } - - $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate; - - return $this; - } - - /** @return self */ - protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint) - { - $constraint->setLocalTable($this); - - if (strlen($constraint->getName()) > 0) { - $name = $constraint->getName(); - } else { - $name = $this->_generateIdentifierName( - array_merge([$this->getName()], $constraint->getLocalColumns()), - 'fk', - $this->_getMaxIdentifierLength(), - ); - } - - $name = $this->normalizeIdentifier($name); - - $this->_fkConstraints[$name] = $constraint; - - /* Add an implicit index (defined by the DBAL) on the foreign key - columns. If there is already a user-defined index that fulfills these - requirements drop the request. In the case of __construct() calling - this method during hydration from schema-details, all the explicitly - added indexes lead to duplicates. This creates computation overhead in - this case, however no duplicate indexes are ever added (based on - columns). */ - $indexName = $this->_generateIdentifierName( - array_merge([$this->getName()], $constraint->getColumns()), - 'idx', - $this->_getMaxIdentifierLength(), - ); - - $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, false, false); - - foreach ($this->_indexes as $existingIndex) { - if ($indexCandidate->isFulfilledBy($existingIndex)) { - return $this; - } - } - - $this->_addIndex($indexCandidate); - $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate; - - return $this; - } - /** * Returns whether this table has a foreign key constraint with the given name. - * - * @param string $name - * - * @return bool */ - public function hasForeignKey($name) + public function hasForeignKey(string $name): bool { $name = $this->normalizeIdentifier($name); @@ -618,19 +545,13 @@ public function hasForeignKey($name) /** * Returns the foreign key constraint with the given name. - * - * @param string $name The constraint name. - * - * @return ForeignKeyConstraint - * - * @throws SchemaException If the foreign key does not exist. */ - public function getForeignKey($name) + public function getForeignKey(string $name): ForeignKeyConstraint { $name = $this->normalizeIdentifier($name); if (! $this->hasForeignKey($name)) { - throw SchemaException::foreignKeyDoesNotExist($name, $this->_name); + throw ForeignKeyDoesNotExist::new($name, $this->_name); } return $this->_fkConstraints[$name]; @@ -639,18 +560,28 @@ public function getForeignKey($name) /** * Removes the foreign key constraint with the given name. * - * @param string $name The constraint name. - * - * @return void - * - * @throws SchemaException + * @deprecated Use {@link dropForeignKey()} instead. + */ + public function removeForeignKey(string $name): void + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6560', + 'Table::removeForeignKey() is deprecated. Use Table::dropForeignKey() instead.', + ); + + $this->dropForeignKey($name); + } + + /** + * Drops the foreign key constraint with the given name. */ - public function removeForeignKey($name) + public function dropForeignKey(string $name): void { $name = $this->normalizeIdentifier($name); if (! $this->hasForeignKey($name)) { - throw SchemaException::foreignKeyDoesNotExist($name, $this->_name); + throw ForeignKeyDoesNotExist::new($name, $this->_name); } unset($this->_fkConstraints[$name]); @@ -668,15 +599,13 @@ public function hasUniqueConstraint(string $name): bool /** * Returns the unique constraint with the given name. - * - * @throws SchemaException If the unique constraint does not exist. */ public function getUniqueConstraint(string $name): UniqueConstraint { $name = $this->normalizeIdentifier($name); if (! $this->hasUniqueConstraint($name)) { - throw SchemaException::uniqueConstraintDoesNotExist($name, $this->_name); + throw UniqueConstraintDoesNotExist::new($name, $this->_name); } return $this->uniqueConstraints[$name]; @@ -685,83 +614,48 @@ public function getUniqueConstraint(string $name): UniqueConstraint /** * Removes the unique constraint with the given name. * - * @throws SchemaException If the unique constraint does not exist. + * @deprecated Use {@link dropUniqueConstraint()} instead. */ public function removeUniqueConstraint(string $name): void { - $name = $this->normalizeIdentifier($name); - - if (! $this->hasUniqueConstraint($name)) { - throw SchemaException::uniqueConstraintDoesNotExist($name, $this->_name); - } - - unset($this->uniqueConstraints[$name]); - } - - /** - * Returns ordered list of columns (primary keys are first, then foreign keys, then the rest) - * - * @return Column[] - */ - public function getColumns() - { - $primaryKeyColumns = $this->getPrimaryKey() !== null ? $this->getPrimaryKeyColumns() : []; - $foreignKeyColumns = $this->getForeignKeyColumns(); - $remainderColumns = $this->filterColumns( - array_merge(array_keys($primaryKeyColumns), array_keys($foreignKeyColumns)), - true, + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6560', + 'Table::removeUniqueConstraint() is deprecated. Use Table::dropUniqueConstraint() instead.', ); - return array_merge($primaryKeyColumns, $foreignKeyColumns, $remainderColumns); + $this->dropUniqueConstraint($name); } /** - * Returns the foreign key columns - * - * @deprecated Use {@see getForeignKey()} and {@see ForeignKeyConstraint::getLocalColumns()} instead. - * - * @return Column[] + * Drops the unique constraint with the given name. */ - public function getForeignKeyColumns() + public function dropUniqueConstraint(string $name): void { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5731', - '%s is deprecated. Use getForeignKey() and ForeignKeyConstraint::getLocalColumns() instead.', - __METHOD__, - ); - - $foreignKeyColumns = []; + $name = $this->normalizeIdentifier($name); - foreach ($this->getForeignKeys() as $foreignKey) { - $foreignKeyColumns = array_merge($foreignKeyColumns, $foreignKey->getLocalColumns()); + if (! $this->hasUniqueConstraint($name)) { + throw UniqueConstraintDoesNotExist::new($name, $this->_name); } - return $this->filterColumns($foreignKeyColumns); + unset($this->uniqueConstraints[$name]); } /** - * Returns only columns that have specified names + * Returns the list of table columns. * - * @param string[] $columnNames - * - * @return Column[] + * @return non-empty-list */ - private function filterColumns(array $columnNames, bool $reverse = false): array + public function getColumns(): array { - return array_filter($this->_columns, static function (string $columnName) use ($columnNames, $reverse): bool { - return in_array($columnName, $columnNames, true) !== $reverse; - }, ARRAY_FILTER_USE_KEY); + /** @phpstan-ignore return.type */ + return array_values($this->_columns); } /** * Returns whether this table has a Column with the given name. - * - * @param string $name The column name. - * - * @return bool */ - public function hasColumn($name) + public function hasColumn(string $name): bool { $name = $this->normalizeIdentifier($name); @@ -770,19 +664,13 @@ public function hasColumn($name) /** * Returns the Column with the given name. - * - * @param string $name The column name. - * - * @return Column - * - * @throws SchemaException If the column does not exist. */ - public function getColumn($name) + public function getColumn(string $name): Column { $name = $this->normalizeIdentifier($name); if (! $this->hasColumn($name)) { - throw SchemaException::columnDoesNotExist($name, $this->_name); + throw ColumnDoesNotExist::new($name, $this->_name); } return $this->_columns[$name]; @@ -791,71 +679,37 @@ public function getColumn($name) /** * Returns the primary key. * - * @return Index|null The primary key, or null if this Table has no primary key. - */ - public function getPrimaryKey() - { - if ($this->_primaryKeyName !== null) { - return $this->getIndex($this->_primaryKeyName); - } - - return null; - } - - /** - * Returns the primary key columns. - * - * @deprecated Use {@see getPrimaryKey()} and {@see Index::getColumns()} instead. - * - * @return Column[] - * - * @throws Exception + * @deprecated Use {@see getPrimaryKeyConstraint()} instead. */ - public function getPrimaryKeyColumns() + public function getPrimaryKey(): ?Index { Deprecation::triggerIfCalledFromOutside( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5731', - '%s is deprecated. Use getPrimaryKey() and Index::getColumns() instead.', + 'https://github.com/doctrine/dbal/pull/6867', + '%s() is deprecated. Use Table::getPrimaryKeyConstraint() instead.', __METHOD__, ); - $primaryKey = $this->getPrimaryKey(); - - if ($primaryKey === null) { - throw new Exception('Table ' . $this->getName() . ' has no primary key.'); + if ($this->_primaryKeyName !== null) { + return $this->getIndex($this->_primaryKeyName); } - return $this->filterColumns($primaryKey->getColumns()); + return null; } - /** - * Returns whether this table has a primary key. - * - * @deprecated Use {@see getPrimaryKey()} instead. - * - * @return bool - */ - public function hasPrimaryKey() + public function getPrimaryKeyConstraint(): ?PrimaryKeyConstraint { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5731', - '%s is deprecated. Use getPrimaryKey() instead.', - __METHOD__, - ); + if ($this->failedToParsePrimaryKeyConstraint) { + throw InvalidState::tableHasInvalidPrimaryKeyConstraint($this->getName()); + } - return $this->_primaryKeyName !== null && $this->hasIndex($this->_primaryKeyName); + return $this->primaryKeyConstraint; } /** * Returns whether this table has an Index with the given name. - * - * @param string $name The index name. - * - * @return bool */ - public function hasIndex($name) + public function hasIndex(string $name): bool { $name = $this->normalizeIdentifier($name); @@ -864,25 +718,20 @@ public function hasIndex($name) /** * Returns the Index with the given name. - * - * @param string $name The index name. - * - * @return Index - * - * @throws SchemaException If the index does not exist. */ - public function getIndex($name) + public function getIndex(string $name): Index { $name = $this->normalizeIdentifier($name); + if (! $this->hasIndex($name)) { - throw SchemaException::indexDoesNotExist($name, $this->_name); + throw IndexDoesNotExist::new($name, $this->_name); } return $this->_indexes[$name]; } - /** @return Index[] */ - public function getIndexes() + /** @return array */ + public function getIndexes(): array { return $this->_indexes; } @@ -890,7 +739,7 @@ public function getIndexes() /** * Returns the unique constraints. * - * @return UniqueConstraint[] + * @return array */ public function getUniqueConstraints(): array { @@ -900,142 +749,457 @@ public function getUniqueConstraints(): array /** * Returns the foreign key constraints. * - * @return ForeignKeyConstraint[] + * @return array */ - public function getForeignKeys() + public function getForeignKeys(): array { return $this->_fkConstraints; } - /** - * @param string $name - * - * @return bool - */ - public function hasOption($name) + public function hasOption(string $name): bool { return isset($this->_options[$name]); } - /** - * @param string $name - * - * @return mixed - */ - public function getOption($name) + public function getOption(string $name): mixed { - return $this->_options[$name]; + return $this->_options[$name] ?? null; } - /** @return mixed[] */ - public function getOptions() + /** @return array */ + public function getOptions(): array { return $this->_options; } + /** + * Clone of a Table triggers a deep clone of all affected assets. + */ + public function __clone() + { + foreach ($this->_columns as $k => $column) { + $this->_columns[$k] = clone $column; + } + + foreach ($this->_indexes as $k => $index) { + $this->_indexes[$k] = clone $index; + } + + foreach ($this->_fkConstraints as $k => $fk) { + $this->_fkConstraints[$k] = clone $fk; + } + } + /** * @deprecated * - * @return void - * - * @throws SchemaException + * @return positive-int */ - public function visit(Visitor $visitor) + protected function _getMaxIdentifierLength(): int { Deprecation::triggerIfCalledFromOutside( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5435', - 'Table::visit() is deprecated.', + 'https://github.com/doctrine/dbal/pull/6635', + '%s is deprecated.', + __METHOD__, ); - $visitor->acceptTable($this); + return $this->maxIdentifierLength; + } - foreach ($this->getColumns() as $column) { - $visitor->acceptColumn($this, $column); - } + protected function _addColumn(Column $column): void + { + $columnName = $column->getName(); + $columnName = $this->normalizeIdentifier($columnName); - foreach ($this->getIndexes() as $index) { - $visitor->acceptIndex($this, $index); + if (isset($this->_columns[$columnName])) { + throw ColumnAlreadyExists::new($this->getName(), $columnName); } - foreach ($this->getForeignKeys() as $constraint) { - $visitor->acceptForeignKey($this, $constraint); - } + $this->_columns[$columnName] = $column; } /** - * Clone of a Table triggers a deep clone of all affected assets. - * - * @return void + * Adds an index to the table. */ - public function __clone() + protected function _addIndex(Index $index): self { - foreach ($this->_columns as $k => $column) { - $this->_columns[$k] = clone $column; + $indexName = $this->normalizeIdentifier($index->getName()); + + $replacedImplicitIndexNames = []; + + foreach ($this->implicitIndexNames as $implicitIndexName => $_) { + if (! isset($this->_indexes[$implicitIndexName])) { + continue; + } + + if ($this->_indexes[$implicitIndexName]->isFulfilledBy($index)) { + $replacedImplicitIndexNames[$implicitIndexName] = true; + } } - foreach ($this->_indexes as $k => $index) { - $this->_indexes[$k] = clone $index; + if ($this->_primaryKeyName !== null && $index->isPrimary()) { + throw PrimaryKeyAlreadyExists::new($this->_name); } - foreach ($this->_fkConstraints as $k => $fk) { - $this->_fkConstraints[$k] = clone $fk; - $this->_fkConstraints[$k]->setLocalTable($this); + if (isset($this->_indexes[$indexName]) && ! isset($replacedImplicitIndexNames[$indexName])) { + throw IndexAlreadyExists::new($indexName, $this->_name); + } + + foreach ($replacedImplicitIndexNames as $name => $_) { + unset($this->_indexes[$name], $this->implicitIndexNames[$name]); + } + + if ($index->isPrimary()) { + $this->_primaryKeyName = $indexName; + + try { + $this->primaryKeyConstraint = $this->parsePrimaryKeyConstraint($index); + $this->failedToParsePrimaryKeyConstraint = false; + } catch (InvalidState) { + $this->primaryKeyConstraint = null; + $this->failedToParsePrimaryKeyConstraint = true; + } } + + $this->_indexes[$indexName] = $index; + + return $this; + } + + private function parsePrimaryKeyConstraint(Index $index): ?PrimaryKeyConstraint + { + $indexedColumns = $index->getIndexedColumns(); + + $columnNames = []; + foreach ($indexedColumns as $indexedColumn) { + if ($indexedColumn->getLength() !== null) { + return null; + } + + $columnNames[] = $indexedColumn->getColumnName(); + } + + // Do not derive the constraint name from the index name in the upgrade path. The primary index name defaults to + // "PRIMARY", while the default constraint name is null (unspecified, to be generated by the database platform). + return new PrimaryKeyConstraint( + null, + $columnNames, + ! $index->hasFlag('nonclustered'), + ); + } + + protected function _addUniqueConstraint(UniqueConstraint $constraint): self + { + $name = $constraint->getName() !== '' + ? $constraint->getName() + : $this->_generateIdentifierName( + array_merge((array) $this->getName(), $constraint->getColumns()), + 'fk', + $this->_getMaxIdentifierLength(), + ); + + $name = $this->normalizeIdentifier($name); + + $this->uniqueConstraints[$name] = $constraint; + + // If there is already an index that fulfills this requirements drop the request. In the case of __construct + // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates. + // This creates computation overhead in this case, however no duplicate indexes are ever added (column based). + $indexName = $this->_generateIdentifierName( + array_merge([$this->getName()], $constraint->getColumns()), + 'idx', + $this->_getMaxIdentifierLength(), + ); + + $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false); + + foreach ($this->_indexes as $existingIndex) { + if ($indexCandidate->isFulfilledBy($existingIndex)) { + return $this; + } + } + + $this->implicitIndexNames[$this->normalizeIdentifier($indexName)] = true; + + return $this; + } + + protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint): self + { + $name = $constraint->getName() !== '' + ? $constraint->getName() + : $this->_generateIdentifierName( + array_merge((array) $this->getName(), $constraint->getLocalColumns()), + 'fk', + $this->_getMaxIdentifierLength(), + ); + + $name = $this->normalizeIdentifier($name); + if (isset($this->_fkConstraints[$name])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/7125', + 'Overwriting an existing foreign key constraint ("%s") is deprecated.', + $name, + ); + } + + $this->_fkConstraints[$name] = $constraint; + + // add an explicit index on the foreign key columns. + // If there is already an index that fulfills this requirements drop the request. In the case of __construct + // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates. + // This creates computation overhead in this case, however no duplicate indexes are ever added (column based). + $indexName = $this->_generateIdentifierName( + array_merge([$this->getName()], $constraint->getLocalColumns()), + 'idx', + $this->_getMaxIdentifierLength(), + ); + + $indexCandidate = $this->_createIndex($constraint->getLocalColumns(), $indexName, false, false); + + foreach ($this->_indexes as $existingIndex) { + if ($indexCandidate->isFulfilledBy($existingIndex)) { + return $this; + } + } + + $this->_addIndex($indexCandidate); + $this->implicitIndexNames[$this->normalizeIdentifier($indexName)] = true; + + return $this; } /** - * @param string[] $columnNames - * @param string[] $flags - * @param mixed[] $options + * Normalizes a given identifier. + * + * Trims quotes and lowercases the given identifier. * - * @throws SchemaException + * @return non-empty-string + */ + private function normalizeIdentifier(string $identifier): string + { + /** @phpstan-ignore return.type */ + return $this->trimQuotes(strtolower($identifier)); + } + + public function setComment(string $comment): self + { + // For keeping backward compatibility with MySQL in previous releases, table comments are stored as options. + $this->addOption('comment', $comment); + + return $this; + } + + public function getComment(): ?string + { + return $this->_options['comment'] ?? null; + } + + /** + * Instantiates a new table editor. + */ + public static function editor(): TableEditor + { + return new TableEditor(); + } + + /** + * Instantiates a new table editor and initializes it with the table's properties. + */ + public function edit(): TableEditor + { + $editor = self::editor() + ->setName($this->getObjectName()) + ->setColumns(...array_values($this->_columns)) + ->setIndexes(...array_values(array_diff_key($this->_indexes, $this->implicitIndexNames))) + ->setPrimaryKeyConstraint($this->primaryKeyConstraint) + ->setUniqueConstraints(...array_values($this->uniqueConstraints)) + ->setForeignKeyConstraints(...array_values($this->_fkConstraints)); + + $options = $this->_options; + + if (isset($options['comment'])) { + $editor->setComment($options['comment']); + unset($options['comment']); + } + + return $editor + ->setOptions($options) + ->setConfiguration( + new TableConfiguration($this->maxIdentifierLength), + ); + } + + /** + * @param non-empty-list $columns + * @param array $flags + * @param array $options */ private function _createUniqueConstraint( - array $columnNames, + array $columns, string $indexName, array $flags = [], - array $options = [] + array $options = [], ): UniqueConstraint { if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) { - throw SchemaException::indexNameInvalid($indexName); + throw IndexNameInvalid::new($indexName); } - foreach ($columnNames as $columnName) { + foreach ($columns as $columnName) { if (! $this->hasColumn($columnName)) { - throw SchemaException::columnDoesNotExist($columnName, $this->_name); + throw ColumnDoesNotExist::new($columnName, $this->_name); } } - return new UniqueConstraint($indexName, $columnNames, $flags, $options); + return new UniqueConstraint($indexName, $columns, $flags, $options); } /** - * Normalizes a given identifier. - * - * Trims quotes and lowercases the given identifier. - * - * @return string The normalized identifier. + * @param non-empty-list $columns + * @param array $flags + * @param array $options */ - private function normalizeIdentifier(?string $identifier): string + private function _createIndex( + array $columns, + string $indexName, + bool $isUnique, + bool $isPrimary, + array $flags = [], + array $options = [], + ): Index { + if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) { + throw IndexNameInvalid::new($indexName); + } + + foreach ($columns as $columnName) { + if (! $this->hasColumn($columnName)) { + throw ColumnDoesNotExist::new($columnName, $this->_name); + } + } + + return new Index($indexName, $columns, $isUnique, $isPrimary, $flags, $options); + } + + /** @param non-empty-string $newName */ + private function renameColumnInIndexes(string $oldName, string $newName): void { - if ($identifier === null) { - return ''; + foreach ($this->_indexes as $key => $index) { + $modified = false; + $columns = []; + foreach ($index->getColumns() as $columnName) { + if ($columnName === $oldName) { + $columns[] = $newName; + $modified = true; + } else { + $columns[] = $columnName; + } + } + + if (! $modified) { + continue; + } + + $this->_indexes[$key] = new Index( + $index->getName(), + $columns, + $index->isUnique(), + $index->isPrimary(), + $index->getFlags(), + $index->getOptions(), + ); } + } - return $this->trimQuotes(strtolower($identifier)); + /** + * @param non-empty-string $oldName + * @param non-empty-string $newName + */ + private function renameColumnInForeignKeyConstraints(string $oldName, string $newName): void + { + foreach ($this->_fkConstraints as $key => $constraint) { + $modified = false; + $localColumns = []; + foreach ($constraint->getLocalColumns() as $columnName) { + if ($columnName === $oldName) { + $localColumns[] = $newName; + $modified = true; + } else { + $localColumns[] = $columnName; + } + } + + if (! $modified) { + continue; + } + + $this->_fkConstraints[$key] = new ForeignKeyConstraint( + $localColumns, // @phpstan-ignore argument.type + $constraint->getForeignTableName(), + $constraint->getForeignColumns(), // @phpstan-ignore argument.type + $constraint->getName(), + $constraint->getOptions(), + ); + } } - public function setComment(?string $comment): self + /** + * @param non-empty-string $oldName + * @param non-empty-string $newName + */ + private function renameColumnInUniqueConstraints(string $oldName, string $newName): void { - // For keeping backward compatibility with MySQL in previous releases, table comments are stored as options. - $this->addOption('comment', $comment); + foreach ($this->uniqueConstraints as $key => $constraint) { + $modified = false; + $columns = []; + foreach ($constraint->getColumns() as $columnName) { + if ($columnName === $oldName) { + $columns[] = $newName; + $modified = true; + } else { + $columns[] = $columnName; + } + } - return $this; + if (! $modified) { + continue; + } + + $this->uniqueConstraints[$key] = new UniqueConstraint( + $constraint->getName(), + $columns, // @phpstan-ignore argument.type + $constraint->getFlags(), + $constraint->getOptions(), + ); + } } - public function getComment(): ?string + /** @return list */ + private function getForeignKeyConstraintNamesByLocalColumnName(string $columnName): array { - return $this->_options['comment'] ?? null; + $names = []; + + foreach ($this->_fkConstraints as $name => $constraint) { + if (in_array($columnName, $constraint->getLocalColumns(), true)) { + $names[] = $name; + } + } + + return $names; + } + + /** @return list */ + private function getUniqueConstraintNamesByColumnName(string $columnName): array + { + $names = []; + + foreach ($this->uniqueConstraints as $name => $constraint) { + if (in_array($columnName, $constraint->getColumns(), true)) { + $names[] = $name; + } + } + + return $names; } } diff --git a/doctrine/dbal/src/Schema/TableConfiguration.php b/doctrine/dbal/src/Schema/TableConfiguration.php new file mode 100644 index 000000000..3348ffe1f --- /dev/null +++ b/doctrine/dbal/src/Schema/TableConfiguration.php @@ -0,0 +1,30 @@ +maxIdentifierLength; + } +} diff --git a/doctrine/dbal/src/Schema/TableDiff.php b/doctrine/dbal/src/Schema/TableDiff.php index 9aaf9e770..966e39ad7 100644 --- a/doctrine/dbal/src/Schema/TableDiff.php +++ b/doctrine/dbal/src/Schema/TableDiff.php @@ -1,8 +1,11 @@ $addedColumns - * @param array $modifiedColumns - * @param array $droppedColumns - * @param array $addedIndexes - * @param array $changedIndexes - * @param array $removedIndexes - * @param list $addedForeignKeys - * @param list $changedForeignKeys - * @param list $removedForeignKeys - * @param array $renamedColumns - * @param array $renamedIndexes + * @param array $droppedForeignKeys + * @param array $addedColumns + * @param array $changedColumns + * @param array $droppedColumns + * @param array $addedIndexes + * @param array $modifiedIndexes + * @param array $droppedIndexes + * @param array $renamedIndexes + * @param array $addedForeignKeys + * @param array $modifiedForeignKeys */ public function __construct( - $tableName, - $addedColumns = [], - $modifiedColumns = [], - $droppedColumns = [], - $addedIndexes = [], - $changedIndexes = [], - $removedIndexes = [], - ?Table $fromTable = null, - $addedForeignKeys = [], - $changedForeignKeys = [], - $removedForeignKeys = [], - $renamedColumns = [], - $renamedIndexes = [] + private readonly Table $oldTable, + private readonly array $addedColumns = [], + private readonly array $changedColumns = [], + private readonly array $droppedColumns = [], + private array $addedIndexes = [], + private readonly array $modifiedIndexes = [], + private array $droppedIndexes = [], + private readonly array $renamedIndexes = [], + private readonly array $addedForeignKeys = [], + private readonly array $modifiedForeignKeys = [], + private readonly array $droppedForeignKeys = [], ) { - $this->name = $tableName; - $this->addedColumns = $addedColumns; - $this->changedColumns = $modifiedColumns; - $this->renamedColumns = $renamedColumns; - $this->removedColumns = $droppedColumns; - $this->addedIndexes = $addedIndexes; - $this->changedIndexes = $changedIndexes; - $this->renamedIndexes = $renamedIndexes; - $this->removedIndexes = $removedIndexes; - $this->addedForeignKeys = $addedForeignKeys; - $this->changedForeignKeys = $changedForeignKeys; - $this->removedForeignKeys = $removedForeignKeys; - - if ($fromTable === null) { + if (count($this->modifiedIndexes) !== 0) { Deprecation::trigger( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5678', - 'Not passing the $fromTable to %s is deprecated.', + 'https://github.com/doctrine/dbal/pull/6831', + 'Passing a non-empty $modifiedIndexes value to %s() is deprecated. Instead, pass dropped' + . ' indexes via $droppedIndexes and added indexes via $addedIndexes.', __METHOD__, ); } - $this->fromTable = $fromTable; + foreach ($droppedForeignKeys as $droppedForeignKey) { + if ($droppedForeignKey->getName() === '') { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/7143', + 'Dropping a foreign key constraints without specifying its name is deprecated.', + ); + break; + } + } + + if (count($modifiedForeignKeys) === 0) { + return; + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6827', + 'Passing a non-empty $modifiedForeignKeys value to %s() is deprecated. Instead, pass dropped' + . ' constraints via $droppedForeignKeys and added constraints via $addedForeignKeys.', + __METHOD__, + ); + } + + public function getOldTable(): Table + { + return $this->oldTable; + } + + /** @return array */ + public function getAddedColumns(): array + { + return $this->addedColumns; + } + + /** @return array */ + public function getChangedColumns(): array + { + return $this->changedColumns; } /** - * @deprecated Use {@see getOldTable()} instead. - * - * @param AbstractPlatform $platform The platform to use for retrieving this table diff's name. + * @deprecated Use {@see getChangedColumns()} instead. * - * @return Identifier + * @return list */ - public function getName(AbstractPlatform $platform) + public function getModifiedColumns(): array { - return new Identifier( - $this->fromTable instanceof Table ? $this->fromTable->getQuotedName($platform) : $this->name, + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6280', + '%s is deprecated, use `getChangedColumns()` instead.', + __METHOD__, ); + + return array_values(array_filter( + $this->getChangedColumns(), + static fn (ColumnDiff $diff): bool => $diff->countChangedProperties() > ($diff->hasNameChanged() ? 1 : 0), + )); } /** - * @deprecated Rename tables via {@link AbstractSchemaManager::renameTable()} instead. + * @deprecated Use {@see getChangedColumns()} instead. * - * @return Identifier|false + * @return array */ - public function getNewName() + public function getRenamedColumns(): array { Deprecation::triggerIfCalledFromOutside( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5663', - '%s is deprecated. Rename tables via AbstractSchemaManager::renameTable() instead.', + 'https://github.com/doctrine/dbal/pull/6280', + '%s is deprecated, you should use `getChangedColumns()` instead.', __METHOD__, ); - - if ($this->newName === false) { - return false; + $renamed = []; + foreach ($this->getChangedColumns() as $diff) { + if (! $diff->hasNameChanged()) { + continue; + } + + $oldColumnName = $diff->getOldColumn()->getName(); + $renamed[$oldColumnName] = $diff->getNewColumn(); } - return new Identifier($this->newName); + return $renamed; } - public function getOldTable(): ?Table - { - return $this->fromTable; - } - - /** @return list */ - public function getAddedColumns(): array - { - return array_values($this->addedColumns); - } - - /** @return list */ - public function getModifiedColumns(): array - { - return array_values($this->changedColumns); - } - - /** @return list */ + /** @return array */ public function getDroppedColumns(): array { - return array_values($this->removedColumns); + return $this->droppedColumns; } - /** @return array */ - public function getRenamedColumns(): array - { - return $this->renamedColumns; - } - - /** @return list */ + /** @return array */ public function getAddedIndexes(): array { - return array_values($this->addedIndexes); + return $this->addedIndexes; } /** @@ -276,16 +171,27 @@ static function (Index $addedIndex) use ($index): bool { ); } - /** @return array */ + /** + * @deprecated Use {@see getAddedIndexes()} and {@see getDroppedIndexes()} instead. + * + * @return array + */ public function getModifiedIndexes(): array { - return array_values($this->changedIndexes); + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6831', + '%s() is deprecated, use getAddedIndexes() and getDroppedIndexes() instead.', + __METHOD__, + ); + + return $this->modifiedIndexes; } - /** @return list */ + /** @return array */ public function getDroppedIndexes(): array { - return array_values($this->removedIndexes); + return $this->droppedIndexes; } /** @@ -294,10 +200,10 @@ public function getDroppedIndexes(): array */ public function unsetDroppedIndex(Index $index): void { - $this->removedIndexes = array_filter( - $this->removedIndexes, - static function (Index $removedIndex) use ($index): bool { - return $removedIndex !== $index; + $this->droppedIndexes = array_filter( + $this->droppedIndexes, + static function (Index $droppedIndex) use ($index): bool { + return $droppedIndex !== $index; }, ); } @@ -308,37 +214,54 @@ public function getRenamedIndexes(): array return $this->renamedIndexes; } - /** @return list */ + /** @return array */ public function getAddedForeignKeys(): array { return $this->addedForeignKeys; } - /** @return list */ + /** + * @deprecated Use {@see getAddedForeignKeys()} and {@see getDroppedForeignKeys()} instead. + * + * @return array + */ public function getModifiedForeignKeys(): array { - return $this->changedForeignKeys; - } + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6827', + '%s() is deprecated, use getDroppedForeignKeys() and getAddedForeignKeys() instead.', + __METHOD__, + ); - /** @return list */ - public function getDroppedForeignKeys(): array - { - return $this->removedForeignKeys; + return $this->modifiedForeignKeys; } /** - * @internal This method exists only for compatibility with the current implementation of the schema comparator. + * @deprecated Use {@see getDroppedForeignKeyConstraintNames()}. * - * @param ForeignKeyConstraint|string $foreignKey + * @return array */ - public function unsetDroppedForeignKey($foreignKey): void + public function getDroppedForeignKeys(): array { - $this->removedForeignKeys = array_filter( - $this->removedForeignKeys, - static function ($removedForeignKey) use ($foreignKey): bool { - return $removedForeignKey !== $foreignKey; - }, - ); + return $this->droppedForeignKeys; + } + + /** @return array */ + public function getDroppedForeignKeyConstraintNames(): array + { + $names = []; + foreach ($this->droppedForeignKeys as $constraint) { + $name = $constraint->getObjectName(); + + if ($name === null) { + throw InvalidState::tableDiffContainsUnnamedDroppedForeignKeyConstraints(); + } + + $names[] = $name; + } + + return $names; } /** @@ -348,14 +271,13 @@ public function isEmpty(): bool { return count($this->addedColumns) === 0 && count($this->changedColumns) === 0 - && count($this->removedColumns) === 0 - && count($this->renamedColumns) === 0 + && count($this->droppedColumns) === 0 && count($this->addedIndexes) === 0 - && count($this->changedIndexes) === 0 - && count($this->removedIndexes) === 0 + && count($this->modifiedIndexes) === 0 + && count($this->droppedIndexes) === 0 && count($this->renamedIndexes) === 0 && count($this->addedForeignKeys) === 0 - && count($this->changedForeignKeys) === 0 - && count($this->removedForeignKeys) === 0; + && count($this->modifiedForeignKeys) === 0 + && count($this->droppedForeignKeys) === 0; } } diff --git a/doctrine/dbal/src/Schema/TableEditor.php b/doctrine/dbal/src/Schema/TableEditor.php new file mode 100644 index 000000000..bc921a7b4 --- /dev/null +++ b/doctrine/dbal/src/Schema/TableEditor.php @@ -0,0 +1,559 @@ + */ + private readonly UnqualifiedNamedObjectSet $columns; + + /** @var UnqualifiedNamedObjectSet */ + private UnqualifiedNamedObjectSet $indexes; + + private ?PrimaryKeyConstraint $primaryKeyConstraint = null; + + /** @var OptionallyUnqualifiedNamedObjectSet */ + private readonly OptionallyUnqualifiedNamedObjectSet $uniqueConstraints; + + /** @var OptionallyUnqualifiedNamedObjectSet */ + private readonly OptionallyUnqualifiedNamedObjectSet $foreignKeyConstraints; + + /** @var array */ + private array $options = []; + + private string $comment = ''; + + private ?TableConfiguration $configuration = null; + + /** @internal Use {@link Table::editor()} or {@link Table::edit()} to create an instance */ + public function __construct() + { + /** @var UnqualifiedNamedObjectSet $columns */ + $columns = new UnqualifiedNamedObjectSet(); + $this->columns = $columns; + + /** @var UnqualifiedNamedObjectSet $indexes */ + $indexes = new UnqualifiedNamedObjectSet(); + $this->indexes = $indexes; + + /** @var OptionallyUnqualifiedNamedObjectSet $uniqueConstraints */ + $uniqueConstraints = new OptionallyUnqualifiedNamedObjectSet(); + $this->uniqueConstraints = $uniqueConstraints; + + /** @var OptionallyUnqualifiedNamedObjectSet $foreignKeyConstraints */ + $foreignKeyConstraints = new OptionallyUnqualifiedNamedObjectSet(); + $this->foreignKeyConstraints = $foreignKeyConstraints; + } + + public function setName(OptionallyQualifiedName $name): self + { + $this->name = $name; + + return $this; + } + + /** + * @param non-empty-string $unqualifiedName + * @param ?non-empty-string $qualifier + */ + public function setUnquotedName(string $unqualifiedName, ?string $qualifier = null): self + { + $this->name = OptionallyQualifiedName::unquoted($unqualifiedName, $qualifier); + + return $this; + } + + /** + * @param non-empty-string $unqualifiedName + * @param ?non-empty-string $qualifier + */ + public function setQuotedName(string $unqualifiedName, ?string $qualifier = null): self + { + $this->name = OptionallyQualifiedName::quoted($unqualifiedName, $qualifier); + + return $this; + } + + public function setColumns(Column $firstColumn, Column ...$otherColumns): self + { + $this->columns->clear(); + + foreach ([$firstColumn, ...$otherColumns] as $column) { + $this->addColumn($column); + } + + return $this; + } + + public function addColumn(Column $column): self + { + try { + $this->columns->add($column); + } catch (ObjectAlreadyExists $e) { + throw InvalidTableModification::columnAlreadyExists($this->name, $e); + } + + return $this; + } + + /** @param callable(ColumnEditor): void $modification */ + public function modifyColumn(UnqualifiedName $columnName, callable $modification): self + { + try { + $this->columns->modify($columnName, static function (Column $column) use ($modification): Column { + $editor = $column->edit(); + + $modification($editor); + + return $editor->create(); + }); + } catch (ObjectDoesNotExist $e) { + throw InvalidTableModification::columnDoesNotExist($this->name, $e); + } catch (ObjectAlreadyExists $e) { + throw InvalidTableModification::columnAlreadyExists($this->name, $e); + } + + return $this; + } + + /** + * @param non-empty-string $columnName + * @param callable(ColumnEditor): void $modification + */ + public function modifyColumnByUnquotedName(string $columnName, callable $modification): self + { + return $this->modifyColumn(UnqualifiedName::unquoted($columnName), $modification); + } + + public function renameColumn(UnqualifiedName $oldColumnName, UnqualifiedName $newColumnName): self + { + $this->modifyColumn($oldColumnName, static function (ColumnEditor $editor) use ($newColumnName): void { + $editor->setName($newColumnName); + }); + + $this->renameColumnInIndexes($oldColumnName, $newColumnName); + $this->renameColumnInPrimaryKeyConstraint($oldColumnName, $newColumnName); + $this->renameColumnInForeignKeyConstraints($oldColumnName, $newColumnName); + $this->renameColumnInUniqueConstraints($oldColumnName, $newColumnName); + + return $this; + } + + private function renameColumnInIndexes(UnqualifiedName $oldColumnName, UnqualifiedName $newColumnName): void + { + foreach ($this->indexes as $index) { + $modified = false; + $columns = []; + + foreach ($index->getIndexedColumns() as $column) { + $columnName = $column->getColumnName(); + if ($this->namesEqual($columnName, $oldColumnName)) { + $columns[] = new Index\IndexedColumn($newColumnName, $column->getLength()); + $modified = true; + } else { + $columns[] = $column; + } + } + + if (! $modified) { + continue; + } + + $this->indexes->modify($index->getObjectName(), static function (Index $index) use ($columns): Index { + return $index->edit() + ->setColumns(...$columns) + ->create(); + }); + } + } + + private function renameColumnInPrimaryKeyConstraint( + UnqualifiedName $oldColumnName, + UnqualifiedName $newColumnName, + ): void { + if ($this->primaryKeyConstraint === null) { + return; + } + + $modified = false; + $columnNames = []; + + foreach ($this->primaryKeyConstraint->getColumnNames() as $columnName) { + if ($this->namesEqual($columnName, $oldColumnName)) { + $columnNames[] = $newColumnName; + $modified = true; + } else { + $columnNames[] = $columnName; + } + } + + if (! $modified) { + return; + } + + $this->primaryKeyConstraint = $this->primaryKeyConstraint->edit() + ->setColumnNames(...$columnNames) + ->create(); + } + + private function renameColumnInUniqueConstraints( + UnqualifiedName $oldColumnName, + UnqualifiedName $newColumnName, + ): void { + $this->renameColumnInConstraints( + $this->uniqueConstraints, + $oldColumnName, + $newColumnName, + static fn (UniqueConstraint $constraint): array => $constraint->getColumnNames(), + static function (UniqueConstraint $constraint, array $columnNames): UniqueConstraint { + return $constraint->edit() + ->setColumnNames(...$columnNames) + ->create(); + }, + ); + } + + private function renameColumnInForeignKeyConstraints( + UnqualifiedName $oldColumnName, + UnqualifiedName $newColumnName, + ): void { + $this->renameColumnInConstraints( + $this->foreignKeyConstraints, + $oldColumnName, + $newColumnName, + static fn (ForeignKeyConstraint $constraint): array => $constraint->getReferencingColumnNames(), + static function (ForeignKeyConstraint $constraint, array $columnNames): ForeignKeyConstraint { + return $constraint->edit() + ->setReferencingColumnNames(...$columnNames) + ->create(); + }, + ); + } + + /** + * Generic method to rename a column in constraints + * + * @param OptionallyUnqualifiedNamedObjectSet $collection + * @param callable(T): list $getColumnNames + * @param callable(T, list): T $modify + * + * @template T of OptionallyNamedObject + */ + private function renameColumnInConstraints( + OptionallyUnqualifiedNamedObjectSet $collection, + UnqualifiedName $oldColumnName, + UnqualifiedName $newColumnName, + callable $getColumnNames, + callable $modify, + ): void { + $constraints = []; + $anyModified = false; + + foreach ($collection as $constraint) { + $newColumnNames = []; + $modified = false; + + foreach ($getColumnNames($constraint) as $columnName) { + if ($this->namesEqual($columnName, $oldColumnName)) { + $newColumnNames[] = $newColumnName; + $modified = true; + } else { + $newColumnNames[] = $columnName; + } + } + + if ($modified) { + $constraint = $modify($constraint, $newColumnNames); + $anyModified = true; + } + + $constraints[] = $constraint; + } + + if (! $anyModified) { + return; + } + + $collection->clear(); + + foreach ($constraints as $constraint) { + $collection->add($constraint); + } + } + + /** + * @param non-empty-string $oldColumnName + * @param non-empty-string $newColumnName + */ + public function renameColumnByUnquotedName(string $oldColumnName, string $newColumnName): self + { + return $this->renameColumn( + UnqualifiedName::unquoted($oldColumnName), + UnqualifiedName::unquoted($newColumnName), + ); + } + + public function dropColumn(UnqualifiedName $columnName): self + { + try { + $this->columns->remove($columnName); + } catch (ObjectDoesNotExist $e) { + throw InvalidTableModification::columnDoesNotExist($this->name, $e); + } + + return $this; + } + + /** @param non-empty-string $columnName */ + public function dropColumnByUnquotedName(string $columnName): self + { + return $this->dropColumn(UnqualifiedName::unquoted($columnName)); + } + + public function setIndexes(Index ...$indexes): self + { + $this->indexes->clear(); + + foreach ($indexes as $index) { + $this->addIndex($index); + } + + return $this; + } + + public function addIndex(Index $index): self + { + try { + $this->indexes->add($index); + } catch (ObjectAlreadyExists $e) { + throw InvalidTableModification::indexAlreadyExists($this->name, $e); + } + + return $this; + } + + public function renameIndex(UnqualifiedName $oldIndexName, UnqualifiedName $newIndexName): self + { + try { + $this->indexes->modify($oldIndexName, static function (Index $index) use ($newIndexName): Index { + return $index->edit() + ->setName($newIndexName) + ->create(); + }); + } catch (ObjectDoesNotExist $e) { + throw InvalidTableModification::indexDoesNotExist($this->name, $e); + } catch (ObjectAlreadyExists $e) { + throw InvalidTableModification::indexAlreadyExists($this->name, $e); + } + + return $this; + } + + /** + * @param non-empty-string $oldIndexName + * @param non-empty-string $newIndexName + */ + public function renameIndexByUnquotedName(string $oldIndexName, string $newIndexName): self + { + return $this->renameIndex( + UnqualifiedName::unquoted($oldIndexName), + UnqualifiedName::unquoted($newIndexName), + ); + } + + public function dropIndex(UnqualifiedName $indexName): self + { + try { + $this->indexes->remove($indexName); + } catch (ObjectDoesNotExist $e) { + throw InvalidTableModification::indexDoesNotExist($this->name, $e); + } + + return $this; + } + + /** @param non-empty-string $indexName */ + public function dropIndexByUnquotedName(string $indexName): self + { + return $this->dropIndex(UnqualifiedName::unquoted($indexName)); + } + + public function setPrimaryKeyConstraint(?PrimaryKeyConstraint $primaryKeyConstraint): self + { + $this->primaryKeyConstraint = $primaryKeyConstraint; + + foreach ($this->indexes->toList() as $index) { + if ($index->isPrimary()) { + $this->indexes->remove($index->getObjectName()); + } + } + + return $this; + } + + public function addPrimaryKeyConstraint(PrimaryKeyConstraint $primaryKeyConstraint): self + { + if ($this->primaryKeyConstraint !== null) { + throw InvalidTableModification::primaryKeyConstraintAlreadyExists($this->name); + } + + return $this->setPrimaryKeyConstraint($primaryKeyConstraint); + } + + public function dropPrimaryKeyConstraint(): self + { + if ($this->primaryKeyConstraint === null) { + throw InvalidTableModification::primaryKeyConstraintDoesNotExist($this->name); + } + + return $this->setPrimaryKeyConstraint(null); + } + + public function setUniqueConstraints(UniqueConstraint ...$uniqueConstraints): self + { + $this->uniqueConstraints->clear(); + + foreach ($uniqueConstraints as $uniqueConstraint) { + $this->addUniqueConstraint($uniqueConstraint); + } + + return $this; + } + + public function addUniqueConstraint(UniqueConstraint $uniqueConstraint): self + { + try { + $this->uniqueConstraints->add($uniqueConstraint); + } catch (ObjectAlreadyExists $e) { + throw InvalidTableModification::uniqueConstraintAlreadyExists($this->name, $e); + } + + return $this; + } + + public function dropUniqueConstraint(UnqualifiedName $constraintName): self + { + try { + $this->uniqueConstraints->remove($constraintName); + } catch (ObjectDoesNotExist $e) { + throw InvalidTableModification::uniqueConstraintDoesNotExist($this->name, $e); + } + + return $this; + } + + /** @param non-empty-string $constraintName */ + public function dropUniqueConstraintByUnquotedName(string $constraintName): self + { + return $this->dropUniqueConstraint(UnqualifiedName::unquoted($constraintName)); + } + + public function setForeignKeyConstraints(ForeignKeyConstraint ...$foreignKeyConstraints): self + { + $this->foreignKeyConstraints->clear(); + + foreach ($foreignKeyConstraints as $foreignKeyConstraint) { + $this->addForeignKeyConstraint($foreignKeyConstraint); + } + + return $this; + } + + public function addForeignKeyConstraint(ForeignKeyConstraint $foreignKeyConstraint): self + { + try { + $this->foreignKeyConstraints->add($foreignKeyConstraint); + } catch (ObjectAlreadyExists $e) { + throw InvalidTableModification::foreignKeyConstraintAlreadyExists($this->name, $e); + } + + return $this; + } + + public function dropForeignKeyConstraint(UnqualifiedName $constraintName): self + { + try { + $this->foreignKeyConstraints->remove($constraintName); + } catch (ObjectDoesNotExist $e) { + throw InvalidTableModification::foreignKeyConstraintDoesNotExist($this->name, $e); + } + + return $this; + } + + /** @param non-empty-string $constraintName */ + public function dropForeignKeyConstraintByUnquotedName(string $constraintName): self + { + return $this->dropForeignKeyConstraint(UnqualifiedName::unquoted($constraintName)); + } + + private function namesEqual(UnqualifiedName $name1, UnqualifiedName $name2): bool + { + return strcasecmp($name1->getIdentifier()->getValue(), $name2->getIdentifier()->getValue()) === 0; + } + + public function setComment(string $comment): self + { + $this->comment = $comment; + + return $this; + } + + /** @param array $options */ + public function setOptions(array $options): self + { + $this->options = $options; + + return $this; + } + + public function setConfiguration(TableConfiguration $configuration): self + { + $this->configuration = $configuration; + + return $this; + } + + public function create(): Table + { + if ($this->name === null) { + throw InvalidTableDefinition::nameNotSet(); + } + + if ($this->columns->isEmpty()) { + throw InvalidTableDefinition::columnsNotSet($this->name); + } + + $options = $this->options; + + if ($this->comment !== '') { + $options['comment'] = $this->comment; + } + + return new Table( + $this->name->toString(), + $this->columns->toList(), + $this->indexes->toList(), + $this->uniqueConstraints->toList(), + $this->foreignKeyConstraints->toList(), + $options, + $this->configuration, + $this->primaryKeyConstraint, + ); + } +} diff --git a/doctrine/dbal/src/Schema/UniqueConstraint.php b/doctrine/dbal/src/Schema/UniqueConstraint.php index f353f303a..6116e0b98 100644 --- a/doctrine/dbal/src/Schema/UniqueConstraint.php +++ b/doctrine/dbal/src/Schema/UniqueConstraint.php @@ -1,51 +1,89 @@ + * @final This class will be made final in DBAL 5.0. */ -class UniqueConstraint extends AbstractAsset implements Constraint +class UniqueConstraint extends AbstractOptionallyNamedObject { /** * Asset identifier instances of the column names the unique constraint is associated with. - * array($columnName => Identifier) * - * @var Identifier[] + * @deprecated + * + * @var array */ - protected $columns = []; + protected array $columns = []; /** - * Platform specific flags. - * array($flagName => true) + * Platform specific flags + * + * @deprecated * - * @var true[] + * @var array */ - protected $flags = []; + protected array $flags = []; /** - * Platform specific options. + * Names of the columns covered by the unique constraint. * - * @var mixed[] + * @var list */ - private array $options; + private array $columnNames = []; + + private bool $failedToParseColumnNames = false; /** - * @param string[] $columns - * @param string[] $flags - * @param mixed[] $options + * @internal Use {@link UniqueConstraint::editor()} to instantiate an editor and + * {@link UniqueConstraintEditor::create()} to create a unique constraint. + * + * @param non-empty-list $columns + * @param array $flags + * @param array $options */ - public function __construct(string $name, array $columns, array $flags = [], array $options = []) - { - $this->_setName($name); + public function __construct( + string $name, + array $columns, + array $flags = [], + private readonly array $options = [], + ) { + if (count($columns) < 1) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6685', + 'Instantiation of a unique constraint without columns is deprecated.', + ); + } + + if (count($options) > 0) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6685', + 'Using %s options is deprecated.', + self::class, + ); + } - $this->options = $options; + parent::__construct($name); foreach ($columns as $column) { $this->addColumn($column); @@ -56,19 +94,69 @@ public function __construct(string $name, array $columns, array $flags = [], arr } } + protected function getNameParser(): UnqualifiedNameParser + { + return Parsers::getUnqualifiedNameParser(); + } + + /** + * Returns the names of the columns the constraint is associated with. + * + * @return non-empty-list + */ + public function getColumnNames(): array + { + if ($this->failedToParseColumnNames) { + throw InvalidState::uniqueConstraintHasInvalidColumnNames($this->getName()); + } + + if (count($this->columnNames) < 1) { + throw InvalidState::uniqueConstraintHasEmptyColumnNames($this->getName()); + } + + return $this->columnNames; + } + /** - * {@inheritDoc} + * @deprecated Use {@see getColumnNames()} instead. + * + * @return non-empty-list */ - public function getColumns() + public function getColumns(): array { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6685', + '%s is deprecated. Use getColumnNames() instead.', + __METHOD__, + ); + + /** @phpstan-ignore return.type */ return array_keys($this->columns); } /** - * {@inheritDoc} + * @deprecated Use {@see getColumnNames()} and {@see UnqualifiedName::toSQL()} instead. + * + * But only if they were defined with one or a column name + * is a keyword reserved by the platform. + * Otherwise, the plain unquoted value as inserted is returned. + * + * @param AbstractPlatform $platform The platform to use for quotation. + * + * @return list + * + * Returns the quoted representation of the column names the constraint is associated with. */ - public function getQuotedColumns(AbstractPlatform $platform) + public function getQuotedColumns(AbstractPlatform $platform): array { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6685', + '%s is deprecated. Use getColumnNames() and UnqualifiedName::toSQL() instead.', + __METHOD__, + ); + $columns = []; foreach ($this->columns as $column) { @@ -78,31 +166,60 @@ public function getQuotedColumns(AbstractPlatform $platform) return $columns; } - /** @return string[] */ + /** + * @deprecated Use {@see getColumnNames()} instead. + * + * @return non-empty-list + */ public function getUnquotedColumns(): array { - return array_map([$this, 'trimQuotes'], $this->getColumns()); + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6685', + '%s is deprecated. Use getColumnNames() instead.', + __METHOD__, + ); + + return array_map($this->trimQuotes(...), $this->getColumns()); } /** + * @deprecated Use {@see isClustered()} instead. + * * Returns platform specific flags for unique constraint. * - * @return string[] + * @return array */ public function getFlags(): array { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6685', + '%s is deprecated. Use isClustered() instead.', + __METHOD__, + ); + return array_keys($this->flags); } /** * Adds flag for a unique constraint that translates to platform specific handling. * + * @deprecated Use {@see UniqueConstraintEditor::setIsClustered()} instead. + * * @return $this * * @example $uniqueConstraint->addFlag('CLUSTERED') */ - public function addFlag(string $flag): UniqueConstraint + public function addFlag(string $flag): self { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6685', + '%s is deprecated.', + __METHOD__, + ); + $this->flags[strtolower($flag)] = true; return $this; @@ -110,45 +227,133 @@ public function addFlag(string $flag): UniqueConstraint /** * Does this unique constraint have a specific flag? + * + * @deprecated Use {@see isClustered()} instead. */ public function hasFlag(string $flag): bool { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6685', + '%s is deprecated. Use isClustered() instead.', + __METHOD__, + ); + return isset($this->flags[strtolower($flag)]); } + /** + * Returns whether the unique constraint is clustered. + */ + public function isClustered(): bool + { + return $this->hasFlag('clustered'); + } + /** * Removes a flag. + * + * @deprecated Use {@see UniqueConstraintEditor::setIsClustered()} instead. */ public function removeFlag(string $flag): void { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6685', + '%s is deprecated.', + __METHOD__, + ); + unset($this->flags[strtolower($flag)]); } - /** - * Does this unique constraint have a specific option? - */ + /** @deprecated */ public function hasOption(string $name): bool { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6685', + '%s is deprecated.', + __METHOD__, + ); + return isset($this->options[strtolower($name)]); } - /** @return mixed */ - public function getOption(string $name) + /** @deprecated */ + public function getOption(string $name): mixed { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6685', + '%s is deprecated.', + __METHOD__, + ); + return $this->options[strtolower($name)]; } - /** @return mixed[] */ + /** + * @deprecated + * + * @return array + */ public function getOptions(): array { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6685', + '%s is deprecated.', + __METHOD__, + ); + return $this->options; } - /** - * Adds a new column to the unique constraint. - */ + /** @deprecated */ protected function addColumn(string $column): void { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6685', + '%s is deprecated.', + __METHOD__, + ); + $this->columns[$column] = new Identifier($column); + + $parser = Parsers::getUnqualifiedNameParser(); + + try { + $this->columnNames[] = $parser->parse($column); + } catch (Throwable $e) { + $this->failedToParseColumnNames = true; + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6685', + 'Unable to parse column name: %s.', + $e->getMessage(), + ); + } + } + + /** + * Instantiates a new unique constraint editor. + */ + public static function editor(): UniqueConstraintEditor + { + return new UniqueConstraintEditor(); + } + + /** + * Instantiates a new unique constraint editor and initializes it with the constraint's properties. + */ + public function edit(): UniqueConstraintEditor + { + return self::editor() + ->setName($this->getObjectName()) + ->setColumnNames(...$this->getColumnNames()) + ->setIsClustered($this->isClustered()); } } diff --git a/doctrine/dbal/src/Schema/UniqueConstraintEditor.php b/doctrine/dbal/src/Schema/UniqueConstraintEditor.php new file mode 100644 index 000000000..d65ab2a73 --- /dev/null +++ b/doctrine/dbal/src/Schema/UniqueConstraintEditor.php @@ -0,0 +1,110 @@ + */ + private array $columnNames = []; + + private bool $isClustered = false; + + /** @internal Use {@link UniqueConstraint::editor()} or {@link UniqueConstraint::edit()} to create an instance */ + public function __construct() + { + } + + public function setName(?UnqualifiedName $name): self + { + $this->name = $name; + + return $this; + } + + /** @param non-empty-string $name */ + public function setUnquotedName(string $name): self + { + $this->name = UnqualifiedName::unquoted($name); + + return $this; + } + + /** @param non-empty-string $name */ + public function setQuotedName(string $name): self + { + $this->name = UnqualifiedName::quoted($name); + + return $this; + } + + public function setColumnNames(UnqualifiedName $firstColumnName, UnqualifiedName ...$otherColumnNames): self + { + $this->columnNames = array_merge([$firstColumnName], array_values($otherColumnNames)); + + return $this; + } + + /** + * @param non-empty-string $firstColumnName + * @param non-empty-string ...$otherColumnNames + */ + public function setUnquotedColumnNames( + string $firstColumnName, + string ...$otherColumnNames, + ): self { + $this->columnNames = array_map( + static fn (string $name): UnqualifiedName => UnqualifiedName::unquoted($name), + array_merge([$firstColumnName], array_values($otherColumnNames)), + ); + + return $this; + } + + /** + * @param non-empty-string $firstColumnName + * @param non-empty-string ...$otherColumnNames + */ + public function setQuotedColumnNames( + string $firstColumnName, + string ...$otherColumnNames, + ): self { + $this->columnNames = array_map( + static fn (string $name): UnqualifiedName => UnqualifiedName::quoted($name), + array_merge([$firstColumnName], array_values($otherColumnNames)), + ); + + return $this; + } + + public function setIsClustered(bool $isClustered): self + { + $this->isClustered = $isClustered; + + return $this; + } + + public function create(): UniqueConstraint + { + if (count($this->columnNames) < 1) { + throw InvalidUniqueConstraintDefinition::columnNamesAreNotSet($this->name); + } + + return new UniqueConstraint( + $this->name?->toString() ?? '', + array_map(static fn (UnqualifiedName $columnName) => $columnName->toString(), $this->columnNames), + $this->isClustered ? ['clustered'] : [], + ); + } +} diff --git a/doctrine/dbal/src/Schema/View.php b/doctrine/dbal/src/Schema/View.php index b19f2ad1f..f5f650125 100644 --- a/doctrine/dbal/src/Schema/View.php +++ b/doctrine/dbal/src/Schema/View.php @@ -1,28 +1,52 @@ */ -class View extends AbstractAsset +class View extends AbstractNamedObject { - /** @var string */ - private $sql; + /** @internal Use {@link View::editor()} to instantiate an editor and {@link ViewEditor::create()} to create a view. */ + public function __construct(string $name, private readonly string $sql) + { + parent::__construct($name); + } + + protected function getNameParser(): OptionallyQualifiedNameParser + { + return Parsers::getOptionallyQualifiedNameParser(); + } + + public function getSql(): string + { + return $this->sql; + } /** - * @param string $name - * @param string $sql + * Instantiates a new view editor. */ - public function __construct($name, $sql) + public static function editor(): ViewEditor { - $this->_setName($name); - $this->sql = $sql; + return new ViewEditor(); } - /** @return string */ - public function getSql() + /** + * Instantiates a new view editor and initializes it with the view's properties. + */ + public function edit(): ViewEditor { - return $this->sql; + return self::editor() + ->setName($this->getObjectName()) + ->setSQL($this->sql); } } diff --git a/doctrine/dbal/src/Schema/ViewEditor.php b/doctrine/dbal/src/Schema/ViewEditor.php new file mode 100644 index 000000000..3952a4fd6 --- /dev/null +++ b/doctrine/dbal/src/Schema/ViewEditor.php @@ -0,0 +1,72 @@ +name = $name; + + return $this; + } + + /** + * @param non-empty-string $unqualifiedName + * @param ?non-empty-string $qualifier + */ + public function setUnquotedName(string $unqualifiedName, ?string $qualifier = null): self + { + $this->name = OptionallyQualifiedName::unquoted($unqualifiedName, $qualifier); + + return $this; + } + + /** + * @param non-empty-string $unqualifiedName + * @param ?non-empty-string $qualifier + */ + public function setQuotedName(string $unqualifiedName, ?string $qualifier = null): self + { + $this->name = OptionallyQualifiedName::quoted($unqualifiedName, $qualifier); + + return $this; + } + + public function setSQL(string $sql): self + { + $this->sql = $sql; + + return $this; + } + + public function create(): View + { + if ($this->name === null) { + throw InvalidViewDefinition::nameNotSet(); + } + + if ($this->sql === null) { + throw InvalidViewDefinition::sqlNotSet($this->name); + } + + return new View( + $this->name->toString(), + $this->sql, + ); + } +} diff --git a/doctrine/dbal/src/Schema/Visitor/AbstractVisitor.php b/doctrine/dbal/src/Schema/Visitor/AbstractVisitor.php deleted file mode 100644 index f8f3b5825..000000000 --- a/doctrine/dbal/src/Schema/Visitor/AbstractVisitor.php +++ /dev/null @@ -1,49 +0,0 @@ -platform = $platform; - } - - /** - * {@inheritDoc} - */ - public function acceptNamespace($namespaceName) - { - if (! $this->platform->supportsSchemas()) { - return; - } - - $this->createNamespaceQueries[] = $this->platform->getCreateSchemaSQL($namespaceName); - } - - /** - * {@inheritDoc} - */ - public function acceptTable(Table $table) - { - $this->createTableQueries = array_merge($this->createTableQueries, $this->platform->getCreateTableSQL($table)); - } - - /** - * {@inheritDoc} - */ - public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) - { - if (! $this->platform->supportsForeignKeyConstraints()) { - return; - } - - $this->createFkConstraintQueries[] = $this->platform->getCreateForeignKeySQL($fkConstraint, $localTable); - } - - /** - * {@inheritDoc} - */ - public function acceptSequence(Sequence $sequence) - { - $this->createSequenceQueries[] = $this->platform->getCreateSequenceSQL($sequence); - } - - /** @return void */ - public function resetQueries() - { - $this->createNamespaceQueries = []; - $this->createTableQueries = []; - $this->createSequenceQueries = []; - $this->createFkConstraintQueries = []; - } - - /** - * Gets all queries collected so far. - * - * @return string[] - */ - public function getQueries() - { - return array_merge( - $this->createNamespaceQueries, - $this->createSequenceQueries, - $this->createTableQueries, - $this->createFkConstraintQueries, - ); - } -} diff --git a/doctrine/dbal/src/Schema/Visitor/DropSchemaSqlCollector.php b/doctrine/dbal/src/Schema/Visitor/DropSchemaSqlCollector.php deleted file mode 100644 index d746b2aee..000000000 --- a/doctrine/dbal/src/Schema/Visitor/DropSchemaSqlCollector.php +++ /dev/null @@ -1,107 +0,0 @@ -platform = $platform; - $this->initializeQueries(); - } - - /** - * {@inheritDoc} - */ - public function acceptTable(Table $table) - { - $this->tables->offsetSet($table); - } - - /** - * {@inheritDoc} - */ - public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) - { - if (strlen($fkConstraint->getName()) === 0) { - throw SchemaException::namedForeignKeyRequired($localTable, $fkConstraint); - } - - $this->constraints->offsetSet($fkConstraint, $localTable); - } - - /** - * {@inheritDoc} - */ - public function acceptSequence(Sequence $sequence) - { - $this->sequences->offsetSet($sequence); - } - - /** @return void */ - public function clearQueries() - { - $this->initializeQueries(); - } - - /** @return string[] */ - public function getQueries() - { - $sql = []; - - foreach ($this->constraints as $fkConstraint) { - assert($fkConstraint instanceof ForeignKeyConstraint); - $localTable = $this->constraints[$fkConstraint]; - $sql[] = $this->platform->getDropForeignKeySQL( - $fkConstraint->getQuotedName($this->platform), - $localTable->getQuotedName($this->platform), - ); - } - - foreach ($this->sequences as $sequence) { - assert($sequence instanceof Sequence); - $sql[] = $this->platform->getDropSequenceSQL($sequence->getQuotedName($this->platform)); - } - - foreach ($this->tables as $table) { - assert($table instanceof Table); - $sql[] = $this->platform->getDropTableSQL($table->getQuotedName($this->platform)); - } - - return $sql; - } - - private function initializeQueries(): void - { - $this->constraints = new SplObjectStorage(); - $this->sequences = new SplObjectStorage(); - $this->tables = new SplObjectStorage(); - } -} diff --git a/doctrine/dbal/src/Schema/Visitor/Graphviz.php b/doctrine/dbal/src/Schema/Visitor/Graphviz.php deleted file mode 100644 index 5eff0d945..000000000 --- a/doctrine/dbal/src/Schema/Visitor/Graphviz.php +++ /dev/null @@ -1,164 +0,0 @@ -output .= $this->createNodeRelation( - $fkConstraint->getLocalTableName() . ':col' . current($fkConstraint->getLocalColumns()) . ':se', - $fkConstraint->getForeignTableName() . ':col' . current($fkConstraint->getForeignColumns()) . ':se', - [ - 'dir' => 'back', - 'arrowtail' => 'dot', - 'arrowhead' => 'normal', - ], - ); - } - - /** - * {@inheritDoc} - */ - public function acceptSchema(Schema $schema) - { - $this->output = 'digraph "' . $schema->getName() . '" {' . "\n"; - $this->output .= 'splines = true;' . "\n"; - $this->output .= 'overlap = false;' . "\n"; - $this->output .= 'outputorder=edgesfirst;' . "\n"; - $this->output .= 'mindist = 0.6;' . "\n"; - $this->output .= 'sep = .2;' . "\n"; - } - - /** - * {@inheritDoc} - */ - public function acceptTable(Table $table) - { - $this->output .= $this->createNode( - $table->getName(), - [ - 'label' => $this->createTableLabel($table), - 'shape' => 'plaintext', - ], - ); - } - - private function createTableLabel(Table $table): string - { - // Start the table - $label = '<
'; - - // The title - $label .= ''; - - // The attributes block - foreach ($table->getColumns() as $column) { - $columnLabel = $column->getName(); - - $label .= '' - . '' - . '' - . ''; - } - - // End the table - $label .= '
' - . '' . $table->getName() . '
' - . '' . $columnLabel . '' - . '' - . '' - . strtolower($column->getType()->getName()) - . '' - . ''; - - $primaryKey = $table->getPrimaryKey(); - - if ($primaryKey !== null && in_array($column->getName(), $primaryKey->getColumns(), true)) { - $label .= "\xe2\x9c\xb7"; - } - - $label .= '
>'; - - return $label; - } - - /** - * @param string $name - * @param string[] $options - */ - private function createNode($name, $options): string - { - $node = $name . ' ['; - foreach ($options as $key => $value) { - $node .= $key . '=' . $value . ' '; - } - - $node .= "]\n"; - - return $node; - } - - /** - * @param string $node1 - * @param string $node2 - * @param string[] $options - */ - private function createNodeRelation($node1, $node2, $options): string - { - $relation = $node1 . ' -> ' . $node2 . ' ['; - foreach ($options as $key => $value) { - $relation .= $key . '=' . $value . ' '; - } - - $relation .= "]\n"; - - return $relation; - } - - /** - * Get Graphviz Output - * - * @return string - */ - public function getOutput() - { - return $this->output . '}'; - } - - /** - * Writes dot language output to a file. This should usually be a *.dot file. - * - * You have to convert the output into a viewable format. For example use "neato" on linux systems - * and execute: - * - * neato -Tpng -o er.png er.dot - * - * @param string $filename - * - * @return void - */ - public function write($filename) - { - file_put_contents($filename, $this->getOutput()); - } -} diff --git a/doctrine/dbal/src/Schema/Visitor/NamespaceVisitor.php b/doctrine/dbal/src/Schema/Visitor/NamespaceVisitor.php deleted file mode 100644 index 443824342..000000000 --- a/doctrine/dbal/src/Schema/Visitor/NamespaceVisitor.php +++ /dev/null @@ -1,20 +0,0 @@ -schema = $schema; - } - - /** - * {@inheritDoc} - */ - public function acceptTable(Table $table) - { - if ($this->schema === null) { - return; - } - - if ($table->isInDefaultNamespace($this->schema->getName())) { - return; - } - - $this->schema->dropTable($table->getName()); - } - - /** - * {@inheritDoc} - */ - public function acceptSequence(Sequence $sequence) - { - if ($this->schema === null) { - return; - } - - if ($sequence->isInDefaultNamespace($this->schema->getName())) { - return; - } - - $this->schema->dropSequence($sequence->getName()); - } - - /** - * {@inheritDoc} - */ - public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) - { - if ($this->schema === null) { - return; - } - - // The table may already be deleted in a previous - // RemoveNamespacedAssets#acceptTable call. Removing Foreign keys that - // point to nowhere. - if (! $this->schema->hasTable($fkConstraint->getForeignTableName())) { - $localTable->removeForeignKey($fkConstraint->getName()); - - return; - } - - $foreignTable = $this->schema->getTable($fkConstraint->getForeignTableName()); - if ($foreignTable->isInDefaultNamespace($this->schema->getName())) { - return; - } - - $localTable->removeForeignKey($fkConstraint->getName()); - } -} diff --git a/doctrine/dbal/src/Schema/Visitor/Visitor.php b/doctrine/dbal/src/Schema/Visitor/Visitor.php deleted file mode 100644 index 8b34864c1..000000000 --- a/doctrine/dbal/src/Schema/Visitor/Visitor.php +++ /dev/null @@ -1,45 +0,0 @@ -Statement for the given SQL and Connection. * * @internal The statement can be only instantiated by {@see Connection}. * - * @param Connection $conn The connection for handling statement errors. - * @param Driver\Statement $statement The underlying driver-level statement. - * @param string $sql The SQL of the statement. + * @param Connection $conn The connection for handling statement errors. + * @param Driver\Statement $stmt The underlying driver-level statement. + * @param string $sql The SQL of the statement. * * @throws Exception */ - public function __construct(Connection $conn, Driver\Statement $statement, string $sql) - { - $this->conn = $conn; - $this->stmt = $statement; - $this->sql = $sql; + public function __construct( + protected Connection $conn, + protected Driver\Statement $stmt, + protected string $sql, + ) { $this->platform = $conn->getDatabasePlatform(); } @@ -83,178 +60,83 @@ public function __construct(Connection $conn, Driver\Statement $statement, strin * type and the value undergoes the conversion routines of the mapping type before * being bound. * - * @param string|int $param The name or position of the parameter. - * @param mixed $value The value of the parameter. - * @param mixed $type Either a PDO binding type or a DBAL mapping type name or instance. - * - * @return bool TRUE on success, FALSE on failure. + * @param string|int $param Parameter identifier. For a prepared statement using named placeholders, + * this will be a parameter name of the form :name. For a prepared statement + * using question mark placeholders, this will be the 1-indexed position + * of the parameter. + * @param mixed $value The value to bind to the parameter. + * @param ParameterType|string|Type $type Either a {@see \Doctrine\DBAL\ParameterType} or a DBAL mapping type name + * or instance. * * @throws Exception */ - public function bindValue($param, $value, $type = ParameterType::STRING) - { + public function bindValue( + string|int $param, + mixed $value, + string|ParameterType|Type $type = ParameterType::STRING, + ): void { $this->params[$param] = $value; $this->types[$param] = $type; - $bindingType = ParameterType::STRING; - - if ($type !== null) { - if (is_string($type)) { - $type = Type::getType($type); - } + if (is_string($type)) { + $type = Type::getType($type); + } + if ($type instanceof Type) { + $value = $type->convertToDatabaseValue($value, $this->platform); + $bindingType = $type->getBindingType(); + } else { $bindingType = $type; - - if ($type instanceof Type) { - $value = $type->convertToDatabaseValue($value, $this->platform); - $bindingType = $type->getBindingType(); - } } try { - return $this->stmt->bindValue($param, $value, $bindingType); + $this->stmt->bindValue($param, $value, $bindingType); } catch (Driver\Exception $e) { throw $this->conn->convertException($e); } } - /** - * Binds a parameter to a value by reference. - * - * Binding a parameter by reference does not support DBAL mapping types. - * - * @deprecated Use {@see bindValue()} instead. - * - * @param string|int $param The name or position of the parameter. - * @param mixed $variable The reference to the variable to bind. - * @param int $type The binding type. - * @param int|null $length Must be specified when using an OUT bind - * so that PHP allocates enough memory to hold the returned value. - * - * @return bool TRUE on success, FALSE on failure. - * - * @throws Exception - */ - public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null) + /** @throws Exception */ + private function execute(): Result { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5563', - '%s is deprecated. Use bindValue() instead.', - __METHOD__, - ); - - $this->params[$param] = $variable; - $this->types[$param] = $type; - - try { - if (func_num_args() > 3) { - return $this->stmt->bindParam($param, $variable, $type, $length); - } - - return $this->stmt->bindParam($param, $variable, $type); - } catch (Driver\Exception $e) { - throw $this->conn->convertException($e); - } - } - - /** - * Executes the statement with the currently bound parameters. - * - * @deprecated Statement::execute() is deprecated, use Statement::executeQuery() or executeStatement() instead - * - * @param mixed[]|null $params - * - * @throws Exception - */ - public function execute($params = null): Result - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/4580', - '%s() is deprecated, use Statement::executeQuery() or Statement::executeStatement() instead', - __METHOD__, - ); - - if ($params !== null) { - $this->params = $params; - } - - $logger = $this->conn->getConfiguration()->getSQLLogger(); - if ($logger !== null) { - $logger->startQuery($this->sql, $this->params, $this->types); - } - try { return new Result( - $this->stmt->execute($params), + $this->stmt->execute(), $this->conn, ); } catch (Driver\Exception $ex) { throw $this->conn->convertExceptionDuringQuery($ex, $this->sql, $this->params, $this->types); - } finally { - if ($logger !== null) { - $logger->stopQuery(); - } } } /** * Executes the statement with the currently bound parameters and return result. * - * @param mixed[] $params - * * @throws Exception */ - public function executeQuery(array $params = []): Result + public function executeQuery(): Result { - if (func_num_args() > 0) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5556', - 'Passing $params to Statement::executeQuery() is deprecated. Bind parameters using' - . ' Statement::bindParam() or Statement::bindValue() instead.', - ); - } - - if ($params === []) { - $params = null; // Workaround as long execute() exists and used internally. - } - - return $this->execute($params); + return $this->execute(); } /** * Executes the statement with the currently bound parameters and return affected rows. * - * @param mixed[] $params + * If the number of rows exceeds {@see PHP_INT_MAX}, it might be returned as string if the driver supports it. + * + * @return int|numeric-string * * @throws Exception */ - public function executeStatement(array $params = []): int + public function executeStatement(): int|string { - if (func_num_args() > 0) { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5556', - 'Passing $params to Statement::executeStatement() is deprecated. Bind parameters using' - . ' Statement::bindParam() or Statement::bindValue() instead.', - ); - } - - if ($params === []) { - $params = null; // Workaround as long execute() exists and used internally. - } - - return $this->execute($params)->rowCount(); + return $this->execute()->rowCount(); } /** * Gets the wrapped driver statement. - * - * @return Driver\Statement */ - public function getWrappedStatement() + public function getWrappedStatement(): Driver\Statement { return $this->stmt; } diff --git a/doctrine/dbal/src/Tools/Console/Command/CommandCompatibility.php b/doctrine/dbal/src/Tools/Console/Command/CommandCompatibility.php deleted file mode 100644 index 0ef081c74..000000000 --- a/doctrine/dbal/src/Tools/Console/Command/CommandCompatibility.php +++ /dev/null @@ -1,63 +0,0 @@ -hasReturnType()) { - /** @internal */ - trait CommandCompatibility - { - protected function configure(): void - { - $this->doConfigure(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - return $this->doExecute($input, $output); - } - } -// Symfony 7 -} elseif ((new ReflectionMethod(Command::class, 'execute'))->hasReturnType()) { - /** @internal */ - trait CommandCompatibility - { - /** @return void */ - protected function configure() - { - $this->doConfigure(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - return $this->doExecute($input, $output); - } - } -} else { - /** @internal */ - trait CommandCompatibility - { - /** @return void */ - protected function configure() - { - $this->doConfigure(); - } - - /** - * {@inheritDoc} - * - * @return int - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - return $this->doExecute($input, $output); - } - } -} diff --git a/doctrine/dbal/src/Tools/Console/Command/ReservedWordsCommand.php b/doctrine/dbal/src/Tools/Console/Command/ReservedWordsCommand.php deleted file mode 100644 index 238af5ed6..000000000 --- a/doctrine/dbal/src/Tools/Console/Command/ReservedWordsCommand.php +++ /dev/null @@ -1,221 +0,0 @@ - */ - private array $keywordLists; - - private ConnectionProvider $connectionProvider; - - public function __construct(ConnectionProvider $connectionProvider) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5431', - 'ReservedWordsCommand is deprecated. Use database documentation instead.', - ); - - parent::__construct(); - - $this->connectionProvider = $connectionProvider; - - $this->keywordLists = [ - 'db2' => new DB2Keywords(), - 'mariadb102' => new MariaDb102Keywords(), - 'mariadb117' => new MariaDb117Keywords(), - 'mysql' => new MySQLKeywords(), - 'mysql57' => new MySQL57Keywords(), - 'mysql80' => new MySQL80Keywords(), - 'mysql84' => new MySQL84Keywords(), - 'oracle' => new OracleKeywords(), - 'pgsql' => new PostgreSQL94Keywords(), - 'pgsql100' => new PostgreSQL100Keywords(), - 'sqlite' => new SQLiteKeywords(), - 'sqlserver' => new SQLServer2012Keywords(), - ]; - } - - /** - * Add or replace a keyword list. - */ - public function setKeywordList(string $name, KeywordList $keywordList): void - { - $this->keywordLists[$name] = $keywordList; - } - - /** - * If you want to add or replace a keywords list use this command. - * - * @param string $name - * @param class-string $class - * - * @return void - */ - public function setKeywordListClass($name, $class) - { - Deprecation::trigger( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/issues/4510', - 'ReservedWordsCommand::setKeywordListClass() is deprecated,' - . ' use ReservedWordsCommand::setKeywordList() instead.', - ); - - $this->keywordLists[$name] = new $class(); - } - - private function doConfigure(): void - { - $this - ->setName('dbal:reserved-words') - ->setDescription('Checks if the current database contains identifiers that are reserved.') - ->setDefinition([ - new InputOption('connection', null, InputOption::VALUE_REQUIRED, 'The named database connection'), - new InputOption( - 'list', - 'l', - InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, - 'Keyword-List name.', - ), - ]) - ->setHelp(<<<'EOT' -Checks if the current database contains tables and columns -with names that are identifiers in this dialect or in other SQL dialects. - -By default all supported platform keywords are checked: - - %command.full_name% - -If you want to check against specific dialects you can -pass them to the command: - - %command.full_name% -l mysql -l pgsql - -The following keyword lists are currently shipped with Doctrine: - - * db2 - * mariadb102 - * mariadb117 - * mysql - * mysql57 - * mysql80 - * mysql84 - * oracle - * pgsql - * pgsql100 - * sqlite - * sqlserver -EOT); - } - - /** @throws Exception */ - private function doExecute(InputInterface $input, OutputInterface $output): int - { - $output->writeln( - 'The dbal:reserved-words command is deprecated.' - . ' Use the documentation on the used database platform(s) instead.', - ); - $output->writeln(''); - - $conn = $this->getConnection($input); - - $keywordLists = $input->getOption('list'); - - if (is_string($keywordLists)) { - $keywordLists = [$keywordLists]; - } elseif (! is_array($keywordLists)) { - $keywordLists = []; - } - - if (count($keywordLists) === 0) { - $keywordLists = array_keys($this->keywordLists); - } - - $keywords = []; - foreach ($keywordLists as $keywordList) { - if (! isset($this->keywordLists[$keywordList])) { - throw new InvalidArgumentException( - "There exists no keyword list with name '" . $keywordList . "'. " . - 'Known lists: ' . implode(', ', array_keys($this->keywordLists)), - ); - } - - $keywords[] = $this->keywordLists[$keywordList]; - } - - $output->write( - 'Checking keyword violations for ' . implode(', ', $keywordLists) . '...', - true, - ); - - $schema = $conn->getSchemaManager()->introspectSchema(); - $visitor = new ReservedKeywordsValidator($keywords); - $schema->visit($visitor); - - $violations = $visitor->getViolations(); - if (count($violations) !== 0) { - $output->write( - 'There are ' . count($violations) . ' reserved keyword violations' - . ' in your database schema:', - true, - ); - - foreach ($violations as $violation) { - $output->write(' - ' . $violation, true); - } - - return 1; - } - - $output->write('No reserved keywords violations have been found!', true); - - return 0; - } - - private function getConnection(InputInterface $input): Connection - { - $connectionName = $input->getOption('connection'); - assert(is_string($connectionName) || $connectionName === null); - - if ($connectionName !== null) { - return $this->connectionProvider->getConnection($connectionName); - } - - return $this->connectionProvider->getDefaultConnection(); - } -} diff --git a/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php b/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php index 64913be67..8ec6f238d 100644 --- a/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php +++ b/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php @@ -1,5 +1,7 @@ connectionProvider = $connectionProvider; } - private function doConfigure(): void + protected function configure(): void { $this ->setName('dbal:run-sql') @@ -56,8 +52,12 @@ private function doConfigure(): void EOT); } - /** @throws Exception */ - private function doExecute(InputInterface $input, OutputInterface $output): int + /** + * {@inheritDoc} + * + * @throws Exception + */ + protected function execute(InputInterface $input, OutputInterface $output): int { $conn = $this->getConnection($input); $io = new SymfonyStyle($input, $output); @@ -65,7 +65,7 @@ private function doExecute(InputInterface $input, OutputInterface $output): int $sql = $input->getArgument('sql'); if ($sql === null) { - throw new RuntimeException("Argument 'SQL' is required in order to execute this command correctly."); + throw new RuntimeException('Argument "sql" is required in order to execute this command correctly.'); } assert(is_string($sql)); diff --git a/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php b/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php index 81ca4182a..049d658a6 100644 --- a/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php +++ b/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php @@ -1,5 +1,7 @@ connection = $connection; - $this->defaultConnectionName = $defaultConnectionName; + public function __construct( + private readonly Connection $connection, + private readonly string $defaultConnectionName = 'default', + ) { } public function getDefaultConnection(): Connection diff --git a/doctrine/dbal/src/Tools/Console/ConsoleRunner.php b/doctrine/dbal/src/Tools/Console/ConsoleRunner.php deleted file mode 100644 index e8fa3c60b..000000000 --- a/doctrine/dbal/src/Tools/Console/ConsoleRunner.php +++ /dev/null @@ -1,81 +0,0 @@ -setCatchExceptions(true); - self::addCommands($cli, $connectionProvider); - $cli->addCommands($commands); - $cli->run(); - } - - /** @return void */ - public static function addCommands(Application $cli, ConnectionProvider $connectionProvider) - { - $cli->addCommands([ - new RunSqlCommand($connectionProvider), - new ReservedWordsCommand($connectionProvider), - ]); - } - - /** - * Prints the instructions to create a configuration file - * - * @deprecated This method will be removed without replacement. - * - * @return void - */ - public static function printCliConfigTemplate() - { - echo <<<'HELP' -You are missing a "cli-config.php" or "config/cli-config.php" file in your -project, which is required to get the Doctrine-DBAL Console working. You can use the -following sample as a template: - -> */ - private array $schemeMapping; - /** @param array> $schemeMapping An array used to map DSN schemes to DBAL drivers */ - public function __construct(array $schemeMapping = []) - { - $this->schemeMapping = $schemeMapping; + public function __construct( + private readonly array $schemeMapping = [], + ) { } /** @@ -38,7 +37,7 @@ public function __construct(array $schemeMapping = []) */ public function parse( #[SensitiveParameter] - string $dsn + string $dsn, ): array { // (pdo-)?sqlite3?:///... => (pdo-)?sqlite3?://localhost/... or else the URL will be invalid $url = preg_replace('#^((?:pdo-)?sqlite3?):///#', '$1://localhost/', $dsn); @@ -51,11 +50,9 @@ public function parse( } foreach ($url as $param => $value) { - if (! is_string($value)) { - continue; + if (is_string($value)) { + $url[$param] = rawurldecode($value); } - - $url[$param] = rawurldecode($value); } $params = []; @@ -110,7 +107,11 @@ private function parseDatabaseUrlPath(array $url, array $params): array return $params; } - $url['path'] = $this->normalizeDatabaseUrlPath($url['path']); + if (isset($params['host'])) { + // Only normalize the path if a host is also available. Otherwise we might trim leading slashes + // from a pure dbname. + $url['path'] = $this->normalizeDatabaseUrlPath($url['path']); + } // If we do not have a known DBAL driver, we do not know any connection URL path semantics to evaluate // and therefore treat the path as a regular DBAL connection URL path. @@ -132,6 +133,8 @@ private function parseDatabaseUrlPath(array $url, array $params): array */ private function normalizeDatabaseUrlPath(string $urlPath): string { + assert($urlPath[0] === '/'); + // Trim leading slash from URL path. return substr($urlPath, 1); } diff --git a/doctrine/dbal/src/TransactionIsolationLevel.php b/doctrine/dbal/src/TransactionIsolationLevel.php index 9020343ab..2b094dbe8 100644 --- a/doctrine/dbal/src/TransactionIsolationLevel.php +++ b/doctrine/dbal/src/TransactionIsolationLevel.php @@ -4,30 +4,10 @@ namespace Doctrine\DBAL; -final class TransactionIsolationLevel +enum TransactionIsolationLevel { - /** - * Transaction isolation level READ UNCOMMITTED. - */ - public const READ_UNCOMMITTED = 1; - - /** - * Transaction isolation level READ COMMITTED. - */ - public const READ_COMMITTED = 2; - - /** - * Transaction isolation level REPEATABLE READ. - */ - public const REPEATABLE_READ = 3; - - /** - * Transaction isolation level SERIALIZABLE. - */ - public const SERIALIZABLE = 4; - - /** @codeCoverageIgnore */ - private function __construct() - { - } + case READ_UNCOMMITTED; + case READ_COMMITTED; + case REPEATABLE_READ; + case SERIALIZABLE; } diff --git a/doctrine/dbal/src/Types/ArrayType.php b/doctrine/dbal/src/Types/ArrayType.php deleted file mode 100644 index 83940bf1e..000000000 --- a/doctrine/dbal/src/Types/ArrayType.php +++ /dev/null @@ -1,91 +0,0 @@ -getClobTypeDeclarationSQL($column); - } - - /** - * {@inheritDoc} - */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) - { - return serialize($value); - } - - /** - * {@inheritDoc} - */ - public function convertToPHPValue($value, AbstractPlatform $platform) - { - if ($value === null) { - return null; - } - - $value = is_resource($value) ? stream_get_contents($value) : $value; - - set_error_handler(function (int $code, string $message): bool { - if ($code === E_DEPRECATED || $code === E_USER_DEPRECATED) { - return false; - } - - throw ConversionException::conversionFailedUnserialization($this->getName(), $message); - }); - - try { - return unserialize($value); - } finally { - restore_error_handler(); - } - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return Types::ARRAY; - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function requiresSQLCommentHint(AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - - return true; - } -} diff --git a/doctrine/dbal/src/Types/AsciiStringType.php b/doctrine/dbal/src/Types/AsciiStringType.php index 4ea92d974..cd928eee3 100644 --- a/doctrine/dbal/src/Types/AsciiStringType.php +++ b/doctrine/dbal/src/Types/AsciiStringType.php @@ -17,13 +17,8 @@ public function getSQLDeclaration(array $column, AbstractPlatform $platform): st return $platform->getAsciiStringTypeDeclarationSQL($column); } - public function getBindingType(): int + public function getBindingType(): ParameterType { return ParameterType::ASCII; } - - public function getName(): string - { - return Types::ASCII_STRING; - } } diff --git a/doctrine/dbal/src/Types/BigIntType.php b/doctrine/dbal/src/Types/BigIntType.php index 8d57a1121..aa1a5afb4 100644 --- a/doctrine/dbal/src/Types/BigIntType.php +++ b/doctrine/dbal/src/Types/BigIntType.php @@ -1,50 +1,60 @@ getBigIntTypeDeclarationSQL($column); } - /** - * {@inheritDoc} - */ - public function getBindingType() + public function getBindingType(): ParameterType { return ParameterType::STRING; } /** - * {@inheritDoc} - * * @param T $value * - * @return (T is null ? null : string) + * @return (T is null ? null : int|string) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): int|string|null { - return $value === null ? null : (string) $value; + if ($value === null || is_int($value)) { + return $value; + } + + assert( + is_string($value) && is_numeric($value), + 'DBAL assumes values outside of the integer range to be returned as string by the database driver.', + ); + + $intValue = 0 + $value; + if (is_int($intValue)) { + return $intValue; + } + + return $value; } } diff --git a/doctrine/dbal/src/Types/BinaryType.php b/doctrine/dbal/src/Types/BinaryType.php index acbbd87ad..d400dd5bf 100644 --- a/doctrine/dbal/src/Types/BinaryType.php +++ b/doctrine/dbal/src/Types/BinaryType.php @@ -1,16 +1,16 @@ getBinaryTypeDeclarationSQL($column); } - /** - * {@inheritDoc} - */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return null; } - if (is_string($value)) { - $fp = fopen('php://temp', 'rb+'); - assert(is_resource($fp)); - fwrite($fp, $value); - fseek($fp, 0); - $value = $fp; + if (is_resource($value)) { + $value = stream_get_contents($value); } - if (! is_resource($value)) { - throw ConversionException::conversionFailed($value, Types::BINARY); + if (! is_string($value)) { + throw ValueNotConvertible::new($value, Types::BINARY); } return $value; } - /** - * {@inheritDoc} - */ - public function getName() - { - return Types::BINARY; - } - - /** - * {@inheritDoc} - */ - public function getBindingType() + public function getBindingType(): ParameterType { return ParameterType::BINARY; } diff --git a/doctrine/dbal/src/Types/BlobType.php b/doctrine/dbal/src/Types/BlobType.php index cfaabec90..c5415bcf1 100644 --- a/doctrine/dbal/src/Types/BlobType.php +++ b/doctrine/dbal/src/Types/BlobType.php @@ -1,9 +1,12 @@ getBlobTypeDeclarationSQL($column); } - /** - * {@inheritDoc} - */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed { if ($value === null) { return null; @@ -43,24 +43,13 @@ public function convertToPHPValue($value, AbstractPlatform $platform) } if (! is_resource($value)) { - throw ConversionException::conversionFailed($value, Types::BLOB); + throw ValueNotConvertible::new($value, Types::BLOB); } return $value; } - /** - * {@inheritDoc} - */ - public function getName() - { - return Types::BLOB; - } - - /** - * {@inheritDoc} - */ - public function getBindingType() + public function getBindingType(): ParameterType { return ParameterType::LARGE_OBJECT; } diff --git a/doctrine/dbal/src/Types/BooleanType.php b/doctrine/dbal/src/Types/BooleanType.php index 7dc7f3a9d..e837e586e 100644 --- a/doctrine/dbal/src/Types/BooleanType.php +++ b/doctrine/dbal/src/Types/BooleanType.php @@ -1,11 +1,11 @@ getBooleanTypeDeclarationSQL($column); } - /** - * {@inheritDoc} - */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed { return $platform->convertBooleansToDatabaseValue($value); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : bool) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?bool { return $platform->convertFromBoolean($value); } - /** - * {@inheritDoc} - */ - public function getName() - { - return Types::BOOLEAN; - } - - /** - * {@inheritDoc} - */ - public function getBindingType() + public function getBindingType(): ParameterType { return ParameterType::BOOLEAN; } - - /** - * @deprecated - * - * @return bool - */ - public function requiresSQLCommentHint(AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - - // We require a commented boolean type in order to distinguish between - // boolean and smallint as both (have to) map to the same native type. - return $platform instanceof DB2Platform; - } } diff --git a/doctrine/dbal/src/Types/ConversionException.php b/doctrine/dbal/src/Types/ConversionException.php index 2401aaeb9..7cc375d2e 100644 --- a/doctrine/dbal/src/Types/ConversionException.php +++ b/doctrine/dbal/src/Types/ConversionException.php @@ -1,121 +1,14 @@ 32 ? substr($value, 0, 20) . '...' : $value; - - return new self('Could not convert database value "' . $value . '" to Doctrine Type ' . $toType, 0, $previous); - } - - /** - * Thrown when a Database to Doctrine Type Conversion fails and we can make a statement - * about the expected format. - * - * @param mixed $value - * @param string $toType - * @param string $expectedFormat - * - * @return ConversionException - */ - public static function conversionFailedFormat($value, $toType, $expectedFormat, ?Throwable $previous = null) - { - $value = strlen($value) > 32 ? substr($value, 0, 20) . '...' : $value; - - return new self( - 'Could not convert database value "' . $value . '" to Doctrine Type ' . - $toType . '. Expected format: ' . $expectedFormat, - 0, - $previous, - ); - } - - /** - * Thrown when the PHP value passed to the converter was not of the expected type. - * - * @param mixed $value - * @param string $toType - * @param string[] $possibleTypes - * - * @return ConversionException - */ - public static function conversionFailedInvalidType( - $value, - $toType, - array $possibleTypes, - ?Throwable $previous = null - ) { - if (is_scalar($value) || $value === null) { - return new self(sprintf( - 'Could not convert PHP value %s to type %s. Expected one of the following types: %s', - var_export($value, true), - $toType, - implode(', ', $possibleTypes), - ), 0, $previous); - } - - return new self(sprintf( - 'Could not convert PHP value of type %s to type %s. Expected one of the following types: %s', - is_object($value) ? get_class($value) : gettype($value), - $toType, - implode(', ', $possibleTypes), - ), 0, $previous); - } - - /** - * @param mixed $value - * @param string $format - * @param string $error - * - * @return ConversionException - */ - public static function conversionFailedSerialization($value, $format, $error /*, ?Throwable $previous = null */) - { - $actualType = is_object($value) ? get_class($value) : gettype($value); - - return new self(sprintf( - "Could not convert PHP type '%s' to '%s', as an '%s' error was triggered by the serialization", - $actualType, - $format, - $error, - ), 0, func_num_args() >= 4 ? func_get_arg(3) : null); - } - - public static function conversionFailedUnserialization(string $format, string $error): self - { - return new self(sprintf( - "Could not convert database value to '%s' as an error was triggered by the unserialization: '%s'", - $format, - $error, - )); - } } diff --git a/doctrine/dbal/src/Types/DateImmutableType.php b/doctrine/dbal/src/Types/DateImmutableType.php index da96b69d5..732efcd1a 100644 --- a/doctrine/dbal/src/Types/DateImmutableType.php +++ b/doctrine/dbal/src/Types/DateImmutableType.php @@ -1,34 +1,35 @@ getDateTypeDeclarationSQL($column); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : string) * * @template T */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return $value; @@ -38,23 +39,21 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform) return $value->format($platform->getDateFormatString()); } - throw ConversionException::conversionFailedInvalidType( + throw InvalidType::new( $value, - $this->getName(), + static::class, ['null', DateTimeImmutable::class], ); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : DateTimeImmutable) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTimeImmutable { if ($value === null || $value instanceof DateTimeImmutable) { return $value; @@ -63,30 +62,13 @@ public function convertToPHPValue($value, AbstractPlatform $platform) $dateTime = DateTimeImmutable::createFromFormat('!' . $platform->getDateFormatString(), $value); if ($dateTime === false) { - throw ConversionException::conversionFailedFormat( + throw InvalidFormat::new( $value, - $this->getName(), + static::class, $platform->getDateFormatString(), ); } return $dateTime; } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function requiresSQLCommentHint(AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - - return true; - } } diff --git a/doctrine/dbal/src/Types/DateIntervalType.php b/doctrine/dbal/src/Types/DateIntervalType.php index 1630dc556..29842ae54 100644 --- a/doctrine/dbal/src/Types/DateIntervalType.php +++ b/doctrine/dbal/src/Types/DateIntervalType.php @@ -1,10 +1,13 @@ format(self::FORMAT); } - throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', DateInterval::class]); + throw InvalidType::new($value, static::class, ['null', DateInterval::class]); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : DateInterval) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateInterval { if ($value === null || $value instanceof DateInterval) { return $value; @@ -87,24 +78,7 @@ public function convertToPHPValue($value, AbstractPlatform $platform) return $interval; } catch (Throwable $exception) { - throw ConversionException::conversionFailedFormat($value, $this->getName(), self::FORMAT, $exception); + throw InvalidFormat::new($value, static::class, self::FORMAT, $exception); } } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function requiresSQLCommentHint(AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - - return true; - } } diff --git a/doctrine/dbal/src/Types/DateTimeImmutableType.php b/doctrine/dbal/src/Types/DateTimeImmutableType.php index a8c7fec96..2d49d1dd7 100644 --- a/doctrine/dbal/src/Types/DateTimeImmutableType.php +++ b/doctrine/dbal/src/Types/DateTimeImmutableType.php @@ -1,35 +1,36 @@ getDateTimeTypeDeclarationSQL($column); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : string) * * @template T */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return $value; @@ -39,23 +40,21 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform) return $value->format($platform->getDateTimeFormatString()); } - throw ConversionException::conversionFailedInvalidType( + throw InvalidType::new( $value, - $this->getName(), + static::class, ['null', DateTimeImmutable::class], ); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : DateTimeImmutable) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTimeImmutable { if ($value === null || $value instanceof DateTimeImmutable) { return $value; @@ -70,29 +69,12 @@ public function convertToPHPValue($value, AbstractPlatform $platform) try { return new DateTimeImmutable($value); } catch (Exception $e) { - throw ConversionException::conversionFailedFormat( + throw InvalidFormat::new( $value, - $this->getName(), + static::class, $platform->getDateTimeFormatString(), $e, ); } } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function requiresSQLCommentHint(AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - - return true; - } } diff --git a/doctrine/dbal/src/Types/DateTimeType.php b/doctrine/dbal/src/Types/DateTimeType.php index 3ff592ae1..9fd0ba028 100644 --- a/doctrine/dbal/src/Types/DateTimeType.php +++ b/doctrine/dbal/src/Types/DateTimeType.php @@ -1,16 +1,15 @@ getDateTimeTypeDeclarationSQL($column); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : string) * * @template T */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return $value; } - if ($value instanceof DateTimeImmutable) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6017', - 'Passing an instance of %s is deprecated, use %s::%s() instead.', - get_class($value), - DateTimeImmutableType::class, - __FUNCTION__, - ); - } - - if ($value instanceof DateTimeInterface) { + if ($value instanceof DateTime) { return $value->format($platform->getDateTimeFormatString()); } - throw ConversionException::conversionFailedInvalidType( + throw InvalidType::new( $value, - $this->getName(), - ['null', DateTime::class, DateTimeImmutable::class], + static::class, + ['null', DateTime::class], ); } /** - * {@inheritDoc} - * * @param T $value * - * @return (T is null ? null : DateTimeInterface) + * @return (T is null ? null : DateTime) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTime { - if ($value instanceof DateTimeImmutable) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6017', - 'Passing an instance of %s is deprecated, use %s::%s() instead.', - get_class($value), - DateTimeImmutableType::class, - __FUNCTION__, - ); - } - - if ($value === null || $value instanceof DateTimeInterface) { + if ($value === null || $value instanceof DateTime) { return $value; } @@ -104,9 +69,9 @@ public function convertToPHPValue($value, AbstractPlatform $platform) try { return new DateTime($value); } catch (Exception $e) { - throw ConversionException::conversionFailedFormat( + throw InvalidFormat::new( $value, - $this->getName(), + static::class, $platform->getDateTimeFormatString(), $e, ); diff --git a/doctrine/dbal/src/Types/DateTimeTzImmutableType.php b/doctrine/dbal/src/Types/DateTimeTzImmutableType.php index 70f2c78b6..350964d1c 100644 --- a/doctrine/dbal/src/Types/DateTimeTzImmutableType.php +++ b/doctrine/dbal/src/Types/DateTimeTzImmutableType.php @@ -1,34 +1,35 @@ getDateTimeTzTypeDeclarationSQL($column); } /** - * {@inheritDoc} - * * @phpstan-param T $value * * @return (T is null ? null : string) * * @template T */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return $value; @@ -38,23 +39,21 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform) return $value->format($platform->getDateTimeTzFormatString()); } - throw ConversionException::conversionFailedInvalidType( + throw InvalidType::new( $value, - $this->getName(), + static::class, ['null', DateTimeImmutable::class], ); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : DateTimeImmutable) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTimeImmutable { if ($value === null || $value instanceof DateTimeImmutable) { return $value; @@ -66,27 +65,10 @@ public function convertToPHPValue($value, AbstractPlatform $platform) return $dateTime; } - throw ConversionException::conversionFailedFormat( + throw InvalidFormat::new( $value, - $this->getName(), + static::class, $platform->getDateTimeTzFormatString(), ); } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function requiresSQLCommentHint(AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - - return true; - } } diff --git a/doctrine/dbal/src/Types/DateTimeTzType.php b/doctrine/dbal/src/Types/DateTimeTzType.php index b3b5db813..98e65696e 100644 --- a/doctrine/dbal/src/Types/DateTimeTzType.php +++ b/doctrine/dbal/src/Types/DateTimeTzType.php @@ -1,14 +1,13 @@ getDateTimeTzTypeDeclarationSQL($column); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : string) * * @template T */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return $value; } - if ($value instanceof DateTimeImmutable) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6017', - 'Passing an instance of %s is deprecated, use %s::%s() instead.', - get_class($value), - DateTimeTzImmutableType::class, - __FUNCTION__, - ); - } - - if ($value instanceof DateTimeInterface) { + if ($value instanceof DateTime) { return $value->format($platform->getDateTimeTzFormatString()); } - throw ConversionException::conversionFailedInvalidType( + throw InvalidType::new( $value, - $this->getName(), + static::class, ['null', DateTime::class], ); } /** - * {@inheritDoc} - * * @param T $value * - * @return (T is null ? null : DateTimeInterface) + * @return (T is null ? null : DateTime) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTime { - if ($value instanceof DateTimeImmutable) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6017', - 'Passing an instance of %s is deprecated, use %s::%s() instead.', - get_class($value), - DateTimeTzImmutableType::class, - __FUNCTION__, - ); - } - - if ($value === null || $value instanceof DateTimeInterface) { + if ($value === null || $value instanceof DateTime) { return $value; } @@ -113,9 +78,9 @@ public function convertToPHPValue($value, AbstractPlatform $platform) return $dateTime; } - throw ConversionException::conversionFailedFormat( + throw InvalidFormat::new( $value, - $this->getName(), + static::class, $platform->getDateTimeTzFormatString(), ); } diff --git a/doctrine/dbal/src/Types/DateType.php b/doctrine/dbal/src/Types/DateType.php index 86a5ab1ec..7dcbe4feb 100644 --- a/doctrine/dbal/src/Types/DateType.php +++ b/doctrine/dbal/src/Types/DateType.php @@ -1,92 +1,57 @@ getDateTypeDeclarationSQL($column); } /** - * {@inheritDoc} - * * @phpstan-param T $value * * @return (T is null ? null : string) * * @template T */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed { if ($value === null) { return $value; } - if ($value instanceof DateTimeInterface) { - if ($value instanceof DateTimeImmutable) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6017', - 'Passing an instance of %s is deprecated, use %s::%s() instead.', - get_class($value), - DateImmutableType::class, - __FUNCTION__, - ); - } - + if ($value instanceof DateTime) { return $value->format($platform->getDateFormatString()); } - throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', DateTime::class]); + throw InvalidType::new($value, static::class, ['null', DateTime::class]); } /** - * {@inheritDoc} - * * @param T $value * - * @return (T is null ? null : DateTimeInterface) + * @return (T is null ? null : DateTime) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTime { - if ($value instanceof DateTimeImmutable) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6017', - 'Passing an instance of %s is deprecated, use %s::%s() instead.', - get_class($value), - DateImmutableType::class, - __FUNCTION__, - ); - } - - if ($value === null || $value instanceof DateTimeInterface) { + if ($value === null || $value instanceof DateTime) { return $value; } @@ -95,9 +60,9 @@ public function convertToPHPValue($value, AbstractPlatform $platform) return $dateTime; } - throw ConversionException::conversionFailedFormat( + throw InvalidFormat::new( $value, - $this->getName(), + static::class, $platform->getDateFormatString(), ); } diff --git a/doctrine/dbal/src/Types/DecimalType.php b/doctrine/dbal/src/Types/DecimalType.php index 308134b08..7301a3383 100644 --- a/doctrine/dbal/src/Types/DecimalType.php +++ b/doctrine/dbal/src/Types/DecimalType.php @@ -1,15 +1,14 @@ getDecimalTypeDeclarationSQL($column); } - /** - * {@inheritDoc} - */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?string { - // Some drivers starting from PHP 8.1 can represent decimals as float/int + // Some drivers can represent decimals as float/int // See also: https://github.com/doctrine/dbal/pull/4818 - if ((PHP_VERSION_ID >= 80100 || $platform instanceof SqlitePlatform) && (is_float($value) || is_int($value))) { + if (is_float($value) || is_int($value)) { return (string) $value; } diff --git a/doctrine/dbal/src/Types/EnumType.php b/doctrine/dbal/src/Types/EnumType.php new file mode 100644 index 000000000..489dc4b5c --- /dev/null +++ b/doctrine/dbal/src/Types/EnumType.php @@ -0,0 +1,18 @@ +getEnumDeclarationSQL($column); + } +} diff --git a/doctrine/dbal/src/Types/Exception/InvalidFormat.php b/doctrine/dbal/src/Types/Exception/InvalidFormat.php new file mode 100644 index 000000000..221da2fbe --- /dev/null +++ b/doctrine/dbal/src/Types/Exception/InvalidFormat.php @@ -0,0 +1,37 @@ + 32 ? substr($value, 0, 20) . '...' : $value, + $toType, + $expectedFormat ?? '', + ), + 0, + $previous, + ); + } +} diff --git a/doctrine/dbal/src/Types/Exception/InvalidType.php b/doctrine/dbal/src/Types/Exception/InvalidType.php new file mode 100644 index 000000000..743e41f1e --- /dev/null +++ b/doctrine/dbal/src/Types/Exception/InvalidType.php @@ -0,0 +1,51 @@ + 32 ? substr($value, 0, 20) . '...' : $value, + $toType, + ); + } + + return new self($message, 0, $previous); + } +} diff --git a/doctrine/dbal/src/Types/FloatType.php b/doctrine/dbal/src/Types/FloatType.php index e01b77413..40e11f9f5 100644 --- a/doctrine/dbal/src/Types/FloatType.php +++ b/doctrine/dbal/src/Types/FloatType.php @@ -1,5 +1,7 @@ getFloatDeclarationSQL($column); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : float) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?float { return $value === null ? null : (float) $value; } diff --git a/doctrine/dbal/src/Types/GuidType.php b/doctrine/dbal/src/Types/GuidType.php index 3c8b7f4f8..cc7cc5fc3 100644 --- a/doctrine/dbal/src/Types/GuidType.php +++ b/doctrine/dbal/src/Types/GuidType.php @@ -1,9 +1,10 @@ getGuidTypeDeclarationSQL($column); } - - /** - * {@inheritDoc} - */ - public function getName() - { - return Types::GUID; - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function requiresSQLCommentHint(AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - - return ! $platform->hasNativeGuidType(); - } } diff --git a/doctrine/dbal/src/Types/IntegerType.php b/doctrine/dbal/src/Types/IntegerType.php index 7c2d7110d..8a711c039 100644 --- a/doctrine/dbal/src/Types/IntegerType.php +++ b/doctrine/dbal/src/Types/IntegerType.php @@ -1,5 +1,7 @@ getIntegerTypeDeclarationSQL($column); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : int) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?int { return $value === null ? null : (int) $value; } - /** - * {@inheritDoc} - */ - public function getBindingType() + public function getBindingType(): ParameterType { return ParameterType::INTEGER; } diff --git a/doctrine/dbal/src/Types/JsonObjectType.php b/doctrine/dbal/src/Types/JsonObjectType.php new file mode 100644 index 000000000..f57172f44 --- /dev/null +++ b/doctrine/dbal/src/Types/JsonObjectType.php @@ -0,0 +1,28 @@ +getJsonTypeDeclarationSQL($column); + } + + protected function isAssociative(): bool + { + return false; + } +} diff --git a/doctrine/dbal/src/Types/JsonType.php b/doctrine/dbal/src/Types/JsonType.php index 27f872c88..08e81897b 100644 --- a/doctrine/dbal/src/Types/JsonType.php +++ b/doctrine/dbal/src/Types/JsonType.php @@ -1,96 +1,28 @@ getJsonTypeDeclarationSQL($column); - } + use JsonTypeConvert; /** * {@inheritDoc} - * - * @param T $value - * - * @return (T is null ? null : string) - * - * @template T */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function getSQLDeclaration(array $column, AbstractPlatform $platform): string { - if ($value === null) { - return null; - } - - try { - return json_encode($value, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION); - } catch (JsonException $e) { - throw ConversionException::conversionFailedSerialization($value, 'json', $e->getMessage(), $e); - } - } - - /** - * {@inheritDoc} - */ - public function convertToPHPValue($value, AbstractPlatform $platform) - { - if ($value === null || $value === '') { - return null; - } - - if (is_resource($value)) { - $value = stream_get_contents($value); - } - - try { - return json_decode($value, true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException $e) { - throw ConversionException::conversionFailed($value, $this->getName(), $e); - } - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return Types::JSON; + return $platform->getJsonTypeDeclarationSQL($column); } - /** - * {@inheritDoc} - * - * @deprecated - */ - public function requiresSQLCommentHint(AbstractPlatform $platform) + protected function isAssociative(): bool { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - - return ! $platform->hasNativeJsonType(); + return true; } } diff --git a/doctrine/dbal/src/Types/JsonTypeConvert.php b/doctrine/dbal/src/Types/JsonTypeConvert.php new file mode 100644 index 000000000..757ee5ab7 --- /dev/null +++ b/doctrine/dbal/src/Types/JsonTypeConvert.php @@ -0,0 +1,61 @@ +getMessage(), $e); + } + } + + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed + { + if ($value === null || $value === '') { + return null; + } + + if (is_resource($value)) { + $value = stream_get_contents($value); + } + + try { + return json_decode($value, $this->isAssociative(), 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw ValueNotConvertible::new($value, 'json', $e->getMessage(), $e); + } + } + + abstract protected function isAssociative(): bool; +} diff --git a/doctrine/dbal/src/Types/JsonbObjectType.php b/doctrine/dbal/src/Types/JsonbObjectType.php new file mode 100644 index 000000000..5c39693e6 --- /dev/null +++ b/doctrine/dbal/src/Types/JsonbObjectType.php @@ -0,0 +1,28 @@ +getJsonbTypeDeclarationSQL($column); + } + + protected function isAssociative(): bool + { + return false; + } +} diff --git a/doctrine/dbal/src/Types/JsonbType.php b/doctrine/dbal/src/Types/JsonbType.php new file mode 100644 index 000000000..f84e5d8db --- /dev/null +++ b/doctrine/dbal/src/Types/JsonbType.php @@ -0,0 +1,21 @@ +getJsonbTypeDeclarationSQL($column); + } +} diff --git a/doctrine/dbal/src/Types/NumberType.php b/doctrine/dbal/src/Types/NumberType.php new file mode 100644 index 000000000..3b3d6d118 --- /dev/null +++ b/doctrine/dbal/src/Types/NumberType.php @@ -0,0 +1,54 @@ +getDecimalTypeDeclarationSQL($column); + } + + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string + { + if ($value === null) { + return null; + } + + if (! $value instanceof Number) { + throw InvalidType::new($value, static::class, ['null', Number::class]); + } + + return (string) $value; + } + + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?Number + { + if ($value === null) { + return null; + } + + // SQLite might return a decimal as float. + if (is_float($value)) { + $value = (string) $value; + } + + try { + return new Number($value); + } catch (TypeError | ValueError $e) { + throw ValueNotConvertible::new($value, static::class, previous: $e); + } + } +} diff --git a/doctrine/dbal/src/Types/ObjectType.php b/doctrine/dbal/src/Types/ObjectType.php deleted file mode 100644 index 497e9c407..000000000 --- a/doctrine/dbal/src/Types/ObjectType.php +++ /dev/null @@ -1,88 +0,0 @@ -getClobTypeDeclarationSQL($column); - } - - /** - * {@inheritDoc} - * - * @param mixed $value - * - * @return string - */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) - { - return serialize($value); - } - - /** - * {@inheritDoc} - */ - public function convertToPHPValue($value, AbstractPlatform $platform) - { - if ($value === null) { - return null; - } - - $value = is_resource($value) ? stream_get_contents($value) : $value; - - set_error_handler(function (int $code, string $message): bool { - throw ConversionException::conversionFailedUnserialization($this->getName(), $message); - }); - - try { - return unserialize($value); - } finally { - restore_error_handler(); - } - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return Types::OBJECT; - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function requiresSQLCommentHint(AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - - return true; - } -} diff --git a/doctrine/dbal/src/Types/PhpDateMappingType.php b/doctrine/dbal/src/Types/PhpDateMappingType.php new file mode 100644 index 000000000..200f2772c --- /dev/null +++ b/doctrine/dbal/src/Types/PhpDateMappingType.php @@ -0,0 +1,14 @@ +getClobTypeDeclarationSQL($column); } - /** - * {@inheritDoc} - * - * @param mixed $value - * - * @return string|null - */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if (! is_array($value) || count($value) === 0) { return null; @@ -43,14 +37,8 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform) return implode(',', $value); } - /** - * {@inheritDoc} - * - * @param mixed $value - * - * @return list - */ - public function convertToPHPValue($value, AbstractPlatform $platform) + /** @return list */ + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): array { if ($value === null) { return []; @@ -60,29 +48,4 @@ public function convertToPHPValue($value, AbstractPlatform $platform) return explode(',', $value); } - - /** - * {@inheritDoc} - */ - public function getName() - { - return Types::SIMPLE_ARRAY; - } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function requiresSQLCommentHint(AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - - return true; - } } diff --git a/doctrine/dbal/src/Types/SmallFloatType.php b/doctrine/dbal/src/Types/SmallFloatType.php new file mode 100644 index 000000000..431ddb287 --- /dev/null +++ b/doctrine/dbal/src/Types/SmallFloatType.php @@ -0,0 +1,30 @@ +getSmallFloatDeclarationSQL($column); + } + + /** + * @param T $value + * + * @return (T is null ? null : float) + * + * @template T + */ + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?float + { + return $value === null ? null : (float) $value; + } +} diff --git a/doctrine/dbal/src/Types/SmallIntType.php b/doctrine/dbal/src/Types/SmallIntType.php index 2c8567a11..ba0899c28 100644 --- a/doctrine/dbal/src/Types/SmallIntType.php +++ b/doctrine/dbal/src/Types/SmallIntType.php @@ -1,5 +1,7 @@ getSmallIntTypeDeclarationSQL($column); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : int) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?int { return $value === null ? null : (int) $value; } - /** - * {@inheritDoc} - */ - public function getBindingType() + public function getBindingType(): ParameterType { return ParameterType::INTEGER; } diff --git a/doctrine/dbal/src/Types/StringType.php b/doctrine/dbal/src/Types/StringType.php index 1992e8fa4..d3f92aaaa 100644 --- a/doctrine/dbal/src/Types/StringType.php +++ b/doctrine/dbal/src/Types/StringType.php @@ -1,5 +1,7 @@ getStringTypeDeclarationSQL($column); } - - /** - * {@inheritDoc} - */ - public function getName() - { - return Types::STRING; - } } diff --git a/doctrine/dbal/src/Types/TextType.php b/doctrine/dbal/src/Types/TextType.php index d060bb2da..a682be5bc 100644 --- a/doctrine/dbal/src/Types/TextType.php +++ b/doctrine/dbal/src/Types/TextType.php @@ -1,5 +1,7 @@ getClobTypeDeclarationSQL($column); } - /** - * {@inheritDoc} - */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed { return is_resource($value) ? stream_get_contents($value) : $value; } - - /** - * {@inheritDoc} - */ - public function getName() - { - return Types::TEXT; - } } diff --git a/doctrine/dbal/src/Types/TimeImmutableType.php b/doctrine/dbal/src/Types/TimeImmutableType.php index 9373f5913..c1c24d8b2 100644 --- a/doctrine/dbal/src/Types/TimeImmutableType.php +++ b/doctrine/dbal/src/Types/TimeImmutableType.php @@ -1,34 +1,35 @@ getTimeTypeDeclarationSQL($column); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : string) * * @template T */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return $value; @@ -38,23 +39,21 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform) return $value->format($platform->getTimeFormatString()); } - throw ConversionException::conversionFailedInvalidType( + throw InvalidType::new( $value, - $this->getName(), + static::class, ['null', DateTimeImmutable::class], ); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : DateTimeImmutable) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTimeImmutable { if ($value === null || $value instanceof DateTimeImmutable) { return $value; @@ -66,27 +65,10 @@ public function convertToPHPValue($value, AbstractPlatform $platform) return $dateTime; } - throw ConversionException::conversionFailedFormat( + throw InvalidFormat::new( $value, - $this->getName(), + static::class, $platform->getTimeFormatString(), ); } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function requiresSQLCommentHint(AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - - return true; - } } diff --git a/doctrine/dbal/src/Types/TimeType.php b/doctrine/dbal/src/Types/TimeType.php index 7356fc206..0f96fd561 100644 --- a/doctrine/dbal/src/Types/TimeType.php +++ b/doctrine/dbal/src/Types/TimeType.php @@ -1,92 +1,57 @@ getTimeTypeDeclarationSQL($column); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : string) * * @template T */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return $value; } - if ($value instanceof DateTimeImmutable) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6017', - 'Passing an instance of %s is deprecated, use %s::%s() instead.', - get_class($value), - TimeImmutableType::class, - __FUNCTION__, - ); - } - - if ($value instanceof DateTimeInterface) { + if ($value instanceof DateTime) { return $value->format($platform->getTimeFormatString()); } - throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', DateTime::class]); + throw InvalidType::new($value, static::class, ['null', DateTime::class]); } /** - * {@inheritDoc} - * * @param T $value * - * @return (T is null ? null : DateTimeInterface) + * @return (T is null ? null : DateTime) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTime { - if ($value instanceof DateTimeImmutable) { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6017', - 'Passing an instance of %s is deprecated, use %s::%s() instead.', - get_class($value), - TimeImmutableType::class, - __FUNCTION__, - ); - } - - if ($value === null || $value instanceof DateTimeInterface) { + if ($value === null || $value instanceof DateTime) { return $value; } @@ -95,9 +60,9 @@ public function convertToPHPValue($value, AbstractPlatform $platform) return $dateTime; } - throw ConversionException::conversionFailedFormat( + throw InvalidFormat::new( $value, - $this->getName(), + static::class, $platform->getTimeFormatString(), ); } diff --git a/doctrine/dbal/src/Types/Type.php b/doctrine/dbal/src/Types/Type.php index 7613811e9..e5c34acd2 100644 --- a/doctrine/dbal/src/Types/Type.php +++ b/doctrine/dbal/src/Types/Type.php @@ -1,14 +1,18 @@ ArrayType::class, Types::ASCII_STRING => AsciiStringType::class, Types::BIGINT => BigIntType::class, Types::BINARY => BinaryType::class, @@ -35,12 +38,17 @@ abstract class Type Types::DATETIMETZ_MUTABLE => DateTimeTzType::class, Types::DATETIMETZ_IMMUTABLE => DateTimeTzImmutableType::class, Types::DECIMAL => DecimalType::class, + Types::NUMBER => NumberType::class, + Types::ENUM => EnumType::class, Types::FLOAT => FloatType::class, Types::GUID => GuidType::class, Types::INTEGER => IntegerType::class, Types::JSON => JsonType::class, - Types::OBJECT => ObjectType::class, + Types::JSON_OBJECT => JsonObjectType::class, + Types::JSONB => JsonbType::class, + Types::JSONB_OBJECT => JsonbObjectType::class, Types::SIMPLE_ARRAY => SimpleArrayType::class, + Types::SMALLFLOAT => SmallFloatType::class, Types::SMALLINT => SmallIntType::class, Types::STRING => StringType::class, Types::TEXT => TextType::class, @@ -50,11 +58,6 @@ abstract class Type private static ?TypeRegistry $typeRegistry = null; - /** @internal Do not instantiate directly - use {@see Type::addType()} method instead. */ - final public function __construct() - { - } - /** * Converts a value from its PHP representation to its database representation * of this type. @@ -66,7 +69,7 @@ final public function __construct() * * @throws ConversionException */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed { return $value; } @@ -82,7 +85,7 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform) * * @throws ConversionException */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed { return $value; } @@ -90,50 +93,36 @@ public function convertToPHPValue($value, AbstractPlatform $platform) /** * Gets the SQL declaration snippet for a column of this type. * - * @param mixed[] $column The column definition - * @param AbstractPlatform $platform The currently used database platform. - * - * @return string + * @param array $column The column definition + * @param AbstractPlatform $platform The currently used database platform. */ - abstract public function getSQLDeclaration(array $column, AbstractPlatform $platform); - - /** - * Gets the name of this type. - * - * @deprecated this method will be removed in Doctrine DBAL 4.0, - * use {@see TypeRegistry::lookupName()} instead. - * - * @return string - */ - abstract public function getName(); + abstract public function getSQLDeclaration(array $column, AbstractPlatform $platform): string; + /** @throws TypesException */ final public static function getTypeRegistry(): TypeRegistry { return self::$typeRegistry ??= self::createTypeRegistry(); } + /** @throws TypesException */ private static function createTypeRegistry(): TypeRegistry { - $instances = []; - - foreach (self::BUILTIN_TYPES_MAP as $name => $class) { - $instances[$name] = new $class(); - } - - return new TypeRegistry($instances); + return new TypeRegistry( + array_map( + static fn ($class) => new $class(), + self::BUILTIN_TYPES_MAP, + ), + ); } /** * Factory method to create type instances. - * Type instances are implemented as flyweights. - * - * @param string $name The name of the type (as returned by getName()). * - * @return Type + * @param string $name The name of the type. * - * @throws Exception + * @throws TypesException */ - public static function getType($name) + public static function getType(string $name): self { return self::getTypeRegistry()->get($name); } @@ -141,7 +130,7 @@ public static function getType($name) /** * Finds a name for the given type. * - * @throws Exception + * @throws TypesException */ public static function lookupName(self $type): string { @@ -151,16 +140,22 @@ public static function lookupName(self $type): string /** * Adds a custom type to the type map. * - * @param string $name The name of the type. This should correspond to what getName() returns. - * @param class-string $className The class name of the custom type. - * - * @return void + * @param string $name The name of the type. + * @param class-string|Type $type The custom type or the class name of the custom type. * * @throws Exception */ - public static function addType($name, $className) + public static function addType(string $name, string|Type $type): void { - self::getTypeRegistry()->register($name, new $className()); + if (is_string($type)) { + try { + $type = new $type(); + } catch (ArgumentCountError $e) { // @phpstan-ignore catch.neverThrown (it can be thrown) + throw TypeArgumentCountError::new($name, $e); + } + } + + self::getTypeRegistry()->register($name, $type); } /** @@ -169,8 +164,10 @@ public static function addType($name, $className) * @param string $name The name of the type. * * @return bool TRUE if type is supported; FALSE otherwise. + * + * @throws TypesException */ - public static function hasType($name) + public static function hasType(string $name): bool { return self::getTypeRegistry()->has($name); } @@ -178,27 +175,28 @@ public static function hasType($name) /** * Overrides an already defined type to use a different implementation. * - * @param string $name - * @param class-string $className - * - * @return void + * @param class-string|Type $type The custom type or the class name of the custom type. * * @throws Exception */ - public static function overrideType($name, $className) + public static function overrideType(string $name, string|Type $type): void { - self::getTypeRegistry()->override($name, new $className()); + if (is_string($type)) { + try { + $type = new $type(); + } catch (ArgumentCountError $e) { // @phpstan-ignore catch.neverThrown (it can be thrown) + throw TypeArgumentCountError::new($name, $e); + } + } + + self::getTypeRegistry()->override($name, $type); } /** * Gets the (preferred) binding type for values of this type that * can be used when binding parameters to prepared statements. - * - * This method should return one of the {@see ParameterType} constants. - * - * @return int */ - public function getBindingType() + public function getBindingType(): ParameterType { return ParameterType::STRING; } @@ -208,56 +206,29 @@ public function getBindingType() * type class * * @return array + * + * @throws TypesException */ - public static function getTypesMap() + public static function getTypesMap(): array { return array_map( - static function (Type $type): string { - return get_class($type); - }, + static fn (Type $type): string => $type::class, self::getTypeRegistry()->getMap(), ); } - /** - * Does working with this column require SQL conversion functions? - * - * This is a metadata function that is required for example in the ORM. - * Usage of {@see convertToDatabaseValueSQL} and - * {@see convertToPHPValueSQL} works for any type and mostly - * does nothing. This method can additionally be used for optimization purposes. - * - * @deprecated Consumers should call {@see convertToDatabaseValueSQL} and {@see convertToPHPValueSQL} - * regardless of the type. - * - * @return bool - */ - public function canRequireSQLConversion() - { - return false; - } - /** * Modifies the SQL expression (identifier, parameter) to convert to a database value. - * - * @param string $sqlExpr - * - * @return string */ - public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform) + public function convertToDatabaseValueSQL(string $sqlExpr, AbstractPlatform $platform): string { return $sqlExpr; } /** * Modifies the SQL expression (identifier, parameter) to convert to a PHP value. - * - * @param string $sqlExpr - * @param AbstractPlatform $platform - * - * @return string */ - public function convertToPHPValueSQL($sqlExpr, $platform) + public function convertToPHPValueSQL(string $sqlExpr, AbstractPlatform $platform): string { return $sqlExpr; } @@ -265,32 +236,10 @@ public function convertToPHPValueSQL($sqlExpr, $platform) /** * Gets an array of database types that map to this Doctrine type. * - * @return string[] + * @return array */ - public function getMappedDatabaseTypes(AbstractPlatform $platform) + public function getMappedDatabaseTypes(AbstractPlatform $platform): array { return []; } - - /** - * If this Doctrine Type maps to an already mapped database type, - * reverse schema engineering can't tell them apart. You need to mark - * one of those types as commented, which will have Doctrine use an SQL - * comment to typehint the actual Doctrine Type. - * - * @deprecated - * - * @return bool - */ - public function requiresSQLCommentHint(AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - - return false; - } } diff --git a/doctrine/dbal/src/Types/TypeRegistry.php b/doctrine/dbal/src/Types/TypeRegistry.php index 9b64c6faf..fce4ab0b2 100644 --- a/doctrine/dbal/src/Types/TypeRegistry.php +++ b/doctrine/dbal/src/Types/TypeRegistry.php @@ -5,12 +5,17 @@ namespace Doctrine\DBAL\Types; use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Types\Exception\TypeAlreadyRegistered; +use Doctrine\DBAL\Types\Exception\TypeNotFound; +use Doctrine\DBAL\Types\Exception\TypeNotRegistered; +use Doctrine\DBAL\Types\Exception\TypesAlreadyExists; +use Doctrine\DBAL\Types\Exception\TypesException; +use Doctrine\DBAL\Types\Exception\UnknownColumnType; use function spl_object_id; /** * The type registry is responsible for holding a map of all known DBAL types. - * The types are stored using the flyweight pattern so that one type only exists as exactly one instance. */ final class TypeRegistry { @@ -19,7 +24,11 @@ final class TypeRegistry /** @var array */ private array $instancesReverseIndex; - /** @param array $instances */ + /** + * @param array $instances + * + * @throws TypesException + */ public function __construct(array $instances = []) { $this->instances = []; @@ -32,13 +41,13 @@ public function __construct(array $instances = []) /** * Finds a type by the given name. * - * @throws Exception + * @throws TypesException */ public function get(string $name): Type { $type = $this->instances[$name] ?? null; if ($type === null) { - throw Exception::unknownColumnType($name); + throw UnknownColumnType::new($name); } return $type; @@ -47,14 +56,14 @@ public function get(string $name): Type /** * Finds a name for the given type. * - * @throws Exception + * @throws TypesException */ public function lookupName(Type $type): string { $name = $this->findTypeName($type); if ($name === null) { - throw Exception::typeNotRegistered($type); + throw TypeNotRegistered::new($type); } return $name; @@ -71,16 +80,16 @@ public function has(string $name): bool /** * Registers a custom type to the type map. * - * @throws Exception + * @throws TypesException */ public function register(string $name, Type $type): void { if (isset($this->instances[$name])) { - throw Exception::typeExists($name); + throw TypesAlreadyExists::new($name); } if ($this->findTypeName($type) !== null) { - throw Exception::typeAlreadyRegistered($type); + throw TypeAlreadyRegistered::new($type); } $this->instances[$name] = $type; @@ -96,11 +105,11 @@ public function override(string $name, Type $type): void { $origType = $this->instances[$name] ?? null; if ($origType === null) { - throw Exception::typeNotFound($name); + throw TypeNotFound::new($name); } if (($this->findTypeName($type) ?? $name) !== $name) { - throw Exception::typeAlreadyRegistered($type); + throw TypeAlreadyRegistered::new($type); } unset($this->instancesReverseIndex[spl_object_id($origType)]); diff --git a/doctrine/dbal/src/Types/Types.php b/doctrine/dbal/src/Types/Types.php index 54b0dfecc..91a3e4d7d 100644 --- a/doctrine/dbal/src/Types/Types.php +++ b/doctrine/dbal/src/Types/Types.php @@ -9,9 +9,6 @@ */ final class Types { - /** @deprecated Use {@link Types::JSON} instead. */ - public const ARRAY = 'array'; - public const ASCII_STRING = 'ascii_string'; public const BIGINT = 'bigint'; public const BINARY = 'binary'; @@ -25,20 +22,22 @@ final class Types public const DATETIMETZ_MUTABLE = 'datetimetz'; public const DATETIMETZ_IMMUTABLE = 'datetimetz_immutable'; public const DECIMAL = 'decimal'; + public const NUMBER = 'number'; public const FLOAT = 'float'; + public const ENUM = 'enum'; public const GUID = 'guid'; public const INTEGER = 'integer'; public const JSON = 'json'; - - /** @deprecated Use {@link Types::JSON} instead. */ - public const OBJECT = 'object'; - - public const SIMPLE_ARRAY = 'simple_array'; - public const SMALLINT = 'smallint'; - public const STRING = 'string'; - public const TEXT = 'text'; - public const TIME_MUTABLE = 'time'; - public const TIME_IMMUTABLE = 'time_immutable'; + public const JSON_OBJECT = 'json_object'; + public const JSONB = 'jsonb'; + public const JSONB_OBJECT = 'jsonb_object'; + public const SIMPLE_ARRAY = 'simple_array'; + public const SMALLFLOAT = 'smallfloat'; + public const SMALLINT = 'smallint'; + public const STRING = 'string'; + public const TEXT = 'text'; + public const TIME_MUTABLE = 'time'; + public const TIME_IMMUTABLE = 'time_immutable'; /** @codeCoverageIgnore */ private function __construct() diff --git a/doctrine/dbal/src/Types/VarDateTimeImmutableType.php b/doctrine/dbal/src/Types/VarDateTimeImmutableType.php index cd6fa890b..67c2cd8b8 100644 --- a/doctrine/dbal/src/Types/VarDateTimeImmutableType.php +++ b/doctrine/dbal/src/Types/VarDateTimeImmutableType.php @@ -1,35 +1,28 @@ format($platform->getDateTimeFormatString()); } - throw ConversionException::conversionFailedInvalidType( + throw InvalidType::new( $value, - $this->getName(), + static::class, ['null', DateTimeImmutable::class], ); } /** - * {@inheritDoc} - * * @param T $value * * @return (T is null ? null : DateTimeImmutable) * * @template T */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTimeImmutable { if ($value === null || $value instanceof DateTimeImmutable) { return $value; @@ -64,26 +55,9 @@ public function convertToPHPValue($value, AbstractPlatform $platform) try { $dateTime = new DateTimeImmutable($value); } catch (Exception $e) { - throw ConversionException::conversionFailed($value, $this->getName(), $e); + throw ValueNotConvertible::new($value, DateTimeImmutable::class, $e->getMessage(), $e); } return $dateTime; } - - /** - * {@inheritDoc} - * - * @deprecated - */ - public function requiresSQLCommentHint(AbstractPlatform $platform) - { - Deprecation::triggerIfCalledFromOutside( - 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/5509', - '%s is deprecated.', - __METHOD__, - ); - - return true; - } } diff --git a/doctrine/dbal/src/Types/VarDateTimeType.php b/doctrine/dbal/src/Types/VarDateTimeType.php index 35ad40328..55dec4930 100644 --- a/doctrine/dbal/src/Types/VarDateTimeType.php +++ b/doctrine/dbal/src/Types/VarDateTimeType.php @@ -1,10 +1,12 @@ getName(), $e); + throw ValueNotConvertible::new($value, DateTime::class, $e->getMessage(), $e); } return $dateTime; diff --git a/doctrine/dbal/src/VersionAwarePlatformDriver.php b/doctrine/dbal/src/VersionAwarePlatformDriver.php deleted file mode 100644 index ffcfcd636..000000000 --- a/doctrine/dbal/src/VersionAwarePlatformDriver.php +++ /dev/null @@ -1,30 +0,0 @@ - => - * - * @var array - */ - private array $listeners = []; - - /** - * Dispatches an event to all registered listeners. - * - * @param string $eventName The name of the event to dispatch. The name of the event is - * the name of the method that is invoked on listeners. - * @param EventArgs|null $eventArgs The event arguments to pass to the event handlers/listeners. - * If not supplied, the single empty EventArgs instance is used. - */ - public function dispatchEvent(string $eventName, EventArgs|null $eventArgs = null): void - { - if (! isset($this->listeners[$eventName])) { - return; - } - - $eventArgs ??= EventArgs::getEmptyInstance(); - - foreach ($this->listeners[$eventName] as $listener) { - $listener->$eventName($eventArgs); - } - } - - /** - * Gets the listeners of a specific event. - * - * @param string $event The name of the event. - * - * @return object[] - */ - public function getListeners(string $event): array - { - return $this->listeners[$event] ?? []; - } - - /** - * Gets all listeners keyed by event name. - * - * @return array The event listeners for the specified event, or all event listeners. - */ - public function getAllListeners(): array - { - return $this->listeners; - } - - /** - * Checks whether an event has any registered listeners. - */ - public function hasListeners(string $event): bool - { - return ! empty($this->listeners[$event]); - } - - /** - * Adds an event listener that listens on the specified events. - * - * @param string|string[] $events The event(s) to listen on. - * @param object $listener The listener object. - */ - public function addEventListener(string|array $events, object $listener): void - { - // Picks the hash code related to that listener - $hash = spl_object_hash($listener); - - foreach ((array) $events as $event) { - // Overrides listener if a previous one was associated already - // Prevents duplicate listeners on same event (same instance only) - $this->listeners[$event][$hash] = $listener; - } - } - - /** - * Removes an event listener from the specified events. - * - * @param string|string[] $events - */ - public function removeEventListener(string|array $events, object $listener): void - { - // Picks the hash code related to that listener - $hash = spl_object_hash($listener); - - foreach ((array) $events as $event) { - unset($this->listeners[$event][$hash]); - } - } - - /** - * Adds an EventSubscriber. - * - * The subscriber is asked for all the events it is interested in and added - * as a listener for these events. - */ - public function addEventSubscriber(EventSubscriber $subscriber): void - { - $this->addEventListener($subscriber->getSubscribedEvents(), $subscriber); - } - - /** - * Removes an EventSubscriber. - * - * The subscriber is asked for all the events it is interested in and removed - * as a listener for these events. - */ - public function removeEventSubscriber(EventSubscriber $subscriber): void - { - $this->removeEventListener($subscriber->getSubscribedEvents(), $subscriber); - } -} diff --git a/doctrine/event-manager/src/EventSubscriber.php b/doctrine/event-manager/src/EventSubscriber.php deleted file mode 100644 index 89cef558a..000000000 --- a/doctrine/event-manager/src/EventSubscriber.php +++ /dev/null @@ -1,21 +0,0 @@ -