From bf6655a534a6775d30cafa67bd801276bda1d98d Mon Sep 17 00:00:00 2001 From: polo Date: Tue, 13 Aug 2024 23:45:21 +0200 Subject: =?UTF-8?q?VERSION=200.2=20doctrine=20ORM=20et=20entit=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dbal/src/Platforms/AbstractMySQLPlatform.php | 842 ++++++++ .../dbal/src/Platforms/AbstractPlatform.php | 2219 ++++++++++++++++++++ vendor/doctrine/dbal/src/Platforms/DB2Platform.php | 593 ++++++ .../dbal/src/Platforms/DateIntervalUnit.php | 17 + .../Platforms/Exception/InvalidPlatformVersion.php | 28 + .../Exception/NoColumnsSpecifiedForTable.php | 18 + .../dbal/src/Platforms/Exception/NotSupported.php | 18 + .../src/Platforms/Exception/PlatformException.php | 11 + .../dbal/src/Platforms/Keywords/DB2Keywords.php | 414 ++++ .../dbal/src/Platforms/Keywords/KeywordList.php | 42 + .../src/Platforms/Keywords/MariaDBKeywords.php | 264 +++ .../src/Platforms/Keywords/MySQL80Keywords.php | 59 + .../dbal/src/Platforms/Keywords/MySQLKeywords.php | 257 +++ .../dbal/src/Platforms/Keywords/OracleKeywords.php | 133 ++ .../src/Platforms/Keywords/PostgreSQLKeywords.php | 119 ++ .../src/Platforms/Keywords/SQLServerKeywords.php | 207 ++ .../dbal/src/Platforms/Keywords/SQLiteKeywords.php | 141 ++ .../dbal/src/Platforms/MariaDB1052Platform.php | 38 + .../dbal/src/Platforms/MariaDB1060Platform.php | 18 + .../dbal/src/Platforms/MariaDBPlatform.php | 165 ++ .../Platforms/MySQL/CharsetMetadataProvider.php | 11 + .../CachingCharsetMetadataProvider.php | 29 + .../ConnectionCharsetMetadataProvider.php | 37 + .../Platforms/MySQL/CollationMetadataProvider.php | 11 + .../CachingCollationMetadataProvider.php | 29 + .../ConnectionCollationMetadataProvider.php | 37 + .../dbal/src/Platforms/MySQL/Comparator.php | 93 + .../src/Platforms/MySQL/DefaultTableOptions.php | 23 + .../dbal/src/Platforms/MySQL80Platform.php | 25 + .../doctrine/dbal/src/Platforms/MySQLPlatform.php | 49 + .../doctrine/dbal/src/Platforms/OraclePlatform.php | 784 +++++++ .../dbal/src/Platforms/PostgreSQLPlatform.php | 784 +++++++ .../dbal/src/Platforms/SQLServer/Comparator.php | 50 + .../SQL/Builder/SQLServerSelectSQLBuilder.php | 84 + .../dbal/src/Platforms/SQLServerPlatform.php | 1223 +++++++++++ .../dbal/src/Platforms/SQLite/Comparator.php | 52 + .../doctrine/dbal/src/Platforms/SQLitePlatform.php | 994 +++++++++ vendor/doctrine/dbal/src/Platforms/TrimMode.php | 13 + 38 files changed, 9931 insertions(+) create mode 100644 vendor/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php create mode 100644 vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php create mode 100644 vendor/doctrine/dbal/src/Platforms/DB2Platform.php create mode 100644 vendor/doctrine/dbal/src/Platforms/DateIntervalUnit.php create mode 100644 vendor/doctrine/dbal/src/Platforms/Exception/InvalidPlatformVersion.php create mode 100644 vendor/doctrine/dbal/src/Platforms/Exception/NoColumnsSpecifiedForTable.php create mode 100644 vendor/doctrine/dbal/src/Platforms/Exception/NotSupported.php create mode 100644 vendor/doctrine/dbal/src/Platforms/Exception/PlatformException.php create mode 100644 vendor/doctrine/dbal/src/Platforms/Keywords/DB2Keywords.php create mode 100644 vendor/doctrine/dbal/src/Platforms/Keywords/KeywordList.php create mode 100644 vendor/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php create mode 100644 vendor/doctrine/dbal/src/Platforms/Keywords/MySQL80Keywords.php create mode 100644 vendor/doctrine/dbal/src/Platforms/Keywords/MySQLKeywords.php create mode 100644 vendor/doctrine/dbal/src/Platforms/Keywords/OracleKeywords.php create mode 100644 vendor/doctrine/dbal/src/Platforms/Keywords/PostgreSQLKeywords.php create mode 100644 vendor/doctrine/dbal/src/Platforms/Keywords/SQLServerKeywords.php create mode 100644 vendor/doctrine/dbal/src/Platforms/Keywords/SQLiteKeywords.php create mode 100644 vendor/doctrine/dbal/src/Platforms/MariaDB1052Platform.php create mode 100644 vendor/doctrine/dbal/src/Platforms/MariaDB1060Platform.php create mode 100644 vendor/doctrine/dbal/src/Platforms/MariaDBPlatform.php create mode 100644 vendor/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider.php create mode 100644 vendor/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/CachingCharsetMetadataProvider.php create mode 100644 vendor/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php create mode 100644 vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php create mode 100644 vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/CachingCollationMetadataProvider.php create mode 100644 vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php create mode 100644 vendor/doctrine/dbal/src/Platforms/MySQL/Comparator.php create mode 100644 vendor/doctrine/dbal/src/Platforms/MySQL/DefaultTableOptions.php create mode 100644 vendor/doctrine/dbal/src/Platforms/MySQL80Platform.php create mode 100644 vendor/doctrine/dbal/src/Platforms/MySQLPlatform.php create mode 100644 vendor/doctrine/dbal/src/Platforms/OraclePlatform.php create mode 100644 vendor/doctrine/dbal/src/Platforms/PostgreSQLPlatform.php create mode 100644 vendor/doctrine/dbal/src/Platforms/SQLServer/Comparator.php create mode 100644 vendor/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php create mode 100644 vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php create mode 100644 vendor/doctrine/dbal/src/Platforms/SQLite/Comparator.php create mode 100644 vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php create mode 100644 vendor/doctrine/dbal/src/Platforms/TrimMode.php (limited to 'vendor/doctrine/dbal/src/Platforms') diff --git a/vendor/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php b/vendor/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php new file mode 100644 index 0000000..770d863 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php @@ -0,0 +1,842 @@ + 0) { + $query .= sprintf(' OFFSET %d', $offset); + } + } elseif ($offset > 0) { + // 2^64-1 is the maximum of unsigned BIGINT, the biggest limit possible + $query .= sprintf(' LIMIT 18446744073709551615 OFFSET %d', $offset); + } + + return $query; + } + + public function quoteSingleIdentifier(string $str): string + { + return '`' . str_replace('`', '``', $str) . '`'; + } + + public function getRegexpExpression(): string + { + return 'RLIKE'; + } + + public function getLocateExpression(string $string, string $substring, ?string $start = null): string + { + if ($start === null) { + return sprintf('LOCATE(%s, %s)', $substring, $string); + } + + return sprintf('LOCATE(%s, %s, %s)', $substring, $string, $start); + } + + public function getConcatExpression(string ...$string): string + { + return sprintf('CONCAT(%s)', implode(', ', $string)); + } + + protected function getDateArithmeticIntervalExpression( + string $date, + string $operator, + string $interval, + DateIntervalUnit $unit, + ): string { + $function = $operator === '+' ? 'DATE_ADD' : 'DATE_SUB'; + + return $function . '(' . $date . ', INTERVAL ' . $interval . ' ' . $unit->value . ')'; + } + + public function getDateDiffExpression(string $date1, string $date2): string + { + return 'DATEDIFF(' . $date1 . ', ' . $date2 . ')'; + } + + public function getCurrentDatabaseExpression(): string + { + return 'DATABASE()'; + } + + public function getLengthExpression(string $string): string + { + return 'CHAR_LENGTH(' . $string . ')'; + } + + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListDatabasesSQL(): string + { + return 'SHOW DATABASES'; + } + + /** @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); + } + + /** + * {@inheritDoc} + */ + public function getJsonTypeDeclarationSQL(array $column): string + { + return 'JSON'; + } + + /** + * Gets the SQL snippet used to declare a CLOB column type. + * TINYTEXT : 2 ^ 8 - 1 = 255 + * TEXT : 2 ^ 16 - 1 = 65535 + * MEDIUMTEXT : 2 ^ 24 - 1 = 16777215 + * LONGTEXT : 2 ^ 32 - 1 = 4294967295 + * + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column): string + { + if (! empty($column['length']) && is_numeric($column['length'])) { + $length = $column['length']; + + if ($length <= static::LENGTH_LIMIT_TINYTEXT) { + return 'TINYTEXT'; + } + + if ($length <= static::LENGTH_LIMIT_TEXT) { + return 'TEXT'; + } + + if ($length <= static::LENGTH_LIMIT_MEDIUMTEXT) { + return 'MEDIUMTEXT'; + } + } + + return 'LONGTEXT'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column): string + { + if (isset($column['version']) && $column['version'] === true) { + return 'TIMESTAMP'; + } + + return 'DATETIME'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column): string + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column): string + { + return 'TIME'; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column): string + { + return 'TINYINT(1)'; + } + + /** + * {@inheritDoc} + * + * MySQL supports this through AUTO_INCREMENT columns. + */ + public function supportsIdentityColumns(): bool + { + return true; + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsInlineColumnComments(): bool + { + return true; + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsColumnCollation(): bool + { + return true; + } + + /** + * The SQL snippet required to elucidate a column type + * + * Returns a column type SELECT snippet string + */ + public function getColumnTypeSQLSnippet(string $tableAlias, string $databaseName): string + { + return $tableAlias . '.COLUMN_TYPE'; + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array + { + $queryFields = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && ! 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 $definition) { + $queryFields .= ', ' . $this->getIndexDeclarationSQL($definition); + } + } + + // attach all primary keys + if (isset($options['primary']) && ! empty($options['primary'])) { + $keyColumns = array_unique(array_values($options['primary'])); + $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + } + + $sql = ['CREATE']; + + if (! empty($options['temporary'])) { + $sql[] = 'TEMPORARY'; + } + + $sql[] = 'TABLE ' . $name . ' (' . $queryFields . ')'; + + $tableOptions = $this->buildTableOptions($options); + + if ($tableOptions !== '') { + $sql[] = $tableOptions; + } + + if (isset($options['partition_options'])) { + $sql[] = $options['partition_options']; + } + + $sql = [implode(' ', $sql)]; + + if (isset($options['foreignKeys'])) { + foreach ($options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $name); + } + } + + return $sql; + } + + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', null); + } + + /** + * Build SQL for table options + * + * @param mixed[] $options + */ + private function buildTableOptions(array $options): string + { + if (isset($options['table_options'])) { + return $options['table_options']; + } + + $tableOptions = []; + + if (isset($options['charset'])) { + $tableOptions[] = sprintf('DEFAULT CHARACTER SET %s', $options['charset']); + } + + if (isset($options['collation'])) { + $tableOptions[] = $this->getColumnCollationDeclarationSQL($options['collation']); + } + + if (isset($options['engine'])) { + $tableOptions[] = sprintf('ENGINE = %s', $options['engine']); + } + + // Auto increment + if (isset($options['auto_increment'])) { + $tableOptions[] = sprintf('AUTO_INCREMENT = %s', $options['auto_increment']); + } + + // Comment + if (isset($options['comment'])) { + $tableOptions[] = sprintf('COMMENT = %s ', $this->quoteStringLiteral($options['comment'])); + } + + // Row format + if (isset($options['row_format'])) { + $tableOptions[] = sprintf('ROW_FORMAT = %s', $options['row_format']); + } + + return implode(' ', $tableOptions); + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff): array + { + $columnSql = []; + $queryParts = []; + + foreach ($diff->getAddedColumns() as $column) { + $columnProperties = array_merge($column->toArray(), [ + 'comment' => $column->getComment(), + ]); + + $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL( + $column->getQuotedName($this), + $columnProperties, + ); + } + + foreach ($diff->getDroppedColumns() as $column) { + $queryParts[] = 'DROP ' . $column->getQuotedName($this); + } + + foreach ($diff->getModifiedColumns() as $columnDiff) { + $newColumn = $columnDiff->getNewColumn(); + + $newColumnProperties = array_merge($newColumn->toArray(), [ + 'comment' => $newColumn->getComment(), + ]); + + $oldColumn = $columnDiff->getOldColumn(); + + $queryParts[] = 'CHANGE ' . $oldColumn->getQuotedName($this) . ' ' + . $this->getColumnDeclarationSQL($newColumn->getQuotedName($this), $newColumnProperties); + } + + foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { + $oldColumnName = new Identifier($oldColumnName); + + $columnProperties = array_merge($column->toArray(), [ + 'comment' => $column->getComment(), + ]); + + $queryParts[] = 'CHANGE ' . $oldColumnName->getQuotedName($this) . ' ' + . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnProperties); + } + + $addedIndexes = $this->indexAssetsByLowerCaseName($diff->getAddedIndexes()); + $modifiedIndexes = $this->indexAssetsByLowerCaseName($diff->getModifiedIndexes()); + $diffModified = false; + + if (isset($addedIndexes['primary'])) { + $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_values(array_unique($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->getOldTable(), + $diff->getAddedColumns(), + $diff->getModifiedColumns(), + $diff->getDroppedColumns(), + $diff->getRenamedColumns(), + array_values($addedIndexes), + array_values($modifiedIndexes), + $diff->getDroppedIndexes(), + $diff->getRenamedIndexes(), + $diff->getAddedForeignKeys(), + $diff->getModifiedForeignKeys(), + $diff->getDroppedForeignKeys(), + ); + } + + $sql = []; + $tableSql = []; + + if (count($queryParts) > 0) { + $sql[] = 'ALTER TABLE ' . $diff->getOldTable()->getQuotedName($this) . ' ' + . implode(', ', $queryParts); + } + + $sql = array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * {@inheritDoc} + */ + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff): array + { + $sql = []; + + $tableNameSQL = $diff->getOldTable()->getQuotedName($this); + + foreach ($diff->getModifiedIndexes() as $changedIndex) { + $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $changedIndex)); + } + + foreach ($diff->getDroppedIndexes() as $droppedIndex) { + $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $droppedIndex)); + + foreach ($diff->getAddedIndexes() as $addedIndex) { + if ($droppedIndex->getColumns() !== $addedIndex->getColumns()) { + continue; + } + + $indexClause = 'INDEX ' . $addedIndex->getName(); + + if ($addedIndex->isPrimary()) { + $indexClause = 'PRIMARY KEY'; + } elseif ($addedIndex->isUnique()) { + $indexClause = 'UNIQUE INDEX ' . $addedIndex->getName(); + } + + $query = 'ALTER TABLE ' . $tableNameSQL . ' DROP INDEX ' . $droppedIndex->getName() . ', '; + $query .= 'ADD ' . $indexClause; + $query .= ' (' . implode(', ', $addedIndex->getQuotedColumns($this)) . ')'; + + $sql[] = $query; + + $diff->unsetAddedIndex($addedIndex); + $diff->unsetDroppedIndex($droppedIndex); + + break; + } + } + + return array_merge( + $sql, + $this->getPreAlterTableAlterIndexForeignKeySQL($diff), + parent::getPreAlterTableIndexForeignKeySQL($diff), + $this->getPreAlterTableRenameIndexForeignKeySQL($diff), + ); + } + + /** + * @return list + * + * @throws Exception + */ + private function getPreAlterTableAlterPrimaryKeySQL(TableDiff $diff, Index $index): array + { + if (! $index->isPrimary()) { + return []; + } + + $table = $diff->getOldTable(); + + $sql = []; + + $tableNameSQL = $table->getQuotedName($this); + + // Dropping primary keys requires to unset autoincrement attribute on the particular column first. + foreach ($index->getColumns() as $columnName) { + if (! $table->hasColumn($columnName)) { + continue; + } + + $column = $table->getColumn($columnName); + + if (! $column->getAutoincrement()) { + continue; + } + + $column->setAutoincrement(false); + + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' MODIFY ' . + $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + + // original autoincrement information might be needed later on by other parts of the table alteration + $column->setAutoincrement(true); + } + + return $sql; + } + + /** + * @param TableDiff $diff The table diff to gather the SQL for. + * + * @return list + * + * @throws Exception + */ + private function getPreAlterTableAlterIndexForeignKeySQL(TableDiff $diff): array + { + $table = $diff->getOldTable(); + + $primaryKey = $table->getPrimaryKey(); + + if ($primaryKey === null) { + return []; + } + + $primaryKeyColumns = []; + + foreach ($primaryKey->getColumns() as $columnName) { + if (! $table->hasColumn($columnName)) { + continue; + } + + $primaryKeyColumns[] = $table->getColumn($columnName); + } + + if (count($primaryKeyColumns) === 0) { + return []; + } + + $sql = []; + + $tableNameSQL = $table->getQuotedName($this); + + foreach ($diff->getModifiedIndexes() as $changedIndex) { + // Changed primary key + if (! $changedIndex->isPrimary()) { + continue; + } + + foreach ($primaryKeyColumns as $column) { + // Check if an autoincrement column was dropped from the primary key. + if (! $column->getAutoincrement() || in_array($column->getName(), $changedIndex->getColumns(), true)) { + continue; + } + + // The autoincrement attribute needs to be removed from the dropped column + // before we can drop and recreate the primary key. + $column->setAutoincrement(false); + + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' MODIFY ' . + $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + + // Restore the autoincrement attribute as it might be needed later on + // by other parts of the table alteration. + $column->setAutoincrement(true); + } + } + + return $sql; + } + + /** + * @param TableDiff $diff The table diff to gather the SQL for. + * + * @return list + */ + protected function getPreAlterTableRenameIndexForeignKeySQL(TableDiff $diff): array + { + return []; + } + + protected function getCreateIndexSQLFlags(Index $index): string + { + $type = ''; + if ($index->isUnique()) { + $type .= 'UNIQUE '; + } elseif ($index->hasFlag('fulltext')) { + $type .= 'FULLTEXT '; + } elseif ($index->hasFlag('spatial')) { + $type .= 'SPATIAL '; + } + + return $type; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column): string + { + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column): string + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column): string + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getFloatDeclarationSQL(array $column): string + { + return 'DOUBLE PRECISION' . $this->getUnsignedDeclaration($column); + } + + /** + * {@inheritDoc} + */ + public function getDecimalTypeDeclarationSQL(array $column): string + { + return parent::getDecimalTypeDeclarationSQL($column) . $this->getUnsignedDeclaration($column); + } + + /** + * Get unsigned declaration for a column. + * + * @param mixed[] $columnDef + */ + private function getUnsignedDeclaration(array $columnDef): string + { + return ! empty($columnDef['unsigned']) ? ' UNSIGNED' : ''; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column): string + { + $autoinc = ''; + if (! empty($column['autoincrement'])) { + $autoinc = ' AUTO_INCREMENT'; + } + + return $this->getUnsignedDeclaration($column) . $autoinc; + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getColumnCharsetDeclarationSQL(string $charset): string + { + return 'CHARACTER SET ' . $charset; + } + + /** @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')) { + $query .= ' MATCH ' . $foreignKey->getOption('match'); + } + + $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey); + + return $query; + } + + public function getDropIndexSQL(string $name, string $table): string + { + return 'DROP INDEX ' . $name . ' ON ' . $table; + } + + /** + * The `ALTER TABLE ... DROP CONSTRAINT` syntax is only available as of MySQL 8.0.19. + * + * @link https://dev.mysql.com/doc/refman/8.0/en/alter-table.html + */ + public function getDropUniqueConstraintSQL(string $name, string $tableName): string + { + return $this->getDropIndexSQL($name, $tableName); + } + + public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string + { + return 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + protected function initializeDoctrineTypeMappings(): void + { + $this->doctrineTypeMapping = [ + 'bigint' => Types::BIGINT, + 'binary' => Types::BINARY, + 'blob' => Types::BLOB, + 'char' => Types::STRING, + 'date' => Types::DATE_MUTABLE, + 'datetime' => Types::DATETIME_MUTABLE, + 'decimal' => Types::DECIMAL, + 'double' => Types::FLOAT, + 'float' => Types::FLOAT, + 'int' => Types::INTEGER, + 'integer' => Types::INTEGER, + 'json' => Types::JSON, + 'longblob' => Types::BLOB, + 'longtext' => Types::TEXT, + 'mediumblob' => Types::BLOB, + 'mediumint' => Types::INTEGER, + 'mediumtext' => Types::TEXT, + 'numeric' => Types::DECIMAL, + 'real' => Types::FLOAT, + 'set' => Types::SIMPLE_ARRAY, + 'smallint' => Types::SMALLINT, + 'string' => Types::STRING, + 'text' => Types::TEXT, + 'time' => Types::TIME_MUTABLE, + 'timestamp' => Types::DATETIME_MUTABLE, + 'tinyblob' => Types::BLOB, + 'tinyint' => Types::BOOLEAN, + 'tinytext' => Types::TEXT, + 'varbinary' => Types::BINARY, + 'varchar' => Types::STRING, + 'year' => Types::DATE_MUTABLE, + ]; + } + + protected function createReservedKeywordsList(): KeywordList + { + return new MySQLKeywords(); + } + + /** + * {@inheritDoc} + * + * MySQL commits a transaction implicitly when DROP TABLE is executed, however not + * if DROP TEMPORARY TABLE is executed. + */ + public function getDropTemporaryTableSQL(string $table): string + { + return 'DROP TEMPORARY TABLE ' . $table; + } + + /** + * Gets the SQL Snippet used to declare a BLOB column type. + * TINYBLOB : 2 ^ 8 - 1 = 255 + * BLOB : 2 ^ 16 - 1 = 65535 + * MEDIUMBLOB : 2 ^ 24 - 1 = 16777215 + * LONGBLOB : 2 ^ 32 - 1 = 4294967295 + * + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column): string + { + if (! empty($column['length']) && is_numeric($column['length'])) { + $length = $column['length']; + + if ($length <= static::LENGTH_LIMIT_TINYBLOB) { + return 'TINYBLOB'; + } + + if ($length <= static::LENGTH_LIMIT_BLOB) { + return 'BLOB'; + } + + if ($length <= static::LENGTH_LIMIT_MEDIUMBLOB) { + return 'MEDIUMBLOB'; + } + } + + return 'LONGBLOB'; + } + + public function quoteStringLiteral(string $str): string + { + // MySQL requires backslashes to be escaped as well. + $str = str_replace('\\', '\\\\', $str); + + return parent::quoteStringLiteral($str); + } + + public function getDefaultTransactionIsolationLevel(): TransactionIsolationLevel + { + return TransactionIsolationLevel::REPEATABLE_READ; + } + + public function supportsColumnLengthIndexes(): bool + { + return true; + } + + public function createSchemaManager(Connection $connection): MySQLSchemaManager + { + return new MySQLSchemaManager($connection, $this); + } + + /** + * @param array $assets + * + * @return array + * + * @template T of AbstractAsset + */ + private function indexAssetsByLowerCaseName(array $assets): array + { + $result = []; + + foreach ($assets as $asset) { + $result[strtolower($asset->getName())] = $asset; + } + + return $result; + } +} diff --git a/vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php b/vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php new file mode 100644 index 0000000..0beb37a --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php @@ -0,0 +1,2219 @@ +initializeDoctrineTypeMappings(); + + foreach (Type::getTypesMap() as $typeName => $className) { + foreach (Type::getType($typeName)->getMappedDatabaseTypes($this) as $dbType) { + $dbType = strtolower($dbType); + $this->doctrineTypeMapping[$dbType] = $typeName; + } + } + } + + /** + * Returns the SQL snippet used to declare a column that can + * store characters in the ASCII character set + * + * @param array $column The column definition. + */ + public function getAsciiStringTypeDeclarationSQL(array $column): string + { + return $this->getStringTypeDeclarationSQL($column); + } + + /** + * Returns the SQL snippet used to declare a string column type. + * + * @param array $column The column definition. + */ + public function getStringTypeDeclarationSQL(array $column): string + { + $length = $column['length'] ?? null; + + if (empty($column['fixed'])) { + try { + return $this->getVarcharTypeDeclarationSQLSnippet($length); + } catch (InvalidColumnType $e) { + throw InvalidColumnDeclaration::fromInvalidColumnType($column['name'], $e); + } + } + + return $this->getCharTypeDeclarationSQLSnippet($length); + } + + /** + * Returns the SQL snippet used to declare a binary string column type. + * + * @param array $column The column definition. + */ + public function getBinaryTypeDeclarationSQL(array $column): string + { + $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 to declare a GUID/UUID 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 array $column The column definition. + */ + public function getGuidTypeDeclarationSQL(array $column): string + { + $column['length'] = 36; + $column['fixed'] = true; + + return $this->getStringTypeDeclarationSQL($column); + } + + /** + * Returns the SQL snippet to declare a JSON 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 + */ + public function getJsonTypeDeclarationSQL(array $column): string + { + return $this->getClobTypeDeclarationSQL($column); + } + + /** + * @param int|null $length The length of the column in characters + * or NULL if the length should be omitted. + */ + protected function getCharTypeDeclarationSQLSnippet(?int $length): string + { + $sql = 'CHAR'; + + if ($length !== null) { + $sql .= sprintf('(%d)', $length); + } + + return $sql; + } + + /** + * @param int|null $length The length of the column in characters + * or NULL if the length should be omitted. + */ + 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 fixed length binary column type. + * + * @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. + * + * @param int|null $length The length of the column in bytes + * or NULL if the length should be omitted. + */ + protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string + { + if ($length === null) { + throw ColumnLengthRequired::new($this, 'VARBINARY'); + } + + return sprintf('VARBINARY(%d)', $length); + } + + /** + * Returns the SQL snippet used to declare a CLOB column type. + * + * @param mixed[] $column + */ + abstract public function getClobTypeDeclarationSQL(array $column): string; + + /** + * Returns the SQL Snippet used to declare a BLOB column type. + * + * @param mixed[] $column + */ + abstract public function getBlobTypeDeclarationSQL(array $column): string; + + /** + * Registers a doctrine type to be used in conjunction with a column type of this platform. + * + * @throws Exception If the type is not found. + */ + public function registerDoctrineTypeMapping(string $dbType, string $doctrineType): void + { + if ($this->doctrineTypeMapping === null) { + $this->initializeAllDoctrineTypeMappings(); + } + + if (! Types\Type::hasType($doctrineType)) { + throw TypeNotFound::new($doctrineType); + } + + $dbType = strtolower($dbType); + $this->doctrineTypeMapping[$dbType] = $doctrineType; + } + + /** + * Gets the Doctrine type that is mapped for the given database column type. + */ + public function getDoctrineTypeMapping(string $dbType): string + { + if ($this->doctrineTypeMapping === null) { + $this->initializeAllDoctrineTypeMappings(); + } + + $dbType = strtolower($dbType); + + if (! isset($this->doctrineTypeMapping[$dbType])) { + throw new InvalidArgumentException(sprintf( + 'Unknown database type "%s" requested, %s may not support it.', + $dbType, + static::class, + )); + } + + return $this->doctrineTypeMapping[$dbType]; + } + + /** + * Checks if a database type is currently supported by this platform. + */ + public function hasDoctrineTypeMappingFor(string $dbType): bool + { + if ($this->doctrineTypeMapping === null) { + $this->initializeAllDoctrineTypeMappings(); + } + + $dbType = strtolower($dbType); + + return isset($this->doctrineTypeMapping[$dbType]); + } + + /** + * Returns the regular expression operator. + */ + public function getRegexpExpression(): string + { + throw NotSupported::new(__METHOD__); + } + + /** + * Returns the SQL snippet to get the length of a text column in characters. + * + * @param string $string SQL expression producing the string. + */ + public function getLengthExpression(string $string): string + { + return 'LENGTH(' . $string . ')'; + } + + /** + * Returns the SQL snippet to get the remainder of the operation of division of dividend by divisor. + * + * @param string $dividend SQL expression producing the dividend. + * @param string $divisor SQL expression producing the divisor. + */ + public function getModExpression(string $dividend, string $divisor): string + { + return 'MOD(' . $dividend . ', ' . $divisor . ')'; + } + + /** + * 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'; + } + + $tokens[] = $str; + + return sprintf('TRIM(%s)', implode(' ', $tokens)); + } + + /** + * Returns the SQL snippet to get the position of the first occurrence of the substring in the 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. + */ + abstract public function getLocateExpression(string $string, string $substring, ?string $start = null): string; + + /** + * Returns an SQL snippet to get a substring inside the string. + * + * Note: Not SQL92, but common functionality. + * + * @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. + */ + public function getSubstringExpression(string $string, string $start, ?string $length = null): string + { + if ($length === null) { + return sprintf('SUBSTRING(%s FROM %s)', $string, $start); + } + + return sprintf('SUBSTRING(%s FROM %s FOR %s)', $string, $start, $length); + } + + /** + * Returns a SQL snippet to concatenate the given strings. + */ + public function getConcatExpression(string ...$string): string + { + return implode(' || ', $string); + } + + /** + * Returns the SQL to calculate the difference in days between the two passed dates. + * + * 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. + * + * @param string $date SQL expression producing the date. + * @param string $seconds SQL expression producing the number of seconds. + */ + public function getDateAddSecondsExpression(string $date, string $seconds): string + { + return $this->getDateArithmeticIntervalExpression($date, '+', $seconds, DateIntervalUnit::SECOND); + } + + /** + * Returns the SQL to subtract the number of given seconds from a date. + * + * @param string $date SQL expression producing the date. + * @param string $seconds SQL expression producing the number of seconds. + */ + public function getDateSubSecondsExpression(string $date, string $seconds): string + { + return $this->getDateArithmeticIntervalExpression($date, '-', $seconds, DateIntervalUnit::SECOND); + } + + /** + * Returns the SQL to add the number of given minutes to a date. + * + * @param string $date SQL expression producing the date. + * @param string $minutes SQL expression producing the number of minutes. + */ + public function getDateAddMinutesExpression(string $date, string $minutes): string + { + return $this->getDateArithmeticIntervalExpression($date, '+', $minutes, DateIntervalUnit::MINUTE); + } + + /** + * Returns the SQL to subtract the number of given minutes from a date. + * + * @param string $date SQL expression producing the date. + * @param string $minutes SQL expression producing the number of minutes. + */ + public function getDateSubMinutesExpression(string $date, string $minutes): string + { + return $this->getDateArithmeticIntervalExpression($date, '-', $minutes, DateIntervalUnit::MINUTE); + } + + /** + * Returns the SQL to add the number of given hours to a date. + * + * @param string $date SQL expression producing the date. + * @param string $hours SQL expression producing the number of hours. + */ + public function getDateAddHourExpression(string $date, string $hours): string + { + return $this->getDateArithmeticIntervalExpression($date, '+', $hours, DateIntervalUnit::HOUR); + } + + /** + * Returns the SQL to subtract the number of given hours to a date. + * + * @param string $date SQL expression producing the date. + * @param string $hours SQL expression producing the number of hours. + */ + public function getDateSubHourExpression(string $date, string $hours): string + { + return $this->getDateArithmeticIntervalExpression($date, '-', $hours, DateIntervalUnit::HOUR); + } + + /** + * Returns the SQL to add the number of given days to a date. + * + * @param string $date SQL expression producing the date. + * @param string $days SQL expression producing the number of days. + */ + public function getDateAddDaysExpression(string $date, string $days): string + { + return $this->getDateArithmeticIntervalExpression($date, '+', $days, DateIntervalUnit::DAY); + } + + /** + * Returns the SQL to subtract the number of given days to a date. + * + * @param string $date SQL expression producing the date. + * @param string $days SQL expression producing the number of days. + */ + public function getDateSubDaysExpression(string $date, string $days): string + { + return $this->getDateArithmeticIntervalExpression($date, '-', $days, DateIntervalUnit::DAY); + } + + /** + * Returns the SQL to add the number of given weeks to a date. + * + * @param string $date SQL expression producing the date. + * @param string $weeks SQL expression producing the number of weeks. + */ + public function getDateAddWeeksExpression(string $date, string $weeks): string + { + return $this->getDateArithmeticIntervalExpression($date, '+', $weeks, DateIntervalUnit::WEEK); + } + + /** + * Returns the SQL to subtract the number of given weeks from a date. + * + * @param string $date SQL expression producing the date. + * @param string $weeks SQL expression producing the number of weeks. + */ + public function getDateSubWeeksExpression(string $date, string $weeks): string + { + return $this->getDateArithmeticIntervalExpression($date, '-', $weeks, DateIntervalUnit::WEEK); + } + + /** + * Returns the SQL to add the number of given months to a date. + * + * @param string $date SQL expression producing the date. + * @param string $months SQL expression producing the number of months. + */ + public function getDateAddMonthExpression(string $date, string $months): string + { + return $this->getDateArithmeticIntervalExpression($date, '+', $months, DateIntervalUnit::MONTH); + } + + /** + * Returns the SQL to subtract the number of given months to a date. + * + * @param string $date SQL expression producing the date. + * @param string $months SQL expression producing the number of months. + */ + public function getDateSubMonthExpression(string $date, string $months): string + { + return $this->getDateArithmeticIntervalExpression($date, '-', $months, DateIntervalUnit::MONTH); + } + + /** + * Returns the SQL to add the number of given quarters to a date. + * + * @param string $date SQL expression producing the date. + * @param string $quarters SQL expression producing the number of quarters. + */ + public function getDateAddQuartersExpression(string $date, string $quarters): string + { + return $this->getDateArithmeticIntervalExpression($date, '+', $quarters, DateIntervalUnit::QUARTER); + } + + /** + * Returns the SQL to subtract the number of given quarters from a date. + * + * @param string $date SQL expression producing the date. + * @param string $quarters SQL expression producing the number of quarters. + */ + public function getDateSubQuartersExpression(string $date, string $quarters): string + { + return $this->getDateArithmeticIntervalExpression($date, '-', $quarters, DateIntervalUnit::QUARTER); + } + + /** + * Returns the SQL to add the number of given years to a date. + * + * @param string $date SQL expression producing the date. + * @param string $years SQL expression producing the number of years. + */ + public function getDateAddYearsExpression(string $date, string $years): string + { + return $this->getDateArithmeticIntervalExpression($date, '+', $years, DateIntervalUnit::YEAR); + } + + /** + * Returns the SQL to subtract the number of given years from a date. + * + * @param string $date SQL expression producing the date. + * @param string $years SQL expression producing the number of years. + */ + public function getDateSubYearsExpression(string $date, string $years): string + { + return $this->getDateArithmeticIntervalExpression($date, '-', $years, DateIntervalUnit::YEAR); + } + + /** + * Returns the SQL for a date arithmetic expression. + * + * @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. + */ + abstract protected function getDateArithmeticIntervalExpression( + string $date, + string $operator, + string $interval, + DateIntervalUnit $unit, + ): string; + + /** + * 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 SQL expression producing the first value. + * @param string $value2 SQL expression producing the second value. + */ + public function getBitAndComparisonExpression(string $value1, string $value2): string + { + return '(' . $value1 . ' & ' . $value2 . ')'; + } + + /** + * Returns the SQL bit OR comparison expression. + * + * @param string $value1 SQL expression producing the first value. + * @param string $value2 SQL expression producing the second value. + */ + public function getBitOrComparisonExpression(string $value1, string $value2): string + { + return '(' . $value1 . ' | ' . $value2 . ')'; + } + + /** + * Returns the SQL expression which represents the currently selected database. + */ + abstract public function getCurrentDatabaseExpression(): string; + + /** + * 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 + */ + public function appendLockHint(string $fromClause, LockMode $lockMode): string + { + return $fromClause; + } + + /** + * Returns the SQL snippet to drop an existing table. + */ + public function getDropTableSQL(string $table): string + { + return 'DROP TABLE ' . $table; + } + + /** + * Returns the SQL to safely drop a temporary table WITHOUT implicitly committing an open transaction. + */ + public function getDropTemporaryTableSQL(string $table): string + { + return $this->getDropTableSQL($table); + } + + /** + * Returns the SQL to drop an index from a table. + */ + public function getDropIndexSQL(string $name, string $table): string + { + return 'DROP INDEX ' . $name; + } + + /** + * Returns the SQL to drop a constraint. + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + protected function getDropConstraintSQL(string $name, string $table): string + { + return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $name; + } + + /** + * Returns the SQL to drop a foreign key. + */ + public function getDropForeignKeySQL(string $foreignKey, string $table): string + { + 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. + * + * @return list The list of SQL statements. + */ + public function getCreateTableSQL(Table $table): array + { + return $this->buildCreateTableSQL($table, true); + } + + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', 'SKIP LOCKED'); + } + + /** + * @internal + * + * @return list + */ + final protected function getCreateTableWithoutForeignKeysSQL(Table $table): array + { + return $this->buildCreateTableSQL($table, false); + } + + /** @return list */ + private function buildCreateTableSQL(Table $table, bool $createForeignKeys): array + { + if (count($table->getColumns()) === 0) { + throw NoColumnsSpecifiedForTable::new($table->getName()); + } + + $tableName = $table->getQuotedName($this); + $options = $table->getOptions(); + $options['uniqueConstraints'] = []; + $options['indexes'] = []; + $options['primary'] = []; + + 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) { + $columnData = $this->columnToArray($column); + + if (in_array($column->getName(), $options['primary'], true)) { + $columnData['primary'] = true; + } + + $columns[] = $columnData; + } + + $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 = $column->getComment(); + + if ($comment === '') { + continue; + } + + $sql[] = $this->getCommentOnColumnSQL($tableName, $column->getQuotedName($this), $comment); + } + } + + return array_merge($sql, $columnSql); + } + + /** + * @param array $tables + * + * @return list + */ + 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 array
$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($comment), + ); + } + + /** @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 sprintf( + 'COMMENT ON COLUMN %s.%s IS %s', + $tableName->getQuotedName($this), + $columnName->getQuotedName($this), + $this->quoteStringLiteral($comment), + ); + } + + /** + * Returns the SQL to create inline comment on a column. + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getInlineColumnCommentSQL(string $comment): string + { + if (! $this->supportsInlineColumnComments()) { + throw NotSupported::new(__METHOD__); + } + + return 'COMMENT ' . $this->quoteStringLiteral($comment); + } + + /** + * Returns the SQL used to create a table. + * + * @param mixed[][] $columns + * @param mixed[] $options + * + * @return array + */ + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array + { + $columnListSql = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $definition) { + $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($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($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; + } + + public function getCreateTemporaryTableSnippetSQL(): string + { + return 'CREATE TEMPORARY TABLE'; + } + + /** + * Generates SQL statements that can be used to apply the diff. + * + * @return list + */ + public function getAlterSchemaSQL(SchemaDiff $diff): array + { + $sql = []; + + if ($this->supportsSchemas()) { + foreach ($diff->getCreatedSchemas() as $schema) { + $sql[] = $this->getCreateSchemaSQL($schema); + } + } + + if ($this->supportsSequences()) { + foreach ($diff->getAlteredSequences() as $sequence) { + $sql[] = $this->getAlterSequenceSQL($sequence); + } + + foreach ($diff->getDroppedSequences() as $sequence) { + $sql[] = $this->getDropSequenceSQL($sequence->getQuotedName($this)); + } + + foreach ($diff->getCreatedSequences() as $sequence) { + $sql[] = $this->getCreateSequenceSQL($sequence); + } + } + + $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 SQL to create a sequence on this platform. + */ + public function getCreateSequenceSQL(Sequence $sequence): string + { + throw NotSupported::new(__METHOD__); + } + + /** + * Returns the SQL to change a sequence on this platform. + */ + public function getAlterSequenceSQL(Sequence $sequence): string + { + throw NotSupported::new(__METHOD__); + } + + /** + * Returns the SQL snippet to drop an existing sequence. + */ + public function getDropSequenceSQL(string $name): string + { + if (! $this->supportsSequences()) { + throw NotSupported::new(__METHOD__); + } + + return 'DROP SEQUENCE ' . $name; + } + + /** + * Returns the SQL to create an index on a table on this platform. + */ + 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); + } + + $query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table; + $query .= ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index); + + return $query; + } + + /** + * Adds condition for partial index. + */ + protected function getPartialIndexSQL(Index $index): string + { + if ($this->supportsPartialIndexes() && $index->hasOption('where')) { + return ' WHERE ' . $index->getOption('where'); + } + + return ''; + } + + /** + * Adds additional flags for index generation. + */ + protected function getCreateIndexSQLFlags(Index $index): string + { + return $index->isUnique() ? 'UNIQUE ' : ''; + } + + /** + * Returns the SQL to create an unnamed primary key constraint. + */ + public function getCreatePrimaryKeySQL(Index $index, string $table): string + { + return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY (' . implode(', ', $index->getQuotedColumns($this)) . ')'; + } + + /** + * Returns the SQL to create a named schema. + */ + public function getCreateSchemaSQL(string $schemaName): string + { + if (! $this->supportsSchemas()) { + throw NotSupported::new(__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 'ALTER TABLE ' . $tableName . ' ADD CONSTRAINT ' . $constraint->getQuotedName($this) . ' UNIQUE' + . ' (' . implode(', ', $constraint->getQuotedColumns($this)) . ')'; + } + + /** + * Returns the SQL snippet to drop a schema. + */ + public function getDropSchemaSQL(string $schemaName): string + { + if (! $this->supportsSchemas()) { + throw NotSupported::new(__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 $identifier The identifier name to be quoted. + * + * @return string The quoted identifier string. + */ + public function quoteIdentifier(string $identifier): string + { + if (str_contains($identifier, '.')) { + $parts = array_map($this->quoteSingleIdentifier(...), explode('.', $identifier)); + + return implode('.', $parts); + } + + return $this->quoteSingleIdentifier($identifier); + } + + /** + * 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(string $str): string + { + return '"' . str_replace('"', '""', $str) . '"'; + } + + /** + * Returns the SQL to create a new foreign key. + * + * @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. + * + * This method returns an array of SQL statements, since some platforms need several statements. + * + * @return list + */ + abstract public function getAlterTableSQL(TableDiff $diff): array; + + public function getRenameTableSQL(string $oldName, string $newName): string + { + 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 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 list The sequence of SQL statements for renaming the given index. + */ + protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array + { + return [ + $this->getDropIndexSQL($oldIndexName, $tableName), + $this->getCreateIndexSQL($index, $tableName), + ]; + } + + /** + * Gets declaration of a number of columns in bulk. + * + * @param mixed[][] $columns A multidimensional array. + * The first dimension determines the ordinal position of the column, + * 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. + */ + public function getColumnDeclarationListSQL(array $columns): string + { + $declarations = []; + + foreach ($columns as $column) { + $declarations[] = $this->getColumnDeclarationSQL($column['name'], $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. + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + * + * @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. + * columnDefinition + * a string that defines the complete column + * + * @return string DBMS specific SQL code portion that should be used to declare the column. + */ + public function getColumnDeclarationSQL(string $name, array $column): string + { + if (isset($column['columnDefinition'])) { + $declaration = $column['columnDefinition']; + } 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' : ''; + + $typeDecl = $column['type']->getSQLDeclaration($column, $this); + $declaration = $typeDecl . $charset . $default . $notnull . $collation; + + if ($this->supportsInlineColumnComments() && isset($column['comment']) && $column['comment'] !== '') { + $declaration .= ' ' . $this->getInlineColumnCommentSQL($column['comment']); + } + } + + return $name . ' ' . $declaration; + } + + /** + * Returns the SQL snippet that declares a floating point column of arbitrary precision. + * + * @param mixed[] $column + */ + public function getDecimalTypeDeclarationSQL(array $column): string + { + if (! isset($column['precision'])) { + $e = ColumnPrecisionRequired::new(); + } elseif (! isset($column['scale'])) { + $e = ColumnScaleRequired::new(); + } else { + $e = null; + } + + if ($e !== null) { + throw InvalidColumnDeclaration::fromInvalidColumnType($column['name'], $e); + } + + return 'NUMERIC(' . $column['precision'] . ', ' . $column['scale'] . ')'; + } + + /** + * Obtains DBMS specific SQL code portion needed to set a default value + * declaration to be used in statements like CREATE TABLE. + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + * + * @param mixed[] $column The column definition array. + * + * @return string DBMS specific SQL code portion needed to set a default value. + */ + public function getDefaultValueDeclarationSQL(array $column): string + { + if (! isset($column['default'])) { + return empty($column['notnull']) ? ' DEFAULT NULL' : ''; + } + + $default = $column['default']; + + 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()) { + return ' DEFAULT ' . $this->getCurrentTimestampSQL(); + } + + if ($type instanceof Types\PhpTimeMappingType && $default === $this->getCurrentTimeSQL()) { + return ' DEFAULT ' . $this->getCurrentTimeSQL(); + } + + if ($type instanceof Types\PhpDateMappingType && $default === $this->getCurrentDateSQL()) { + 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); + } + + /** + * 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. + * + * @return string DBMS specific SQL code portion needed to set a CHECK constraint. + */ + public function getCheckDeclarationSQL(array $definition): string + { + $constraints = []; + foreach ($definition as $def) { + if (is_string($def)) { + $constraints[] = 'CHECK (' . $def . ')'; + } else { + if (isset($def['min'])) { + $constraints[] = 'CHECK (' . $def['name'] . ' >= ' . $def['min'] . ')'; + } + + if (! isset($def['max'])) { + continue; + } + + $constraints[] = 'CHECK (' . $def['name'] . ' <= ' . $def['max'] . ')'; + } + } + + return implode(', ', $constraints); + } + + /** + * Obtains DBMS specific SQL code portion needed to set a unique + * constraint declaration to be used in statements like CREATE TABLE. + * + * @param UniqueConstraint $constraint The unique constraint definition. + * + * @return string DBMS specific SQL code portion needed to set a constraint. + */ + public function getUniqueConstraintDeclarationSQL(UniqueConstraint $constraint): string + { + $columns = $constraint->getColumns(); + + if (count($columns) === 0) { + throw new InvalidArgumentException('Incomplete definition. "columns" required.'); + } + + $chunks = ['CONSTRAINT']; + + if ($constraint->getName() !== '') { + $chunks[] = $constraint->getQuotedName($this); + } + + $chunks[] = 'UNIQUE'; + + if ($constraint->hasFlag('clustered')) { + $chunks[] = 'CLUSTERED'; + } + + $chunks[] = sprintf('(%s)', implode(', ', $columns)); + + return implode(' ', $chunks); + } + + /** + * Obtains DBMS specific SQL code portion needed to set an index + * declaration to be used in statements like CREATE TABLE. + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + * + * @param Index $index The index definition. + * + * @return string DBMS specific SQL code portion needed to set an index. + */ + public function getIndexDeclarationSQL(Index $index): string + { + $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); + } + + /** + * Some vendors require temporary table names to be qualified specially. + */ + public function getTemporaryTableName(string $tableName): string + { + return $tableName; + } + + /** + * 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. + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + * + * @return string DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a column declaration. + */ + public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey): string + { + $sql = $this->getForeignKeyBaseDeclarationSQL($foreignKey); + $sql .= $this->getAdvancedForeignKeyOptionsSQL($foreignKey); + + return $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. + * + * @param ForeignKeyConstraint $foreignKey The foreign key definition. + */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey): string + { + $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; + } + + /** + * Returns the given referential action in uppercase if valid, otherwise throws an exception. + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + * + * @param string $action The foreign key referential action. + */ + public function getForeignKeyReferentialActionSQL(string $action): string + { + $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)), + }; + } + + /** + * 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. + */ + public function getForeignKeyBaseDeclarationSQL(ForeignKeyConstraint $foreignKey): string + { + $sql = ''; + if ($foreignKey->getName() !== '') { + $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)) . ')'; + } + + /** + * Obtains DBMS specific SQL code portion needed to set the CHARACTER SET + * of a column declaration to be used in statements like CREATE TABLE. + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + * + * @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. + */ + public function getColumnCharsetDeclarationSQL(string $charset): string + { + return ''; + } + + /** + * Obtains DBMS specific SQL code portion needed to set the COLLATION + * of a column declaration to be used in statements like CREATE TABLE. + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + * + * @param string $collation The name of the collation. + * + * @return string DBMS specific SQL code portion needed to set the COLLATION + * of a column declaration. + */ + public function getColumnCollationDeclarationSQL(string $collation): string + { + return $this->supportsColumnCollation() ? 'COLLATE ' . $this->quoteSingleIdentifier($collation) : ''; + } + + /** + * 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. + * + * 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 mixed A boolean database value or an array of them. + */ + public function convertBooleans(mixed $item): mixed + { + if (is_array($item)) { + foreach ($item as $k => $value) { + if (! is_bool($value)) { + continue; + } + + $item[$k] = (int) $value; + } + } elseif (is_bool($item)) { + $item = (int) $item; + } + + return $item; + } + + /** + * 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 + * + * @return (T is null ? null : bool) + * + * @template T + */ + public function convertFromBoolean(mixed $item): ?bool + { + if ($item === null) { + return null; + } + + return (bool) $item; + } + + /** + * This method should handle the prepared statements case. When there is no + * distinction, it's OK to use the same method. + * + * Note: if the input is not a boolean the original input might be returned. + * + * @param mixed $item A boolean or an array of them. + * + * @return mixed A boolean database value or an array of them. + */ + public function convertBooleansToDatabaseValue(mixed $item): mixed + { + return $this->convertBooleans($item); + } + + /** + * Returns the SQL specific for the platform to get the current date. + */ + public function getCurrentDateSQL(): string + { + return 'CURRENT_DATE'; + } + + /** + * Returns the SQL specific for the platform to get the current time. + */ + public function getCurrentTimeSQL(): string + { + return 'CURRENT_TIME'; + } + + /** + * Returns the SQL specific for the platform to get the current timestamp + */ + public function getCurrentTimestampSQL(): string + { + return 'CURRENT_TIMESTAMP'; + } + + /** + * Returns the SQL for a given transaction isolation level Connection constant. + */ + protected function _getTransactionIsolationLevelSQL(TransactionIsolationLevel $level): string + { + return match ($level) { + TransactionIsolationLevel::READ_UNCOMMITTED => 'READ UNCOMMITTED', + TransactionIsolationLevel::READ_COMMITTED => 'READ COMMITTED', + TransactionIsolationLevel::REPEATABLE_READ => 'REPEATABLE READ', + TransactionIsolationLevel::SERIALIZABLE => 'SERIALIZABLE', + }; + } + + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListDatabasesSQL(): string + { + throw NotSupported::new(__METHOD__); + } + + /** @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 SQL to list all views of a database or user. + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + */ + abstract public function getListViewsSQL(string $database): string; + + public function getCreateViewSQL(string $name, string $sql): string + { + return 'CREATE VIEW ' . $name . ' AS ' . $sql; + } + + public function getDropViewSQL(string $name): string + { + return 'DROP VIEW ' . $name; + } + + public function getSequenceNextValSQL(string $sequence): string + { + throw NotSupported::new(__METHOD__); + } + + /** + * Returns the SQL to create a new database. + * + * @param string $name The name of the database that should be created. + */ + public function getCreateDatabaseSQL(string $name): string + { + return 'CREATE DATABASE ' . $name; + } + + /** + * Returns the SQL snippet to drop an existing database. + * + * @param string $name The name of the database that should be dropped. + */ + public function getDropDatabaseSQL(string $name): string + { + return 'DROP DATABASE ' . $name; + } + + /** + * Returns the SQL to set the transaction isolation level. + */ + abstract public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string; + + /** + * Obtains DBMS specific SQL to be used to create datetime columns in + * statements like CREATE TABLE. + * + * @param mixed[] $column + */ + abstract public function getDateTimeTypeDeclarationSQL(array $column): string; + + /** + * Obtains DBMS specific SQL to be used to create datetime with timezone offset columns. + * + * @param mixed[] $column + */ + public function getDateTimeTzTypeDeclarationSQL(array $column): string + { + return $this->getDateTimeTypeDeclarationSQL($column); + } + + /** + * Obtains DBMS specific SQL to be used to create date columns in statements + * like CREATE TABLE. + * + * @param mixed[] $column + */ + abstract public function getDateTypeDeclarationSQL(array $column): string; + + /** + * Obtains DBMS specific SQL to be used to create time columns in statements + * like CREATE TABLE. + * + * @param mixed[] $column + */ + abstract public function getTimeTypeDeclarationSQL(array $column): string; + + /** @param mixed[] $column */ + public function getFloatDeclarationSQL(array $column): string + { + return 'DOUBLE PRECISION'; + } + + /** + * Gets the default transaction isolation level of the platform. + * + * @return TransactionIsolationLevel The default isolation level. + */ + public function getDefaultTransactionIsolationLevel(): TransactionIsolationLevel + { + return TransactionIsolationLevel::READ_COMMITTED; + } + + /* supports*() methods */ + + /** + * Whether the platform supports sequences. + */ + public function supportsSequences(): bool + { + return false; + } + + /** + * Whether the platform supports identity columns. + * + * Identity columns are columns that receive an auto-generated value from the + * database on insert of a row. + */ + public function supportsIdentityColumns(): bool + { + return false; + } + + /** + * Whether the platform supports partial indexes. + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function supportsPartialIndexes(): bool + { + return false; + } + + /** + * Whether the platform supports indexes with column length definitions. + */ + public function supportsColumnLengthIndexes(): bool + { + return false; + } + + /** + * Whether the platform supports savepoints. + */ + public function supportsSavepoints(): bool + { + return true; + } + + /** + * Whether the platform supports releasing savepoints. + */ + public function supportsReleaseSavepoints(): bool + { + return $this->supportsSavepoints(); + } + + /** + * Whether the platform supports database schemas. + */ + public function supportsSchemas(): bool + { + return false; + } + + /** + * Whether this platform support to add inline column comments as postfix. + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function supportsInlineColumnComments(): bool + { + return false; + } + + /** + * Whether this platform support the proprietary syntax "COMMENT ON asset". + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function supportsCommentOnStatement(): bool + { + return false; + } + + /** + * Does this platform support column collation? + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function supportsColumnCollation(): bool + { + return false; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored datetime value of this platform. + * + * @return string The format string. + */ + public function getDateTimeFormatString(): string + { + return 'Y-m-d H:i:s'; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored datetime with timezone value of this platform. + * + * @return string The format string. + */ + public function getDateTimeTzFormatString(): string + { + return 'Y-m-d H:i:s'; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored date value of this platform. + * + * @return string The format string. + */ + public function getDateFormatString(): string + { + return 'Y-m-d'; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored time value of this platform. + * + * @return string The format string. + */ + public function getTimeFormatString(): string + { + return 'H:i:s'; + } + + /** + * Adds an driver-specific LIMIT clause to the query. + */ + final public function modifyLimitQuery(string $query, ?int $limit, int $offset = 0): string + { + if ($offset < 0) { + throw new InvalidArgumentException(sprintf( + 'Offset must be a positive integer or zero, %d given.', + $offset, + )); + } + + return $this->doModifyLimitQuery($query, $limit, $offset); + } + + /** + * Adds an platform-specific LIMIT clause to the query. + */ + protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string + { + if ($limit !== null) { + $query .= sprintf(' LIMIT %d', $limit); + } + + if ($offset > 0) { + $query .= sprintf(' OFFSET %d', $offset); + } + + return $query; + } + + /** + * Maximum length of any given database identifier, like tables or column names. + */ + public function getMaxIdentifierLength(): int + { + return 63; + } + + /** + * Returns the insert SQL for an empty insert statement. + */ + public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string + { + return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (null)'; + } + + /** + * Generates a Truncate Table SQL statement for a given table. + * + * Cascade is not supported on many platforms but would optionally cascade the truncate by + * following the foreign keys. + */ + public function getTruncateTableSQL(string $tableName, bool $cascade = false): string + { + $tableIdentifier = new Identifier($tableName); + + return 'TRUNCATE ' . $tableIdentifier->getQuotedName($this); + } + + /** + * This is for test reasons, many vendors have special requirements for dummy statements. + */ + public function getDummySelectSQL(string $expression = '1'): string + { + return sprintf('SELECT %s', $expression); + } + + /** + * Returns the SQL to create a new savepoint. + */ + public function createSavePoint(string $savepoint): string + { + return 'SAVEPOINT ' . $savepoint; + } + + /** + * Returns the SQL to release a savepoint. + */ + public function releaseSavePoint(string $savepoint): string + { + return 'RELEASE SAVEPOINT ' . $savepoint; + } + + /** + * Returns the SQL to rollback a savepoint. + */ + public function rollbackSavePoint(string $savepoint): string + { + return 'ROLLBACK TO SAVEPOINT ' . $savepoint; + } + + /** + * Returns the keyword list instance of this platform. + */ + final public function getReservedKeywordsList(): KeywordList + { + // Store the instance so it doesn't need to be generated on every request. + return $this->_keywords ??= $this->createReservedKeywordsList(); + } + + /** + * Creates an instance of the reserved keyword list of this platform. + */ + abstract protected function createReservedKeywordsList(): KeywordList; + + /** + * Quotes a literal string. + * This method is NOT meant to fix SQL injections! + * It is only meant to escape this platform's string literal + * quote character inside the given literal string. + * + * @param string $str The literal string to be quoted. + * + * @return string The quoted literal string. + */ + public function quoteStringLiteral(string $str): string + { + return "'" . str_replace("'", "''", $str) . "'"; + } + + /** + * Escapes metacharacters in a string intended to be used with a LIKE + * operator. + * + * @param string $inputString a literal, unquoted string + * @param string $escapeChar should be reused by the caller in the LIKE + * expression. + */ + final public function escapeStringForLike(string $inputString, string $escapeChar): string + { + $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. + */ + private function columnToArray(Column $column): array + { + return array_merge($column->toArray(), [ + 'name' => $column->getQuotedName($this), + 'version' => $column->hasPlatformOption('version') ? $column->getPlatformOption('version') : false, + 'comment' => $column->getComment(), + ]); + } + + /** @internal */ + public function createSQLParser(): Parser + { + return new Parser(false); + } + + protected function getLikeWildcardCharacters(): string + { + return '%_'; + } + + /** + * Compares the definitions of the given columns in the context of this platform. + */ + public function columnsEqual(Column $column1, Column $column2): bool + { + $column1Array = $this->columnToArray($column1); + $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']); + + if ( + $this->getColumnDeclarationSQL('', $column1Array) + !== $this->getColumnDeclarationSQL('', $column2Array) + ) { + return false; + } + + // If the platform supports inline comments, all comparison is already done above + if ($this->supportsInlineColumnComments()) { + return true; + } + + return $column1->getComment() === $column2->getComment(); + } + + /** + * 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/vendor/doctrine/dbal/src/Platforms/DB2Platform.php b/vendor/doctrine/dbal/src/Platforms/DB2Platform.php new file mode 100644 index 0000000..a00b24b --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/DB2Platform.php @@ -0,0 +1,593 @@ +doctrineTypeMapping = [ + 'bigint' => Types::BIGINT, + 'binary' => Types::BINARY, + 'blob' => Types::BLOB, + 'character' => Types::STRING, + 'clob' => Types::TEXT, + 'date' => Types::DATE_MUTABLE, + 'decimal' => Types::DECIMAL, + 'double' => Types::FLOAT, + 'integer' => Types::INTEGER, + 'real' => Types::FLOAT, + 'smallint' => Types::SMALLINT, + 'time' => Types::TIME_MUTABLE, + 'timestamp' => Types::DATETIME_MUTABLE, + 'varbinary' => Types::BINARY, + 'varchar' => Types::STRING, + ]; + } + + protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string + { + return $this->getCharTypeDeclarationSQLSnippet($length) . ' FOR BIT DATA'; + } + + protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string + { + return $this->getVarcharTypeDeclarationSQLSnippet($length) . ' FOR BIT DATA'; + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column): string + { + // todo clob(n) with $column['length']; + return 'CLOB(1M)'; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column): string + { + return 'SMALLINT'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column): string + { + return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column): string + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column): string + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column): string + { + $autoinc = ''; + if (! empty($column['autoincrement'])) { + $autoinc = ' GENERATED BY DEFAULT AS IDENTITY'; + } + + return $autoinc; + } + + public function getBitAndComparisonExpression(string $value1, string $value2): string + { + return 'BITAND(' . $value1 . ', ' . $value2 . ')'; + } + + public function getBitOrComparisonExpression(string $value1, string $value2): string + { + return 'BITOR(' . $value1 . ', ' . $value2 . ')'; + } + + 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 $date . ' ' . $operator . ' ' . $interval . ' ' . $unit->value; + } + + public function getDateDiffExpression(string $date1, string $date2): string + { + return 'DAYS(' . $date1 . ') - DAYS(' . $date2 . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column): string + { + if (isset($column['version']) && $column['version'] === true) { + return 'TIMESTAMP(0) WITH DEFAULT'; + } + + return 'TIMESTAMP(0)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column): string + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column): string + { + return 'TIME'; + } + + public function getTruncateTableSQL(string $tableName, bool $cascade = false): string + { + $tableIdentifier = new Identifier($tableName); + + return 'TRUNCATE ' . $tableIdentifier->getQuotedName($this) . ' IMMEDIATE'; + } + + public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string + { + throw NotSupported::new(__METHOD__); + } + + /** @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'; + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsCommentOnStatement(): bool + { + return true; + } + + public function getCurrentDateSQL(): string + { + return 'CURRENT DATE'; + } + + public function getCurrentTimeSQL(): string + { + return 'CURRENT TIME'; + } + + public function getCurrentTimestampSQL(): string + { + return 'CURRENT TIMESTAMP'; + } + + /** @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 NotSupported::new(__METHOD__); + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array + { + $indexes = []; + if (isset($options['indexes'])) { + $indexes = $options['indexes']; + } + + $options['indexes'] = []; + + $sqls = parent::_getCreateTableSQL($name, $columns, $options); + + foreach ($indexes as $definition) { + $sqls[] = $this->getCreateIndexSQL($definition, $name); + } + + return $sqls; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff): array + { + $sql = []; + $columnSql = []; + $commentsSQL = []; + + $tableNameSQL = $diff->getOldTable()->getQuotedName($this); + + $queryParts = []; + foreach ($diff->getAddedColumns() as $column) { + $columnDef = $column->toArray(); + $queryPart = 'ADD COLUMN ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef); + + // Adding non-nullable columns to a table requires a default value to be specified. + if ( + ! empty($columnDef['notnull']) && + ! isset($columnDef['default']) && + empty($columnDef['autoincrement']) + ) { + $queryPart .= ' WITH DEFAULT'; + } + + $queryParts[] = $queryPart; + + $comment = $column->getComment(); + + if ($comment === '') { + continue; + } + + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $column->getQuotedName($this), + $comment, + ); + } + + foreach ($diff->getDroppedColumns() as $column) { + $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); + } + + foreach ($diff->getModifiedColumns() as $columnDiff) { + if ($columnDiff->hasCommentChanged()) { + $newColumn = $columnDiff->getNewColumn(); + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $newColumn->getQuotedName($this), + $newColumn->getComment(), + ); + } + + $this->gatherAlterColumnSQL( + $tableNameSQL, + $columnDiff, + $sql, + $queryParts, + ); + } + + foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { + $oldColumnName = new Identifier($oldColumnName); + + $queryParts[] = 'RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . + ' TO ' . $column->getQuotedName($this); + } + + 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( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $commentsSQL, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); + + return array_merge($sql, $columnSql); + } + + public function getRenameTableSQL(string $oldName, string $newName): string + { + 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 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, + ): void { + $alterColumnClauses = $this->getAlterColumnClausesSQL($columnDiff); + + if (empty($alterColumnClauses)) { + return; + } + + // If we have a single column alteration, we can append the clause to the main query. + if (count($alterColumnClauses) === 1) { + $queryParts[] = current($alterColumnClauses); + + return; + } + + // We have multiple alterations for the same column, + // so we need to trigger a complete ALTER TABLE statement + // for each ALTER COLUMN clause. + foreach ($alterColumnClauses as $alterColumnClause) { + $sql[] = 'ALTER TABLE ' . $table . ' ' . $alterColumnClause; + } + } + + /** + * Returns the ALTER COLUMN SQL clauses for altering a column described by the given column diff. + * + * @return string[] + */ + private function getAlterColumnClausesSQL(ColumnDiff $columnDiff): array + { + $newColumn = $columnDiff->getNewColumn()->toArray(); + + $alterClause = 'ALTER COLUMN ' . $columnDiff->getNewColumn()->getQuotedName($this); + + if ($newColumn['columnDefinition'] !== null) { + return [$alterClause . ' ' . $newColumn['columnDefinition']]; + } + + $clauses = []; + + if ( + $columnDiff->hasTypeChanged() || + $columnDiff->hasLengthChanged() || + $columnDiff->hasPrecisionChanged() || + $columnDiff->hasScaleChanged() || + $columnDiff->hasFixedChanged() + ) { + $clauses[] = $alterClause . ' SET DATA TYPE ' . $newColumn['type']->getSQLDeclaration($newColumn, $this); + } + + if ($columnDiff->hasNotNullChanged()) { + $clauses[] = $newColumn['notnull'] ? $alterClause . ' SET NOT NULL' : $alterClause . ' DROP NOT NULL'; + } + + if ($columnDiff->hasDefaultChanged()) { + if (isset($newColumn['default'])) { + $defaultClause = $this->getDefaultValueDeclarationSQL($newColumn); + + if ($defaultClause !== '') { + $clauses[] = $alterClause . ' SET' . $defaultClause; + } + } else { + $clauses[] = $alterClause . ' DROP DEFAULT'; + } + } + + return $clauses; + } + + /** + * {@inheritDoc} + */ + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff): array + { + $sql = []; + + $tableNameSQL = $diff->getOldTable()->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->getQuotedName($this), $tableNameSQL); + } + + $sql[] = $this->getCreateIndexSQL($addedIndex, $tableNameSQL); + + $diff->unsetAddedIndex($addedIndex); + $diff->unsetDroppedIndex($droppedIndex); + + break; + } + } + + return array_merge($sql, parent::getPreAlterTableIndexForeignKeySQL($diff)); + } + + /** + * {@inheritDoc} + */ + protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array + { + if (str_contains($tableName, '.')) { + [$schema] = explode('.', $tableName); + $oldIndexName = $schema . '.' . $oldIndexName; + } + + return ['RENAME INDEX ' . $oldIndexName . ' TO ' . $index->getQuotedName($this)]; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getDefaultValueDeclarationSQL(array $column): string + { + if (! empty($column['autoincrement'])) { + return ''; + } + + if (! empty($column['version'])) { + if ((string) $column['type'] !== 'DateTime') { + $column['default'] = '1'; + } + } + + return parent::getDefaultValueDeclarationSQL($column); + } + + public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string + { + return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)'; + } + + public function getCreateTemporaryTableSnippetSQL(): string + { + return 'DECLARE GLOBAL TEMPORARY TABLE'; + } + + public function getTemporaryTableName(string $tableName): string + { + return 'SESSION.' . $tableName; + } + + protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string + { + if ($offset > 0) { + $query .= sprintf(' OFFSET %d ROWS', $offset); + } + + if ($limit !== null) { + $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit); + } + + return $query; + } + + public function getLocateExpression(string $string, string $substring, ?string $start = null): string + { + if ($start === null) { + return sprintf('LOCATE(%s, %s)', $substring, $string); + } + + return sprintf('LOCATE(%s, %s, %s)', $substring, $string, $start); + } + + 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 getLengthExpression(string $string): string + { + return 'LENGTH(' . $string . ', CODEUNITS32)'; + } + + public function getCurrentDatabaseExpression(): string + { + return 'CURRENT_USER'; + } + + public function supportsIdentityColumns(): bool + { + return true; + } + + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, 'WITH RR USE AND KEEP UPDATE LOCKS', null); + } + + public function getDummySelectSQL(string $expression = '1'): string + { + return sprintf('SELECT %s FROM sysibm.sysdummy1', $expression); + } + + /** + * {@inheritDoc} + * + * DB2 supports savepoints, but they work semantically different than on other vendor platforms. + * + * TODO: We have to investigate how to get DB2 up and running with savepoints. + */ + public function supportsSavepoints(): bool + { + return false; + } + + protected function createReservedKeywordsList(): KeywordList + { + return new DB2Keywords(); + } + + public function createSchemaManager(Connection $connection): DB2SchemaManager + { + return new DB2SchemaManager($connection, $this); + } +} diff --git a/vendor/doctrine/dbal/src/Platforms/DateIntervalUnit.php b/vendor/doctrine/dbal/src/Platforms/DateIntervalUnit.php new file mode 100644 index 0000000..ba783f3 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/DateIntervalUnit.php @@ -0,0 +1,17 @@ +keywords === null) { + $this->initializeKeywords(); + } + + return isset($this->keywords[strtoupper($word)]); + } + + protected function initializeKeywords(): void + { + $this->keywords = array_flip(array_map('strtoupper', $this->getKeywords())); + } + + /** + * Returns the list of keywords. + * + * @return string[] + */ + abstract protected function getKeywords(): array; +} diff --git a/vendor/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php b/vendor/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php new file mode 100644 index 0000000..6857cd3 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php @@ -0,0 +1,264 @@ +getQuotedName($this)]; + } +} diff --git a/vendor/doctrine/dbal/src/Platforms/MariaDB1060Platform.php b/vendor/doctrine/dbal/src/Platforms/MariaDB1060Platform.php new file mode 100644 index 0000000..028473d --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/MariaDB1060Platform.php @@ -0,0 +1,18 @@ +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)) { + continue; + } + + $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableName); + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff): array + { + return array_merge( + parent::getPostAlterTableIndexForeignKeySQL($diff), + $this->getPostAlterTableRenameIndexForeignKeySQL($diff), + ); + } + + /** @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)) { + continue; + } + + $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName); + } + + return $sql; + } + + /** + * 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 + { + $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); + } + + protected function createReservedKeywordsList(): KeywordList + { + return new MariaDBKeywords(); + } +} diff --git a/vendor/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider.php b/vendor/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider.php new file mode 100644 index 0000000..665543e --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider.php @@ -0,0 +1,11 @@ + */ + 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/vendor/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php b/vendor/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php new file mode 100644 index 0000000..65b63df --- /dev/null +++ b/vendor/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/vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php b/vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php new file mode 100644 index 0000000..d52ca74 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php @@ -0,0 +1,11 @@ + */ + private array $cache = []; + + public function __construct(private readonly CollationMetadataProvider $collationMetadataProvider) + { + } + + public function getCollationCharset(string $collation): ?string + { + if (array_key_exists($collation, $this->cache)) { + return $this->cache[$collation]; + } + + return $this->cache[$collation] = $this->collationMetadataProvider->getCollationCharset($collation); + } +} diff --git a/vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php b/vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php new file mode 100644 index 0000000..fcd9995 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php @@ -0,0 +1,37 @@ +connection->fetchOne( + <<<'SQL' +SELECT CHARACTER_SET_NAME +FROM information_schema.COLLATIONS +WHERE COLLATION_NAME = ?; +SQL + , + [$collation], + ); + + if ($charset !== false) { + return $charset; + } + + return null; + } +} diff --git a/vendor/doctrine/dbal/src/Platforms/MySQL/Comparator.php b/vendor/doctrine/dbal/src/Platforms/MySQL/Comparator.php new file mode 100644 index 0000000..ebe025d --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/MySQL/Comparator.php @@ -0,0 +1,93 @@ +normalizeTable($oldTable), + $this->normalizeTable($newTable), + ); + } + + private function normalizeTable(Table $table): Table + { + $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(); + } + + $tableOptions = [ + 'charset' => $charset, + 'collation' => $collation, + ]; + + $table = clone $table; + + foreach ($table->getColumns() as $column) { + $originalOptions = $column->getPlatformOptions(); + $normalizedOptions = $this->normalizeOptions($originalOptions); + + $overrideOptions = array_diff_assoc($normalizedOptions, $tableOptions); + + if ($overrideOptions === $originalOptions) { + continue; + } + + $column->setPlatformOptions($overrideOptions); + } + + return $table; + } + + /** + * @param array $options + * + * @return array + */ + private function normalizeOptions(array $options): array + { + 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/vendor/doctrine/dbal/src/Platforms/MySQL/DefaultTableOptions.php b/vendor/doctrine/dbal/src/Platforms/MySQL/DefaultTableOptions.php new file mode 100644 index 0000000..ede3ba2 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/MySQL/DefaultTableOptions.php @@ -0,0 +1,23 @@ +charset; + } + + public function getCollation(): string + { + return $this->collation; + } +} diff --git a/vendor/doctrine/dbal/src/Platforms/MySQL80Platform.php b/vendor/doctrine/dbal/src/Platforms/MySQL80Platform.php new file mode 100644 index 0000000..14a81a6 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/MySQL80Platform.php @@ -0,0 +1,25 @@ +getQuotedName($this)]; + } + + protected function createReservedKeywordsList(): KeywordList + { + return new MySQLKeywords(); + } +} diff --git a/vendor/doctrine/dbal/src/Platforms/OraclePlatform.php b/vendor/doctrine/dbal/src/Platforms/OraclePlatform.php new file mode 100644 index 0000000..314f6ee --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/OraclePlatform.php @@ -0,0 +1,784 @@ +multiplyInterval($interval, 3); + break; + + case DateIntervalUnit::YEAR: + $interval = $this->multiplyInterval($interval, 12); + break; + } + + return 'ADD_MONTHS(' . $date . ', ' . $operator . $interval . ')'; + + default: + $calculationClause = ''; + + switch ($unit) { + case DateIntervalUnit::SECOND: + $calculationClause = '/24/60/60'; + break; + + case DateIntervalUnit::MINUTE: + $calculationClause = '/24/60'; + break; + + case DateIntervalUnit::HOUR: + $calculationClause = '/24'; + break; + + case DateIntervalUnit::WEEK: + $calculationClause = '*7'; + break; + } + + return '(' . $date . $operator . $interval . $calculationClause . ')'; + } + } + + public function getDateDiffExpression(string $date1, string $date2): string + { + return sprintf('TRUNC(%s) - TRUNC(%s)', $date1, $date2); + } + + public function getBitAndComparisonExpression(string $value1, string $value2): string + { + return 'BITAND(' . $value1 . ', ' . $value2 . ')'; + } + + public function getCurrentDatabaseExpression(): string + { + return "SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')"; + } + + public function getBitOrComparisonExpression(string $value1, string $value2): string + { + return '(' . $value1 . '-' . + $this->getBitAndComparisonExpression($value1, $value2) + . '+' . $value2 . ')'; + } + + public function getCreatePrimaryKeySQL(Index $index, string $table): string + { + return 'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $index->getQuotedName($this) + . ' PRIMARY KEY (' . implode(', ', $index->getQuotedColumns($this)) . ')'; + } + + /** + * {@inheritDoc} + * + * Need to specifiy minvalue, since start with is hidden in the system and MINVALUE <= START WITH. + * 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): string + { + return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . + ' START WITH ' . $sequence->getInitialValue() . + ' MINVALUE ' . $sequence->getInitialValue() . + ' INCREMENT BY ' . $sequence->getAllocationSize() . + $this->getSequenceCacheSQL($sequence); + } + + public function getAlterSequenceSQL(Sequence $sequence): string + { + return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . + ' INCREMENT BY ' . $sequence->getAllocationSize() + . $this->getSequenceCacheSQL($sequence); + } + + /** + * Cache definition for sequences + */ + private function getSequenceCacheSQL(Sequence $sequence): string + { + if ($sequence->getCache() === 0) { + return ' NOCACHE'; + } + + if ($sequence->getCache() === 1) { + return ' NOCACHE'; + } + + if ($sequence->getCache() > 1) { + return ' CACHE ' . $sequence->getCache(); + } + + return ''; + } + + public function getSequenceNextValSQL(string $sequence): string + { + return 'SELECT ' . $sequence . '.nextval FROM DUAL'; + } + + public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string + { + return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + protected function _getTransactionIsolationLevelSQL(TransactionIsolationLevel $level): string + { + 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): string + { + return 'NUMBER(1)'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column): string + { + return 'NUMBER(10)'; + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column): string + { + return 'NUMBER(20)'; + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column): string + { + return 'NUMBER(5)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column): string + { + return 'TIMESTAMP(0)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzTypeDeclarationSQL(array $column): string + { + return 'TIMESTAMP(0) WITH TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column): string + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column): string + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column): string + { + return ''; + } + + protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string + { + if ($length === null) { + throw ColumnLengthRequired::new($this, 'VARCHAR2'); + } + + return sprintf('VARCHAR2(%d)', $length); + } + + protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string + { + if ($length === null) { + throw ColumnLengthRequired::new($this, 'RAW'); + } + + return sprintf('RAW(%d)', $length); + } + + protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string + { + return $this->getBinaryTypeDeclarationSQLSnippet($length); + } + + /** + * {@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 getListDatabasesSQL(): string + { + return 'SELECT username FROM all_users'; + } + + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListSequencesSQL(string $database): string + { + 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(string $name, array $columns, array $options = []): array + { + $indexes = $options['indexes'] ?? []; + $options['indexes'] = []; + $sql = parent::_getCreateTableSQL($name, $columns, $options); + + foreach ($columns as $column) { + if (isset($column['sequence'])) { + $sql[] = $this->getCreateSequenceSQL($column['sequence']); + } + + if ( + empty($column['autoincrement']) + ) { + continue; + } + + $sql = array_merge($sql, $this->getCreateAutoincrementSql($column['name'], $name)); + } + + foreach ($indexes as $index) { + $sql[] = $this->getCreateIndexSQL($index, $name); + } + + return $sql; + } + + /** @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'; + } + + /** @return array */ + protected function getCreateAutoincrementSql(string $name, string $table, int $start = 1): array + { + $tableIdentifier = $this->normalizeIdentifier($table); + $quotedTableName = $tableIdentifier->getQuotedName($this); + $unquotedTableName = $tableIdentifier->getName(); + + $nameIdentifier = $this->normalizeIdentifier($name); + $quotedName = $nameIdentifier->getQuotedName($this); + $unquotedName = $nameIdentifier->getName(); + + $sql = []; + + $autoincrementIdentifierName = $this->getAutoincrementIdentifierName($tableIdentifier); + + $idx = new Index($autoincrementIdentifierName, [$quotedName], true, true); + + $sql[] = "DECLARE + constraints_Count NUMBER; +BEGIN + SELECT COUNT(CONSTRAINT_NAME) INTO constraints_Count + FROM USER_CONSTRAINTS + WHERE TABLE_NAME = '" . $unquotedTableName . "' + AND CONSTRAINT_TYPE = 'P'; + IF constraints_Count = 0 OR constraints_Count = '' THEN + EXECUTE IMMEDIATE '" . $this->getCreateIndexSQL($idx, $quotedTableName) . "'; + END IF; +END;"; + + $sequenceName = $this->getIdentitySequenceName( + $tableIdentifier->isQuoted() ? $quotedTableName : $unquotedTableName, + ); + $sequence = new Sequence($sequenceName, $start); + $sql[] = $this->getCreateSequenceSQL($sequence); + + $sql[] = 'CREATE TRIGGER ' . $autoincrementIdentifierName . ' + BEFORE INSERT + ON ' . $quotedTableName . ' + 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; + 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; + WHILE (last_InsertID > last_Sequence) LOOP + SELECT ' . $sequenceName . '.NEXTVAL INTO last_Sequence FROM DUAL; + END LOOP; + SELECT ' . $sequenceName . '.NEXTVAL INTO last_Sequence FROM DUAL; + END IF; +END;'; + + return $sql; + } + + /** + * @internal The method should be only used from within the OracleSchemaManager class hierarchy. + * + * Returns the SQL statements to drop the autoincrement for the given table name. + * + * @param string $table The table name to drop the autoincrement for. + * + * @return string[] + */ + 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 [ + 'DROP TRIGGER ' . $autoincrementIdentifierName, + $this->getDropSequenceSQL($identitySequenceName), + $this->getDropConstraintSQL($autoincrementIdentifierName, $table->getQuotedName($this)), + ]; + } + + /** + * Normalizes the given identifier. + * + * Uppercases the given identifier if it is not quoted by intention + * to reflect Oracle's internal auto uppercasing strategy of unquoted identifiers. + * + * @param string $name The identifier to normalize. + */ + private function normalizeIdentifier(string $name): Identifier + { + $identifier = new Identifier($name); + + return $identifier->isQuoted() ? $identifier : new Identifier(strtoupper($name)); + } + + /** + * Adds suffix to identifier, + * + * if the new string exceeds max identifier length, + * keeps $suffix, cuts from $identifier as much as the part exceeding. + */ + private function addSuffix(string $identifier, string $suffix): string + { + $maxPossibleLengthWithoutSuffix = $this->getMaxIdentifierLength() - strlen($suffix); + if (strlen($identifier) > $maxPossibleLengthWithoutSuffix) { + $identifier = substr($identifier, 0, $maxPossibleLengthWithoutSuffix); + } + + return $identifier . $suffix; + } + + /** + * Returns the autoincrement primary key identifier name for the given table identifier. + * + * Quotes the autoincrement primary key identifier name + * if the given table name is quoted by intention. + */ + private function getAutoincrementIdentifierName(Identifier $table): string + { + $identifierName = $this->addSuffix($table->getName(), '_AI_PK'); + + return $table->isQuoted() + ? $this->quoteSingleIdentifier($identifierName) + : $identifierName; + } + + public function getDropForeignKeySQL(string $foreignKey, string $table): string + { + return $this->getDropConstraintSQL($foreignKey, $table); + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey): string + { + $referentialAction = ''; + + if ($foreignKey->hasOption('onDelete')) { + $referentialAction = $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onDelete')); + } + + if ($referentialAction !== '') { + return ' ON DELETE ' . $referentialAction; + } + + return ''; + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getForeignKeyReferentialActionSQL(string $action): string + { + $action = strtoupper($action); + + return match ($action) { + 'RESTRICT', + 'NO ACTION' => '', + 'CASCADE', + 'SET NULL' => $action, + default => throw new InvalidArgumentException(sprintf('Invalid foreign key action "%s".', $action)), + }; + } + + public function getCreateDatabaseSQL(string $name): string + { + return 'CREATE USER ' . $name; + } + + public function getDropDatabaseSQL(string $name): string + { + return 'DROP USER ' . $name . ' CASCADE'; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff): array + { + $sql = []; + $commentsSQL = []; + $columnSql = []; + + $addColumnSQL = []; + + $tableNameSQL = $diff->getOldTable()->getQuotedName($this); + + foreach ($diff->getAddedColumns() as $column) { + $addColumnSQL[] = $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + $comment = $column->getComment(); + + if ($comment === '') { + continue; + } + + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $column->getQuotedName($this), + $comment, + ); + } + + if (count($addColumnSQL) > 0) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ADD (' . implode(', ', $addColumnSQL) . ')'; + } + + $modifyColumnSQL = []; + foreach ($diff->getModifiedColumns() as $columnDiff) { + $newColumn = $columnDiff->getNewColumn(); + $oldColumn = $columnDiff->getOldColumn(); + + $newColumnProperties = $newColumn->toArray(); + $oldColumnProperties = $oldColumn->toArray(); + + $oldSQL = $this->getColumnDeclarationSQL('', $oldColumnProperties); + $newSQL = $this->getColumnDeclarationSQL('', $newColumnProperties); + + if ($newSQL !== $oldSQL) { + if (! $columnDiff->hasNotNullChanged()) { + unset($newColumnProperties['notnull']); + $newSQL = $this->getColumnDeclarationSQL('', $newColumnProperties); + } + + $modifyColumnSQL[] = $newColumn->getQuotedName($this) . $newSQL; + } + + if (! $columnDiff->hasCommentChanged()) { + continue; + } + + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $newColumn->getQuotedName($this), + $newColumn->getComment(), + ); + } + + if (count($modifyColumnSQL) > 0) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' MODIFY (' . implode(', ', $modifyColumnSQL) . ')'; + } + + foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { + $oldColumnName = new Identifier($oldColumnName); + + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) + . ' TO ' . $column->getQuotedName($this); + } + + $dropColumnSQL = []; + foreach ($diff->getDroppedColumns() as $column) { + $dropColumnSQL[] = $column->getQuotedName($this); + } + + if (count($dropColumnSQL) > 0) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' DROP (' . implode(', ', $dropColumnSQL) . ')'; + } + + return array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $commentsSQL, + $this->getPostAlterTableIndexForeignKeySQL($diff), + $columnSql, + ); + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getColumnDeclarationSQL(string $name, array $column): string + { + if (isset($column['columnDefinition'])) { + $declaration = $column['columnDefinition']; + } else { + $default = $this->getDefaultValueDeclarationSQL($column); + + $notnull = ''; + + if (isset($column['notnull'])) { + $notnull = $column['notnull'] ? ' NOT NULL' : ' NULL'; + } + + $typeDecl = $column['type']->getSQLDeclaration($column, $this); + $declaration = $typeDecl . $default . $notnull; + } + + return $name . ' ' . $declaration; + } + + /** + * {@inheritDoc} + */ + protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array + { + if (str_contains($tableName, '.')) { + [$schema] = explode('.', $tableName); + $oldIndexName = $schema . '.' . $oldIndexName; + } + + return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)]; + } + + protected function getIdentitySequenceName(string $tableName): string + { + $table = new Identifier($tableName); + + // No usage of column name to preserve BC compatibility with <2.5 + $identitySequenceName = $this->addSuffix($table->getName(), '_SEQ'); + + if ($table->isQuoted()) { + $identitySequenceName = '"' . $identitySequenceName . '"'; + } + + $identitySequenceIdentifier = $this->normalizeIdentifier($identitySequenceName); + + return $identitySequenceIdentifier->getQuotedName($this); + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsCommentOnStatement(): bool + { + return true; + } + + protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string + { + if ($offset > 0) { + $query .= sprintf(' OFFSET %d ROWS', $offset); + } + + if ($limit !== null) { + $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit); + } + + return $query; + } + + public function getCreateTemporaryTableSnippetSQL(): string + { + return 'CREATE GLOBAL TEMPORARY TABLE'; + } + + public function getDateTimeTzFormatString(): string + { + return 'Y-m-d H:i:sP'; + } + + public function getDateFormatString(): string + { + return 'Y-m-d 00:00:00'; + } + + public function getTimeFormatString(): string + { + return '1900-01-01 H:i:s'; + } + + public function getMaxIdentifierLength(): int + { + return 128; + } + + public function supportsSequences(): bool + { + return true; + } + + public function supportsReleaseSavepoints(): bool + { + return false; + } + + public function getTruncateTableSQL(string $tableName, bool $cascade = false): string + { + $tableIdentifier = new Identifier($tableName); + + return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this); + } + + public function getDummySelectSQL(string $expression = '1'): string + { + return sprintf('SELECT %s FROM DUAL', $expression); + } + + protected function initializeDoctrineTypeMappings(): void + { + $this->doctrineTypeMapping = [ + 'binary_double' => Types::FLOAT, + 'binary_float' => Types::FLOAT, + 'binary_integer' => Types::BOOLEAN, + 'blob' => Types::BLOB, + 'char' => Types::STRING, + 'clob' => Types::TEXT, + 'date' => Types::DATE_MUTABLE, + 'float' => Types::FLOAT, + 'integer' => Types::INTEGER, + 'long' => Types::STRING, + 'long raw' => Types::BLOB, + 'nchar' => Types::STRING, + 'nclob' => Types::TEXT, + 'number' => Types::INTEGER, + 'nvarchar2' => Types::STRING, + 'pls_integer' => Types::BOOLEAN, + 'raw' => Types::BINARY, + 'rowid' => Types::STRING, + 'timestamp' => Types::DATETIME_MUTABLE, + 'timestamptz' => Types::DATETIMETZ_MUTABLE, + 'urowid' => Types::STRING, + 'varchar' => Types::STRING, + 'varchar2' => Types::STRING, + ]; + } + + public function releaseSavePoint(string $savepoint): string + { + return ''; + } + + protected function createReservedKeywordsList(): KeywordList + { + return new OracleKeywords(); + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column): string + { + return 'BLOB'; + } + + public function createSchemaManager(Connection $connection): OracleSchemaManager + { + return new OracleSchemaManager($connection, $this); + } +} diff --git a/vendor/doctrine/dbal/src/Platforms/PostgreSQLPlatform.php b/vendor/doctrine/dbal/src/Platforms/PostgreSQLPlatform.php new file mode 100644 index 0000000..166ee75 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/PostgreSQLPlatform.php @@ -0,0 +1,784 @@ + [ + 't', + 'true', + 'y', + 'yes', + 'on', + '1', + ], + 'false' => [ + 'f', + 'false', + 'n', + 'no', + 'off', + '0', + ], + ]; + + /** + * PostgreSQL has different behavior with some drivers + * with regard to how booleans have to be handled. + * + * Enables use of 'true'/'false' or otherwise 1 and 0 instead. + */ + public function setUseBooleanTrueFalseStrings(bool $flag): void + { + $this->useBooleanTrueFalseStrings = $flag; + } + + public function getRegexpExpression(): string + { + return 'SIMILAR TO'; + } + + public function getLocateExpression(string $string, string $substring, ?string $start = null): string + { + if ($start !== null) { + $string = $this->getSubstringExpression($string, $start); + + return 'CASE WHEN (POSITION(' . $substring . ' IN ' . $string . ') = 0) THEN 0' + . ' ELSE (POSITION(' . $substring . ' IN ' . $string . ') + ' . $start . ' - 1) END'; + } + + return sprintf('POSITION(%s IN %s)', $substring, $string); + } + + protected function getDateArithmeticIntervalExpression( + string $date, + string $operator, + string $interval, + DateIntervalUnit $unit, + ): string { + if ($unit === DateIntervalUnit::QUARTER) { + $interval = $this->multiplyInterval($interval, 3); + $unit = DateIntervalUnit::MONTH; + } + + return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit->value . "')::interval)"; + } + + public function getDateDiffExpression(string $date1, string $date2): string + { + return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))'; + } + + public function getCurrentDatabaseExpression(): string + { + return 'CURRENT_DATABASE()'; + } + + public function supportsSequences(): bool + { + return true; + } + + public function supportsSchemas(): bool + { + return true; + } + + public function supportsIdentityColumns(): bool + { + return true; + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsPartialIndexes(): bool + { + return true; + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsCommentOnStatement(): bool + { + return true; + } + + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListDatabasesSQL(): string + { + return 'SELECT datname FROM pg_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, + minimum_value AS min_value, + increment AS increment_by + FROM information_schema.sequences + WHERE sequence_catalog = ' . $this->quoteStringLiteral($database) . " + AND sequence_schema NOT LIKE 'pg\_%' + AND sequence_schema != 'information_schema'"; + } + + /** @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, + view_definition AS definition + FROM information_schema.views + WHERE view_definition IS NOT NULL'; + } + + /** @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')) { + $query .= ' MATCH ' . $foreignKey->getOption('match'); + } + + $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey); + + if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) { + $query .= ' DEFERRABLE'; + } else { + $query .= ' NOT DEFERRABLE'; + } + + if ( + $foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false + ) { + $query .= ' INITIALLY DEFERRED'; + } else { + $query .= ' INITIALLY IMMEDIATE'; + } + + return $query; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff): array + { + $sql = []; + $commentsSQL = []; + $columnSql = []; + + $table = $diff->getOldTable(); + + $tableNameSQL = $table->getQuotedName($this); + + foreach ($diff->getAddedColumns() as $addedColumn) { + $query = 'ADD ' . $this->getColumnDeclarationSQL( + $addedColumn->getQuotedName($this), + $addedColumn->toArray(), + ); + + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + + $comment = $addedColumn->getComment(); + + if ($comment === '') { + continue; + } + + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $addedColumn->getQuotedName($this), + $comment, + ); + } + + foreach ($diff->getDroppedColumns() as $droppedColumn) { + $query = 'DROP ' . $droppedColumn->getQuotedName($this); + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + foreach ($diff->getModifiedColumns() as $columnDiff) { + $oldColumn = $columnDiff->getOldColumn(); + $newColumn = $columnDiff->getNewColumn(); + + $oldColumnName = $oldColumn->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; + + // here was a server version check before, but DBAL API does not support this anymore. + $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this); + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + if ($columnDiff->hasDefaultChanged()) { + $defaultClause = $newColumn->getDefault() === null + ? ' DROP DEFAULT' + : ' SET' . $this->getDefaultValueDeclarationSQL($newColumn->toArray()); + + $query = 'ALTER ' . $oldColumnName . $defaultClause; + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + if ($columnDiff->hasNotNullChanged()) { + $query = 'ALTER ' . $oldColumnName . ' ' . ($newColumn->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL'; + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + if ($columnDiff->hasAutoIncrementChanged()) { + if ($newColumn->getAutoincrement()) { + $query = 'ADD GENERATED BY DEFAULT AS IDENTITY'; + } else { + $query = 'DROP IDENTITY'; + } + + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ALTER ' . $oldColumnName . ' ' . $query; + } + + $newComment = $newColumn->getComment(); + $oldComment = $columnDiff->getOldColumn()->getComment(); + + if ($columnDiff->hasCommentChanged() || $oldComment !== $newComment) { + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $newColumn->getQuotedName($this), + $newComment, + ); + } + + if (! $columnDiff->hasLengthChanged()) { + continue; + } + + $query = 'ALTER ' . $oldColumnName . ' TYPE ' + . $newColumn->getType()->getSQLDeclaration($newColumn->toArray(), $this); + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { + $oldColumnName = new Identifier($oldColumnName); + + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) + . ' TO ' . $column->getQuotedName($this); + } + + return array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $commentsSQL, + $this->getPostAlterTableIndexForeignKeySQL($diff), + $columnSql, + ); + } + + /** + * {@inheritDoc} + */ + protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array + { + if (str_contains($tableName, '.')) { + [$schema] = explode('.', $tableName); + $oldIndexName = $schema . '.' . $oldIndexName; + } + + return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)]; + } + + 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); + } + + public function getAlterSequenceSQL(Sequence $sequence): string + { + return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . + ' INCREMENT BY ' . $sequence->getAllocationSize() . + $this->getSequenceCacheSQL($sequence); + } + + /** + * Cache definition for sequences + */ + private function getSequenceCacheSQL(Sequence $sequence): string + { + if ($sequence->getCache() > 1) { + return ' CACHE ' . $sequence->getCache(); + } + + return ''; + } + + public function getDropSequenceSQL(string $name): string + { + return parent::getDropSequenceSQL($name) . ' CASCADE'; + } + + public function getDropForeignKeySQL(string $foreignKey, string $table): string + { + return $this->getDropConstraintSQL($foreignKey, $table); + } + + public function getDropIndexSQL(string $name, string $table): string + { + if ($name === '"primary"') { + $constraintName = $table . '_pkey'; + + return $this->getDropConstraintSQL($constraintName, $table); + } + + return parent::getDropIndexSQL($name, $table); + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array + { + $queryFields = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['primary']) && ! empty($options['primary'])) { + $keyColumns = array_unique(array_values($options['primary'])); + $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + } + + $unlogged = isset($options['unlogged']) && $options['unlogged'] === true ? ' UNLOGGED' : ''; + + $query = 'CREATE' . $unlogged . ' TABLE ' . $name . ' (' . $queryFields . ')'; + + $sql = [$query]; + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach ($options['indexes'] as $index) { + $sql[] = $this->getCreateIndexSQL($index, $name); + } + } + + if (isset($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $uniqueConstraint) { + $sql[] = $this->getCreateUniqueConstraintSQL($uniqueConstraint, $name); + } + } + + if (isset($options['foreignKeys'])) { + foreach ($options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $name); + } + } + + return $sql; + } + + /** + * Converts a single boolean value. + * + * First converts the value to its native PHP boolean type + * and passes it to the given callback function to be reconverted + * into any custom representation. + * + * @param mixed $value The value to convert. + * @param callable $callback The callback function to use for converting the real boolean value. + * + * @throws UnexpectedValueException + */ + private function convertSingleBooleanValue(mixed $value, callable $callback): mixed + { + if ($value === null) { + return $callback(null); + } + + if (is_bool($value) || is_numeric($value)) { + return $callback((bool) $value); + } + + if (! is_string($value)) { + return $callback(true); + } + + /** + * Better safe than sorry: http://php.net/in_array#106319 + */ + if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) { + return $callback(false); + } + + if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) { + return $callback(true); + } + + throw new UnexpectedValueException(sprintf( + 'Unrecognized boolean literal, %s given.', + $value, + )); + } + + /** + * Converts one or multiple boolean values. + * + * First converts the value(s) to their native PHP boolean type + * and passes them to the given callback function to be reconverted + * into any custom representation. + * + * @param mixed $item The value(s) to convert. + * @param callable $callback The callback function to use for converting the real boolean value(s). + */ + private function doConvertBooleans(mixed $item, callable $callback): mixed + { + if (is_array($item)) { + foreach ($item as $key => $value) { + $item[$key] = $this->convertSingleBooleanValue($value, $callback); + } + + return $item; + } + + return $this->convertSingleBooleanValue($item, $callback); + } + + /** + * {@inheritDoc} + * + * Postgres wants boolean values converted to the strings 'true'/'false'. + */ + public function convertBooleans(mixed $item): mixed + { + if (! $this->useBooleanTrueFalseStrings) { + return parent::convertBooleans($item); + } + + return $this->doConvertBooleans( + $item, + /** @param mixed $value */ + static function ($value): string { + if ($value === null) { + return 'NULL'; + } + + return $value === true ? 'true' : 'false'; + }, + ); + } + + public function convertBooleansToDatabaseValue(mixed $item): mixed + { + if (! $this->useBooleanTrueFalseStrings) { + return parent::convertBooleansToDatabaseValue($item); + } + + return $this->doConvertBooleans( + $item, + /** @param mixed $value */ + static function ($value): ?int { + return $value === null ? null : (int) $value; + }, + ); + } + + /** + * @param T $item + * + * @return (T is null ? null : bool) + * + * @template T + */ + public function convertFromBoolean(mixed $item): ?bool + { + if (in_array($item, $this->booleanLiterals['false'], true)) { + return false; + } + + return parent::convertFromBoolean($item); + } + + public function getSequenceNextValSQL(string $sequence): string + { + return "SELECT NEXTVAL('" . $sequence . "')"; + } + + public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string + { + return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL ' + . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column): string + { + return 'BOOLEAN'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column): string + { + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column): string + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column): string + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getGuidTypeDeclarationSQL(array $column): string + { + return 'UUID'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column): string + { + return 'TIMESTAMP(0) WITHOUT TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzTypeDeclarationSQL(array $column): string + { + return 'TIMESTAMP(0) WITH TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column): string + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column): string + { + return 'TIME(0) WITHOUT TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column): string + { + if (! empty($column['autoincrement'])) { + return ' GENERATED BY DEFAULT AS IDENTITY'; + } + + return ''; + } + + protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string + { + $sql = 'VARCHAR'; + + if ($length !== null) { + $sql .= sprintf('(%d)', $length); + } + + return $sql; + } + + protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string + { + return 'BYTEA'; + } + + protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string + { + return 'BYTEA'; + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column): string + { + return 'TEXT'; + } + + public function getDateTimeTzFormatString(): string + { + return 'Y-m-d H:i:sO'; + } + + public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string + { + return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)'; + } + + public function getTruncateTableSQL(string $tableName, bool $cascade = false): string + { + $tableIdentifier = new Identifier($tableName); + $sql = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this); + + if ($cascade) { + $sql .= ' CASCADE'; + } + + return $sql; + } + + protected function initializeDoctrineTypeMappings(): void + { + $this->doctrineTypeMapping = [ + 'bigint' => Types::BIGINT, + 'bigserial' => Types::BIGINT, + 'bool' => Types::BOOLEAN, + 'boolean' => Types::BOOLEAN, + 'bpchar' => Types::STRING, + '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, + 'float8' => Types::FLOAT, + 'inet' => Types::STRING, + 'int' => Types::INTEGER, + 'int2' => Types::SMALLINT, + 'int4' => Types::INTEGER, + 'int8' => Types::BIGINT, + 'integer' => Types::INTEGER, + 'interval' => Types::STRING, + 'json' => Types::JSON, + 'jsonb' => Types::JSON, + 'money' => Types::DECIMAL, + 'numeric' => Types::DECIMAL, + 'serial' => Types::INTEGER, + 'serial4' => Types::INTEGER, + 'serial8' => Types::BIGINT, + 'real' => Types::FLOAT, + 'smallint' => Types::SMALLINT, + 'text' => Types::TEXT, + 'time' => Types::TIME_MUTABLE, + 'timestamp' => Types::DATETIME_MUTABLE, + 'timestamptz' => Types::DATETIMETZ_MUTABLE, + 'timetz' => Types::TIME_MUTABLE, + 'tsvector' => Types::TEXT, + 'uuid' => Types::GUID, + 'varchar' => Types::STRING, + 'year' => Types::DATE_MUTABLE, + '_varchar' => Types::STRING, + ]; + } + + protected function createReservedKeywordsList(): KeywordList + { + return new PostgreSQLKeywords(); + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column): string + { + return 'BYTEA'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getDefaultValueDeclarationSQL(array $column): string + { + if (isset($column['autoincrement']) && $column['autoincrement'] === true) { + return ''; + } + + return parent::getDefaultValueDeclarationSQL($column); + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsColumnCollation(): bool + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getJsonTypeDeclarationSQL(array $column): string + { + if (! empty($column['jsonb'])) { + return 'JSONB'; + } + + return 'JSON'; + } + + public function createSchemaManager(Connection $connection): PostgreSQLSchemaManager + { + return new PostgreSQLSchemaManager($connection, $this); + } +} diff --git a/vendor/doctrine/dbal/src/Platforms/SQLServer/Comparator.php b/vendor/doctrine/dbal/src/Platforms/SQLServer/Comparator.php new file mode 100644 index 0000000..aa8d9fb --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/SQLServer/Comparator.php @@ -0,0 +1,50 @@ +normalizeColumns($oldTable), + $this->normalizeColumns($newTable), + ); + } + + private function normalizeColumns(Table $table): Table + { + $table = clone $table; + + foreach ($table->getColumns() as $column) { + $options = $column->getPlatformOptions(); + + if (! isset($options['collation']) || $options['collation'] !== $this->databaseCollation) { + continue; + } + + unset($options['collation']); + $column->setPlatformOptions($options); + } + + return $table; + } +} diff --git a/vendor/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php b/vendor/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php new file mode 100644 index 0000000..ea6bb60 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php @@ -0,0 +1,84 @@ +isDistinct()) { + $parts[] = 'DISTINCT'; + } + + $parts[] = implode(', ', $query->getColumns()); + + $from = $query->getFrom(); + + if (count($from) > 0) { + $parts[] = 'FROM ' . implode(', ', $from); + } + + $forUpdate = $query->getForUpdate(); + + if ($forUpdate !== null) { + $with = ['UPDLOCK', 'ROWLOCK']; + + if ($forUpdate->getConflictResolutionMode() === ConflictResolutionMode::SKIP_LOCKED) { + $with[] = 'READPAST'; + } + + $parts[] = 'WITH (' . implode(', ', $with) . ')'; + } + + $where = $query->getWhere(); + + if ($where !== null) { + $parts[] = 'WHERE ' . $where; + } + + $groupBy = $query->getGroupBy(); + + if (count($groupBy) > 0) { + $parts[] = 'GROUP BY ' . implode(', ', $groupBy); + } + + $having = $query->getHaving(); + + if ($having !== null) { + $parts[] = 'HAVING ' . $having; + } + + $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/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php b/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php new file mode 100644 index 0000000..7a10a32 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php @@ -0,0 +1,1223 @@ +getConvertExpression('date', 'GETDATE()'); + } + + public function getCurrentTimeSQL(): string + { + return $this->getConvertExpression('time', 'GETDATE()'); + } + + /** + * Returns an expression that converts an expression of one data type to another. + * + * @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(string $dataType, string $expression): string + { + return sprintf('CONVERT(%s, %s)', $dataType, $expression); + } + + protected function getDateArithmeticIntervalExpression( + string $date, + string $operator, + string $interval, + DateIntervalUnit $unit, + ): string { + $factorClause = ''; + + if ($operator === '-') { + $factorClause = '-1 * '; + } + + return 'DATEADD(' . $unit->value . ', ' . $factorClause . $interval . ', ' . $date . ')'; + } + + public function getDateDiffExpression(string $date1, string $date2): string + { + return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')'; + } + + /** + * {@inheritDoc} + * + * Microsoft SQL Server supports this through AUTO_INCREMENT columns. + */ + public function supportsIdentityColumns(): bool + { + return true; + } + + public function supportsReleaseSavepoints(): bool + { + return false; + } + + public function supportsSchemas(): bool + { + return true; + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsColumnCollation(): bool + { + return true; + } + + public function supportsSequences(): bool + { + return true; + } + + public function getAlterSequenceSQL(Sequence $sequence): string + { + return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . + ' INCREMENT BY ' . $sequence->getAllocationSize(); + } + + public function getCreateSequenceSQL(Sequence $sequence): string + { + return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . + ' START WITH ' . $sequence->getInitialValue() . + ' INCREMENT BY ' . $sequence->getAllocationSize() . + ' MINVALUE ' . $sequence->getInitialValue(); + } + + /** @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( + seq.increment AS VARCHAR(MAX) + ) AS increment, -- CAST avoids driver error for sql_variant type + CAST( + seq.start_value AS VARCHAR(MAX) + ) AS start_value -- CAST avoids driver error for sql_variant type + FROM sys.sequences AS seq'; + } + + public function getSequenceNextValSQL(string $sequence): string + { + return 'SELECT NEXT VALUE FOR ' . $sequence; + } + + public function getDropForeignKeySQL(string $foreignKey, string $table): string + { + return $this->getDropConstraintSQL($foreignKey, $table); + } + + public function getDropIndexSQL(string $name, string $table): string + { + return 'DROP INDEX ' . $name . ' ON ' . $table; + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array + { + $defaultConstraintsSql = []; + $commentsSql = []; + + $tableComment = $options['comment'] ?? null; + if ($tableComment !== null) { + $commentsSql[] = $this->getCommentOnTableSQL($name, $tableComment); + } + + // @todo does other code breaks because of this? + // force primary keys to be not null + foreach ($columns as &$column) { + if (! empty($column['primary'])) { + $column['notnull'] = true; + } + + // Build default constraints SQL statements. + if (isset($column['default'])) { + $defaultConstraintsSql[] = 'ALTER TABLE ' . $name . + ' ADD' . $this->getDefaultConstraintDeclarationSQL($column); + } + + if (empty($column['comment']) && ! is_numeric($column['comment'])) { + continue; + } + + $commentsSql[] = $this->getCreateColumnCommentSQL($name, $column['name'], $column['comment']); + } + + $columnListSql = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $definition) { + $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($definition); + } + } + + if (isset($options['primary']) && ! empty($options['primary'])) { + $flags = ''; + if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) { + $flags = ' NONCLUSTERED'; + } + + $columnListSql .= ', PRIMARY KEY' . $flags + . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')'; + } + + $query = 'CREATE TABLE ' . $name . ' (' . $columnListSql; + + $check = $this->getCheckDeclarationSQL($columns); + if (! empty($check)) { + $query .= ', ' . $check; + } + + $query .= ')'; + + $sql = [$query]; + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach ($options['indexes'] as $index) { + $sql[] = $this->getCreateIndexSQL($index, $name); + } + } + + if (isset($options['foreignKeys'])) { + foreach ($options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $name); + } + } + + return array_merge($sql, $commentsSql, $defaultConstraintsSql); + } + + public function getCreatePrimaryKeySQL(Index $index, string $table): string + { + $sql = 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY'; + + if ($index->hasFlag('nonclustered')) { + $sql .= ' NONCLUSTERED'; + } + + return $sql . ' (' . implode(', ', $index->getQuotedColumns($this)) . ')'; + } + + private function unquoteSingleIdentifier(string $possiblyQuotedName): string + { + return str_starts_with($possiblyQuotedName, '[') && str_ends_with($possiblyQuotedName, ']') + ? substr($possiblyQuotedName, 1, -1) + : $possiblyQuotedName; + } + + /** + * Returns the SQL statement for creating a column comment. + * + * SQL Server does not support native column comments, + * therefore the extended properties functionality is used + * as a workaround to store them. + * The property name used to store column comments is "MS_Description" + * which provides compatibility with SQL Server Management Studio, + * 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 $comment The column's comment. + */ + protected function getCreateColumnCommentSQL(string $tableName, string $columnName, string $comment): string + { + if (str_contains($tableName, '.')) { + [$schemaName, $tableName] = explode('.', $tableName); + } else { + $schemaName = 'dbo'; + } + + return $this->getAddExtendedPropertySQL( + 'MS_Description', + $comment, + 'SCHEMA', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($schemaName)), + 'TABLE', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), + 'COLUMN', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($columnName)), + ); + } + + /** + * Returns the SQL snippet for declaring a default constraint. + * + * @param mixed[] $column Column definition. + */ + protected function getDefaultConstraintDeclarationSQL(array $column): string + { + if (! isset($column['default'])) { + throw new InvalidArgumentException('Incomplete column definition. "default" required.'); + } + + $columnName = new Identifier($column['name']); + + return $this->getDefaultValueDeclarationSQL($column) . ' FOR ' . $columnName->getQuotedName($this); + } + + public function getCreateIndexSQL(Index $index, string $table): string + { + $constraint = parent::getCreateIndexSQL($index, $table); + + if ($index->isUnique() && ! $index->isPrimary()) { + $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index); + } + + return $constraint; + } + + protected function getCreateIndexSQLFlags(Index $index): string + { + $type = ''; + if ($index->isUnique()) { + $type .= 'UNIQUE '; + } + + if ($index->hasFlag('clustered')) { + $type .= 'CLUSTERED '; + } elseif ($index->hasFlag('nonclustered')) { + $type .= 'NONCLUSTERED '; + } + + return $type; + } + + /** + * Extend unique key constraint with required filters + */ + private function _appendUniqueConstraintDefinition(string $sql, Index $index): string + { + $fields = []; + + foreach ($index->getQuotedColumns($this) as $field) { + $fields[] = $field . ' IS NOT NULL'; + } + + return $sql . ' WHERE ' . implode(' AND ', $fields); + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff): array + { + $queryParts = []; + $sql = []; + $columnSql = []; + $commentsSql = []; + + $table = $diff->getOldTable(); + + $tableName = $table->getName(); + + foreach ($diff->getAddedColumns() as $column) { + $columnProperties = $column->toArray(); + + $addColumnSql = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnProperties); + + if (isset($columnProperties['default'])) { + $addColumnSql .= $this->getDefaultValueDeclarationSQL($columnProperties); + } + + $queryParts[] = $addColumnSql; + + $comment = $column->getComment(); + + if ($comment === '') { + continue; + } + + $commentsSql[] = $this->getCreateColumnCommentSQL( + $tableName, + $column->getQuotedName($this), + $comment, + ); + } + + foreach ($diff->getDroppedColumns() as $column) { + if ($column->getDefault() !== null) { + $queryParts[] = $this->getAlterTableDropDefaultConstraintClause($column); + } + + $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); + } + + foreach ($diff->getModifiedColumns() as $columnDiff) { + $newColumn = $columnDiff->getNewColumn(); + $newComment = $newColumn->getComment(); + $hasNewComment = $newComment !== ''; + + $oldColumn = $columnDiff->getOldColumn(); + $oldComment = $oldColumn->getComment(); + $hasOldComment = $oldComment !== ''; + + if ($hasOldComment && $hasNewComment && $newComment !== $oldComment) { + $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, + ); + } + + $columnNameSQL = $newColumn->getQuotedName($this); + + $oldDeclarationSQL = $this->getColumnDeclarationSQL($columnNameSQL, $oldColumn->toArray()); + $newDeclarationSQL = $this->getColumnDeclarationSQL($columnNameSQL, $newColumn->toArray()); + + $declarationSQLChanged = $newDeclarationSQL !== $oldDeclarationSQL; + $defaultChanged = $columnDiff->hasDefaultChanged(); + + if (! $declarationSQLChanged && ! $defaultChanged) { + continue; + } + + $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff); + + if ($requireDropDefaultConstraint) { + $queryParts[] = $this->getAlterTableDropDefaultConstraintClause($oldColumn); + } + + if ($declarationSQLChanged) { + $queryParts[] = 'ALTER COLUMN ' . $newDeclarationSQL; + } + + if ( + $newColumn->getDefault() === null + || (! $requireDropDefaultConstraint && ! $defaultChanged) + ) { + continue; + } + + $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($tableName, $newColumn); + } + + $tableNameSQL = $table->getQuotedName($this); + + foreach ($diff->getRenamedColumns() as $oldColumnName => $newColumn) { + $oldColumnName = new Identifier($oldColumnName); + + $sql[] = sprintf( + "sp_rename '%s.%s', '%s', 'COLUMN'", + $tableNameSQL, + $oldColumnName->getQuotedName($this), + $newColumn->getQuotedName($this), + ); + } + + foreach ($queryParts as $query) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + return array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $commentsSql, + $this->getPostAlterTableIndexForeignKeySQL($diff), + $columnSql, + ); + } + + public function getRenameTableSQL(string $oldName, string $newName): string + { + return sprintf( + 'sp_rename %s, %s', + $this->quoteStringLiteral($oldName), + $this->quoteStringLiteral($newName), + ); + } + + /** + * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement. + * + * @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(string $tableName, Column $column): string + { + $columnDef = $column->toArray(); + $columnDef['name'] = $column->getQuotedName($this); + + return 'ADD' . $this->getDefaultConstraintDeclarationSQL($columnDef); + } + + /** + * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement. + */ + private function getAlterTableDropDefaultConstraintClause(Column $column): string + { + 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->quoteIdentifier( + $column->getPlatformOption(self::OPTION_DEFAULT_CONSTRAINT_NAME), + ); + } + + /** + * Checks whether a column alteration requires dropping its default constraint first. + * + * Different to other database vendors SQL Server implements column default values + * as constraints and therefore changes in a column's default value as well as changes + * in a column's type require dropping the default constraint first before being to + * alter the particular column to the new definition. + */ + private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff): bool + { + // We only need to drop an existing default constraint if we know the + // column was defined with a default value before. + if ($columnDiff->getOldColumn()->getDefault() === null) { + return false; + } + + // We need to drop an existing default constraint if the column was + // defined with a default value before and it has changed. + if ($columnDiff->hasDefaultChanged()) { + return true; + } + + // We need to drop an existing default constraint if the column was + // defined with a default value before and the native column type has changed. + return $columnDiff->hasTypeChanged() || $columnDiff->hasFixedChanged(); + } + + /** + * Returns the SQL statement for altering a column comment. + * + * SQL Server does not support native column comments, + * therefore the extended properties functionality is used + * as a workaround to store them. + * The property name used to store column comments is "MS_Description" + * which provides compatibility with SQL Server Management Studio, + * 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 $comment The column's comment. + */ + protected function getAlterColumnCommentSQL(string $tableName, string $columnName, string $comment): string + { + if (str_contains($tableName, '.')) { + [$schemaName, $tableName] = explode('.', $tableName); + } else { + $schemaName = 'dbo'; + } + + return $this->getUpdateExtendedPropertySQL( + 'MS_Description', + $comment, + 'SCHEMA', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($schemaName)), + 'TABLE', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), + 'COLUMN', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($columnName)), + ); + } + + /** + * Returns the SQL statement for dropping a column comment. + * + * SQL Server does not support native column comments, + * therefore the extended properties functionality is used + * as a workaround to store them. + * The property name used to store column comments is "MS_Description" + * which provides compatibility with SQL Server Management Studio, + * 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 drop the comment for. + */ + protected function getDropColumnCommentSQL(string $tableName, string $columnName): string + { + if (str_contains($tableName, '.')) { + [$schemaName, $tableName] = explode('.', $tableName); + } else { + $schemaName = 'dbo'; + } + + return $this->getDropExtendedPropertySQL( + 'MS_Description', + 'SCHEMA', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($schemaName)), + 'TABLE', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), + 'COLUMN', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($columnName)), + ); + } + + /** + * {@inheritDoc} + */ + protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array + { + return [sprintf( + "EXEC sp_rename N'%s.%s', N'%s', N'INDEX'", + $tableName, + $oldIndexName, + $index->getQuotedName($this), + ), + ]; + } + + /** + * Returns the SQL statement for adding an extended property to a database object. + * + * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx + * + * @param string $name The name of the property to add. + * @param string|null $value The value of the property to add. + * @param string|null $level0Type The type of the object at level 0 the property belongs to. + * @param string|null $level0Name The name of the object at level 0 the property belongs to. + * @param string|null $level1Type The type of the object at level 1 the property belongs to. + * @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. + */ + 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 { + 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 + : '' + ); + } + + /** + * Returns the SQL statement for dropping an extended property from a database object. + * + * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx + * + * @param string $name The name of the property to drop. + * @param string|null $level0Type The type of the object at level 0 the property belongs to. + * @param string|null $level0Name The name of the object at level 0 the property belongs to. + * @param string|null $level1Type The type of the object at level 1 the property belongs to. + * @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. + */ + protected function getDropExtendedPropertySQL( + string $name, + ?string $level0Type = null, + ?string $level0Name = null, + ?string $level1Type = null, + ?string $level1Name = null, + ?string $level2Type = null, + ?string $level2Name = null, + ): string { + 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 + : '' + ); + } + + /** + * Returns the SQL statement for updating an extended property of a database object. + * + * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx + * + * @param string $name The name of the property to update. + * @param string|null $value The value of the property to update. + * @param string|null $level0Type The type of the object at level 0 the property belongs to. + * @param string|null $level0Name The name of the object at level 0 the property belongs to. + * @param string|null $level1Type The type of the object at level 1 the property belongs to. + * @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. + */ + 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 { + 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 + : '' + ); + } + + public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string + { + return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES'; + } + + /** @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"; + } + + public function getLocateExpression(string $string, string $substring, ?string $start = null): string + { + if ($start === null) { + return sprintf('CHARINDEX(%s, %s)', $substring, $string); + } + + return sprintf('CHARINDEX(%s, %s, %s)', $substring, $string, $start); + } + + public function getModExpression(string $dividend, string $divisor): string + { + return $dividend . ' % ' . $divisor; + } + + 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 . " + ']%'"; + + if ($mode === TrimMode::LEADING) { + return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)'; + } + + if ($mode === TrimMode::TRAILING) { + return 'reverse(stuff(reverse(' . $str . '), 1, ' + . 'patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))'; + } + + return 'reverse(stuff(reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)), 1, ' + . 'patindex(' . $pattern . ', reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str + . ') - 1, null))) - 1, null))'; + } + + public function getConcatExpression(string ...$string): string + { + return sprintf('CONCAT(%s)', implode(', ', $string)); + } + + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListDatabasesSQL(): string + { + return 'SELECT * FROM sys.databases'; + } + + public function getSubstringExpression(string $string, string $start, ?string $length = null): string + { + if ($length === null) { + return sprintf('SUBSTRING(%s, %s, LEN(%s) - %s + 1)', $string, $start, $string, $start); + } + + return sprintf('SUBSTRING(%s, %s, %s)', $string, $start, $length); + } + + public function getLengthExpression(string $string): string + { + return 'LEN(' . $string . ')'; + } + + public function getCurrentDatabaseExpression(): string + { + return 'DB_NAME()'; + } + + public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string + { + return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column): string + { + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column): string + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column): string + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getGuidTypeDeclarationSQL(array $column): string + { + return 'UNIQUEIDENTIFIER'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzTypeDeclarationSQL(array $column): string + { + return 'DATETIMEOFFSET(6)'; + } + + protected function getCharTypeDeclarationSQLSnippet(?int $length): string + { + $sql = 'NCHAR'; + + if ($length !== null) { + $sql .= sprintf('(%d)', $length); + } + + return $sql; + } + + protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string + { + if ($length === null) { + throw ColumnLengthRequired::new($this, 'NVARCHAR'); + } + + return sprintf('NVARCHAR(%d)', $length); + } + + /** + * {@inheritDoc} + */ + public function getAsciiStringTypeDeclarationSQL(array $column): string + { + $length = $column['length'] ?? null; + + if (empty($column['fixed'])) { + return parent::getVarcharTypeDeclarationSQLSnippet($length); + } + + return parent::getCharTypeDeclarationSQLSnippet($length); + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column): string + { + return 'VARCHAR(MAX)'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column): string + { + return ! empty($column['autoincrement']) ? ' IDENTITY' : ''; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column): string + { + // 3 - microseconds precision length + // http://msdn.microsoft.com/en-us/library/ms187819.aspx + return 'DATETIME2(6)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column): string + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column): string + { + return 'TIME(0)'; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column): string + { + return 'BIT'; + } + + protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string + { + if ($limit === null && $offset <= 0) { + return $query; + } + + if ($this->shouldAddOrderBy($query)) { + if (preg_match('/^SELECT\s+DISTINCT/im', $query) > 0) { + // SQL Server won't let us order by a non-selected column in a DISTINCT query, + // so we have to do this madness. This says, order by the first column in the + // result. SQL Server's docs say that a nonordered query's result order is non- + // deterministic anyway, so this won't do anything that a bunch of update and + // deletes to the table wouldn't do anyway. + $query .= ' ORDER BY 1'; + } else { + // In another DBMS, we could do ORDER BY 0, but SQL Server gets angry if you + // use constant expressions in the order by list. + $query .= ' ORDER BY (SELECT 0)'; + } + } + + // This looks somewhat like MYSQL, but limit/offset are in inverse positions + // Supposedly SQL:2008 core standard. + // Per TSQL spec, FETCH NEXT n ROWS ONLY is not valid without OFFSET n ROWS. + $query .= sprintf(' OFFSET %d ROWS', $offset); + + if ($limit !== null) { + $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit); + } + + return $query; + } + + public function convertBooleans(mixed $item): mixed + { + if (is_array($item)) { + foreach ($item as $key => $value) { + if (! is_bool($value) && ! is_numeric($value)) { + continue; + } + + $item[$key] = (int) (bool) $value; + } + } elseif (is_bool($item) || is_numeric($item)) { + $item = (int) (bool) $item; + } + + return $item; + } + + public function getCreateTemporaryTableSnippetSQL(): string + { + return 'CREATE TABLE'; + } + + public function getTemporaryTableName(string $tableName): string + { + return '#' . $tableName; + } + + public function getDateTimeFormatString(): string + { + return 'Y-m-d H:i:s.u'; + } + + public function getDateFormatString(): string + { + return 'Y-m-d'; + } + + public function getTimeFormatString(): string + { + return 'H:i:s'; + } + + public function getDateTimeTzFormatString(): string + { + return 'Y-m-d H:i:s.u P'; + } + + protected function initializeDoctrineTypeMappings(): void + { + $this->doctrineTypeMapping = [ + 'bigint' => Types::BIGINT, + 'binary' => Types::BINARY, + 'bit' => Types::BOOLEAN, + 'blob' => Types::BLOB, + 'char' => Types::STRING, + 'date' => Types::DATE_MUTABLE, + 'datetime' => Types::DATETIME_MUTABLE, + 'datetime2' => Types::DATETIME_MUTABLE, + 'datetimeoffset' => Types::DATETIMETZ_MUTABLE, + 'decimal' => Types::DECIMAL, + 'double' => Types::FLOAT, + 'double precision' => Types::FLOAT, + 'float' => Types::FLOAT, + 'image' => Types::BLOB, + 'int' => Types::INTEGER, + 'money' => Types::INTEGER, + 'nchar' => Types::STRING, + 'ntext' => Types::TEXT, + 'numeric' => Types::DECIMAL, + 'nvarchar' => Types::STRING, + 'real' => Types::FLOAT, + 'smalldatetime' => Types::DATETIME_MUTABLE, + 'smallint' => Types::SMALLINT, + 'smallmoney' => Types::INTEGER, + 'text' => Types::TEXT, + 'time' => Types::TIME_MUTABLE, + 'tinyint' => Types::SMALLINT, + 'uniqueidentifier' => Types::GUID, + 'varbinary' => Types::BINARY, + 'varchar' => Types::STRING, + ]; + } + + public function createSavePoint(string $savepoint): string + { + return 'SAVE TRANSACTION ' . $savepoint; + } + + public function releaseSavePoint(string $savepoint): string + { + return ''; + } + + public function rollbackSavePoint(string $savepoint): string + { + return 'ROLLBACK TRANSACTION ' . $savepoint; + } + + /** @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') { + return 'NO ACTION'; + } + + return parent::getForeignKeyReferentialActionSQL($action); + } + + public function appendLockHint(string $fromClause, LockMode $lockMode): string + { + return match ($lockMode) { + LockMode::NONE, + LockMode::OPTIMISTIC => $fromClause, + LockMode::PESSIMISTIC_READ => $fromClause . ' WITH (HOLDLOCK, ROWLOCK)', + LockMode::PESSIMISTIC_WRITE => $fromClause . ' WITH (UPDLOCK, ROWLOCK)', + }; + } + + protected function createReservedKeywordsList(): KeywordList + { + return new SQLServerKeywords(); + } + + public function quoteSingleIdentifier(string $str): string + { + return '[' . str_replace(']', ']]', $str) . ']'; + } + + public function getTruncateTableSQL(string $tableName, bool $cascade = false): string + { + $tableIdentifier = new Identifier($tableName); + + return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this); + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column): string + { + return 'VARBINARY(MAX)'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getColumnDeclarationSQL(string $name, array $column): string + { + if (isset($column['columnDefinition'])) { + $declaration = $column['columnDefinition']; + } else { + $collation = ! empty($column['collation']) ? + ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : ''; + + $notnull = ! empty($column['notnull']) ? ' NOT NULL' : ''; + + $typeDecl = $column['type']->getSQLDeclaration($column, $this); + $declaration = $typeDecl . $collation . $notnull; + } + + return $name . ' ' . $declaration; + } + + /** + * SQL Server does not support quoting collation identifiers. + */ + public function getColumnCollationDeclarationSQL(string $collation): string + { + return 'COLLATE ' . $collation; + } + + public function columnsEqual(Column $column1, Column $column2): bool + { + if (! parent::columnsEqual($column1, $column2)) { + return false; + } + + return $this->getDefaultValueDeclarationSQL($column1->toArray()) + === $this->getDefaultValueDeclarationSQL($column2->toArray()); + } + + protected function getLikeWildcardCharacters(): string + { + return parent::getLikeWildcardCharacters() . '[]^'; + } + + protected function getCommentOnTableSQL(string $tableName, string $comment): string + { + return $this->getAddExtendedPropertySQL( + 'MS_Description', + $comment, + 'SCHEMA', + $this->quoteStringLiteral('dbo'), + 'TABLE', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), + ); + } + + 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 + $matches = []; + $matchesCount = preg_match_all('/[\\s]+order\\s+by\\s/im', $query, $matches, PREG_OFFSET_CAPTURE); + if ($matchesCount === 0) { + return true; + } + + // ORDER BY instance may be in a subquery after ORDER BY + // e.g. SELECT col1 FROM test ORDER BY (SELECT col2 from test ORDER BY col2) + // if in the searched query ORDER BY clause was found where + // number of open parentheses after the occurrence of the clause is equal to + // number of closed brackets after the occurrence of the clause, + // it means that ORDER BY is included in the query being checked + while ($matchesCount > 0) { + $orderByPos = $matches[0][--$matchesCount][1]; + $openBracketsCount = substr_count($query, '(', $orderByPos); + $closedBracketsCount = substr_count($query, ')', $orderByPos); + if ($openBracketsCount === $closedBracketsCount) { + return false; + } + } + + return true; + } + + public function createSchemaManager(Connection $connection): SQLServerSchemaManager + { + return new SQLServerSchemaManager($connection, $this); + } +} diff --git a/vendor/doctrine/dbal/src/Platforms/SQLite/Comparator.php b/vendor/doctrine/dbal/src/Platforms/SQLite/Comparator.php new file mode 100644 index 0000000..f27e1b4 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/SQLite/Comparator.php @@ -0,0 +1,52 @@ +normalizeColumns($oldTable), + $this->normalizeColumns($newTable), + ); + } + + private function normalizeColumns(Table $table): Table + { + $table = clone $table; + + foreach ($table->getColumns() as $column) { + $options = $column->getPlatformOptions(); + + if (! isset($options['collation']) || strcasecmp($options['collation'], 'binary') !== 0) { + continue; + } + + unset($options['collation']); + $column->setPlatformOptions($options); + } + + return $table; + } +} diff --git a/vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php b/vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php new file mode 100644 index 0000000..acec163 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php @@ -0,0 +1,994 @@ + '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' : ''; + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey): string + { + return parent::getForeignKeyDeclarationSQL(new ForeignKeyConstraint( + $foreignKey->getQuotedLocalColumns($this), + $foreignKey->getQuotedForeignTableName($this), + $foreignKey->getQuotedForeignColumns($this), + $foreignKey->getName(), + $foreignKey->getOptions(), + )); + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array + { + $queryFields = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $definition) { + $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($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) { + foreach ($columns as $column) { + if ($column['name'] === $keyColumn && ! empty($column['autoincrement'])) { + return ''; + } + } + } + + return ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + } + + 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 + { + if ($comment === '') { + return ''; + } + + return '--' . str_replace("\n", "\n--", $comment) . "\n"; + } + + private function getInlineTableCommentSQL(string $comment): string + { + return $this->getInlineColumnCommentSQL($comment); + } + + 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' => 'float', + 'serial' => 'integer', + 'smallint' => 'smallint', + 'string' => 'string', + 'text' => 'text', + 'time' => 'time', + 'timestamp' => 'datetime', + 'tinyint' => 'boolean', + 'tinytext' => 'text', + 'varchar' => 'string', + 'varchar2' => 'string', + ]; + } + + protected function createReservedKeywordsList(): KeywordList + { + 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, $table) as $index) { + if ($index->isPrimary()) { + continue; + } + + $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; + } + + 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 = []; + $columnSql = []; + + 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->getRenamedColumns() as $oldColumnName => $column) { + $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) { + $oldColumnName = strtolower($columnDiff->getOldColumn()->getName()); + $newColumn = $columnDiff->getNewColumn(); + + $columns = $this->replaceColumn( + $table->getName(), + $columns, + $oldColumnName, + $newColumn, + ); + + if (! isset($newColumnNames[$oldColumnName])) { + continue; + } + + $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, $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->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), $columnSql); + } + + /** + * Replace the column with the given name with the new column. + * + * @param array $columns + * + * @return array + * + * @throws Exception + */ + 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 + * + * @throws Exception + */ + private function getSimpleAlterTableSQL(TableDiff $diff): array|false + { + 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(); + + $sql = []; + $columnSql = []; + + foreach ($diff->getAddedColumns() as $column) { + $definition = array_merge([ + 'unique' => null, + 'autoincrement' => null, + 'default' => null, + ], $column->toArray()); + + $type = $definition['type']; + + /** @psalm-suppress RiskyTruthyFalsyComparison */ + 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); + + $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' ADD COLUMN ' + . $this->getColumnDeclarationSQL($definition['name'], $definition); + } + + return array_merge($sql, $columnSql); + } + + /** @return string[] */ + private function getColumnNamesInAlteredTable(TableDiff $diff, Table $oldTable): array + { + $columns = []; + + foreach ($oldTable->getColumns() as $column) { + $columnName = $column->getName(); + $columns[strtolower($columnName)] = $columnName; + } + + 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) { + $oldColumnName = $columnDiff->getOldColumn()->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 $oldTable): array + { + $indexes = $oldTable->getIndexes(); + $columnNames = $this->getColumnNamesInAlteredTable($diff, $oldTable); + + 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 = $index->getName(); + + if ($indexName === '') { + continue; + } + + 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 ForeignKeyConstraint[] */ + private function getForeignKeysInAlteredTable(TableDiff $diff, Table $oldTable): array + { + $foreignKeys = $oldTable->getForeignKeys(); + $columnNames = $this->getColumnNamesInAlteredTable($diff, $oldTable); + + 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) { + $constraintName = $constraint->getName(); + + if ($constraintName === '') { + continue; + } + + 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 Index[] */ + private function getPrimaryIndexInAlteredTable(TableDiff $diff, Table $oldTable): array + { + $primaryIndex = []; + + foreach ($this->getIndexesInAlteredTable($diff, $oldTable) 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/vendor/doctrine/dbal/src/Platforms/TrimMode.php b/vendor/doctrine/dbal/src/Platforms/TrimMode.php new file mode 100644 index 0000000..31c2375 --- /dev/null +++ b/vendor/doctrine/dbal/src/Platforms/TrimMode.php @@ -0,0 +1,13 @@ +