diff --git a/app/composer.lock b/app/composer.lock index ff0065eed..64dc9760d 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -455,16 +455,16 @@ }, { "name": "doctrine/dbal", - "version": "3.3.6", + "version": "3.3.7", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "9e7f76dd1cde81c62574fdffa5a9c655c847ad21" + "reference": "9f79d4650430b582f4598fe0954ef4d52fbc0a8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/9e7f76dd1cde81c62574fdffa5a9c655c847ad21", - "reference": "9e7f76dd1cde81c62574fdffa5a9c655c847ad21", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/9f79d4650430b582f4598fe0954ef4d52fbc0a8a", + "reference": "9f79d4650430b582f4598fe0954ef4d52fbc0a8a", "shasum": "" }, "require": { @@ -479,11 +479,11 @@ "require-dev": { "doctrine/coding-standard": "9.0.0", "jetbrains/phpstorm-stubs": "2022.1", - "phpstan/phpstan": "1.6.3", + "phpstan/phpstan": "1.7.13", "phpstan/phpstan-strict-rules": "^1.2", "phpunit/phpunit": "9.5.20", "psalm/plugin-phpunit": "0.16.1", - "squizlabs/php_codesniffer": "3.6.2", + "squizlabs/php_codesniffer": "3.7.0", "symfony/cache": "^5.2|^6.0", "symfony/console": "^2.7|^3.0|^4.0|^5.0|^6.0", "vimeo/psalm": "4.23.0" @@ -546,7 +546,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.3.6" + "source": "https://github.com/doctrine/dbal/tree/3.3.7" }, "funding": [ { @@ -562,7 +562,7 @@ "type": "tidelift" } ], - "time": "2022-05-02T17:21:01+00:00" + "time": "2022-06-13T21:43:03+00:00" }, { "name": "doctrine/deprecations", diff --git a/app/vendor/composer/installed.json b/app/vendor/composer/installed.json index 18e0b9a03..6b866c7e8 100644 --- a/app/vendor/composer/installed.json +++ b/app/vendor/composer/installed.json @@ -1330,17 +1330,17 @@ }, { "name": "doctrine/dbal", - "version": "3.3.6", - "version_normalized": "3.3.6.0", + "version": "3.3.7", + "version_normalized": "3.3.7.0", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "9e7f76dd1cde81c62574fdffa5a9c655c847ad21" + "reference": "9f79d4650430b582f4598fe0954ef4d52fbc0a8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/9e7f76dd1cde81c62574fdffa5a9c655c847ad21", - "reference": "9e7f76dd1cde81c62574fdffa5a9c655c847ad21", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/9f79d4650430b582f4598fe0954ef4d52fbc0a8a", + "reference": "9f79d4650430b582f4598fe0954ef4d52fbc0a8a", "shasum": "" }, "require": { @@ -1355,11 +1355,11 @@ "require-dev": { "doctrine/coding-standard": "9.0.0", "jetbrains/phpstorm-stubs": "2022.1", - "phpstan/phpstan": "1.6.3", + "phpstan/phpstan": "1.7.13", "phpstan/phpstan-strict-rules": "^1.2", "phpunit/phpunit": "9.5.20", "psalm/plugin-phpunit": "0.16.1", - "squizlabs/php_codesniffer": "3.6.2", + "squizlabs/php_codesniffer": "3.7.0", "symfony/cache": "^5.2|^6.0", "symfony/console": "^2.7|^3.0|^4.0|^5.0|^6.0", "vimeo/psalm": "4.23.0" @@ -1367,7 +1367,7 @@ "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." }, - "time": "2022-05-02T17:21:01+00:00", + "time": "2022-06-13T21:43:03+00:00", "bin": [ "bin/doctrine-dbal" ], @@ -1424,7 +1424,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.3.6" + "source": "https://github.com/doctrine/dbal/tree/3.3.7" }, "funding": [ { diff --git a/app/vendor/composer/installed.php b/app/vendor/composer/installed.php index ba144fc0d..11e61a103 100644 --- a/app/vendor/composer/installed.php +++ b/app/vendor/composer/installed.php @@ -5,7 +5,7 @@ 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => '20a630fdda018e7cfdfe697ae7c25d9ac5d9bb20', + 'reference' => 'c30962d4b82e0bf29c54e2ea414abd003a620bec', 'name' => 'cakephp/app', 'dev' => true, ), @@ -25,7 +25,7 @@ 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => '20a630fdda018e7cfdfe697ae7c25d9ac5d9bb20', + 'reference' => 'c30962d4b82e0bf29c54e2ea414abd003a620bec', 'dev_requirement' => false, ), 'cakephp/bake' => array( @@ -272,12 +272,12 @@ 'dev_requirement' => false, ), 'doctrine/dbal' => array( - 'pretty_version' => '3.3.6', - 'version' => '3.3.6.0', + 'pretty_version' => '3.3.7', + 'version' => '3.3.7.0', 'type' => 'library', 'install_path' => __DIR__ . '/../doctrine/dbal', 'aliases' => array(), - 'reference' => '9e7f76dd1cde81c62574fdffa5a9c655c847ad21', + 'reference' => '9f79d4650430b582f4598fe0954ef4d52fbc0a8a', 'dev_requirement' => false, ), 'doctrine/deprecations' => array( diff --git a/app/vendor/doctrine/dbal/composer.json b/app/vendor/doctrine/dbal/composer.json index 17b124034..b08057936 100644 --- a/app/vendor/doctrine/dbal/composer.json +++ b/app/vendor/doctrine/dbal/composer.json @@ -42,11 +42,11 @@ "require-dev": { "doctrine/coding-standard": "9.0.0", "jetbrains/phpstorm-stubs": "2022.1", - "phpstan/phpstan": "1.6.3", + "phpstan/phpstan": "1.7.13", "phpstan/phpstan-strict-rules": "^1.2", "phpunit/phpunit": "9.5.20", "psalm/plugin-phpunit": "0.16.1", - "squizlabs/php_codesniffer": "3.6.2", + "squizlabs/php_codesniffer": "3.7.0", "symfony/cache": "^5.2|^6.0", "symfony/console": "^2.7|^3.0|^4.0|^5.0|^6.0", "vimeo/psalm": "4.23.0" diff --git a/app/vendor/doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php b/app/vendor/doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php new file mode 100644 index 000000000..29e73d1f2 --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php @@ -0,0 +1,48 @@ + 0) { + $offset -= 1; + } + + $pos = strpos($str, $substr, $offset); + + if ($pos !== false) { + return $pos + 1; + } + + return 0; + } +} diff --git a/app/vendor/doctrine/dbal/src/Driver/IBMDB2/Exception/Factory.php b/app/vendor/doctrine/dbal/src/Driver/IBMDB2/Exception/Factory.php new file mode 100644 index 000000000..91b9b439c --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Driver/IBMDB2/Exception/Factory.php @@ -0,0 +1,35 @@ +wrappedConnection = $wrappedConnection; + } + + public function prepare(string $sql): Statement + { + return $this->wrappedConnection->prepare($sql); + } + + public function query(string $sql): Result + { + return $this->wrappedConnection->query($sql); + } + + /** + * {@inheritdoc} + */ + public function quote($value, $type = ParameterType::STRING) + { + return $this->wrappedConnection->quote($value, $type); + } + + public function exec(string $sql): int + { + return $this->wrappedConnection->exec($sql); + } + + /** + * {@inheritdoc} + */ + public function lastInsertId($name = null) + { + if ($name !== null) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4687', + 'The usage of Connection::lastInsertId() with a sequence name is deprecated.' + ); + } + + return $this->wrappedConnection->lastInsertId($name); + } + + /** + * {@inheritdoc} + */ + public function beginTransaction() + { + return $this->wrappedConnection->beginTransaction(); + } + + /** + * {@inheritdoc} + */ + public function commit() + { + return $this->wrappedConnection->commit(); + } + + /** + * {@inheritdoc} + */ + public function rollBack() + { + return $this->wrappedConnection->rollBack(); + } + + /** + * {@inheritdoc} + */ + public function getServerVersion() + { + if (! $this->wrappedConnection instanceof ServerInfoAwareConnection) { + throw new LogicException('The underlying connection is not a ServerInfoAwareConnection'); + } + + return $this->wrappedConnection->getServerVersion(); + } + + /** + * @return resource|object + */ + public function getNativeConnection() + { + if (! method_exists($this->wrappedConnection, 'getNativeConnection')) { + throw new LogicException(sprintf( + 'The driver connection %s does not support accessing the native connection.', + get_class($this->wrappedConnection) + )); + } + + return $this->wrappedConnection->getNativeConnection(); + } +} diff --git a/app/vendor/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php b/app/vendor/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php new file mode 100644 index 000000000..ab1f508f7 --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php @@ -0,0 +1,61 @@ +wrappedDriver = $wrappedDriver; + } + + /** + * {@inheritdoc} + */ + public function connect(array $params) + { + return $this->wrappedDriver->connect($params); + } + + /** + * {@inheritdoc} + */ + public function getDatabasePlatform() + { + return $this->wrappedDriver->getDatabasePlatform(); + } + + /** + * {@inheritdoc} + */ + public function getSchemaManager(Connection $conn, AbstractPlatform $platform) + { + return $this->wrappedDriver->getSchemaManager($conn, $platform); + } + + public function getExceptionConverter(): ExceptionConverter + { + return $this->wrappedDriver->getExceptionConverter(); + } + + /** + * {@inheritdoc} + */ + public function createDatabasePlatformForVersion($version) + { + if ($this->wrappedDriver instanceof VersionAwarePlatformDriver) { + return $this->wrappedDriver->createDatabasePlatformForVersion($version); + } + + return $this->wrappedDriver->getDatabasePlatform(); + } +} diff --git a/app/vendor/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php b/app/vendor/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php new file mode 100644 index 000000000..ebc63c570 --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php @@ -0,0 +1,79 @@ +wrappedResult = $result; + } + + /** + * {@inheritdoc} + */ + public function fetchNumeric() + { + return $this->wrappedResult->fetchNumeric(); + } + + /** + * {@inheritdoc} + */ + public function fetchAssociative() + { + return $this->wrappedResult->fetchAssociative(); + } + + /** + * {@inheritdoc} + */ + public function fetchOne() + { + return $this->wrappedResult->fetchOne(); + } + + /** + * {@inheritdoc} + */ + public function fetchAllNumeric(): array + { + return $this->wrappedResult->fetchAllNumeric(); + } + + /** + * {@inheritdoc} + */ + public function fetchAllAssociative(): array + { + return $this->wrappedResult->fetchAllAssociative(); + } + + /** + * {@inheritdoc} + */ + public function fetchFirstColumn(): array + { + return $this->wrappedResult->fetchFirstColumn(); + } + + public function rowCount(): int + { + return $this->wrappedResult->rowCount(); + } + + public function columnCount(): int + { + return $this->wrappedResult->columnCount(); + } + + public function free(): void + { + $this->wrappedResult->free(); + } +} diff --git a/app/vendor/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php b/app/vendor/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php new file mode 100644 index 000000000..a646cd30c --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php @@ -0,0 +1,42 @@ +wrappedStatement = $wrappedStatement; + } + + /** + * {@inheritdoc} + */ + public function bindValue($param, $value, $type = ParameterType::STRING) + { + return $this->wrappedStatement->bindValue($param, $value, $type); + } + + /** + * {@inheritdoc} + */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null) + { + return $this->wrappedStatement->bindParam($param, $variable, $type, $length); + } + + /** + * {@inheritdoc} + */ + public function execute($params = null): Result + { + return $this->wrappedStatement->execute($params); + } +} diff --git a/app/vendor/doctrine/dbal/src/Event/TransactionBeginEventArgs.php b/app/vendor/doctrine/dbal/src/Event/TransactionBeginEventArgs.php new file mode 100644 index 000000000..946e8f2eb --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Event/TransactionBeginEventArgs.php @@ -0,0 +1,9 @@ +connection = $connection; + } + + public function getConnection(): Connection + { + return $this->connection; + } +} diff --git a/app/vendor/doctrine/dbal/src/Event/TransactionRollBackEventArgs.php b/app/vendor/doctrine/dbal/src/Event/TransactionRollBackEventArgs.php new file mode 100644 index 000000000..607a5f94a --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Event/TransactionRollBackEventArgs.php @@ -0,0 +1,9 @@ +logger = $logger; + } + + public function __destruct() + { + $this->logger->info('Disconnecting'); + } + + public function prepare(string $sql): DriverStatement + { + return new Statement( + parent::prepare($sql), + $this->logger, + $sql + ); + } + + public function query(string $sql): Result + { + $this->logger->debug('Executing query: {sql}', ['sql' => $sql]); + + return parent::query($sql); + } + + public function exec(string $sql): int + { + $this->logger->debug('Executing statement: {sql}', ['sql' => $sql]); + + return parent::exec($sql); + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + $this->logger->debug('Beginning transaction'); + + return parent::beginTransaction(); + } + + /** + * {@inheritDoc} + */ + public function commit() + { + $this->logger->debug('Committing transaction'); + + return parent::commit(); + } + + /** + * {@inheritDoc} + */ + public function rollBack() + { + $this->logger->debug('Rolling back transaction'); + + return parent::rollBack(); + } +} diff --git a/app/vendor/doctrine/dbal/src/Logging/Driver.php b/app/vendor/doctrine/dbal/src/Logging/Driver.php new file mode 100644 index 000000000..533dcc05c --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Logging/Driver.php @@ -0,0 +1,56 @@ +logger = $logger; + } + + /** + * {@inheritDoc} + */ + public function connect(array $params) + { + $this->logger->info('Connecting with parameters {params}', ['params' => $this->maskPassword($params)]); + + return new Connection( + parent::connect($params), + $this->logger + ); + } + + /** + * @param array $params Connection parameters + * + * @return array + */ + private function maskPassword(array $params): array + { + if (isset($params['password'])) { + $params['password'] = ''; + } + + if (isset($params['url'])) { + $params['url'] = ''; + } + + return $params; + } +} diff --git a/app/vendor/doctrine/dbal/src/Logging/Middleware.php b/app/vendor/doctrine/dbal/src/Logging/Middleware.php new file mode 100644 index 000000000..4d5c6b061 --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Logging/Middleware.php @@ -0,0 +1,25 @@ +logger = $logger; + } + + public function wrap(DriverInterface $driver): DriverInterface + { + return new Driver($driver, $this->logger); + } +} diff --git a/app/vendor/doctrine/dbal/src/Logging/Statement.php b/app/vendor/doctrine/dbal/src/Logging/Statement.php new file mode 100644 index 000000000..e993767aa --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Logging/Statement.php @@ -0,0 +1,76 @@ +|array */ + private $params = []; + + /** @var array|array */ + private $types = []; + + /** + * @internal This statement can be only instantiated by its connection. + */ + public function __construct(StatementInterface $statement, LoggerInterface $logger, string $sql) + { + parent::__construct($statement); + + $this->logger = $logger; + $this->sql = $sql; + } + + /** + * {@inheritdoc} + */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null) + { + $this->params[$param] = &$variable; + $this->types[$param] = $type; + + return parent::bindParam($param, $variable, $type, ...array_slice(func_get_args(), 3)); + } + + /** + * {@inheritdoc} + */ + public function bindValue($param, $value, $type = ParameterType::STRING) + { + $this->params[$param] = $value; + $this->types[$param] = $type; + + return parent::bindValue($param, $value, $type); + } + + /** + * {@inheritdoc} + */ + public function execute($params = null): ResultInterface + { + $this->logger->debug('Executing statement: {sql} (parameters: {params}, types: {types})', [ + 'sql' => $this->sql, + 'params' => $params ?? $this->params, + 'types' => $this->types, + ]); + + return parent::execute($params); + } +} diff --git a/app/vendor/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php b/app/vendor/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php new file mode 100644 index 000000000..b80291492 --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php @@ -0,0 +1,1188 @@ + 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; + } + + /** + * {@inheritDoc} + */ + public function getIdentifierQuoteCharacter() + { + return '`'; + } + + /** + * {@inheritDoc} + */ + public function getRegexpExpression() + { + return 'RLIKE'; + } + + /** + * {@inheritDoc} + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos === false) { + return 'LOCATE(' . $substr . ', ' . $str . ')'; + } + + return 'LOCATE(' . $substr . ', ' . $str . ', ' . $startPos . ')'; + } + + /** + * {@inheritDoc} + */ + public function getConcatExpression() + { + return sprintf('CONCAT(%s)', implode(', ', func_get_args())); + } + + /** + * {@inheritdoc} + */ + protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit) + { + $function = $operator === '+' ? 'DATE_ADD' : 'DATE_SUB'; + + return $function . '(' . $date . ', INTERVAL ' . $interval . ' ' . $unit . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateDiffExpression($date1, $date2) + { + return 'DATEDIFF(' . $date1 . ', ' . $date2 . ')'; + } + + public function getCurrentDatabaseExpression(): string + { + return 'DATABASE()'; + } + + /** + * {@inheritDoc} + */ + public function getLengthExpression($column) + { + return 'CHAR_LENGTH(' . $column . ')'; + } + + /** + * {@inheritDoc} + */ + public function getListDatabasesSQL() + { + return 'SHOW DATABASES'; + } + + /** + * {@inheritDoc} + */ + public function getListTableConstraintsSQL($table) + { + return 'SHOW INDEX FROM ' . $table; + } + + /** + * {@inheritDoc} + * + * Two approaches to listing the table indexes. The information_schema is + * preferred, because it doesn't cause problems with SQL keywords such as "order" or "table". + */ + public function getListTableIndexesSQL($table, $database = null) + { + if ($database !== null) { + return 'SELECT NON_UNIQUE AS Non_Unique, INDEX_NAME AS Key_name, COLUMN_NAME AS Column_Name,' . + ' SUB_PART AS Sub_Part, INDEX_TYPE AS Index_Type' . + ' FROM information_schema.STATISTICS WHERE TABLE_NAME = ' . $this->quoteStringLiteral($table) . + ' AND TABLE_SCHEMA = ' . $this->quoteStringLiteral($database) . + ' ORDER BY SEQ_IN_INDEX ASC'; + } + + return 'SHOW INDEX FROM ' . $table; + } + + /** + * {@inheritDoc} + */ + public function getListViewsSQL($database) + { + return 'SELECT * FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ' . $this->quoteStringLiteral($database); + } + + /** + * @param string $table + * @param string|null $database + * + * @return string + */ + public function getListTableForeignKeysSQL($table, $database = null) + { + // The schema name is passed multiple times as a literal in the WHERE clause instead of using a JOIN condition + // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions + // caused by https://bugs.mysql.com/bug.php?id=81347 + return 'SELECT k.CONSTRAINT_NAME, k.COLUMN_NAME, k.REFERENCED_TABLE_NAME, ' . + 'k.REFERENCED_COLUMN_NAME /*!50116 , c.UPDATE_RULE, c.DELETE_RULE */ ' . + 'FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE k /*!50116 ' . + 'INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS c ON ' . + 'c.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND ' . + 'c.TABLE_NAME = k.TABLE_NAME */ ' . + 'WHERE k.TABLE_NAME = ' . $this->quoteStringLiteral($table) . ' ' . + 'AND k.TABLE_SCHEMA = ' . $this->getDatabaseNameSQL($database) . ' /*!50116 ' . + 'AND c.CONSTRAINT_SCHEMA = ' . $this->getDatabaseNameSQL($database) . ' */' . + 'ORDER BY k.ORDINAL_POSITION'; + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + { + return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(255)') + : ($length > 0 ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'); + } + + /** + * {@inheritdoc} + */ + protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed) + { + return $fixed + ? 'BINARY(' . ($length > 0 ? $length : 255) . ')' + : 'VARBINARY(' . ($length > 0 ? $length : 255) . ')'; + } + + /** + * 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) + { + 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) + { + if (isset($column['version']) && $column['version'] === true) { + return 'TIMESTAMP'; + } + + return 'DATETIME'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column) + { + return 'TIME'; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column) + { + return 'TINYINT(1)'; + } + + /** + * {@inheritDoc} + * + * @deprecated + * + * MySQL prefers "autoincrement" identity columns since sequences can only + * be emulated with a table. + */ + public function prefersIdentityColumns() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pulls/1519', + 'AbstractMySQLPlatform::prefersIdentityColumns() is deprecated.' + ); + + return true; + } + + /** + * {@inheritDoc} + * + * MySQL supports this through AUTO_INCREMENT columns. + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsInlineColumnComments() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsColumnCollation() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getListTablesSQL() + { + return "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"; + } + + /** + * {@inheritDoc} + */ + public function getListTableColumnsSQL($table, $database = null) + { + return 'SELECT COLUMN_NAME AS Field, COLUMN_TYPE AS Type, IS_NULLABLE AS `Null`, ' . + 'COLUMN_KEY AS `Key`, COLUMN_DEFAULT AS `Default`, EXTRA AS Extra, COLUMN_COMMENT AS Comment, ' . + 'CHARACTER_SET_NAME AS CharacterSet, COLLATION_NAME AS Collation ' . + 'FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ' . $this->getDatabaseNameSQL($database) . + ' AND TABLE_NAME = ' . $this->quoteStringLiteral($table) . + ' ORDER BY ORDINAL_POSITION ASC'; + } + + public function getListTableMetadataSQL(string $table, ?string $database = null): string + { + return sprintf( + <<<'SQL' +SELECT t.ENGINE, + t.AUTO_INCREMENT, + t.TABLE_COMMENT, + t.CREATE_OPTIONS, + t.TABLE_COLLATION, + ccsa.CHARACTER_SET_NAME +FROM information_schema.TABLES t + INNER JOIN information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` ccsa + ON ccsa.COLLATION_NAME = t.TABLE_COLLATION +WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = %s AND TABLE_NAME = %s +SQL + , + $this->getDatabaseNameSQL($database), + $this->quoteStringLiteral($table) + ); + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($name, array $columns, array $options = []) + { + $queryFields = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $constraintName => $definition) { + $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($constraintName, $definition); + } + } + + // add all indexes + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach ($options['indexes'] as $indexName => $definition) { + $queryFields .= ', ' . $this->getIndexDeclarationSQL($indexName, $definition); + } + } + + // attach all primary keys + if (isset($options['primary']) && ! empty($options['primary'])) { + $keyColumns = array_unique(array_values($options['primary'])); + $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + } + + $query = 'CREATE '; + + if (! empty($options['temporary'])) { + $query .= 'TEMPORARY '; + } + + $query .= 'TABLE ' . $name . ' (' . $queryFields . ') '; + $query .= $this->buildTableOptions($options); + $query .= $this->buildPartitionOptions($options); + + $sql = [$query]; + $engine = 'INNODB'; + + if (isset($options['engine'])) { + $engine = strtoupper(trim($options['engine'])); + } + + // Propagate foreign key constraints only for InnoDB. + if (isset($options['foreignKeys']) && $engine === 'INNODB') { + foreach ((array) $options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $name); + } + } + + return $sql; + } + + /** + * {@inheritdoc} + */ + public function getDefaultValueDeclarationSQL($column) + { + // Unset the default value if the given column definition does not allow default values. + if ($column['type'] instanceof TextType || $column['type'] instanceof BlobType) { + $column['default'] = null; + } + + return parent::getDefaultValueDeclarationSQL($column); + } + + /** + * Build SQL for table options + * + * @param mixed[] $options + */ + private function buildTableOptions(array $options): string + { + if (isset($options['table_options'])) { + return $options['table_options']; + } + + $tableOptions = []; + + // Charset + if (! isset($options['charset'])) { + $options['charset'] = 'utf8'; + } + + $tableOptions[] = sprintf('DEFAULT CHARACTER SET %s', $options['charset']); + + if (isset($options['collate'])) { + $options['collation'] = $options['collate']; + } + + // Collation + if (! isset($options['collation'])) { + $options['collation'] = $options['charset'] . '_unicode_ci'; + } + + $tableOptions[] = $this->getColumnCollationDeclarationSQL($options['collation']); + + // Engine + if (! isset($options['engine'])) { + $options['engine'] = 'InnoDB'; + } + + $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); + } + + /** + * Build SQL for partition options. + * + * @param mixed[] $options + */ + private function buildPartitionOptions(array $options): string + { + return isset($options['partition_options']) + ? ' ' . $options['partition_options'] + : ''; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $columnSql = []; + $queryParts = []; + $newName = $diff->getNewName(); + + if ($newName !== false) { + $queryParts[] = 'RENAME TO ' . $newName->getQuotedName($this); + } + + foreach ($diff->addedColumns as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $columnArray = array_merge($column->toArray(), [ + 'comment' => $this->getColumnComment($column), + ]); + + $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray); + } + + foreach ($diff->removedColumns as $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $queryParts[] = 'DROP ' . $column->getQuotedName($this); + } + + foreach ($diff->changedColumns as $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + $column = $columnDiff->column; + $columnArray = $column->toArray(); + + // Don't propagate default value changes for unsupported column types. + if ( + $columnDiff->hasChanged('default') && + count($columnDiff->changedProperties) === 1 && + ($columnArray['type'] instanceof TextType || $columnArray['type'] instanceof BlobType) + ) { + continue; + } + + $columnArray['comment'] = $this->getColumnComment($column); + $queryParts[] = 'CHANGE ' . ($columnDiff->getOldColumnName()->getQuotedName($this)) . ' ' + . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray); + } + + foreach ($diff->renamedColumns as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $oldColumnName = new Identifier($oldColumnName); + $columnArray = $column->toArray(); + $columnArray['comment'] = $this->getColumnComment($column); + $queryParts[] = 'CHANGE ' . $oldColumnName->getQuotedName($this) . ' ' + . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray); + } + + if (isset($diff->addedIndexes['primary'])) { + $keyColumns = array_unique(array_values($diff->addedIndexes['primary']->getColumns())); + $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')'; + unset($diff->addedIndexes['primary']); + } elseif (isset($diff->changedIndexes['primary'])) { + // Necessary in case the new primary key includes a new auto_increment column + foreach ($diff->changedIndexes['primary']->getColumns() as $columnName) { + if (isset($diff->addedColumns[$columnName]) && $diff->addedColumns[$columnName]->getAutoincrement()) { + $keyColumns = array_unique(array_values($diff->changedIndexes['primary']->getColumns())); + $queryParts[] = 'DROP PRIMARY KEY'; + $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')'; + unset($diff->changedIndexes['primary']); + break; + } + } + } + + $sql = []; + $tableSql = []; + + if (! $this->onSchemaAlterTable($diff, $tableSql)) { + if (count($queryParts) > 0) { + $sql[] = 'ALTER TABLE ' . $diff->getName($this)->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) + { + $sql = []; + $table = $diff->getName($this)->getQuotedName($this); + + foreach ($diff->changedIndexes as $changedIndex) { + $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $changedIndex)); + } + + foreach ($diff->removedIndexes as $remKey => $remIndex) { + $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $remIndex)); + + foreach ($diff->addedIndexes as $addKey => $addIndex) { + if ($remIndex->getColumns() !== $addIndex->getColumns()) { + continue; + } + + $indexClause = 'INDEX ' . $addIndex->getName(); + + if ($addIndex->isPrimary()) { + $indexClause = 'PRIMARY KEY'; + } elseif ($addIndex->isUnique()) { + $indexClause = 'UNIQUE INDEX ' . $addIndex->getName(); + } + + $query = 'ALTER TABLE ' . $table . ' DROP INDEX ' . $remIndex->getName() . ', '; + $query .= 'ADD ' . $indexClause; + $query .= ' (' . $this->getIndexFieldDeclarationListSQL($addIndex) . ')'; + + $sql[] = $query; + + unset($diff->removedIndexes[$remKey], $diff->addedIndexes[$addKey]); + + break; + } + } + + $engine = 'INNODB'; + + if ($diff->fromTable instanceof Table && $diff->fromTable->hasOption('engine')) { + $engine = strtoupper(trim($diff->fromTable->getOption('engine'))); + } + + // Suppress foreign key constraint propagation on non-supporting engines. + if ($engine !== 'INNODB') { + $diff->addedForeignKeys = []; + $diff->changedForeignKeys = []; + $diff->removedForeignKeys = []; + } + + $sql = array_merge( + $sql, + $this->getPreAlterTableAlterIndexForeignKeySQL($diff), + parent::getPreAlterTableIndexForeignKeySQL($diff), + $this->getPreAlterTableRenameIndexForeignKeySQL($diff) + ); + + return $sql; + } + + /** + * @return string[] + * + * @throws Exception + */ + private function getPreAlterTableAlterPrimaryKeySQL(TableDiff $diff, Index $index): array + { + $sql = []; + + if (! $index->isPrimary() || ! $diff->fromTable instanceof Table) { + return $sql; + } + + $tableName = $diff->getName($this)->getQuotedName($this); + + // Dropping primary keys requires to unset autoincrement attribute on the particular column first. + foreach ($index->getColumns() as $columnName) { + if (! $diff->fromTable->hasColumn($columnName)) { + continue; + } + + $column = $diff->fromTable->getColumn($columnName); + + if ($column->getAutoincrement() !== true) { + continue; + } + + $column->setAutoincrement(false); + + $sql[] = 'ALTER TABLE ' . $tableName . ' 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 string[] + * + * @throws Exception + */ + private function getPreAlterTableAlterIndexForeignKeySQL(TableDiff $diff): array + { + $sql = []; + $table = $diff->getName($this)->getQuotedName($this); + + foreach ($diff->changedIndexes as $changedIndex) { + // Changed primary key + if (! $changedIndex->isPrimary() || ! ($diff->fromTable instanceof Table)) { + continue; + } + + foreach ($diff->fromTable->getPrimaryKeyColumns() as $columnName => $column) { + // Check if an autoincrement column was dropped from the primary key. + if (! $column->getAutoincrement() || in_array($columnName, $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 ' . $table . ' 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 string[] + */ + protected function getPreAlterTableRenameIndexForeignKeySQL(TableDiff $diff) + { + $sql = []; + $tableName = $diff->getName($this)->getQuotedName($this); + + foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) { + if (in_array($foreignKey, $diff->changedForeignKeys, true)) { + continue; + } + + $sql[] = $this->getDropForeignKeySQL($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 + { + if (empty($diff->renamedIndexes) || ! $diff->fromTable instanceof Table) { + return []; + } + + $foreignKeys = []; + /** @var ForeignKeyConstraint[] $remainingForeignKeys */ + $remainingForeignKeys = array_diff_key( + $diff->fromTable->getForeignKeys(), + $diff->removedForeignKeys + ); + + foreach ($remainingForeignKeys as $foreignKey) { + foreach ($diff->renamedIndexes as $index) { + if ($foreignKey->intersectsIndexColumns($index)) { + $foreignKeys[] = $foreignKey; + + break; + } + } + } + + return $foreignKeys; + } + + /** + * {@inheritdoc} + */ + protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff) + { + return array_merge( + parent::getPostAlterTableIndexForeignKeySQL($diff), + $this->getPostAlterTableRenameIndexForeignKeySQL($diff) + ); + } + + /** + * @param TableDiff $diff The table diff to gather the SQL for. + * + * @return string[] + */ + protected function getPostAlterTableRenameIndexForeignKeySQL(TableDiff $diff) + { + $sql = []; + $newName = $diff->getNewName(); + + if ($newName !== false) { + $tableName = $newName->getQuotedName($this); + } else { + $tableName = $diff->getName($this)->getQuotedName($this); + } + + foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) { + if (in_array($foreignKey, $diff->changedForeignKeys, true)) { + continue; + } + + $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName); + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + protected function getCreateIndexSQLFlags(Index $index) + { + $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) + { + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column) + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column) + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritdoc} + */ + public function getFloatDeclarationSQL(array $column) + { + return 'DOUBLE PRECISION' . $this->getUnsignedDeclaration($column); + } + + /** + * {@inheritdoc} + */ + public function getDecimalTypeDeclarationSQL(array $column) + { + 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) + { + $autoinc = ''; + if (! empty($column['autoincrement'])) { + $autoinc = ' AUTO_INCREMENT'; + } + + return $this->getUnsignedDeclaration($column) . $autoinc; + } + + /** + * {@inheritDoc} + */ + public function getColumnCharsetDeclarationSQL($charset) + { + return 'CHARACTER SET ' . $charset; + } + + /** + * {@inheritDoc} + */ + public function getColumnCollationDeclarationSQL($collation) + { + return 'COLLATE ' . $this->quoteSingleIdentifier($collation); + } + + /** + * {@inheritDoc} + */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) + { + $query = ''; + if ($foreignKey->hasOption('match')) { + $query .= ' MATCH ' . $foreignKey->getOption('match'); + } + + $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey); + + return $query; + } + + /** + * {@inheritDoc} + */ + public function getDropIndexSQL($index, $table = null) + { + if ($index instanceof Index) { + $indexName = $index->getQuotedName($this); + } elseif (is_string($index)) { + $indexName = $index; + } else { + throw new InvalidArgumentException( + __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.' + ); + } + + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } elseif (! is_string($table)) { + throw new InvalidArgumentException( + __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.' + ); + } + + if ($index instanceof Index && $index->isPrimary()) { + // MySQL primary keys are always named "PRIMARY", + // so we cannot use them in statements because of them being keyword. + return $this->getDropPrimaryKeySQL($table); + } + + return 'DROP INDEX ' . $indexName . ' ON ' . $table; + } + + /** + * @param string $table + * + * @return string + */ + protected function getDropPrimaryKeySQL($table) + { + return 'ALTER TABLE ' . $table . ' DROP PRIMARY KEY'; + } + + /** + * 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); + } + + /** + * {@inheritDoc} + */ + public function getSetTransactionIsolationSQL($level) + { + return 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4749', + 'AbstractMySQLPlatform::getName() is deprecated. Identify platforms by their class.' + ); + + return 'mysql'; + } + + /** + * {@inheritDoc} + */ + public function getReadLockSQL() + { + return 'LOCK IN SHARE MODE'; + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = [ + 'bigint' => 'bigint', + 'binary' => 'binary', + 'blob' => 'blob', + 'char' => 'string', + 'date' => 'date', + 'datetime' => 'datetime', + 'decimal' => 'decimal', + 'double' => 'float', + 'float' => 'float', + 'int' => 'integer', + 'integer' => 'integer', + 'longblob' => 'blob', + 'longtext' => 'text', + 'mediumblob' => 'blob', + 'mediumint' => 'integer', + 'mediumtext' => 'text', + 'numeric' => 'decimal', + 'real' => 'float', + 'set' => 'simple_array', + 'smallint' => 'smallint', + 'string' => 'string', + 'text' => 'text', + 'time' => 'time', + 'timestamp' => 'datetime', + 'tinyblob' => 'blob', + 'tinyint' => 'boolean', + 'tinytext' => 'text', + 'varbinary' => 'binary', + 'varchar' => 'string', + 'year' => 'date', + ]; + } + + /** + * {@inheritDoc} + */ + public function getVarcharMaxLength() + { + return 65535; + } + + /** + * {@inheritdoc} + */ + public function getBinaryMaxLength() + { + return 65535; + } + + /** + * {@inheritDoc} + * + * @deprecated Implement {@see createReservedKeywordsList()} instead. + */ + protected function getReservedKeywordsClass() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4510', + 'AbstractMySQLPlatform::getReservedKeywordsClass() is deprecated,' + . ' use AbstractMySQLPlatform::createReservedKeywordsList() instead.' + ); + + return Keywords\MySQLKeywords::class; + } + + /** + * {@inheritDoc} + * + * MySQL commits a transaction implicitly when DROP TABLE is executed, however not + * if DROP TEMPORARY TABLE is executed. + */ + public function getDropTemporaryTableSQL($table) + { + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } elseif (! is_string($table)) { + throw new InvalidArgumentException( + __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.' + ); + } + + return 'DROP TEMPORARY TABLE ' . $table; + } + + /** + * 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) + { + 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'; + } + + /** + * {@inheritdoc} + */ + public function quoteStringLiteral($str) + { + $str = str_replace('\\', '\\\\', $str); // MySQL requires backslashes to be escaped aswell. + + return parent::quoteStringLiteral($str); + } + + /** + * {@inheritdoc} + */ + public function getDefaultTransactionIsolationLevel() + { + return TransactionIsolationLevel::REPEATABLE_READ; + } + + public function supportsColumnLengthIndexes(): bool + { + return true; + } + + private function getDatabaseNameSQL(?string $databaseName): string + { + if ($databaseName !== null) { + return $this->quoteStringLiteral($databaseName); + } + + return $this->getCurrentDatabaseExpression(); + } +} diff --git a/app/vendor/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php b/app/vendor/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php new file mode 100644 index 000000000..93c595f28 --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php @@ -0,0 +1,267 @@ +doctrineTypeMapping['json'] = Types::JSON; + } +} diff --git a/app/vendor/doctrine/dbal/src/Platforms/MySQL/Comparator.php b/app/vendor/doctrine/dbal/src/Platforms/MySQL/Comparator.php new file mode 100644 index 000000000..81c54dbc4 --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Platforms/MySQL/Comparator.php @@ -0,0 +1,66 @@ +normalizeColumns($fromTable), + $this->normalizeColumns($toTable) + ); + } + + private function normalizeColumns(Table $table): Table + { + $defaults = array_intersect_key($table->getOptions(), [ + 'charset' => null, + 'collation' => null, + ]); + + if ($defaults === []) { + return $table; + } + + $table = clone $table; + + foreach ($table->getColumns() as $column) { + $options = $column->getPlatformOptions(); + $diff = array_diff_assoc($options, $defaults); + + if ($diff === $options) { + continue; + } + + $column->setPlatformOptions($diff); + } + + return $table; + } +} diff --git a/app/vendor/doctrine/dbal/src/Platforms/PostgreSQLPlatform.php b/app/vendor/doctrine/dbal/src/Platforms/PostgreSQLPlatform.php new file mode 100644 index 000000000..477e08af9 --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Platforms/PostgreSQLPlatform.php @@ -0,0 +1,1276 @@ + [ + '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. + * + * @param bool $flag + * + * @return void + */ + public function setUseBooleanTrueFalseStrings($flag) + { + $this->useBooleanTrueFalseStrings = (bool) $flag; + } + + /** + * {@inheritDoc} + */ + public function getSubstringExpression($string, $start, $length = null) + { + if ($length === null) { + return 'SUBSTRING(' . $string . ' FROM ' . $start . ')'; + } + + return 'SUBSTRING(' . $string . ' FROM ' . $start . ' FOR ' . $length . ')'; + } + + /** + * {@inheritDoc} + * + * @deprecated Generate dates within the application. + */ + public function getNowExpression() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4753', + 'PostgreSQLPlatform::getNowExpression() is deprecated. Generate dates within the application.' + ); + + return 'LOCALTIMESTAMP(0)'; + } + + /** + * {@inheritDoc} + */ + public function getRegexpExpression() + { + return 'SIMILAR TO'; + } + + /** + * {@inheritDoc} + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos !== false) { + $str = $this->getSubstringExpression($str, $startPos); + + return 'CASE WHEN (POSITION(' . $substr . ' IN ' . $str . ') = 0) THEN 0' + . ' ELSE (POSITION(' . $substr . ' IN ' . $str . ') + ' . $startPos . ' - 1) END'; + } + + return 'POSITION(' . $substr . ' IN ' . $str . ')'; + } + + /** + * {@inheritdoc} + */ + protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit) + { + if ($unit === DateIntervalUnit::QUARTER) { + $interval *= 3; + $unit = DateIntervalUnit::MONTH; + } + + return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)"; + } + + /** + * {@inheritDoc} + */ + public function getDateDiffExpression($date1, $date2) + { + return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))'; + } + + public function getCurrentDatabaseExpression(): string + { + return 'CURRENT_DATABASE()'; + } + + /** + * {@inheritDoc} + */ + public function supportsSequences() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsSchemas() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function getDefaultSchemaName() + { + return 'public'; + } + + /** + * {@inheritDoc} + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function supportsPartialIndexes() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function usesSequenceEmulatedIdentityColumns() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function getIdentitySequenceName($tableName, $columnName) + { + return $tableName . '_' . $columnName . '_seq'; + } + + /** + * {@inheritDoc} + */ + public function supportsCommentOnStatement() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function hasNativeGuidType() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getListDatabasesSQL() + { + return 'SELECT datname FROM pg_database'; + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see PostgreSQLSchemaManager::listSchemaNames()} instead. + */ + public function getListNamespacesSQL() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4503', + 'PostgreSQLPlatform::getListNamespacesSQL() is deprecated,' + . ' use PostgreSQLSchemaManager::listSchemaNames() instead.' + ); + + return "SELECT schema_name AS nspname + FROM information_schema.schemata + WHERE schema_name NOT LIKE 'pg\_%' + AND schema_name != 'information_schema'"; + } + + /** + * {@inheritDoc} + */ + public function getListSequencesSQL($database) + { + return "SELECT sequence_name AS relname, + sequence_schema AS schemaname + FROM information_schema.sequences + WHERE sequence_schema NOT LIKE 'pg\_%' + AND sequence_schema != 'information_schema'"; + } + + /** + * {@inheritDoc} + */ + public function getListTablesSQL() + { + return "SELECT quote_ident(table_name) AS table_name, + table_schema AS schema_name + FROM information_schema.tables + WHERE table_schema NOT LIKE 'pg\_%' + AND table_schema != 'information_schema' + AND table_name != 'geometry_columns' + AND table_name != 'spatial_ref_sys' + AND table_type != 'VIEW'"; + } + + /** + * {@inheritDoc} + */ + public function getListViewsSQL($database) + { + 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'; + } + + /** + * @param string $table + * @param string|null $database + * + * @return string + */ + public function getListTableForeignKeysSQL($table, $database = null) + { + return 'SELECT quote_ident(r.conname) as conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef + FROM pg_catalog.pg_constraint r + WHERE r.conrelid = + ( + SELECT c.oid + FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n + WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace + ) + AND r.contype = 'f'"; + } + + /** + * {@inheritDoc} + */ + public function getListTableConstraintsSQL($table) + { + $table = new Identifier($table); + $table = $this->quoteStringLiteral($table->getName()); + + return sprintf( + <<<'SQL' +SELECT + quote_ident(relname) as relname +FROM + pg_class +WHERE oid IN ( + SELECT indexrelid + FROM pg_index, pg_class + WHERE pg_class.relname = %s + AND pg_class.oid = pg_index.indrelid + AND (indisunique = 't' OR indisprimary = 't') + ) +SQL + , + $table + ); + } + + /** + * {@inheritDoc} + * + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html + */ + public function getListTableIndexesSQL($table, $database = null) + { + return 'SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary, + pg_index.indkey, pg_index.indrelid, + pg_get_expr(indpred, indrelid) AS where + FROM pg_class, pg_index + WHERE oid IN ( + SELECT indexrelid + FROM pg_index si, pg_class sc, pg_namespace sn + WHERE ' . $this->getTableWhereClause($table, 'sc', 'sn') . ' + AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid + ) AND pg_index.indexrelid = oid'; + } + + /** + * @param string $table + * @param string $classAlias + * @param string $namespaceAlias + */ + private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n'): string + { + $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND "; + if (strpos($table, '.') !== false) { + [$schema, $table] = explode('.', $table); + $schema = $this->quoteStringLiteral($schema); + } else { + $schema = 'ANY(current_schemas(false))'; + } + + $table = new Identifier($table); + $table = $this->quoteStringLiteral($table->getName()); + + return $whereClause . sprintf( + '%s.relname = %s AND %s.nspname = %s', + $classAlias, + $table, + $namespaceAlias, + $schema + ); + } + + /** + * {@inheritDoc} + */ + public function getListTableColumnsSQL($table, $database = null) + { + return "SELECT + a.attnum, + quote_ident(a.attname) AS field, + t.typname AS type, + format_type(a.atttypid, a.atttypmod) AS complete_type, + (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation, + (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type, + (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM + pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type, + a.attnotnull AS isnotnull, + (SELECT 't' + FROM pg_index + WHERE c.oid = pg_index.indrelid + AND pg_index.indkey[0] = a.attnum + AND pg_index.indisprimary = 't' + ) AS pri, + (SELECT pg_get_expr(adbin, adrelid) + FROM pg_attrdef + WHERE c.oid = pg_attrdef.adrelid + AND pg_attrdef.adnum=a.attnum + ) AS default, + (SELECT pg_description.description + FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid + ) AS comment + FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n + WHERE " . $this->getTableWhereClause($table, 'c', 'n') . ' + AND a.attnum > 0 + AND a.attrelid = c.oid + AND a.atttypid = t.oid + AND n.oid = c.relnamespace + ORDER BY a.attnum'; + } + + /** + * {@inheritDoc} + */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) + { + $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('feferred') && $foreignKey->getOption('feferred') !== false) + || ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) + ) { + $query .= ' INITIALLY DEFERRED'; + } else { + $query .= ' INITIALLY IMMEDIATE'; + } + + return $query; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $sql = []; + $commentsSQL = []; + $columnSql = []; + + foreach ($diff->addedColumns as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query; + + $comment = $this->getColumnComment($column); + + if ($comment === null || $comment === '') { + continue; + } + + $commentsSQL[] = $this->getCommentOnColumnSQL( + $diff->getName($this)->getQuotedName($this), + $column->getQuotedName($this), + $comment + ); + } + + foreach ($diff->removedColumns as $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $query = 'DROP ' . $column->getQuotedName($this); + $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query; + } + + foreach ($diff->changedColumns as $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + if ($this->isUnchangedBinaryColumn($columnDiff)) { + continue; + } + + $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($this); + $column = $columnDiff->column; + + if ( + $columnDiff->hasChanged('type') + || $columnDiff->hasChanged('precision') + || $columnDiff->hasChanged('scale') + || $columnDiff->hasChanged('fixed') + ) { + $type = $column->getType(); + + // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type + $columnDefinition = $column->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 ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query; + } + + if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) { + $defaultClause = $column->getDefault() === null + ? ' DROP DEFAULT' + : ' SET' . $this->getDefaultValueDeclarationSQL($column->toArray()); + $query = 'ALTER ' . $oldColumnName . $defaultClause; + $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query; + } + + if ($columnDiff->hasChanged('notnull')) { + $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL'; + $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query; + } + + if ($columnDiff->hasChanged('autoincrement')) { + if ($column->getAutoincrement()) { + // add autoincrement + $seqName = $this->getIdentitySequenceName($diff->name, $oldColumnName); + + $sql[] = 'CREATE SEQUENCE ' . $seqName; + $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM ' + . $diff->getName($this)->getQuotedName($this) . '))'; + $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')"; + $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query; + } else { + // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have + $query = 'ALTER ' . $oldColumnName . ' DROP DEFAULT'; + $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query; + } + } + + $newComment = $this->getColumnComment($column); + $oldComment = $this->getOldColumnComment($columnDiff); + + if ( + $columnDiff->hasChanged('comment') + || ($columnDiff->fromColumn !== null && $oldComment !== $newComment) + ) { + $commentsSQL[] = $this->getCommentOnColumnSQL( + $diff->getName($this)->getQuotedName($this), + $column->getQuotedName($this), + $newComment + ); + } + + if (! $columnDiff->hasChanged('length')) { + continue; + } + + $query = 'ALTER ' . $oldColumnName . ' TYPE ' + . $column->getType()->getSQLDeclaration($column->toArray(), $this); + $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query; + } + + foreach ($diff->renamedColumns as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $oldColumnName = new Identifier($oldColumnName); + + $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . + ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this); + } + + $tableSql = []; + + if (! $this->onSchemaAlterTable($diff, $tableSql)) { + $sql = array_merge($sql, $commentsSQL); + + $newName = $diff->getNewName(); + + if ($newName !== false) { + $sql[] = sprintf( + 'ALTER TABLE %s RENAME TO %s', + $diff->getName($this)->getQuotedName($this), + $newName->getQuotedName($this) + ); + } + + $sql = array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $this->getPostAlterTableIndexForeignKeySQL($diff) + ); + } + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * Checks whether a given column diff is a logically unchanged binary type column. + * + * Used to determine whether a column alteration for a binary type column can be skipped. + * Doctrine's {@see BinaryType} and {@see BlobType} are mapped to the same database column type on this platform + * as this platform does not have a native VARBINARY/BINARY column type. Therefore the comparator + * might detect differences for binary type columns which do not have to be propagated + * to database as there actually is no difference at database level. + */ + private function isUnchangedBinaryColumn(ColumnDiff $columnDiff): bool + { + $columnType = $columnDiff->column->getType(); + + if (! $columnType instanceof BinaryType && ! $columnType instanceof BlobType) { + return false; + } + + $fromColumn = $columnDiff->fromColumn instanceof Column ? $columnDiff->fromColumn : null; + + if ($fromColumn !== null) { + $fromColumnType = $fromColumn->getType(); + + if (! $fromColumnType instanceof BinaryType && ! $fromColumnType instanceof BlobType) { + return false; + } + + return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0; + } + + if ($columnDiff->hasChanged('type')) { + return false; + } + + return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0; + } + + /** + * {@inheritdoc} + */ + protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) + { + if (strpos($tableName, '.') !== false) { + [$schema] = explode('.', $tableName); + $oldIndexName = $schema . '.' . $oldIndexName; + } + + return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)]; + } + + /** + * {@inheritdoc} + */ + public function getCommentOnColumnSQL($tableName, $columnName, $comment) + { + $tableName = new Identifier($tableName); + $columnName = new Identifier($columnName); + $comment = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment); + + return sprintf( + 'COMMENT ON COLUMN %s.%s IS %s', + $tableName->getQuotedName($this), + $columnName->getQuotedName($this), + $comment + ); + } + + /** + * {@inheritDoc} + */ + public function getCreateSequenceSQL(Sequence $sequence) + { + return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . + ' INCREMENT BY ' . $sequence->getAllocationSize() . + ' MINVALUE ' . $sequence->getInitialValue() . + ' START ' . $sequence->getInitialValue() . + $this->getSequenceCacheSQL($sequence); + } + + /** + * {@inheritDoc} + */ + public function getAlterSequenceSQL(Sequence $sequence) + { + 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 ''; + } + + /** + * {@inheritDoc} + */ + public function getDropSequenceSQL($sequence) + { + return parent::getDropSequenceSQL($sequence) . ' CASCADE'; + } + + /** + * {@inheritDoc} + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + return $this->getDropConstraintSQL($foreignKey, $table); + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($name, array $columns, array $options = []) + { + $queryFields = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['primary']) && ! empty($options['primary'])) { + $keyColumns = array_unique(array_values($options['primary'])); + $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + } + + $query = 'CREATE 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->getCreateConstraintSQL($uniqueConstraint, $name); + } + } + + if (isset($options['foreignKeys'])) { + foreach ((array) $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. + * + * @return mixed + * + * @throws UnexpectedValueException + */ + private function convertSingleBooleanValue($value, $callback) + { + 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'", $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). + * + * @return mixed + */ + private function doConvertBooleans($item, $callback) + { + 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($item) + { + if (! $this->useBooleanTrueFalseStrings) { + return parent::convertBooleans($item); + } + + return $this->doConvertBooleans( + $item, + /** + * @param mixed $value + */ + static function ($value) { + if ($value === null) { + return 'NULL'; + } + + return $value === true ? 'true' : 'false'; + } + ); + } + + /** + * {@inheritDoc} + */ + public function convertBooleansToDatabaseValue($item) + { + if (! $this->useBooleanTrueFalseStrings) { + return parent::convertBooleansToDatabaseValue($item); + } + + return $this->doConvertBooleans( + $item, + /** + * @param mixed $value + */ + static function ($value): ?int { + return $value === null ? null : (int) $value; + } + ); + } + + /** + * {@inheritDoc} + */ + public function convertFromBoolean($item) + { + if ($item !== null && in_array(strtolower($item), $this->booleanLiterals['false'], true)) { + return false; + } + + return parent::convertFromBoolean($item); + } + + /** + * {@inheritDoc} + */ + public function getSequenceNextValSQL($sequence) + { + return "SELECT NEXTVAL('" . $sequence . "')"; + } + + /** + * {@inheritDoc} + */ + public function getSetTransactionIsolationSQL($level) + { + return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL ' + . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column) + { + return 'BOOLEAN'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column) + { + if (! empty($column['autoincrement'])) { + return 'SERIAL'; + } + + return 'INT'; + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column) + { + if (! empty($column['autoincrement'])) { + return 'BIGSERIAL'; + } + + return 'BIGINT'; + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column) + { + if (! empty($column['autoincrement'])) { + return 'SMALLSERIAL'; + } + + return 'SMALLINT'; + } + + /** + * {@inheritDoc} + */ + public function getGuidTypeDeclarationSQL(array $column) + { + return 'UUID'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column) + { + return 'TIMESTAMP(0) WITHOUT TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzTypeDeclarationSQL(array $column) + { + return 'TIMESTAMP(0) WITH TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column) + { + return 'TIME(0) WITHOUT TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column) + { + return ''; + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + { + return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(255)') + : ($length > 0 ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'); + } + + /** + * {@inheritdoc} + */ + protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed) + { + return 'BYTEA'; + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column) + { + return 'TEXT'; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4749', + 'PostgreSQLPlatform::getName() is deprecated. Identify platforms by their class.' + ); + + return 'postgresql'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzFormatString() + { + return 'Y-m-d H:i:sO'; + } + + /** + * {@inheritDoc} + */ + public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName) + { + return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)'; + } + + /** + * {@inheritDoc} + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + $tableIdentifier = new Identifier($tableName); + $sql = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this); + + if ($cascade) { + $sql .= ' CASCADE'; + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + public function getReadLockSQL() + { + return 'FOR SHARE'; + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = [ + 'bigint' => 'bigint', + 'bigserial' => 'bigint', + 'bool' => 'boolean', + 'boolean' => 'boolean', + 'bpchar' => 'string', + 'bytea' => 'blob', + 'char' => 'string', + 'date' => 'date', + 'datetime' => 'datetime', + 'decimal' => 'decimal', + 'double' => 'float', + 'double precision' => 'float', + 'float' => 'float', + 'float4' => 'float', + 'float8' => 'float', + 'inet' => 'string', + 'int' => 'integer', + 'int2' => 'smallint', + 'int4' => 'integer', + 'int8' => 'bigint', + 'integer' => 'integer', + 'interval' => 'string', + 'json' => 'json', + 'jsonb' => 'json', + 'money' => 'decimal', + 'numeric' => 'decimal', + 'serial' => 'integer', + 'serial4' => 'integer', + 'serial8' => 'bigint', + 'real' => 'float', + 'smallint' => 'smallint', + 'text' => 'text', + 'time' => 'time', + 'timestamp' => 'datetime', + 'timestamptz' => 'datetimetz', + 'timetz' => 'time', + 'tsvector' => 'text', + 'uuid' => 'guid', + 'varchar' => 'string', + 'year' => 'date', + '_varchar' => 'string', + ]; + } + + /** + * {@inheritDoc} + */ + public function getVarcharMaxLength() + { + return 65535; + } + + /** + * {@inheritdoc} + */ + public function getBinaryMaxLength() + { + return 0; + } + + /** + * {@inheritdoc} + */ + public function getBinaryDefaultLength() + { + return 0; + } + + /** + * {@inheritdoc} + */ + public function hasNativeJsonType() + { + return true; + } + + /** + * {@inheritDoc} + * + * @deprecated Implement {@see createReservedKeywordsList()} instead. + */ + protected function getReservedKeywordsClass() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4510', + 'PostgreSQLPlatform::getReservedKeywordsClass() is deprecated,' + . ' use PostgreSQLPlatform::createReservedKeywordsList() instead.' + ); + + return Keywords\PostgreSQL94Keywords::class; + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column) + { + return 'BYTEA'; + } + + /** + * {@inheritdoc} + */ + public function getDefaultValueDeclarationSQL($column) + { + if ($this->isSerialColumn($column)) { + return ''; + } + + return parent::getDefaultValueDeclarationSQL($column); + } + + /** + * {@inheritdoc} + */ + public function supportsColumnCollation() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function getColumnCollationDeclarationSQL($collation) + { + return 'COLLATE ' . $this->quoteSingleIdentifier($collation); + } + + /** + * {@inheritdoc} + */ + public function getJsonTypeDeclarationSQL(array $column) + { + if (! empty($column['jsonb'])) { + return 'JSONB'; + } + + return 'JSON'; + } + + /** + * @param mixed[] $column + */ + private function isSerialColumn(array $column): bool + { + return isset($column['type'], $column['autoincrement']) + && $column['autoincrement'] === true + && $this->isIntegerType($column['type']); + } + + /** + * Check whether the type of a column is changed in a way that invalidates the default value for the column + */ + private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff): bool + { + if ($columnDiff->fromColumn === null) { + return $columnDiff->hasChanged('type'); + } + + $oldTypeIsInteger = $this->isIntegerType($columnDiff->fromColumn->getType()); + $newTypeIsInteger = $this->isIntegerType($columnDiff->column->getType()); + + // default should not be changed when switching between integer types and the default comes from a sequence + return $columnDiff->hasChanged('type') + && ! ($oldTypeIsInteger && $newTypeIsInteger && $columnDiff->column->getAutoincrement()); + } + + private function isIntegerType(Type $type): bool + { + return $type instanceof PhpIntegerMappingType; + } + + private function getOldColumnComment(ColumnDiff $columnDiff): ?string + { + return $columnDiff->fromColumn !== null ? $this->getColumnComment($columnDiff->fromColumn) : null; + } + + public function getListTableMetadataSQL(string $table, ?string $schema = null): string + { + if ($schema !== null) { + $table = $schema . '.' . $table; + } + + return sprintf( + <<<'SQL' +SELECT obj_description(%s::regclass) AS table_comment; +SQL + , + $this->quoteStringLiteral($table) + ); + } +} diff --git a/app/vendor/doctrine/dbal/src/Platforms/SQLServer/Comparator.php b/app/vendor/doctrine/dbal/src/Platforms/SQLServer/Comparator.php new file mode 100644 index 000000000..0db1a6a57 --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Platforms/SQLServer/Comparator.php @@ -0,0 +1,56 @@ +databaseCollation = $databaseCollation; + } + + /** + * {@inheritDoc} + */ + public function diffTable(Table $fromTable, Table $toTable) + { + $fromTable = clone $fromTable; + $toTable = clone $toTable; + + $this->normalizeColumns($fromTable); + $this->normalizeColumns($toTable); + + return parent::diffTable($fromTable, $toTable); + } + + private function normalizeColumns(Table $table): void + { + foreach ($table->getColumns() as $column) { + $options = $column->getPlatformOptions(); + + if (! isset($options['collation']) || $options['collation'] !== $this->databaseCollation) { + continue; + } + + unset($options['collation']); + $column->setPlatformOptions($options); + } + } +} diff --git a/app/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php b/app/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php new file mode 100644 index 000000000..732cb6363 --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php @@ -0,0 +1,1660 @@ +getConvertExpression('date', 'GETDATE()'); + } + + /** + * {@inheritdoc} + */ + public function getCurrentTimeSQL() + { + 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($dataType, $expression): string + { + return sprintf('CONVERT(%s, %s)', $dataType, $expression); + } + + /** + * {@inheritdoc} + */ + protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit) + { + $factorClause = ''; + + if ($operator === '-') { + $factorClause = '-1 * '; + } + + return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateDiffExpression($date1, $date2) + { + return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')'; + } + + /** + * {@inheritDoc} + * + * Microsoft SQL Server prefers "autoincrement" identity columns + * since sequences can only be emulated with a table. + * + * @deprecated + */ + public function prefersIdentityColumns() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pulls/1519', + 'SQLServerPlatform::prefersIdentityColumns() is deprecated.' + ); + + return true; + } + + /** + * {@inheritDoc} + * + * Microsoft SQL Server supports this through AUTO_INCREMENT columns. + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsReleaseSavepoints() + { + return false; + } + + /** + * {@inheritdoc} + */ + public function supportsSchemas() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function getDefaultSchemaName() + { + return 'dbo'; + } + + /** + * {@inheritDoc} + */ + public function supportsColumnCollation() + { + 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(); + } + + /** + * {@inheritdoc} + */ + public function getListSequencesSQL($database) + { + 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'; + } + + /** + * {@inheritdoc} + */ + public function getSequenceNextValSQL($sequence) + { + return 'SELECT NEXT VALUE FOR ' . $sequence; + } + + /** + * {@inheritDoc} + */ + public function hasNativeGuidType() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + if (! $foreignKey instanceof ForeignKeyConstraint) { + $foreignKey = new Identifier($foreignKey); + } + + if (! $table instanceof Table) { + $table = new Identifier($table); + } + + $foreignKey = $foreignKey->getQuotedName($this); + $table = $table->getQuotedName($this); + + return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey; + } + + /** + * {@inheritDoc} + */ + public function getDropIndexSQL($index, $table = null) + { + if ($index instanceof Index) { + $index = $index->getQuotedName($this); + } elseif (! is_string($index)) { + throw new InvalidArgumentException( + __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.' + ); + } + + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } elseif (! is_string($table)) { + throw new InvalidArgumentException( + __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.' + ); + } + + return 'DROP INDEX ' . $index . ' ON ' . $table; + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($name, array $columns, array $options = []) + { + $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($name, $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 $constraintName => $definition) { + $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($constraintName, $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 ((array) $options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $name); + } + } + + return array_merge($sql, $commentsSql, $defaultConstraintsSql); + } + + /** + * {@inheritDoc} + */ + public function getCreatePrimaryKeySQL(Index $index, $table) + { + if ($table instanceof Table) { + $identifier = $table->getQuotedName($this); + } else { + $identifier = $table; + } + + $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY'; + + if ($index->hasFlag('nonclustered')) { + $sql .= ' NONCLUSTERED'; + } + + return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')'; + } + + /** + * 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|null $comment The column's comment. + * + * @return string + */ + protected function getCreateColumnCommentSQL($tableName, $columnName, $comment) + { + if (strpos($tableName, '.') !== false) { + [$schemaSQL, $tableSQL] = explode('.', $tableName); + $schemaSQL = $this->quoteStringLiteral($schemaSQL); + $tableSQL = $this->quoteStringLiteral($tableSQL); + } else { + $schemaSQL = "'dbo'"; + $tableSQL = $this->quoteStringLiteral($tableName); + } + + return $this->getAddExtendedPropertySQL( + 'MS_Description', + $comment, + 'SCHEMA', + $schemaSQL, + 'TABLE', + $tableSQL, + 'COLUMN', + $columnName + ); + } + + /** + * Returns the SQL snippet for declaring a default constraint. + * + * @internal The method should be only used from within the SQLServerPlatform class hierarchy. + * + * @param string $table Name of the table to return the default constraint declaration for. + * @param mixed[] $column Column definition. + * + * @return string + * + * @throws InvalidArgumentException + */ + public function getDefaultConstraintDeclarationSQL($table, array $column) + { + if (! isset($column['default'])) { + throw new InvalidArgumentException("Incomplete column definition. 'default' required."); + } + + $columnName = new Identifier($column['name']); + + return ' CONSTRAINT ' . + $this->generateDefaultConstraintName($table, $column['name']) . + $this->getDefaultValueDeclarationSQL($column) . + ' FOR ' . $columnName->getQuotedName($this); + } + + /** + * {@inheritDoc} + */ + public function getCreateIndexSQL(Index $index, $table) + { + $constraint = parent::getCreateIndexSQL($index, $table); + + if ($index->isUnique() && ! $index->isPrimary()) { + $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index); + } + + return $constraint; + } + + /** + * {@inheritDoc} + */ + protected function getCreateIndexSQLFlags(Index $index) + { + $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 + * + * @param string $sql + */ + private function _appendUniqueConstraintDefinition($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) + { + $queryParts = []; + $sql = []; + $columnSql = []; + $commentsSql = []; + + foreach ($diff->addedColumns as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $columnDef = $column->toArray(); + $addColumnSql = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef); + if (isset($columnDef['default'])) { + $addColumnSql .= ' CONSTRAINT ' . + $this->generateDefaultConstraintName($diff->name, $column->getQuotedName($this)) . + $this->getDefaultValueDeclarationSQL($columnDef); + } + + $queryParts[] = $addColumnSql; + + $comment = $this->getColumnComment($column); + + if (empty($comment) && ! is_numeric($comment)) { + continue; + } + + $commentsSql[] = $this->getCreateColumnCommentSQL( + $diff->name, + $column->getQuotedName($this), + $comment + ); + } + + foreach ($diff->removedColumns as $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); + } + + foreach ($diff->changedColumns as $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + $column = $columnDiff->column; + $comment = $this->getColumnComment($column); + $hasComment = ! empty($comment) || is_numeric($comment); + + if ($columnDiff->fromColumn instanceof Column) { + $fromComment = $this->getColumnComment($columnDiff->fromColumn); + $hasFromComment = ! empty($fromComment) || is_numeric($fromComment); + + if ($hasFromComment && $hasComment && $fromComment !== $comment) { + $commentsSql[] = $this->getAlterColumnCommentSQL( + $diff->name, + $column->getQuotedName($this), + $comment + ); + } elseif ($hasFromComment && ! $hasComment) { + $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this)); + } elseif (! $hasFromComment && $hasComment) { + $commentsSql[] = $this->getCreateColumnCommentSQL( + $diff->name, + $column->getQuotedName($this), + $comment + ); + } + } + + // Do not add query part if only comment has changed. + if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) { + continue; + } + + $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff); + + if ($requireDropDefaultConstraint) { + $queryParts[] = $this->getAlterTableDropDefaultConstraintClause( + $diff->name, + $columnDiff->oldColumnName + ); + } + + $columnDef = $column->toArray(); + + $queryParts[] = 'ALTER COLUMN ' . + $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef); + + if ( + ! isset($columnDef['default']) + || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default')) + ) { + continue; + } + + $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column); + } + + foreach ($diff->renamedColumns as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $oldColumnName = new Identifier($oldColumnName); + + $sql[] = "sp_rename '" . + $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) . + "', '" . $column->getQuotedName($this) . "', 'COLUMN'"; + + // Recreate default constraint with new column name if necessary (for future reference). + if ($column->getDefault() === null) { + continue; + } + + $queryParts[] = $this->getAlterTableDropDefaultConstraintClause( + $diff->name, + $oldColumnName->getQuotedName($this) + ); + $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column); + } + + $tableSql = []; + + if ($this->onSchemaAlterTable($diff, $tableSql)) { + return array_merge($tableSql, $columnSql); + } + + foreach ($queryParts as $query) { + $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query; + } + + $sql = array_merge($sql, $commentsSql); + + $newName = $diff->getNewName(); + + if ($newName !== false) { + $sql[] = "sp_rename '" . $diff->getName($this)->getQuotedName($this) . "', '" . $newName->getName() . "'"; + + /** + * Rename table's default constraints names + * to match the new table name. + * This is necessary to ensure that the default + * constraints can be referenced in future table + * alterations as the table name is encoded in + * default constraints' names. + */ + $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " . + "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " . + "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " . + "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " . + 'FROM sys.default_constraints dc ' . + 'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' . + "WHERE tbl.name = '" . $newName->getName() . "';" . + 'EXEC sp_executesql @sql'; + } + + $sql = array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $this->getPostAlterTableIndexForeignKeySQL($diff) + ); + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * 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($tableName, Column $column): string + { + $columnDef = $column->toArray(); + $columnDef['name'] = $column->getQuotedName($this); + + return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef); + } + + /** + * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement. + * + * @param string $tableName The name of the table to generate the clause for. + * @param string $columnName The name of the column to generate the clause for. + */ + private function getAlterTableDropDefaultConstraintClause($tableName, $columnName): string + { + return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName); + } + + /** + * 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 can only decide whether to drop an existing default constraint + // if we know the original default value. + if (! $columnDiff->fromColumn instanceof Column) { + return false; + } + + // We only need to drop an existing default constraint if we know the + // column was defined with a default value before. + if ($columnDiff->fromColumn->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->hasChanged('default')) { + 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->hasChanged('type') || $columnDiff->hasChanged('fixed'); + } + + /** + * 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|null $comment The column's comment. + * + * @return string + */ + protected function getAlterColumnCommentSQL($tableName, $columnName, $comment) + { + if (strpos($tableName, '.') !== false) { + [$schemaSQL, $tableSQL] = explode('.', $tableName); + $schemaSQL = $this->quoteStringLiteral($schemaSQL); + $tableSQL = $this->quoteStringLiteral($tableSQL); + } else { + $schemaSQL = "'dbo'"; + $tableSQL = $this->quoteStringLiteral($tableName); + } + + return $this->getUpdateExtendedPropertySQL( + 'MS_Description', + $comment, + 'SCHEMA', + $schemaSQL, + 'TABLE', + $tableSQL, + 'COLUMN', + $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. + * + * @return string + */ + protected function getDropColumnCommentSQL($tableName, $columnName) + { + if (strpos($tableName, '.') !== false) { + [$schemaSQL, $tableSQL] = explode('.', $tableName); + $schemaSQL = $this->quoteStringLiteral($schemaSQL); + $tableSQL = $this->quoteStringLiteral($tableSQL); + } else { + $schemaSQL = "'dbo'"; + $tableSQL = $this->quoteStringLiteral($tableName); + } + + return $this->getDropExtendedPropertySQL( + 'MS_Description', + 'SCHEMA', + $schemaSQL, + 'TABLE', + $tableSQL, + 'COLUMN', + $columnName + ); + } + + /** + * {@inheritdoc} + */ + protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) + { + return [sprintf( + "EXEC sp_rename N'%s.%s', N'%s', N'INDEX'", + $tableName, + $oldIndexName, + $index->getQuotedName($this) + ), + ]; + } + + /** + * Returns the SQL statement for adding an extended property to a database object. + * + * @internal The method should be only used from within the SQLServerPlatform class hierarchy. + * + * @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. + * + * @return string + */ + public function getAddExtendedPropertySQL( + $name, + $value = null, + $level0Type = null, + $level0Name = null, + $level1Type = null, + $level1Name = null, + $level2Type = null, + $level2Name = null + ) { + return 'EXEC sp_addextendedproperty ' . + 'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' . + 'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' . + 'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' . + 'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name; + } + + /** + * Returns the SQL statement for dropping an extended property from a database object. + * + * @internal The method should be only used from within the SQLServerPlatform class hierarchy. + * + * @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. + * + * @return string + */ + public function getDropExtendedPropertySQL( + $name, + $level0Type = null, + $level0Name = null, + $level1Type = null, + $level1Name = null, + $level2Type = null, + $level2Name = null + ) { + return 'EXEC sp_dropextendedproperty ' . + 'N' . $this->quoteStringLiteral($name) . ', ' . + 'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' . + 'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' . + 'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name; + } + + /** + * Returns the SQL statement for updating an extended property of a database object. + * + * @internal The method should be only used from within the SQLServerPlatform class hierarchy. + * + * @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. + * + * @return string + */ + public function getUpdateExtendedPropertySQL( + $name, + $value = null, + $level0Type = null, + $level0Name = null, + $level1Type = null, + $level1Name = null, + $level2Type = null, + $level2Name = null + ) { + return 'EXEC sp_updateextendedproperty ' . + 'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' . + 'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' . + 'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' . + 'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name; + } + + /** + * {@inheritDoc} + */ + public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName) + { + return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES'; + } + + /** + * {@inheritDoc} + */ + public function getListTablesSQL() + { + // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams + // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication + return 'SELECT name, SCHEMA_NAME (uid) AS schema_name FROM sysobjects' + . " WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name"; + } + + /** + * {@inheritDoc} + */ + public function getListTableColumnsSQL($table, $database = null) + { + return "SELECT col.name, + type.name AS type, + col.max_length AS length, + ~col.is_nullable AS notnull, + def.definition AS [default], + col.scale, + col.precision, + col.is_identity AS autoincrement, + col.collation_name AS collation, + CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type + FROM sys.columns AS col + JOIN sys.types AS type + ON col.user_type_id = type.user_type_id + JOIN sys.objects AS obj + ON col.object_id = obj.object_id + JOIN sys.schemas AS scm + ON obj.schema_id = scm.schema_id + LEFT JOIN sys.default_constraints def + ON col.default_object_id = def.object_id + AND col.object_id = def.parent_object_id + LEFT JOIN sys.extended_properties AS prop + ON obj.object_id = prop.major_id + AND col.column_id = prop.minor_id + AND prop.name = 'MS_Description' + WHERE obj.type = 'U' + AND " . $this->getTableWhereClause($table, 'scm.name', 'obj.name'); + } + + /** + * @param string $table + * @param string|null $database + * + * @return string + */ + public function getListTableForeignKeysSQL($table, $database = null) + { + return 'SELECT f.name AS ForeignKey, + SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName, + OBJECT_NAME (f.parent_object_id) AS TableName, + COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName, + SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName, + OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName, + COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName, + f.delete_referential_action_desc, + f.update_referential_action_desc + FROM sys.foreign_keys AS f + INNER JOIN sys.foreign_key_columns AS fc + INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id + ON f.OBJECT_ID = fc.constraint_object_id + WHERE ' . + $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)') . + ' ORDER BY fc.constraint_column_id'; + } + + /** + * {@inheritDoc} + */ + public function getListTableIndexesSQL($table, $database = null) + { + return "SELECT idx.name AS key_name, + col.name AS column_name, + ~idx.is_unique AS non_unique, + idx.is_primary_key AS [primary], + CASE idx.type + WHEN '1' THEN 'clustered' + WHEN '2' THEN 'nonclustered' + ELSE NULL + END AS flags + FROM sys.tables AS tbl + JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id + JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id + JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id + JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id + WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . ' + ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC'; + } + + /** + * {@inheritDoc} + */ + public function getListViewsSQL($database) + { + return "SELECT name, definition FROM sysobjects + INNER JOIN sys.sql_modules ON sysobjects.id = sys.sql_modules.object_id + WHERE type = 'V' ORDER BY name"; + } + + /** + * Returns the where clause to filter schema and table name in a query. + * + * @param string $table The full qualified name of the table. + * @param string $schemaColumn The name of the column to compare the schema to in the where clause. + * @param string $tableColumn The name of the column to compare the table to in the where clause. + */ + private function getTableWhereClause($table, $schemaColumn, $tableColumn): string + { + if (strpos($table, '.') !== false) { + [$schema, $table] = explode('.', $table); + $schema = $this->quoteStringLiteral($schema); + $table = $this->quoteStringLiteral($table); + } else { + $schema = 'SCHEMA_NAME()'; + $table = $this->quoteStringLiteral($table); + } + + return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema); + } + + /** + * {@inheritDoc} + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos === false) { + return 'CHARINDEX(' . $substr . ', ' . $str . ')'; + } + + return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')'; + } + + /** + * {@inheritDoc} + */ + public function getModExpression($expression1, $expression2) + { + return $expression1 . ' % ' . $expression2; + } + + /** + * {@inheritDoc} + */ + public function getTrimExpression($str, $mode = TrimMode::UNSPECIFIED, $char = false) + { + if ($char === false) { + switch ($mode) { + case TrimMode::LEADING: + $trimFn = 'LTRIM'; + break; + + case TrimMode::TRAILING: + $trimFn = 'RTRIM'; + break; + + default: + return 'LTRIM(RTRIM(' . $str . '))'; + } + + return $trimFn . '(' . $str . ')'; + } + + $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))'; + } + + /** + * {@inheritDoc} + */ + public function getConcatExpression() + { + return sprintf('CONCAT(%s)', implode(', ', func_get_args())); + } + + /** + * {@inheritDoc} + */ + public function getListDatabasesSQL() + { + return 'SELECT * FROM sys.databases'; + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see SQLServerSchemaManager::listSchemaNames()} instead. + */ + public function getListNamespacesSQL() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4503', + 'SQLServerPlatform::getListNamespacesSQL() is deprecated,' + . ' use SQLServerSchemaManager::listSchemaNames() instead.' + ); + + return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')"; + } + + /** + * {@inheritDoc} + */ + public function getSubstringExpression($string, $start, $length = null) + { + if ($length !== null) { + return 'SUBSTRING(' . $string . ', ' . $start . ', ' . $length . ')'; + } + + return 'SUBSTRING(' . $string . ', ' . $start . ', LEN(' . $string . ') - ' . $start . ' + 1)'; + } + + /** + * {@inheritDoc} + */ + public function getLengthExpression($column) + { + return 'LEN(' . $column . ')'; + } + + public function getCurrentDatabaseExpression(): string + { + return 'DB_NAME()'; + } + + /** + * {@inheritDoc} + */ + public function getSetTransactionIsolationSQL($level) + { + return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column) + { + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column) + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column) + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getGuidTypeDeclarationSQL(array $column) + { + return 'UNIQUEIDENTIFIER'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzTypeDeclarationSQL(array $column) + { + return 'DATETIMEOFFSET(6)'; + } + + /** + * {@inheritDoc} + */ + public function getAsciiStringTypeDeclarationSQL(array $column): string + { + $length = $column['length'] ?? null; + + if (! isset($column['fixed'])) { + return sprintf('VARCHAR(%d)', $length ?? 255); + } + + return sprintf('CHAR(%d)', $length ?? 255); + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + { + return $fixed + ? ($length > 0 ? 'NCHAR(' . $length . ')' : 'CHAR(255)') + : ($length > 0 ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)'); + } + + /** + * {@inheritdoc} + */ + protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed) + { + return $fixed + ? 'BINARY(' . ($length > 0 ? $length : 255) . ')' + : 'VARBINARY(' . ($length > 0 ? $length : 255) . ')'; + } + + /** + * {@inheritdoc} + */ + public function getBinaryMaxLength() + { + return 8000; + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column) + { + return 'VARCHAR(MAX)'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column) + { + return ! empty($column['autoincrement']) ? ' IDENTITY' : ''; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column) + { + // 3 - microseconds precision length + // http://msdn.microsoft.com/en-us/library/ms187819.aspx + return 'DATETIME2(6)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column) + { + return 'TIME(0)'; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column) + { + return 'BIT'; + } + + /** + * {@inheritDoc} + */ + protected function doModifyLimitQuery($query, $limit, $offset) + { + 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; + } + + /** + * {@inheritDoc} + */ + public function convertBooleans($item) + { + 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; + } + + /** + * {@inheritDoc} + */ + public function getCreateTemporaryTableSnippetSQL() + { + return 'CREATE TABLE'; + } + + /** + * {@inheritDoc} + */ + public function getTemporaryTableName($tableName) + { + return '#' . $tableName; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeFormatString() + { + return 'Y-m-d H:i:s.u'; + } + + /** + * {@inheritDoc} + */ + public function getDateFormatString() + { + return 'Y-m-d'; + } + + /** + * {@inheritDoc} + */ + public function getTimeFormatString() + { + return 'H:i:s'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzFormatString() + { + return 'Y-m-d H:i:s.u P'; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'mssql'; + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = [ + 'bigint' => 'bigint', + 'binary' => 'binary', + 'bit' => 'boolean', + 'blob' => 'blob', + 'char' => 'string', + 'date' => 'date', + 'datetime' => 'datetime', + 'datetime2' => 'datetime', + 'datetimeoffset' => 'datetimetz', + 'decimal' => 'decimal', + 'double' => 'float', + 'double precision' => 'float', + 'float' => 'float', + 'image' => 'blob', + 'int' => 'integer', + 'money' => 'integer', + 'nchar' => 'string', + 'ntext' => 'text', + 'numeric' => 'decimal', + 'nvarchar' => 'string', + 'real' => 'float', + 'smalldatetime' => 'datetime', + 'smallint' => 'smallint', + 'smallmoney' => 'integer', + 'text' => 'text', + 'time' => 'time', + 'tinyint' => 'smallint', + 'uniqueidentifier' => 'guid', + 'varbinary' => 'binary', + 'varchar' => 'string', + ]; + } + + /** + * {@inheritDoc} + */ + public function createSavePoint($savepoint) + { + return 'SAVE TRANSACTION ' . $savepoint; + } + + /** + * {@inheritDoc} + */ + public function releaseSavePoint($savepoint) + { + return ''; + } + + /** + * {@inheritDoc} + */ + public function rollbackSavePoint($savepoint) + { + return 'ROLLBACK TRANSACTION ' . $savepoint; + } + + /** + * {@inheritdoc} + */ + public function getForeignKeyReferentialActionSQL($action) + { + // 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, int $lockMode): string + { + switch ($lockMode) { + case LockMode::NONE: + case LockMode::OPTIMISTIC: + return $fromClause; + + case LockMode::PESSIMISTIC_READ: + return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)'; + + case LockMode::PESSIMISTIC_WRITE: + return $fromClause . ' WITH (UPDLOCK, ROWLOCK)'; + + default: + throw InvalidLockMode::fromLockMode($lockMode); + } + } + + /** + * {@inheritDoc} + */ + public function getForUpdateSQL() + { + return ' '; + } + + /** + * {@inheritDoc} + * + * @deprecated Implement {@see createReservedKeywordsList()} instead. + */ + protected function getReservedKeywordsClass() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4510', + 'SQLServerPlatform::getReservedKeywordsClass() is deprecated,' + . ' use SQLServerPlatform::createReservedKeywordsList() instead.' + ); + + return Keywords\SQLServer2012Keywords::class; + } + + /** + * {@inheritDoc} + */ + public function quoteSingleIdentifier($str) + { + return '[' . str_replace(']', ']]', $str) . ']'; + } + + /** + * {@inheritDoc} + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + $tableIdentifier = new Identifier($tableName); + + return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this); + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column) + { + return 'VARBINARY(MAX)'; + } + + /** + * {@inheritdoc} + * + * Modifies column declaration order as it differs in Microsoft SQL Server. + */ + public function getColumnDeclarationSQL($name, array $column) + { + if (isset($column['columnDefinition'])) { + $columnDef = $this->getCustomTypeDeclarationSQL($column); + } else { + $collation = ! empty($column['collation']) ? + ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : ''; + + $notnull = ! empty($column['notnull']) ? ' NOT NULL' : ''; + + $unique = ! empty($column['unique']) ? + ' ' . $this->getUniqueFieldDeclarationSQL() : ''; + + $check = ! empty($column['check']) ? + ' ' . $column['check'] : ''; + + $typeDecl = $column['type']->getSQLDeclaration($column, $this); + $columnDef = $typeDecl . $collation . $notnull . $unique . $check; + } + + return $name . ' ' . $columnDef; + } + + 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() . '[]^'; + } + + /** + * Returns a unique default constraint name for a table and column. + * + * @param string $table Name of the table to generate the unique default constraint name for. + * @param string $column Name of the column in the table to generate the unique default constraint name for. + */ + private function generateDefaultConstraintName($table, $column): string + { + return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column); + } + + /** + * Returns a hash value for a given identifier. + * + * @param string $identifier Identifier to generate a hash value for. + */ + private function generateIdentifierName($identifier): string + { + // Always generate name for unquoted identifiers to ensure consistency. + $identifier = new Identifier($identifier); + + return strtoupper(dechex(crc32($identifier->getName()))); + } + + protected function getCommentOnTableSQL(string $tableName, ?string $comment): string + { + return sprintf( + <<<'SQL' + EXEC sys.sp_addextendedproperty @name=N'MS_Description', + @value=N%s, @level0type=N'SCHEMA', @level0name=N'dbo', + @level1type=N'TABLE', @level1name=N%s + SQL + , + $this->quoteStringLiteral((string) $comment), + $this->quoteStringLiteral($tableName) + ); + } + + public function getListTableMetadataSQL(string $table): string + { + return sprintf( + <<<'SQL' + SELECT + p.value AS [table_comment] + FROM + sys.tables AS tbl + INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1 + WHERE + (tbl.name=N%s and SCHEMA_NAME(tbl.schema_id)=N'dbo' and p.name=N'MS_Description') + SQL + , + $this->quoteStringLiteral($table) + ); + } + + /** + * @param string $query + */ + private function shouldAddOrderBy($query): bool + { + // 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; + } +} diff --git a/app/vendor/doctrine/dbal/src/Platforms/SQLite/Comparator.php b/app/vendor/doctrine/dbal/src/Platforms/SQLite/Comparator.php new file mode 100644 index 000000000..d27ee86d7 --- /dev/null +++ b/app/vendor/doctrine/dbal/src/Platforms/SQLite/Comparator.php @@ -0,0 +1,53 @@ +normalizeColumns($fromTable); + $this->normalizeColumns($toTable); + + return parent::diffTable($fromTable, $toTable); + } + + private function normalizeColumns(Table $table): void + { + foreach ($table->getColumns() as $column) { + $options = $column->getPlatformOptions(); + + if (! isset($options['collation']) || strcasecmp($options['collation'], 'binary') !== 0) { + continue; + } + + unset($options['collation']); + $column->setPlatformOptions($options); + } + } +} diff --git a/app/vendor/doctrine/dbal/src/SQL/Parser/Exception.php b/app/vendor/doctrine/dbal/src/SQL/Parser/Exception.php new file mode 100644 index 000000000..0c14b3584 --- /dev/null +++ b/app/vendor/doctrine/dbal/src/SQL/Parser/Exception.php @@ -0,0 +1,11 @@ +createNamespaceQueries, - $this->createTableQueries, $this->createSequenceQueries, + $this->createTableQueries, $this->createFkConstraintQueries ); }