CONTRIBUTING.md000064400000000556152062402520007001 0ustar00This repository has [guidelines specific to testing][testing guidelines], and Doctrine has [general contributing guidelines][contributor workflow], make sure you follow both. [contributor workflow]: https://www.doctrine-project.org/contribute/index.html [testing guidelines]: https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/testing.html LICENSE000064400000002051152062402520005545 0ustar00Copyright (c) 2006-2018 Doctrine Project Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. README.md000064400000012161152062402520006022 0ustar00# Doctrine DBAL | [5.0-dev][5.0] | [4.1-dev][4.1] | [4.0-dev][4.0] | [3.9-dev][3.9] | [3.8][3.8] | |:---------------------------------------------------:|:---------------------------------------------------:|:---------------------------------------------------:|:---------------------------------------------------:|:---------------------------------------------------:| | [![GitHub Actions][GA 5.0 image]][GA 5.0] | [![GitHub Actions][GA 4.1 image]][GA 4.1] | [![GitHub Actions][GA 4.0 image]][GA 4.0] | [![GitHub Actions][GA 3.9 image]][GA 3.9] | [![GitHub Actions][GA 3.8 image]][GA 3.8] | | [![AppVeyor][AppVeyor 5.0 image]][AppVeyor 5.0] | [![AppVeyor][AppVeyor 4.1 image]][AppVeyor 4.1] | [![AppVeyor][AppVeyor 4.0 image]][AppVeyor 4.0] | [![AppVeyor][AppVeyor 3.9 image]][AppVeyor 3.9] | [![AppVeyor][AppVeyor 3.8 image]][AppVeyor 3.8] | | [![Code Coverage][Coverage 5.0 image]][CodeCov 5.0] | [![Code Coverage][Coverage 4.1 image]][CodeCov 4.1] | [![Code Coverage][Coverage 4.0 image]][CodeCov 4.0] | [![Code Coverage][Coverage 3.9 image]][CodeCov 3.9] | [![Code Coverage][Coverage 3.8 image]][CodeCov 3.8] | | N/A | N/A | N/A | N/A | [![Type Coverage][TypeCov 3.8 image]][TypeCov 3.8] | Powerful ***D***ata***B***ase ***A***bstraction ***L***ayer with many features for database schema introspection and schema management. ## More resources: * [Website](http://www.doctrine-project.org/projects/dbal.html) * [Documentation](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/) * [Issue Tracker](https://github.com/doctrine/dbal/issues) [Coverage 5.0 image]: https://codecov.io/gh/doctrine/dbal/branch/5.0.x/graph/badge.svg [5.0]: https://github.com/doctrine/dbal/tree/5.0.x [CodeCov 5.0]: https://codecov.io/gh/doctrine/dbal/branch/5.0.x [AppVeyor 5.0]: https://ci.appveyor.com/project/doctrine/dbal/branch/5.0.x [AppVeyor 5.0 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/5.0.x?svg=true [GA 5.0]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A5.0.x [GA 5.0 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=5.0.x [Coverage 4.1 image]: https://codecov.io/gh/doctrine/dbal/branch/4.1.x/graph/badge.svg [4.1]: https://github.com/doctrine/dbal/tree/4.1.x [CodeCov 4.1]: https://codecov.io/gh/doctrine/dbal/branch/4.1.x [AppVeyor 4.1]: https://ci.appveyor.com/project/doctrine/dbal/branch/4.1.x [AppVeyor 4.1 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/4.1.x?svg=true [GA 4.1]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.1.x [GA 4.1 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=4.1.x [Coverage 4.0 image]: https://codecov.io/gh/doctrine/dbal/branch/4.0.x/graph/badge.svg [4.0]: https://github.com/doctrine/dbal/tree/4.0.x [CodeCov 4.0]: https://codecov.io/gh/doctrine/dbal/branch/4.0.x [AppVeyor 4.0]: https://ci.appveyor.com/project/doctrine/dbal/branch/4.0.x [AppVeyor 4.0 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/4.0.x?svg=true [GA 4.0]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.0.x [GA 4.0 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=4.0.x [Coverage 3.9 image]: https://codecov.io/gh/doctrine/dbal/branch/3.9.x/graph/badge.svg [3.9]: https://github.com/doctrine/dbal/tree/3.9.x [CodeCov 3.9]: https://codecov.io/gh/doctrine/dbal/branch/3.9.x [AppVeyor 3.9]: https://ci.appveyor.com/project/doctrine/dbal/branch/3.9.x [AppVeyor 3.9 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/3.9.x?svg=true [GA 3.9]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A3.9.x [GA 3.9 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=3.9.x [Coverage 3.8 image]: https://codecov.io/gh/doctrine/dbal/branch/3.8.x/graph/badge.svg [3.8]: https://github.com/doctrine/dbal/tree/3.8.x [CodeCov 3.8]: https://codecov.io/gh/doctrine/dbal/branch/3.8.x [AppVeyor 3.8]: https://ci.appveyor.com/project/doctrine/dbal/branch/3.8.x [AppVeyor 3.8 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/3.8.x?svg=true [GA 3.8]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A3.8.x [GA 3.8 image]: https://github.com/doctrine/dbal/workflows/Continuous%20Integration/badge.svg?branch=3.8.x [TypeCov 3.8]: https://shepherd.dev/github/doctrine/dbal [TypeCov 3.8 image]: https://shepherd.dev/github/doctrine/dbal/coverage.svg composer.json000064400000004165152062402520007272 0ustar00{ "name": "doctrine/dbal", "type": "library", "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", "keywords": [ "abstraction", "database", "dbal", "db2", "mariadb", "mssql", "mysql", "pgsql", "postgresql", "oci8", "oracle", "pdo", "queryobject", "sasql", "sql", "sqlite", "sqlserver", "sqlsrv" ], "homepage": "https://www.doctrine-project.org/projects/dbal.html", "license": "MIT", "authors": [ {"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"}, {"name": "Roman Borschel", "email": "roman@code-factory.org"}, {"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"}, {"name": "Jonathan Wage", "email": "jonwage@gmail.com"} ], "require": { "php": "^8.1", "doctrine/deprecations": "^0.5.3|^1", "psr/cache": "^1|^2|^3", "psr/log": "^1|^2|^3" }, "require-dev": { "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.2", "phpstan/phpstan": "1.10.57", "phpstan/phpstan-phpunit": "1.3.15", "phpstan/phpstan-strict-rules": "^1.5", "phpunit/phpunit": "10.5.9", "psalm/plugin-phpunit": "0.18.4", "slevomat/coding-standard": "8.13.1", "squizlabs/php_codesniffer": "3.8.1", "symfony/cache": "^6.3.8|^7.0", "symfony/console": "^5.4|^6.3|^7.0", "vimeo/psalm": "5.16.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." }, "config": { "sort-packages": true, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, "composer/package-versions-deprecated": true } }, "autoload": { "psr-4": { "Doctrine\\DBAL\\": "src" } }, "autoload-dev": { "psr-4": { "Doctrine\\DBAL\\Tests\\": "tests" } } } src/ArrayParameterType.php000064400000001604152062402520011624 0ustar00 ParameterType::INTEGER, self::STRING => ParameterType::STRING, self::ASCII => ParameterType::ASCII, self::BINARY => ParameterType::BINARY, }; } } src/ArrayParameters/Exception.php000064400000000226152062402520013102 0ustar00> $data */ public function __construct(private array $data) { $this->columnCount = $data === [] ? 0 : count($data[0]); } public function fetchNumeric(): array|false { $row = $this->fetch(); if ($row === false) { return false; } return array_values($row); } public function fetchAssociative(): array|false { return $this->fetch(); } public function fetchOne(): mixed { $row = $this->fetch(); if ($row === false) { return false; } return reset($row); } /** * {@inheritDoc} */ public function fetchAllNumeric(): array { return FetchUtils::fetchAllNumeric($this); } /** * {@inheritDoc} */ public function fetchAllAssociative(): array { return FetchUtils::fetchAllAssociative($this); } /** * {@inheritDoc} */ public function fetchFirstColumn(): array { return FetchUtils::fetchFirstColumn($this); } public function rowCount(): int { return count($this->data); } public function columnCount(): int { return $this->columnCount; } public function free(): void { $this->data = []; } /** @return array|false */ private function fetch(): array|false { if (! isset($this->data[$this->num])) { return false; } return $this->data[$this->num++]; } } src/Cache/CacheException.php000064400000000270152062402520011726 0ustar00resultCache; } public function getLifetime(): int { return $this->lifetime; } /** @throws CacheException */ public function getCacheKey(): string { if ($this->cacheKey === null) { throw NoCacheKey::new(); } return $this->cacheKey; } /** * Generates the real cache key from query, params, types and connection parameters. * * @param list|array $params * @param array $connectionParams * @psalm-param array|array $types * * @return array{string, string} */ public function generateCacheKeys(string $sql, array $params, array $types, array $connectionParams = []): array { if (isset($connectionParams['password'])) { unset($connectionParams['password']); } $realCacheKey = 'query=' . $sql . '¶ms=' . serialize($params) . '&types=' . serialize($types) . '&connectionParams=' . hash('sha256', serialize($connectionParams)); // should the key be automatically generated using the inputs or is the cache key set? $cacheKey = $this->cacheKey ?? sha1($realCacheKey); return [$cacheKey, $realCacheKey]; } public function setResultCache(CacheItemPoolInterface $cache): QueryCacheProfile { return new QueryCacheProfile($this->lifetime, $this->cacheKey, $cache); } public function setCacheKey(?string $cacheKey): self { return new QueryCacheProfile($this->lifetime, $cacheKey, $this->resultCache); } public function setLifetime(int $lifetime): self { return new QueryCacheProfile($lifetime, $this->cacheKey, $this->resultCache); } } src/ColumnCase.php000064400000000434152062402520010074 0ustar00schemaAssetsFilter = static function (): bool { return true; }; } /** * Gets the cache driver implementation that is used for query result caching. */ public function getResultCache(): ?CacheItemPoolInterface { return $this->resultCache; } /** * Sets the cache driver implementation that is used for query result caching. */ public function setResultCache(CacheItemPoolInterface $cache): void { $this->resultCache = $cache; } /** * Sets the callable to use to filter schema assets. */ public function setSchemaAssetsFilter(callable $schemaAssetsFilter): void { $this->schemaAssetsFilter = $schemaAssetsFilter; } /** * Returns the callable to use to filter schema assets. */ public function getSchemaAssetsFilter(): callable { return $this->schemaAssetsFilter; } /** * Sets the default auto-commit mode for connections. * * If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either * the method commit or the method rollback. By default, new connections are in auto-commit mode. * * @see getAutoCommit * * @param bool $autoCommit True to enable auto-commit mode; false to disable it */ public function setAutoCommit(bool $autoCommit): void { $this->autoCommit = $autoCommit; } /** * Returns the default auto-commit mode for connections. * * @see setAutoCommit * * @return bool True if auto-commit mode is enabled by default for connections, false otherwise. */ public function getAutoCommit(): bool { return $this->autoCommit; } /** * @param Middleware[] $middlewares * * @return $this */ public function setMiddlewares(array $middlewares): self { $this->middlewares = $middlewares; return $this; } /** @return Middleware[] */ public function getMiddlewares(): array { return $this->middlewares; } public function getSchemaManagerFactory(): ?SchemaManagerFactory { return $this->schemaManagerFactory; } /** @return $this */ public function setSchemaManagerFactory(SchemaManagerFactory $schemaManagerFactory): self { $this->schemaManagerFactory = $schemaManagerFactory; return $this; } /** @return true */ public function getDisableTypeComments(): bool { return true; } /** * @param true $disableTypeComments * * @return $this */ public function setDisableTypeComments(bool $disableTypeComments): self { if (! $disableTypeComments) { throw new InvalidArgumentException('Column comments cannot be enabled anymore.'); } return $this; } } src/Connection.php000064400000122567152062402520010156 0ustar00, WrapperParameterType>|array * @psalm-consistent-constructor */ class Connection implements ServerVersionProvider { /** * The wrapped driver connection. */ protected ?DriverConnection $_conn = null; protected Configuration $_config; /** * The current auto-commit mode of this connection. */ private bool $autoCommit = true; /** * The transaction nesting level. */ private int $transactionNestingLevel = 0; /** * The currently active transaction isolation level or NULL before it has been determined. */ private ?TransactionIsolationLevel $transactionIsolationLevel = null; /** * The parameters used during creation of the Connection instance. * * @var array * @psalm-var Params */ private array $params; /** * The database platform object used by the connection or NULL before it's initialized. */ private ?AbstractPlatform $platform = null; private ?ExceptionConverter $exceptionConverter = null; private ?Parser $parser = null; /** * Flag that indicates whether the current transaction is marked for rollback only. */ private bool $isRollbackOnly = false; private SchemaManagerFactory $schemaManagerFactory; /** * Initializes a new instance of the Connection class. * * @internal The connection can be only instantiated by the driver manager. * * @param array $params The connection parameters. * @param Driver $driver The driver to use. * @param Configuration|null $config The configuration, optional. * @psalm-param Params $params */ public function __construct( #[SensitiveParameter] array $params, protected Driver $driver, ?Configuration $config = null, ) { $this->_config = $config ?? new Configuration(); $this->params = $params; $this->autoCommit = $this->_config->getAutoCommit(); $this->schemaManagerFactory = $this->_config->getSchemaManagerFactory() ?? new DefaultSchemaManagerFactory(); } /** * Gets the parameters used during instantiation. * * @internal * * @return array * @psalm-return Params */ public function getParams(): array { return $this->params; } /** * Gets the name of the currently selected database. * * @return string|null The name of the database or NULL if a database is not selected. * The platforms which don't support the concept of a database (e.g. embedded databases) * must always return a string as an indicator of an implicitly selected database. * * @throws Exception */ public function getDatabase(): ?string { $platform = $this->getDatabasePlatform(); $query = $platform->getDummySelectSQL($platform->getCurrentDatabaseExpression()); $database = $this->fetchOne($query); assert(is_string($database) || $database === null); return $database; } /** * Gets the DBAL driver instance. */ public function getDriver(): Driver { return $this->driver; } /** * Gets the Configuration used by the Connection. */ public function getConfiguration(): Configuration { return $this->_config; } /** * Gets the DatabasePlatform for the connection. * * @throws Exception */ public function getDatabasePlatform(): AbstractPlatform { if ($this->platform === null) { $versionProvider = $this; if (isset($this->params['serverVersion'])) { $versionProvider = new StaticServerVersionProvider($this->params['serverVersion']); } elseif (isset($this->params['primary']['serverVersion'])) { $versionProvider = new StaticServerVersionProvider($this->params['primary']['serverVersion']); } $this->platform = $this->driver->getDatabasePlatform($versionProvider); } return $this->platform; } /** * Creates an expression builder for the connection. */ public function createExpressionBuilder(): ExpressionBuilder { return new ExpressionBuilder($this); } /** * Establishes the connection with the database and returns the underlying connection. * * @throws Exception */ protected function connect(): DriverConnection { if ($this->_conn !== null) { return $this->_conn; } try { $connection = $this->_conn = $this->driver->connect($this->params); } catch (Driver\Exception $e) { throw $this->convertException($e); } if ($this->autoCommit === false) { $this->beginTransaction(); } return $connection; } /** * {@inheritDoc} * * @throws Exception */ public function getServerVersion(): string { return $this->connect()->getServerVersion(); } /** * Returns the current auto-commit mode for this connection. * * @see setAutoCommit * * @return bool True if auto-commit mode is currently enabled for this connection, false otherwise. */ public function isAutoCommit(): bool { return $this->autoCommit; } /** * Sets auto-commit mode for this connection. * * If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either * the method commit or the method rollback. By default, new connections are in auto-commit mode. * * NOTE: If this method is called during a transaction and the auto-commit mode is changed, the transaction is * committed. If this method is called and the auto-commit mode is not changed, the call is a no-op. * * @see isAutoCommit * * @throws ConnectionException * @throws DriverException */ public function setAutoCommit(bool $autoCommit): void { // Mode not changed, no-op. if ($autoCommit === $this->autoCommit) { return; } $this->autoCommit = $autoCommit; // Commit all currently active transactions if any when switching auto-commit mode. if ($this->_conn === null || $this->transactionNestingLevel === 0) { return; } $this->commitAll(); } /** * Prepares and executes an SQL query and returns the first row of the result * as an associative array. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return array|false False is returned if no rows are found. * * @throws Exception */ public function fetchAssociative(string $query, array $params = [], array $types = []): array|false { return $this->executeQuery($query, $params, $types)->fetchAssociative(); } /** * Prepares and executes an SQL query and returns the first row of the result * as a numerically indexed array. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return list|false False is returned if no rows are found. * * @throws Exception */ public function fetchNumeric(string $query, array $params = [], array $types = []): array|false { return $this->executeQuery($query, $params, $types)->fetchNumeric(); } /** * Prepares and executes an SQL query and returns the value of a single column * of the first row of the result. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return mixed|false False is returned if no rows are found. * * @throws Exception */ public function fetchOne(string $query, array $params = [], array $types = []): mixed { return $this->executeQuery($query, $params, $types)->fetchOne(); } /** * Whether an actual connection to the database is established. */ public function isConnected(): bool { return $this->_conn !== null; } /** * Checks whether a transaction is currently active. * * @return bool TRUE if a transaction is currently active, FALSE otherwise. */ public function isTransactionActive(): bool { return $this->transactionNestingLevel > 0; } /** * Adds condition based on the criteria to the query components * * @param array $criteria Map of key columns to their values * * @return array{list, list, list} */ private function getCriteriaCondition(array $criteria): array { $columns = $values = $conditions = []; foreach ($criteria as $columnName => $value) { if ($value === null) { $conditions[] = $columnName . ' IS NULL'; continue; } $columns[] = $columnName; $values[] = $value; $conditions[] = $columnName . ' = ?'; } return [$columns, $values, $conditions]; } /** * Executes an SQL DELETE statement on a table. * * Table expression and columns are not escaped and are not safe for user-input. * * @param array $criteria * @param array, string|ParameterType|Type>|array $types * * @return int|numeric-string The number of affected rows. * * @throws Exception */ public function delete(string $table, array $criteria = [], array $types = []): int|string { [$columns, $values, $conditions] = $this->getCriteriaCondition($criteria); $sql = 'DELETE FROM ' . $table; if ($conditions !== []) { $sql .= ' WHERE ' . implode(' AND ', $conditions); } return $this->executeStatement( $sql, $values, is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types, ); } /** * Closes the connection. */ public function close(): void { $this->_conn = null; $this->transactionNestingLevel = 0; } /** * Sets the transaction isolation level. * * @param TransactionIsolationLevel $level The level to set. * * @throws Exception */ public function setTransactionIsolation(TransactionIsolationLevel $level): void { $this->transactionIsolationLevel = $level; $this->executeStatement($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level)); } /** * Gets the currently active transaction isolation level. * * @return TransactionIsolationLevel The current transaction isolation level. * * @throws Exception */ public function getTransactionIsolation(): TransactionIsolationLevel { return $this->transactionIsolationLevel ??= $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel(); } /** * Executes an SQL UPDATE statement on a table. * * Table expression and columns are not escaped and are not safe for user-input. * * @param array $data * @param array $criteria * @param array, string|ParameterType|Type>|array $types * * @return int|numeric-string The number of affected rows. * * @throws Exception */ public function update(string $table, array $data, array $criteria = [], array $types = []): int|string { $columns = $values = $conditions = $set = []; foreach ($data as $columnName => $value) { $columns[] = $columnName; $values[] = $value; $set[] = $columnName . ' = ?'; } [$criteriaColumns, $criteriaValues, $criteriaConditions] = $this->getCriteriaCondition($criteria); $columns = array_merge($columns, $criteriaColumns); $values = array_merge($values, $criteriaValues); $conditions = array_merge($conditions, $criteriaConditions); if (is_string(key($types))) { $types = $this->extractTypeValues($columns, $types); } $sql = 'UPDATE ' . $table . ' SET ' . implode(', ', $set); if ($conditions !== []) { $sql .= ' WHERE ' . implode(' AND ', $conditions); } return $this->executeStatement($sql, $values, $types); } /** * Inserts a table row with specified data. * * Table expression and columns are not escaped and are not safe for user-input. * * @param array $data * @param array, string|ParameterType|Type>|array $types * * @return int|numeric-string The number of affected rows. * * @throws Exception */ public function insert(string $table, array $data, array $types = []): int|string { if (count($data) === 0) { return $this->executeStatement('INSERT INTO ' . $table . ' () VALUES ()'); } $columns = []; $values = []; $set = []; foreach ($data as $columnName => $value) { $columns[] = $columnName; $values[] = $value; $set[] = '?'; } return $this->executeStatement( 'INSERT INTO ' . $table . ' (' . implode(', ', $columns) . ')' . ' VALUES (' . implode(', ', $set) . ')', $values, is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types, ); } /** * Extract ordered type list from an ordered column list and type map. * * @param array $columns * @param array|array $types * * @return array, string|ParameterType|Type> */ private function extractTypeValues(array $columns, array $types): array { $typeValues = []; foreach ($columns as $columnName) { $typeValues[] = $types[$columnName] ?? ParameterType::STRING; } return $typeValues; } /** * Quotes a string so it can be safely used as a table or column name, even if * it is a reserved name. * * Delimiting style depends on the underlying database platform that is being used. * * NOTE: Just because you CAN use quoted identifiers does not mean * you SHOULD use them. In general, they end up causing way more * problems than they solve. * * @param string $identifier The identifier to be quoted. * * @return string The quoted identifier. */ public function quoteIdentifier(string $identifier): string { return $this->getDatabasePlatform()->quoteIdentifier($identifier); } /** * The usage of this method is discouraged. Use prepared statements * or {@see AbstractPlatform::quoteStringLiteral()} instead. */ public function quote(string $value): string { return $this->connect()->quote($value); } /** * Prepares and executes an SQL query and returns the result as an array of numeric arrays. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return list> * * @throws Exception */ public function fetchAllNumeric(string $query, array $params = [], array $types = []): array { return $this->executeQuery($query, $params, $types)->fetchAllNumeric(); } /** * Prepares and executes an SQL query and returns the result as an array of associative arrays. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return list> * * @throws Exception */ public function fetchAllAssociative(string $query, array $params = [], array $types = []): array { return $this->executeQuery($query, $params, $types)->fetchAllAssociative(); } /** * Prepares and executes an SQL query and returns the result as an associative array with the keys * mapped to the first column and the values mapped to the second column. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return array * * @throws Exception */ public function fetchAllKeyValue(string $query, array $params = [], array $types = []): array { return $this->executeQuery($query, $params, $types)->fetchAllKeyValue(); } /** * Prepares and executes an SQL query and returns the result as an associative array with the keys mapped * to the first column and the values being an associative array representing the rest of the columns * and their values. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return array> * * @throws Exception */ public function fetchAllAssociativeIndexed(string $query, array $params = [], array $types = []): array { return $this->executeQuery($query, $params, $types)->fetchAllAssociativeIndexed(); } /** * Prepares and executes an SQL query and returns the result as an array of the first column values. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return list * * @throws Exception */ public function fetchFirstColumn(string $query, array $params = [], array $types = []): array { return $this->executeQuery($query, $params, $types)->fetchFirstColumn(); } /** * Prepares and executes an SQL query and returns the result as an iterator over rows represented as numeric arrays. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return Traversable> * * @throws Exception */ public function iterateNumeric(string $query, array $params = [], array $types = []): Traversable { return $this->executeQuery($query, $params, $types)->iterateNumeric(); } /** * Prepares and executes an SQL query and returns the result as an iterator over rows represented * as associative arrays. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return Traversable> * * @throws Exception */ public function iterateAssociative(string $query, array $params = [], array $types = []): Traversable { return $this->executeQuery($query, $params, $types)->iterateAssociative(); } /** * Prepares and executes an SQL query and returns the result as an iterator with the keys * mapped to the first column and the values mapped to the second column. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return Traversable * * @throws Exception */ public function iterateKeyValue(string $query, array $params = [], array $types = []): Traversable { return $this->executeQuery($query, $params, $types)->iterateKeyValue(); } /** * Prepares and executes an SQL query and returns the result as an iterator with the keys mapped * to the first column and the values being an associative array representing the rest of the columns * and their values. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return Traversable> * * @throws Exception */ public function iterateAssociativeIndexed(string $query, array $params = [], array $types = []): Traversable { return $this->executeQuery($query, $params, $types)->iterateAssociativeIndexed(); } /** * Prepares and executes an SQL query and returns the result as an iterator over the first column values. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return Traversable * * @throws Exception */ public function iterateColumn(string $query, array $params = [], array $types = []): Traversable { return $this->executeQuery($query, $params, $types)->iterateColumn(); } /** * Prepares an SQL statement. * * @param string $sql The SQL statement to prepare. * * @throws Exception */ public function prepare(string $sql): Statement { $connection = $this->connect(); try { $statement = $connection->prepare($sql); } catch (Driver\Exception $e) { throw $this->convertExceptionDuringQuery($e, $sql); } return new Statement($this, $statement, $sql); } /** * Executes an, optionally parameterized, SQL query. * * If the query is parametrized, a prepared statement is used. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @throws Exception */ public function executeQuery( string $sql, array $params = [], array $types = [], ?QueryCacheProfile $qcp = null, ): Result { if ($qcp !== null) { return $this->executeCacheQuery($sql, $params, $types, $qcp); } $connection = $this->connect(); try { if (count($params) > 0) { [$sql, $params, $types] = $this->expandArrayParameters($sql, $params, $types); $stmt = $connection->prepare($sql); $this->bindParameters($stmt, $params, $types); $result = $stmt->execute(); } else { $result = $connection->query($sql); } return new Result($result, $this); } catch (Driver\Exception $e) { throw $this->convertExceptionDuringQuery($e, $sql, $params, $types); } } /** * Executes a caching query. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @throws CacheException * @throws Exception */ public function executeCacheQuery(string $sql, array $params, array $types, QueryCacheProfile $qcp): Result { $resultCache = $qcp->getResultCache() ?? $this->_config->getResultCache(); if ($resultCache === null) { throw NoResultDriverConfigured::new(); } $connectionParams = $this->params; unset($connectionParams['password']); [$cacheKey, $realKey] = $qcp->generateCacheKeys($sql, $params, $types, $connectionParams); $item = $resultCache->getItem($cacheKey); if ($item->isHit()) { $value = $item->get(); if (! is_array($value)) { $value = []; } if (isset($value[$realKey])) { return new Result(new ArrayResult($value[$realKey]), $this); } } else { $value = []; } $data = $this->fetchAllAssociative($sql, $params, $types); $value[$realKey] = $data; $item->set($value); $lifetime = $qcp->getLifetime(); if ($lifetime > 0) { $item->expiresAfter($lifetime); } $resultCache->save($item); return new Result(new ArrayResult($data), $this); } /** * Executes an SQL statement with the given parameters and returns the number of affected rows. * * Could be used for: * - DML statements: INSERT, UPDATE, DELETE, etc. * - DDL statements: CREATE, DROP, ALTER, etc. * - DCL statements: GRANT, REVOKE, etc. * - Session control statements: ALTER SESSION, SET, DECLARE, etc. * - Other statements that don't yield a row set. * * This method supports PDO binding types as well as DBAL mapping types. * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return int|numeric-string * * @throws Exception */ public function executeStatement(string $sql, array $params = [], array $types = []): int|string { $connection = $this->connect(); try { if (count($params) > 0) { [$sql, $params, $types] = $this->expandArrayParameters($sql, $params, $types); $stmt = $connection->prepare($sql); $this->bindParameters($stmt, $params, $types); return $stmt->execute() ->rowCount(); } return $connection->exec($sql); } catch (Driver\Exception $e) { throw $this->convertExceptionDuringQuery($e, $sql, $params, $types); } } /** * Returns the current transaction nesting level. * * @return int The nesting level. A value of 0 means there's no active transaction. */ public function getTransactionNestingLevel(): int { return $this->transactionNestingLevel; } /** * Returns the ID of the last inserted row. * * If the underlying driver does not support identity columns, an exception is thrown. * * @throws Exception */ public function lastInsertId(): int|string { try { return $this->connect()->lastInsertId(); } catch (Driver\Exception $e) { throw $this->convertException($e); } } /** * Executes a function in a transaction. * * The function gets passed this Connection instance as an (optional) parameter. * * If an exception occurs during execution of the function or transaction commit, * the transaction is rolled back and the exception re-thrown. * * @param Closure(self):T $func The function to execute transactionally. * * @return T The value returned by $func * * @throws Throwable * * @template T */ public function transactional(Closure $func): mixed { $this->beginTransaction(); try { $res = $func($this); $this->commit(); return $res; } catch (Throwable $e) { $this->rollBack(); throw $e; } } /** * Sets if nested transactions should use savepoints. * * @deprecated No replacement planned * * @throws Exception */ public function setNestTransactionsWithSavepoints(bool $nestTransactionsWithSavepoints): void { if (! $nestTransactionsWithSavepoints) { throw new InvalidArgumentException(sprintf( 'Calling %s with false to enable nesting transactions without savepoints is no longer supported.', __METHOD__, )); } Deprecation::trigger( 'doctrine/dbal', 'https://github.com/doctrine/dbal/pull/5383', '%s is deprecated and will be removed in 5.0', __METHOD__, ); } /** * Gets if nested transactions should use savepoints. * * @deprecated No replacement planned */ public function getNestTransactionsWithSavepoints(): bool { Deprecation::trigger( 'doctrine/dbal', 'https://github.com/doctrine/dbal/pull/5383', '%s is deprecated and will be removed in 5.0', __METHOD__, ); return true; } /** * Returns the savepoint name to use for nested transactions. */ protected function _getNestedTransactionSavePointName(): string { return 'DOCTRINE_' . $this->transactionNestingLevel; } /** @throws Exception */ public function beginTransaction(): void { $connection = $this->connect(); ++$this->transactionNestingLevel; if ($this->transactionNestingLevel === 1) { $connection->beginTransaction(); } else { $this->createSavepoint($this->_getNestedTransactionSavePointName()); } } /** @throws Exception */ public function commit(): void { if ($this->transactionNestingLevel === 0) { throw NoActiveTransaction::new(); } if ($this->isRollbackOnly) { throw CommitFailedRollbackOnly::new(); } $connection = $this->connect(); if ($this->transactionNestingLevel === 1) { try { $connection->commit(); } catch (Driver\Exception $e) { throw $this->convertException($e); } } else { $this->releaseSavepoint($this->_getNestedTransactionSavePointName()); } --$this->transactionNestingLevel; if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) { return; } $this->beginTransaction(); } /** * Commits all current nesting transactions. * * @throws Exception */ private function commitAll(): void { while ($this->transactionNestingLevel !== 0) { if ($this->autoCommit === false && $this->transactionNestingLevel === 1) { // When in no auto-commit mode, the last nesting commit immediately starts a new transaction. // Therefore we need to do the final commit here and then leave to avoid an infinite loop. $this->commit(); return; } $this->commit(); } } /** @throws Exception */ public function rollBack(): void { if ($this->transactionNestingLevel === 0) { throw NoActiveTransaction::new(); } $connection = $this->connect(); if ($this->transactionNestingLevel === 1) { $this->transactionNestingLevel = 0; try { $connection->rollBack(); } catch (Driver\Exception $e) { throw $this->convertException($e); } finally { $this->isRollbackOnly = false; if ($this->autoCommit === false) { $this->beginTransaction(); } } } else { $this->rollbackSavepoint($this->_getNestedTransactionSavePointName()); --$this->transactionNestingLevel; } } /** * Creates a new savepoint. * * @param string $savepoint The name of the savepoint to create. * * @throws Exception */ public function createSavepoint(string $savepoint): void { $platform = $this->getDatabasePlatform(); if (! $platform->supportsSavepoints()) { throw SavepointsNotSupported::new(); } $this->executeStatement($platform->createSavePoint($savepoint)); } /** * Releases the given savepoint. * * @param string $savepoint The name of the savepoint to release. * * @throws Exception */ public function releaseSavepoint(string $savepoint): void { $platform = $this->getDatabasePlatform(); if (! $platform->supportsSavepoints()) { throw SavepointsNotSupported::new(); } if (! $platform->supportsReleaseSavepoints()) { return; } $this->executeStatement($platform->releaseSavePoint($savepoint)); } /** * Rolls back to the given savepoint. * * @param string $savepoint The name of the savepoint to rollback to. * * @throws Exception */ public function rollbackSavepoint(string $savepoint): void { $platform = $this->getDatabasePlatform(); if (! $platform->supportsSavepoints()) { throw SavepointsNotSupported::new(); } $this->executeStatement($platform->rollbackSavePoint($savepoint)); } /** * Provides access to the native database connection. * * @return resource|object * * @throws Exception */ public function getNativeConnection() { return $this->connect()->getNativeConnection(); } /** * Creates a SchemaManager that can be used to inspect or change the * database schema through the connection. * * @throws Exception */ public function createSchemaManager(): AbstractSchemaManager { return $this->schemaManagerFactory->createSchemaManager($this); } /** * Marks the current transaction so that the only possible * outcome for the transaction to be rolled back. * * @throws ConnectionException If no transaction is active. */ public function setRollbackOnly(): void { if ($this->transactionNestingLevel === 0) { throw NoActiveTransaction::new(); } $this->isRollbackOnly = true; } /** * Checks whether the current transaction is marked for rollback only. * * @throws ConnectionException If no transaction is active. */ public function isRollbackOnly(): bool { if ($this->transactionNestingLevel === 0) { throw NoActiveTransaction::new(); } return $this->isRollbackOnly; } /** * Converts a given value to its database representation according to the conversion * rules of a specific DBAL mapping type. * * @param mixed $value The value to convert. * @param string $type The name of the DBAL mapping type. * * @return mixed The converted value. * * @throws Exception */ public function convertToDatabaseValue(mixed $value, string $type): mixed { return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform()); } /** * Converts a given value to its PHP representation according to the conversion * rules of a specific DBAL mapping type. * * @param mixed $value The value to convert. * @param string $type The name of the DBAL mapping type. * * @return mixed The converted type. * * @throws Exception */ public function convertToPHPValue(mixed $value, string $type): mixed { return Type::getType($type)->convertToPHPValue($value, $this->getDatabasePlatform()); } /** * Binds a set of parameters, some or all of which are typed with a PDO binding type * or DBAL mapping type, to a given statement. * * @param list|array $params * @param array|array $types * * @throws Exception */ private function bindParameters(DriverStatement $stmt, array $params, array $types): void { // Check whether parameters are positional or named. Mixing is not allowed. if (is_int(key($params))) { $bindIndex = 1; foreach ($params as $key => $value) { if (array_key_exists($key, $types)) { $type = $types[$key]; [$value, $bindingType] = $this->getBindingInfo($value, $type); } else { $bindingType = ParameterType::STRING; } $stmt->bindValue($bindIndex, $value, $bindingType); ++$bindIndex; } } else { // Named parameters foreach ($params as $name => $value) { if (array_key_exists($name, $types)) { $type = $types[$name]; [$value, $bindingType] = $this->getBindingInfo($value, $type); } else { $bindingType = ParameterType::STRING; } $stmt->bindValue($name, $value, $bindingType); } } } /** * Gets the binding type of a given type. * * @param mixed $value The value to bind. * @param string|ParameterType|Type $type The type to bind. * * @return array{mixed, ParameterType} [0] => the (escaped) value, [1] => the binding type. * * @throws Exception */ private function getBindingInfo(mixed $value, string|ParameterType|Type $type): array { if (is_string($type)) { $type = Type::getType($type); } if ($type instanceof Type) { $value = $type->convertToDatabaseValue($value, $this->getDatabasePlatform()); $bindingType = $type->getBindingType(); } else { $bindingType = $type; } return [$value, $bindingType]; } /** * Creates a new instance of a SQL query builder. */ public function createQueryBuilder(): QueryBuilder { return new Query\QueryBuilder($this); } /** * @internal * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types */ final public function convertExceptionDuringQuery( Driver\Exception $e, string $sql, array $params = [], array $types = [], ): DriverException { return $this->handleDriverException($e, new Query($sql, $params, $types)); } /** @internal */ final public function convertException(Driver\Exception $e): DriverException { return $this->handleDriverException($e, null); } /** * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return array{ * string, * list|array, * array, string|ParameterType|Type>|array * } */ private function expandArrayParameters(string $sql, array $params, array $types): array { $needsConversion = false; $nonArrayTypes = []; if (is_string(key($params))) { $needsConversion = true; } else { foreach ($types as $key => $type) { if ($type instanceof ArrayParameterType) { $needsConversion = true; break; } $nonArrayTypes[$key] = $type; } } if (! $needsConversion) { return [$sql, $params, $nonArrayTypes]; } $this->parser ??= $this->getDatabasePlatform()->createSQLParser(); $visitor = new ExpandArrayParameters($params, $types); $this->parser->parse($sql, $visitor); return [ $visitor->getSQL(), $visitor->getParameters(), $visitor->getTypes(), ]; } private function handleDriverException( Driver\Exception $driverException, ?Query $query, ): DriverException { $this->exceptionConverter ??= $this->driver->getExceptionConverter(); $exception = $this->exceptionConverter->convert($driverException, $query); if ($exception instanceof ConnectionLost) { $this->close(); } return $exception; } } src/Connection/StaticServerVersionProvider.php000064400000000540152062402520015637 0ustar00version; } } src/ConnectionException.php000064400000000231152062402520012014 0ustar00executeQuery("DELETE FROM table"); * * Be aware that Connection#executeQuery is a method specifically for READ * operations only. * * Use Connection#executeStatement for any SQL statement that changes/updates * state in the database (UPDATE, INSERT, DELETE or DDL statements). * * This connection is limited to replica operations using the * Connection#executeQuery operation only, because it wouldn't be compatible * with the ORM or SchemaManager code otherwise. Both use all the other * operations in a context where writes could happen to a replica, which makes * this restricted approach necessary. * * You can manually connect to the primary at any time by calling: * * $conn->ensureConnectedToPrimary(); * * Instantiation through the DriverManager looks like: * * @psalm-import-type Params from DriverManager * @psalm-import-type OverrideParams from DriverManager * @example * * $conn = DriverManager::getConnection(array( * 'wrapperClass' => 'Doctrine\DBAL\Connections\PrimaryReadReplicaConnection', * 'driver' => 'pdo_mysql', * 'primary' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''), * 'replica' => array( * array('user' => 'replica1', 'password' => '', 'host' => '', 'dbname' => ''), * array('user' => 'replica2', 'password' => '', 'host' => '', 'dbname' => ''), * ) * )); * * You can also pass 'driverOptions' and any other documented option to each of this drivers * to pass additional information. */ class PrimaryReadReplicaConnection extends Connection { /** * Primary and Replica connection (one of the randomly picked replicas). * * @var array */ protected array $connections = ['primary' => null, 'replica' => null]; /** * You can keep the replica connection and then switch back to it * during the request if you know what you are doing. */ protected bool $keepReplica = false; /** * Creates Primary Replica Connection. * * @internal The connection can be only instantiated by the driver manager. * * @param array $params * @psalm-param Params $params */ public function __construct(array $params, Driver $driver, ?Configuration $config = null) { if (! isset($params['replica'], $params['primary'])) { throw new InvalidArgumentException('primary or replica configuration missing'); } if (count($params['replica']) === 0) { throw new InvalidArgumentException('You have to configure at least one replica.'); } if (isset($params['driver'])) { $params['primary']['driver'] = $params['driver']; foreach ($params['replica'] as $replicaKey => $replica) { $params['replica'][$replicaKey]['driver'] = $params['driver']; } } $this->keepReplica = ! empty($params['keepReplica']); parent::__construct($params, $driver, $config); } /** * Checks if the connection is currently towards the primary or not. */ public function isConnectedToPrimary(): bool { return $this->_conn !== null && $this->_conn === $this->connections['primary']; } public function connect(?string $connectionName = null): DriverConnection { if ($connectionName !== null) { throw new InvalidArgumentException( 'Passing a connection name as first argument is not supported anymore.' . ' Use ensureConnectedToPrimary()/ensureConnectedToReplica() instead.', ); } return $this->performConnect(); } protected function performConnect(?string $connectionName = null): DriverConnection { $requestedConnectionChange = ($connectionName !== null); $connectionName ??= 'replica'; if ($connectionName !== 'replica' && $connectionName !== 'primary') { throw new InvalidArgumentException('Invalid option to connect(), only primary or replica allowed.'); } // If we have a connection open, and this is not an explicit connection // change request, then abort right here, because we are already done. // This prevents writes to the replica in case of "keepReplica" option enabled. if ($this->_conn !== null && ! $requestedConnectionChange) { return $this->_conn; } $forcePrimaryAsReplica = false; if ($this->getTransactionNestingLevel() > 0) { $connectionName = 'primary'; $forcePrimaryAsReplica = true; } if (isset($this->connections[$connectionName])) { $this->_conn = $this->connections[$connectionName]; if ($forcePrimaryAsReplica && ! $this->keepReplica) { $this->connections['replica'] = $this->_conn; } return $this->_conn; } if ($connectionName === 'primary') { $this->connections['primary'] = $this->_conn = $this->connectTo($connectionName); // Set replica connection to primary to avoid invalid reads if (! $this->keepReplica) { $this->connections['replica'] = $this->connections['primary']; } } else { $this->connections['replica'] = $this->_conn = $this->connectTo($connectionName); } return $this->_conn; } /** * Connects to the primary node of the database cluster. * * All following statements after this will be executed against the primary node. */ public function ensureConnectedToPrimary(): void { $this->performConnect('primary'); } /** * Connects to a replica node of the database cluster. * * All following statements after this will be executed against the replica node, * unless the keepReplica option is set to false and a primary connection * was already opened. */ public function ensureConnectedToReplica(): void { $this->performConnect('replica'); } /** * Connects to a specific connection. * * @throws Exception */ protected function connectTo(string $connectionName): DriverConnection { $params = $this->getParams(); assert(isset($params['primary'])); if ($connectionName === 'primary') { $connectionParams = $params['primary']; } else { assert(isset($params['replica'])); $connectionParams = $this->chooseReplicaConnectionParameters($params['primary'], $params['replica']); } try { return $this->driver->connect($connectionParams); } catch (DriverException $e) { throw $this->convertException($e); } } /** * @param OverrideParams $primary * @param array $replicas * * @return array * @psalm-return OverrideParams */ protected function chooseReplicaConnectionParameters( #[SensitiveParameter] array $primary, #[SensitiveParameter] array $replicas, ): array { $params = $replicas[array_rand($replicas)]; if (! isset($params['charset']) && isset($primary['charset'])) { $params['charset'] = $primary['charset']; } return $params; } /** * {@inheritDoc} */ public function executeStatement(string $sql, array $params = [], array $types = []): int|string { $this->ensureConnectedToPrimary(); return parent::executeStatement($sql, $params, $types); } public function beginTransaction(): void { $this->ensureConnectedToPrimary(); parent::beginTransaction(); } public function commit(): void { $this->ensureConnectedToPrimary(); parent::commit(); } public function rollBack(): void { $this->ensureConnectedToPrimary(); parent::rollBack(); } public function close(): void { unset($this->connections['primary'], $this->connections['replica']); parent::close(); $this->_conn = null; $this->connections = ['primary' => null, 'replica' => null]; } public function createSavepoint(string $savepoint): void { $this->ensureConnectedToPrimary(); parent::createSavepoint($savepoint); } public function releaseSavepoint(string $savepoint): void { $this->ensureConnectedToPrimary(); parent::releaseSavepoint($savepoint); } public function rollbackSavepoint(string $savepoint): void { $this->ensureConnectedToPrimary(); parent::rollbackSavepoint($savepoint); } public function prepare(string $sql): Statement { $this->ensureConnectedToPrimary(); return parent::prepare($sql); } } src/Driver.php000064400000002465152062402520007304 0ustar00 $params All connection parameters. * @psalm-param Params $params All connection parameters. * * @return DriverConnection The database connection. * * @throws Exception */ public function connect( #[SensitiveParameter] array $params, ): DriverConnection; /** * Gets the DatabasePlatform instance that provides all the metadata about * the platform this driver connects to. * * @return AbstractPlatform The database platform. */ public function getDatabasePlatform(ServerVersionProvider $versionProvider): AbstractPlatform; /** * Gets the ExceptionConverter that can be used to convert driver-level exceptions into DBAL exceptions. */ public function getExceptionConverter(): ExceptionConverter; } src/Driver/API/ExceptionConverter.php000064400000001516152062402520013537 0ustar00getCode()) { -104 => new SyntaxErrorException($exception, $query), -203 => new NonUniqueFieldNameException($exception, $query), -204 => new TableNotFoundException($exception, $query), -206 => new InvalidFieldNameException($exception, $query), -407 => new NotNullConstraintViolationException($exception, $query), -530, -531, -532, -20356 => new ForeignKeyConstraintViolationException($exception, $query), -601 => new TableExistsException($exception, $query), -803 => new UniqueConstraintViolationException($exception, $query), -1336, -30082 => new ConnectionException($exception, $query), default => new DriverException($exception, $query), }; } } src/Driver/API/MySQL/ExceptionConverter.php000064400000006076152062402520014512 0ustar00getCode()) { 1008 => new DatabaseDoesNotExist($exception, $query), 1213 => new DeadlockException($exception, $query), 1205 => new LockWaitTimeoutException($exception, $query), 1050 => new TableExistsException($exception, $query), 1051, 1146 => new TableNotFoundException($exception, $query), 1216, 1217, 1451, 1452, 1701 => new ForeignKeyConstraintViolationException($exception, $query), 1062, 1557, 1569, 1586 => new UniqueConstraintViolationException($exception, $query), 1054, 1166, 1611 => new InvalidFieldNameException($exception, $query), 1052, 1060, 1110 => new NonUniqueFieldNameException($exception, $query), 1064, 1149, 1287, 1341, 1342, 1343, 1344, 1382, 1479, 1541, 1554, 1626 => new SyntaxErrorException($exception, $query), 1044, 1045, 1046, 1049, 1095, 1142, 1143, 1227, 1370, 1429, 2002, 2005, 2054 => new ConnectionException($exception, $query), 2006 => new ConnectionLost($exception, $query), 1048, 1121, 1138, 1171, 1252, 1263, 1364, 1566 => new NotNullConstraintViolationException($exception, $query), default => new DriverException($exception, $query), }; } } src/Driver/API/OCI/ExceptionConverter.php000064400000004214152062402520014147 0ustar00getCode()) { 1, 2299, 38911 => new UniqueConstraintViolationException($exception, $query), 904 => new InvalidFieldNameException($exception, $query), 918, 960 => new NonUniqueFieldNameException($exception, $query), 923 => new SyntaxErrorException($exception, $query), 942 => new TableNotFoundException($exception, $query), 955 => new TableExistsException($exception, $query), 1017, 12545 => new ConnectionException($exception, $query), 1400 => new NotNullConstraintViolationException($exception, $query), 1918 => new DatabaseDoesNotExist($exception, $query), 2289, 2443, 4080 => new DatabaseObjectNotFoundException($exception, $query), 2266, 2291, 2292 => new ForeignKeyConstraintViolationException($exception, $query), default => new DriverException($exception, $query), }; } } src/Driver/API/PostgreSQL/ExceptionConverter.php000064400000005606152062402520015546 0ustar00getSQLState()) { case '40001': case '40P01': return new DeadlockException($exception, $query); case '0A000': // Foreign key constraint violations during a TRUNCATE operation // are considered "feature not supported" in PostgreSQL. if (str_contains($exception->getMessage(), 'truncate')) { return new ForeignKeyConstraintViolationException($exception, $query); } break; case '23502': return new NotNullConstraintViolationException($exception, $query); case '23503': return new ForeignKeyConstraintViolationException($exception, $query); case '23505': return new UniqueConstraintViolationException($exception, $query); case '3D000': return new DatabaseDoesNotExist($exception, $query); case '3F000': return new SchemaDoesNotExist($exception, $query); case '42601': return new SyntaxErrorException($exception, $query); case '42702': return new NonUniqueFieldNameException($exception, $query); case '42703': return new InvalidFieldNameException($exception, $query); case '42P01': return new TableNotFoundException($exception, $query); case '42P07': return new TableExistsException($exception, $query); case '08006': return new ConnectionException($exception, $query); } return new DriverException($exception, $query); } } src/Driver/API/SQLSrv/ExceptionConverter.php000064400000004020152062402520014662 0ustar00getCode()) { 102 => new SyntaxErrorException($exception, $query), 207 => new InvalidFieldNameException($exception, $query), 208 => new TableNotFoundException($exception, $query), 209 => new NonUniqueFieldNameException($exception, $query), 515 => new NotNullConstraintViolationException($exception, $query), 547, 4712 => new ForeignKeyConstraintViolationException($exception, $query), 2601, 2627 => new UniqueConstraintViolationException($exception, $query), 2714 => new TableExistsException($exception, $query), 3701, 15151 => new DatabaseObjectNotFoundException($exception, $query), 11001, 18456 => new ConnectionException($exception, $query), default => new DriverException($exception, $query), }; } } src/Driver/API/SQLite/ExceptionConverter.php000064400000006356152062402520014707 0ustar00getMessage(), 'database is locked')) { return new LockWaitTimeoutException($exception, $query); } if ( str_contains($exception->getMessage(), 'must be unique') || str_contains($exception->getMessage(), 'is not unique') || str_contains($exception->getMessage(), 'are not unique') || str_contains($exception->getMessage(), 'UNIQUE constraint failed') ) { return new UniqueConstraintViolationException($exception, $query); } if ( str_contains($exception->getMessage(), 'may not be NULL') || str_contains($exception->getMessage(), 'NOT NULL constraint failed') ) { return new NotNullConstraintViolationException($exception, $query); } if (str_contains($exception->getMessage(), 'no such table:')) { return new TableNotFoundException($exception, $query); } if (str_contains($exception->getMessage(), 'already exists')) { return new TableExistsException($exception, $query); } if (str_contains($exception->getMessage(), 'has no column named')) { return new InvalidFieldNameException($exception, $query); } if (str_contains($exception->getMessage(), 'ambiguous column name')) { return new NonUniqueFieldNameException($exception, $query); } if (str_contains($exception->getMessage(), 'syntax error')) { return new SyntaxErrorException($exception, $query); } if (str_contains($exception->getMessage(), 'attempt to write a readonly database')) { return new ReadOnlyException($exception, $query); } if (str_contains($exception->getMessage(), 'unable to open database file')) { return new ConnectionException($exception, $query); } if (str_contains($exception->getMessage(), 'FOREIGN KEY constraint failed')) { return new ForeignKeyConstraintViolationException($exception, $query); } return new DriverException($exception, $query); } } src/Driver/AbstractDB2Driver.php000064400000001332152062402520012503 0ustar00sqlState; } } src/Driver/AbstractMySQLDriver.php000064400000005277152062402520013115 0ustar00getServerVersion(); if (stripos($version, 'mariadb') !== false) { $mariaDbVersion = $this->getMariaDbMysqlVersionNumber($version); if (version_compare($mariaDbVersion, '10.6.0', '>=')) { return new MariaDB1060Platform(); } if (version_compare($mariaDbVersion, '10.5.2', '>=')) { return new MariaDB1052Platform(); } return new MariaDBPlatform(); } if (version_compare($version, '8.0.0', '>=')) { return new MySQL80Platform(); } return new MySQLPlatform(); } public function getExceptionConverter(): ExceptionConverterInterface { return new ExceptionConverter(); } /** * Detect MariaDB server version, including hack for some mariadb distributions * that starts with the prefix '5.5.5-' * * @param string $versionString Version string as returned by mariadb server, i.e. '5.5.5-Mariadb-10.0.8-xenial' * * @throws InvalidPlatformVersion */ private function getMariaDbMysqlVersionNumber(string $versionString): string { if ( preg_match( '/^(?:5\.5\.5-)?(mariadb-)?(?P\d+)\.(?P\d+)\.(?P\d+)/i', $versionString, $versionParts, ) === 0 ) { throw InvalidPlatformVersion::new( $versionString, '^(?:5\.5\.5-)?(mariadb-)?..', ); } return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch']; } } src/Driver/AbstractOracleDriver.php000064400000002220152062402520013336 0ustar00 $params The connection parameters to return the Easy Connect String for. */ protected function getEasyConnectString(array $params): string { return (string) EasyConnectString::fromConnectionParameters($params); } } src/Driver/AbstractOracleDriver/EasyConnectString.php000064400000005162152062402520016750 0ustar00string; } /** * Creates the object from an array representation * * @param mixed[] $params */ public static function fromArray(array $params): self { return new self(self::renderParams($params)); } /** * Creates the object from the given DBAL connection parameters. * * @param mixed[] $params */ public static function fromConnectionParameters(array $params): self { if (isset($params['connectstring'])) { return new self($params['connectstring']); } if (! isset($params['host'])) { return new self($params['dbname'] ?? ''); } $connectData = []; if (isset($params['servicename']) || isset($params['dbname'])) { $serviceKey = 'SID'; if (isset($params['service'])) { $serviceKey = 'SERVICE_NAME'; } $serviceName = $params['servicename'] ?? $params['dbname']; $connectData[$serviceKey] = $serviceName; } if (isset($params['instancename'])) { $connectData['INSTANCE_NAME'] = $params['instancename']; } if (! empty($params['pooled'])) { $connectData['SERVER'] = 'POOLED'; } return self::fromArray([ 'DESCRIPTION' => [ 'ADDRESS' => [ 'PROTOCOL' => 'TCP', 'HOST' => $params['host'], 'PORT' => $params['port'] ?? 1521, ], 'CONNECT_DATA' => $connectData, ], ]); } /** @param mixed[] $params */ private static function renderParams(array $params): string { $chunks = []; foreach ($params as $key => $value) { $string = self::renderValue($value); if ($string === '') { continue; } $chunks[] = sprintf('(%s=%s)', $key, $string); } return implode('', $chunks); } private static function renderValue(mixed $value): string { if (is_array($value)) { return self::renderParams($value); } return (string) $value; } } src/Driver/AbstractPostgreSQLDriver.php000064400000001375152062402520014146 0ustar00exec('PRAGMA foreign_keys=ON'); return $connection; } }; } } src/Driver/Connection.php000064400000005061152062402520011376 0ustar00fetchNumeric(); if ($row === false) { return false; } return $row[0]; } /** * @return list> * * @throws Exception */ public static function fetchAllNumeric(Result $result): array { $rows = []; while (($row = $result->fetchNumeric()) !== false) { $rows[] = $row; } return $rows; } /** * @return list> * * @throws Exception */ public static function fetchAllAssociative(Result $result): array { $rows = []; while (($row = $result->fetchAssociative()) !== false) { $rows[] = $row; } return $rows; } /** * @return list * * @throws Exception */ public static function fetchFirstColumn(Result $result): array { $rows = []; while (($row = $result->fetchOne()) !== false) { $rows[] = $row; } return $rows; } } src/Driver/IBMDB2/Connection.php000064400000006237152062402520012303 0ustar00connection); assert($serverInfo instanceof stdClass); return $serverInfo->DBMS_VER; } public function prepare(string $sql): Statement { $stmt = @db2_prepare($this->connection, $sql); if ($stmt === false) { throw PrepareFailed::new(error_get_last()); } return new Statement($stmt); } public function query(string $sql): Result { return $this->prepare($sql)->execute(); } public function quote(string $value): string { return "'" . db2_escape_string($value) . "'"; } public function exec(string $sql): int|string { $stmt = @db2_exec($this->connection, $sql); if ($stmt === false) { throw StatementError::new(); } $numRows = db2_num_rows($stmt); if ($numRows === false) { throw StatementError::new(); } return $numRows; } public function lastInsertId(): string { $lastInsertId = db2_last_insert_id($this->connection); if ($lastInsertId === null) { throw NoIdentityValue::new(); } return $lastInsertId; } public function beginTransaction(): void { if (db2_autocommit($this->connection, DB2_AUTOCOMMIT_OFF) !== true) { throw ConnectionError::new($this->connection); } } public function commit(): void { if (! db2_commit($this->connection)) { throw ConnectionError::new($this->connection); } if (db2_autocommit($this->connection, DB2_AUTOCOMMIT_ON) !== true) { throw ConnectionError::new($this->connection); } } public function rollBack(): void { if (! db2_rollback($this->connection)) { throw ConnectionError::new($this->connection); } if (db2_autocommit($this->connection, DB2_AUTOCOMMIT_ON) !== true) { throw ConnectionError::new($this->connection); } } /** @return resource */ public function getNativeConnection() { return $this->connection; } } src/Driver/IBMDB2/DataSourceName.php000064400000003361152062402520013032 0ustar00string; } /** * Creates the object from an array representation * * @param array $params */ public static function fromArray( #[SensitiveParameter] array $params, ): self { $chunks = []; foreach ($params as $key => $value) { $chunks[] = sprintf('%s=%s', $key, $value); } return new self(implode(';', $chunks)); } /** * Creates the object from the given DBAL connection parameters. * * @param array $params */ public static function fromConnectionParameters(#[SensitiveParameter] array $params,): self { if (isset($params['dbname']) && str_contains($params['dbname'], '=')) { return new self($params['dbname']); } $dsnParams = []; foreach ( [ 'host' => 'HOSTNAME', 'port' => 'PORT', 'protocol' => 'PROTOCOL', 'dbname' => 'DATABASE', 'user' => 'UID', 'password' => 'PWD', ] as $dbalParam => $dsnParam ) { if (! isset($params[$dbalParam])) { continue; } $dsnParams[$dsnParam] = $params[$dbalParam]; } return self::fromArray($dsnParams); } } src/Driver/IBMDB2/Driver.php000064400000002110152062402520011421 0ustar00toString(); $username = $params['user'] ?? ''; $password = $params['password'] ?? ''; $driverOptions = $params['driverOptions'] ?? []; if (! empty($params['persistent'])) { $connection = db2_pconnect($dataSourceName, $username, $password, $driverOptions); } else { $connection = db2_connect($dataSourceName, $username, $password, $driverOptions); } if ($connection === false) { throw ConnectionFailed::new(); } return new Connection($connection); } } src/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php000064400000001064152062402520017043 0ustar00statement); if ($row === false && db2_stmt_error($this->statement) !== '02000') { throw StatementError::new($this->statement); } return $row; } public function fetchAssociative(): array|false { $row = @db2_fetch_assoc($this->statement); if ($row === false && db2_stmt_error($this->statement) !== '02000') { throw StatementError::new($this->statement); } return $row; } public function fetchOne(): mixed { return FetchUtils::fetchOne($this); } /** * {@inheritDoc} */ public function fetchAllNumeric(): array { return FetchUtils::fetchAllNumeric($this); } /** * {@inheritDoc} */ public function fetchAllAssociative(): array { return FetchUtils::fetchAllAssociative($this); } /** * {@inheritDoc} */ public function fetchFirstColumn(): array { return FetchUtils::fetchFirstColumn($this); } public function rowCount(): int { $numRows = @db2_num_rows($this->statement); if ($numRows === false) { throw StatementError::new($this->statement); } return $numRows; } public function columnCount(): int { $count = db2_num_fields($this->statement); if ($count !== false) { return $count; } return 0; } public function free(): void { db2_free_result($this->statement); } } src/Driver/IBMDB2/Statement.php000064400000007602152062402520012145 0ustar00 */ private array $lobs = []; /** * @internal The statement can be only instantiated by its driver connection. * * @param resource $stmt */ public function __construct(private readonly mixed $stmt) { } public function bindValue(int|string $param, mixed $value, ParameterType $type): void { assert(is_int($param)); switch ($type) { case ParameterType::INTEGER: $this->bind($param, $value, DB2_PARAM_IN, DB2_LONG); break; case ParameterType::LARGE_OBJECT: $this->lobs[$param] = &$value; break; default: $this->bind($param, $value, DB2_PARAM_IN, DB2_CHAR); break; } } /** @throws Exception */ private function bind(int $position, mixed &$variable, int $parameterType, int $dataType): void { $this->parameters[$position] =& $variable; if (! db2_bind_param($this->stmt, $position, '', $parameterType, $dataType)) { throw StatementError::new($this->stmt); } } public function execute(): Result { $handles = $this->bindLobs(); $result = @db2_execute($this->stmt, $this->parameters); foreach ($handles as $handle) { fclose($handle); } $this->lobs = []; if ($result === false) { throw StatementError::new($this->stmt); } return new Result($this->stmt); } /** * @return list * * @throws Exception */ private function bindLobs(): array { $handles = []; foreach ($this->lobs as $param => $value) { if (is_resource($value)) { $handle = $handles[] = $this->createTemporaryFile(); $path = stream_get_meta_data($handle)['uri']; $this->copyStreamToStream($value, $handle); $this->bind($param, $path, DB2_PARAM_FILE, DB2_BINARY); } else { $this->bind($param, $value, DB2_PARAM_IN, DB2_CHAR); } unset($value); } return $handles; } /** * @return resource * * @throws Exception */ private function createTemporaryFile() { $handle = @tmpfile(); if ($handle === false) { throw CannotCreateTemporaryFile::new(error_get_last()); } return $handle; } /** * @param resource $source * @param resource $target * * @throws Exception */ private function copyStreamToStream($source, $target): void { if (@stream_copy_to_stream($source, $target) === false) { throw CannotCopyStreamToStream::new(error_get_last()); } } } src/Driver/Middleware.php000064400000000250152062402520011347 0ustar00wrappedConnection->prepare($sql); } public function query(string $sql): Result { return $this->wrappedConnection->query($sql); } public function quote(string $value): string { return $this->wrappedConnection->quote($value); } public function exec(string $sql): int|string { return $this->wrappedConnection->exec($sql); } public function lastInsertId(): int|string { return $this->wrappedConnection->lastInsertId(); } public function beginTransaction(): void { $this->wrappedConnection->beginTransaction(); } public function commit(): void { $this->wrappedConnection->commit(); } public function rollBack(): void { $this->wrappedConnection->rollBack(); } public function getServerVersion(): string { return $this->wrappedConnection->getServerVersion(); } /** * {@inheritDoc} */ public function getNativeConnection() { return $this->wrappedConnection->getNativeConnection(); } } src/Driver/Middleware/AbstractDriverMiddleware.php000064400000001750152062402520016272 0ustar00wrappedDriver->connect($params); } public function getDatabasePlatform(ServerVersionProvider $versionProvider): AbstractPlatform { return $this->wrappedDriver->getDatabasePlatform($versionProvider); } public function getExceptionConverter(): ExceptionConverter { return $this->wrappedDriver->getExceptionConverter(); } } src/Driver/Middleware/AbstractResultMiddleware.php000064400000002505152062402520016314 0ustar00wrappedResult->fetchNumeric(); } public function fetchAssociative(): array|false { return $this->wrappedResult->fetchAssociative(); } public function fetchOne(): mixed { 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|string { return $this->wrappedResult->rowCount(); } public function columnCount(): int { return $this->wrappedResult->columnCount(); } public function free(): void { $this->wrappedResult->free(); } } src/Driver/Middleware/AbstractStatementMiddleware.php000064400000001142152062402520016776 0ustar00wrappedStatement->bindValue($param, $value, $type); } public function execute(): Result { return $this->wrappedStatement->execute(); } } src/Driver/Mysqli/Connection.php000064400000005267152062402520012664 0ustar00connection->get_server_info(); } public function prepare(string $sql): Statement { try { $stmt = $this->connection->prepare($sql); } catch (mysqli_sql_exception $e) { throw ConnectionError::upcast($e); } if ($stmt === false) { throw ConnectionError::new($this->connection); } return new Statement($stmt); } public function query(string $sql): Result { return $this->prepare($sql)->execute(); } public function quote(string $value): string { return "'" . $this->connection->escape_string($value) . "'"; } public function exec(string $sql): int|string { try { $result = $this->connection->query($sql); } catch (mysqli_sql_exception $e) { throw ConnectionError::upcast($e); } if ($result === false) { throw ConnectionError::new($this->connection); } return $this->connection->affected_rows; } public function lastInsertId(): int|string { $lastInsertId = $this->connection->insert_id; if ($lastInsertId === 0) { throw Exception\NoIdentityValue::new(); } return $this->connection->insert_id; } public function beginTransaction(): void { $this->connection->begin_transaction(); } public function commit(): void { try { if (! $this->connection->commit()) { throw ConnectionError::new($this->connection); } } catch (mysqli_sql_exception $e) { throw ConnectionError::upcast($e); } } public function rollBack(): void { try { if (! $this->connection->rollback()) { throw ConnectionError::new($this->connection); } } catch (mysqli_sql_exception $e) { throw ConnectionError::upcast($e); } } public function getNativeConnection(): mysqli { return $this->connection; } } src/Driver/Mysqli/Driver.php000064400000006205152062402520012011 0ustar00compilePreInitializers($params) as $initializer) { $initializer->initialize($connection); } try { $success = @$connection->real_connect( $host, $params['user'] ?? '', $params['password'] ?? '', $params['dbname'] ?? '', $params['port'] ?? 0, $params['unix_socket'] ?? '', $params['driverOptions'][Connection::OPTION_FLAGS] ?? 0, ); } catch (mysqli_sql_exception $e) { throw ConnectionFailed::upcast($e); } if (! $success) { throw ConnectionFailed::new($connection); } foreach ($this->compilePostInitializers($params) as $initializer) { $initializer->initialize($connection); } return new Connection($connection); } /** * @param array $params * * @return Generator */ private function compilePreInitializers( #[SensitiveParameter] array $params, ): Generator { unset($params['driverOptions'][Connection::OPTION_FLAGS]); if (isset($params['driverOptions']) && $params['driverOptions'] !== []) { yield new Options($params['driverOptions']); } if ( ! isset($params['ssl_key']) && ! isset($params['ssl_cert']) && ! isset($params['ssl_ca']) && ! isset($params['ssl_capath']) && ! isset($params['ssl_cipher']) ) { return; } yield new Secure( $params['ssl_key'] ?? '', $params['ssl_cert'] ?? '', $params['ssl_ca'] ?? '', $params['ssl_capath'] ?? '', $params['ssl_cipher'] ?? '', ); } /** * @param array $params * * @return Generator */ private function compilePostInitializers( #[SensitiveParameter] array $params, ): Generator { if (! isset($params['charset'])) { return; } yield new Charset($params['charset']); } } src/Driver/Mysqli/Exception/ConnectionError.php000064400000001326152062402520015624 0ustar00error, $connection->sqlstate, $connection->errno); } public static function upcast(mysqli_sql_exception $exception): self { $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate'); return new self($exception->getMessage(), $p->getValue($exception), $exception->getCode(), $exception); } } src/Driver/Mysqli/Exception/ConnectionFailed.php000064400000001452152062402520015717 0ustar00connect_error; assert($error !== null); return new self($error, 'HY000', $connection->connect_errno); } public static function upcast(mysqli_sql_exception $exception): self { $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate'); return new self($exception->getMessage(), $p->getValue($exception), $exception->getCode(), $exception); } } src/Driver/Mysqli/Exception/FailedReadingStreamOffset.php000064400000000657152062402520017522 0ustar00error), $connection->sqlstate, $connection->errno, ); } public static function upcast(mysqli_sql_exception $exception, string $charset): self { $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate'); return new self( sprintf('Failed to set charset "%s": %s', $charset, $exception->getMessage()), $p->getValue($exception), $exception->getCode(), $exception, ); } } src/Driver/Mysqli/Exception/InvalidOption.php000064400000000703152062402520015270 0ustar00error, $statement->sqlstate, $statement->errno); } public static function upcast(mysqli_sql_exception $exception): self { $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate'); return new self($exception->getMessage(), $p->getValue($exception), $exception->getCode(), $exception); } } src/Driver/Mysqli/Initializer.php000064400000000353152062402520013037 0ustar00set_charset($this->charset); } catch (mysqli_sql_exception $e) { throw InvalidCharset::upcast($e, $this->charset); } if ($success) { return; } throw InvalidCharset::fromCharset($connection, $this->charset); } } src/Driver/Mysqli/Initializer/Options.php000064400000001250152062402520014467 0ustar00 $options */ public function __construct(private readonly array $options) { } public function initialize(mysqli $connection): void { foreach ($this->options as $option => $value) { if (! mysqli_options($connection, $option, $value)) { throw InvalidOption::fromOption($option, $value); } } } } src/Driver/Mysqli/Initializer/Secure.php000064400000001214152062402520014262 0ustar00ssl_set($this->key, $this->cert, $this->ca, $this->capath, $this->cipher); } } src/Driver/Mysqli/Result.php000064400000011173152062402520012034 0ustar00 */ private readonly array $columnNames; /** @var mixed[] */ private array $boundValues = []; /** * @internal The result can be only instantiated by its driver connection or statement. * * @throws Exception */ public function __construct(private readonly mysqli_stmt $statement) { $meta = $statement->result_metadata(); $this->hasColumns = $meta !== false; $this->columnNames = $meta !== false ? array_column($meta->fetch_fields(), 'name') : []; if ($meta === false) { return; } $meta->free(); // Store result of every execution which has it. Otherwise it will be impossible // to execute a new statement in case if the previous one has non-fetched rows // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html $this->statement->store_result(); // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql, // it will have to allocate as much memory as it may be needed for the given column type // (e.g. for a LONGBLOB column it's 4 gigabytes) // @link https://bugs.php.net/bug.php?id=51386#1270673122 // // Make sure that the values are bound after each execution. Otherwise, if free() has been // previously called on the result, the values are unbound making the statement unusable. // // It's also important that row values are bound after _each_ call to store_result(). Otherwise, // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated // to the length of the ones fetched during the previous execution. $this->boundValues = array_fill(0, count($this->columnNames), null); // The following is necessary as PHP cannot handle references to properties properly $refs = &$this->boundValues; if (! $this->statement->bind_result(...$refs)) { throw StatementError::new($this->statement); } } public function fetchNumeric(): array|false { try { $ret = $this->statement->fetch(); } catch (mysqli_sql_exception $e) { throw StatementError::upcast($e); } if ($ret === false) { throw StatementError::new($this->statement); } if ($ret === null) { return false; } $values = []; foreach ($this->boundValues as $v) { $values[] = $v; } return $values; } public function fetchAssociative(): array|false { $values = $this->fetchNumeric(); if ($values === false) { return false; } return array_combine($this->columnNames, $values); } public function fetchOne(): mixed { return FetchUtils::fetchOne($this); } /** * {@inheritDoc} */ public function fetchAllNumeric(): array { return FetchUtils::fetchAllNumeric($this); } /** * {@inheritDoc} */ public function fetchAllAssociative(): array { return FetchUtils::fetchAllAssociative($this); } /** * {@inheritDoc} */ public function fetchFirstColumn(): array { return FetchUtils::fetchFirstColumn($this); } public function rowCount(): int|string { if ($this->hasColumns) { return $this->statement->num_rows; } return $this->statement->affected_rows; } public function columnCount(): int { return $this->statement->field_count; } public function free(): void { $this->statement->free_result(); } } src/Driver/Mysqli/Statement.php000064400000010677152062402520012532 0ustar00stmt->param_count; $this->types = str_repeat(self::PARAMETER_TYPE_STRING, $paramCount); $this->boundValues = array_fill(1, $paramCount, null); } public function bindValue(int|string $param, mixed $value, ParameterType $type): void { assert(is_int($param)); $this->types[$param - 1] = $this->convertParameterType($type); $this->values[$param] = $value; $this->boundValues[$param] =& $this->values[$param]; } public function execute(): Result { if (count($this->boundValues) > 0) { $this->bindParameters(); } try { if (! $this->stmt->execute()) { throw StatementError::new($this->stmt); } } catch (mysqli_sql_exception $e) { throw StatementError::upcast($e); } return new Result($this->stmt); } /** * Binds parameters with known types previously bound to the statement * * @throws Exception */ private function bindParameters(): void { $streams = $values = []; $types = $this->types; foreach ($this->boundValues as $parameter => $value) { assert(is_int($parameter)); if (! isset($types[$parameter - 1])) { $types[$parameter - 1] = self::PARAMETER_TYPE_STRING; } if ($types[$parameter - 1] === self::PARAMETER_TYPE_BINARY) { if (is_resource($value)) { if (get_resource_type($value) !== 'stream') { throw NonStreamResourceUsedAsLargeObject::new($parameter); } $streams[$parameter] = $value; $values[$parameter] = null; continue; } $types[$parameter - 1] = self::PARAMETER_TYPE_STRING; } $values[$parameter] = $value; } if (! $this->stmt->bind_param($types, ...$values)) { throw StatementError::new($this->stmt); } $this->sendLongData($streams); } /** * Handle $this->_longData after regular query parameters have been bound * * @param array $streams * * @throws Exception */ private function sendLongData(array $streams): void { foreach ($streams as $paramNr => $stream) { while (! feof($stream)) { $chunk = fread($stream, 8192); if ($chunk === false) { throw FailedReadingStreamOffset::new($paramNr); } if (! $this->stmt->send_long_data($paramNr - 1, $chunk)) { throw StatementError::new($this->stmt); } } } } private function convertParameterType(ParameterType $type): string { return match ($type) { ParameterType::NULL, ParameterType::STRING, ParameterType::ASCII, ParameterType::BINARY => self::PARAMETER_TYPE_STRING, ParameterType::INTEGER, ParameterType::BOOLEAN => self::PARAMETER_TYPE_INTEGER, ParameterType::LARGE_OBJECT => self::PARAMETER_TYPE_BINARY, }; } } src/Driver/OCI8/Connection.php000064400000005714152062402520012105 0ustar00parser = new Parser(false); $this->executionMode = new ExecutionMode(); } public function getServerVersion(): string { $version = oci_server_version($this->connection); assert($version !== false); $result = preg_match('/\s+(\d+\.\d+\.\d+\.\d+\.\d+)\s+/', $version, $matches); assert($result === 1); return $matches[1]; } /** @throws Parser\Exception */ public function prepare(string $sql): Statement { $visitor = new ConvertPositionalToNamedPlaceholders(); $this->parser->parse($sql, $visitor); $statement = oci_parse($this->connection, $visitor->getSQL()); assert(is_resource($statement)); return new Statement($this->connection, $statement, $visitor->getParameterMap(), $this->executionMode); } /** * @throws Exception * @throws Parser\Exception */ public function query(string $sql): Result { return $this->prepare($sql)->execute(); } public function quote(string $value): string { return "'" . addcslashes(str_replace("'", "''", $value), "\000\n\r\\\032") . "'"; } /** * @throws Exception * @throws Parser\Exception */ public function exec(string $sql): int|string { return $this->prepare($sql)->execute()->rowCount(); } public function lastInsertId(): int|string { throw IdentityColumnsNotSupported::new(); } public function beginTransaction(): void { $this->executionMode->disableAutoCommit(); } public function commit(): void { if (! oci_commit($this->connection)) { throw Error::new($this->connection); } $this->executionMode->enableAutoCommit(); } public function rollBack(): void { if (! oci_rollback($this->connection)) { throw Error::new($this->connection); } $this->executionMode->enableAutoCommit(); } /** @return resource */ public function getNativeConnection() { return $this->connection; } } src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php000064400000002474152062402520017266 0ustar00). * * Oracle does not support positional parameters, hence this method converts all * positional parameters into artificially named parameters. * * @internal This class is not covered by the backward compatibility promise */ final class ConvertPositionalToNamedPlaceholders implements Visitor { /** @var list */ private array $buffer = []; /** @var array */ private array $parameterMap = []; public function acceptOther(string $sql): void { $this->buffer[] = $sql; } public function acceptPositionalParameter(string $sql): void { $position = count($this->parameterMap) + 1; $param = ':param' . $position; $this->parameterMap[$position] = $param; $this->buffer[] = $param; } public function acceptNamedParameter(string $sql): void { $this->buffer[] = $sql; } public function getSQL(): string { return implode('', $this->buffer); } /** @return array */ public function getParameterMap(): array { return $this->parameterMap; } } src/Driver/OCI8/Driver.php000064400000003263152062402520011236 0ustar00getEasyConnectString($params); $persistent = ! empty($params['persistent']); $exclusive = ! empty($params['driverOptions']['exclusive']); if ($persistent && $exclusive) { throw InvalidConfiguration::forPersistentAndExclusive(); } if ($persistent) { $connection = @oci_pconnect($username, $password, $connectionString, $charset, $sessionMode); } elseif ($exclusive) { $connection = @oci_new_connect($username, $password, $connectionString, $charset, $sessionMode); } else { $connection = @oci_connect($username, $password, $connectionString, $charset, $sessionMode); } if ($connection === false) { throw ConnectionFailed::new(); } return new Connection($connection); } } src/Driver/OCI8/Exception/ConnectionFailed.php000064400000000711152062402520015140 0ustar00isAutoCommitEnabled = true; } public function disableAutoCommit(): void { $this->isAutoCommitEnabled = false; } public function isAutoCommitEnabled(): bool { return $this->isAutoCommitEnabled; } } src/Driver/OCI8/Middleware/InitializeSession.php000064400000002411152062402520015517 0ustar00exec( 'ALTER SESSION SET' . " NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'" . " NLS_TIME_FORMAT = 'HH24:MI:SS'" . " NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'" . " NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS'" . " NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS TZH:TZM'" . " NLS_NUMERIC_CHARACTERS = '.,'", ); return $connection; } }; } } src/Driver/OCI8/Result.php000064400000005352152062402520011262 0ustar00fetch(OCI_NUM); } public function fetchAssociative(): array|false { return $this->fetch(OCI_ASSOC); } public function fetchOne(): mixed { return FetchUtils::fetchOne($this); } /** * {@inheritDoc} */ public function fetchAllNumeric(): array { return $this->fetchAll(OCI_NUM, OCI_FETCHSTATEMENT_BY_ROW); } /** * {@inheritDoc} */ public function fetchAllAssociative(): array { return $this->fetchAll(OCI_ASSOC, OCI_FETCHSTATEMENT_BY_ROW); } /** * {@inheritDoc} */ public function fetchFirstColumn(): array { return $this->fetchAll(OCI_NUM, OCI_FETCHSTATEMENT_BY_COLUMN)[0]; } public function rowCount(): int { $count = oci_num_rows($this->statement); if ($count !== false) { return $count; } return 0; } public function columnCount(): int { $count = oci_num_fields($this->statement); if ($count !== false) { return $count; } return 0; } public function free(): void { oci_cancel($this->statement); } /** @throws Exception */ private function fetch(int $mode): mixed { $result = oci_fetch_array($this->statement, $mode | OCI_RETURN_NULLS | OCI_RETURN_LOBS); if ($result === false && oci_error($this->statement) !== false) { throw Error::new($this->statement); } return $result; } /** @return array */ private function fetchAll(int $mode, int $fetchStructure): array { oci_fetch_all( $this->statement, $result, 0, -1, $mode | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS, ); return $result; } } src/Driver/OCI8/Statement.php000064400000005331152062402520011745 0ustar00 $parameterMap */ public function __construct( private readonly mixed $connection, private readonly mixed $statement, private readonly array $parameterMap, private readonly ExecutionMode $executionMode, ) { } public function bindValue(int|string $param, mixed $value, ParameterType $type): void { if (is_int($param)) { if (! isset($this->parameterMap[$param])) { throw UnknownParameterIndex::new($param); } $param = $this->parameterMap[$param]; } if ($type === ParameterType::LARGE_OBJECT) { if ($value !== null) { $lob = oci_new_descriptor($this->connection, OCI_D_LOB); $lob->writeTemporary($value, OCI_TEMP_BLOB); $value =& $lob; } else { $type = ParameterType::STRING; } } if ( ! @oci_bind_by_name( $this->statement, $param, $value, -1, $this->convertParameterType($type), ) ) { throw Error::new($this->statement); } } /** * Converts DBAL parameter type to oci8 parameter type */ private function convertParameterType(ParameterType $type): int { return match ($type) { ParameterType::BINARY => OCI_B_BIN, ParameterType::LARGE_OBJECT => OCI_B_BLOB, default => SQLT_CHR, }; } public function execute(): Result { if ($this->executionMode->isAutoCommitEnabled()) { $mode = OCI_COMMIT_ON_SUCCESS; } else { $mode = OCI_NO_AUTO_COMMIT; } $ret = @oci_execute($this->statement, $mode); if (! $ret) { throw Error::new($this->statement); } return new Result($this->statement); } } src/Driver/PDO/Connection.php000064400000007264152062402520012027 0ustar00setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } public function exec(string $sql): int { try { $result = $this->connection->exec($sql); assert($result !== false); return $result; } catch (PDOException $exception) { throw Exception::new($exception); } } public function getServerVersion(): string { return $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION); } public function prepare(string $sql): Statement { try { $stmt = $this->connection->prepare($sql); assert($stmt instanceof PDOStatement); return new Statement($stmt); } catch (PDOException $exception) { throw Exception::new($exception); } } public function query(string $sql): Result { try { $stmt = $this->connection->query($sql); assert($stmt instanceof PDOStatement); return new Result($stmt); } catch (PDOException $exception) { throw Exception::new($exception); } } public function quote(string $value): string { return $this->connection->quote($value); } public function lastInsertId(): int|string { try { $value = $this->connection->lastInsertId(); } catch (PDOException $exception) { assert($exception->errorInfo !== null); [$sqlState] = $exception->errorInfo; // if the PDO driver does not support this capability, PDO::lastInsertId() triggers an IM001 SQLSTATE // see https://www.php.net/manual/en/pdo.lastinsertid.php if ($sqlState === 'IM001') { throw IdentityColumnsNotSupported::new(); } // PDO PGSQL throws a 'lastval is not yet defined in this session' error when no identity value is // available, with SQLSTATE 55000 'Object Not In Prerequisite State' if ($sqlState === '55000' && $this->connection->getAttribute(PDO::ATTR_DRIVER_NAME) === 'pgsql') { throw NoIdentityValue::new($exception); } throw Exception::new($exception); } // pdo_mysql & pdo_sqlite return '0', pdo_sqlsrv returns '' or false depending on the PHP version if ($value === '0' || $value === '' || $value === false) { throw NoIdentityValue::new(); } return $value; } public function beginTransaction(): void { try { $this->connection->beginTransaction(); } catch (PDOException $exception) { throw Exception::new($exception); } } public function commit(): void { try { $this->connection->commit(); } catch (PDOException $exception) { throw Exception::new($exception); } } public function rollBack(): void { try { $this->connection->rollBack(); } catch (PDOException $exception) { throw Exception::new($exception); } } public function getNativeConnection(): PDO { return $this->connection; } } src/Driver/PDO/Exception.php000064400000001151152062402520011653 0ustar00errorInfo !== null) { [$sqlState, $code] = $exception->errorInfo; $code ??= 0; } else { $code = $exception->getCode(); $sqlState = null; } return new self($exception->getMessage(), $sqlState, $code, $exception); } } src/Driver/PDO/MySQL/Driver.php000064400000003456152062402520012127 0ustar00constructPdoDsn($safeParams), $params['user'] ?? '', $params['password'] ?? '', $driverOptions, ); } catch (PDOException $exception) { throw Exception::new($exception); } return new Connection($pdo); } /** * Constructs the MySQL PDO DSN. * * @param mixed[] $params */ private function constructPdoDsn(array $params): string { $dsn = 'mysql:'; if (isset($params['host']) && $params['host'] !== '') { $dsn .= 'host=' . $params['host'] . ';'; } if (isset($params['port'])) { $dsn .= 'port=' . $params['port'] . ';'; } if (isset($params['dbname'])) { $dsn .= 'dbname=' . $params['dbname'] . ';'; } if (isset($params['unix_socket'])) { $dsn .= 'unix_socket=' . $params['unix_socket'] . ';'; } if (isset($params['charset'])) { $dsn .= 'charset=' . $params['charset'] . ';'; } return $dsn; } } src/Driver/PDO/OCI/Driver.php000064400000002606152062402520011570 0ustar00constructPdoDsn($params), $params['user'] ?? '', $params['password'] ?? '', $driverOptions, ); } catch (PDOException $exception) { throw Exception::new($exception); } return new Connection($pdo); } /** * Constructs the Oracle PDO DSN. * * @param mixed[] $params */ private function constructPdoDsn(array $params): string { $dsn = 'oci:dbname=' . $this->getEasyConnectString($params); if (isset($params['charset'])) { $dsn .= ';charset=' . $params['charset']; } return $dsn; } } src/Driver/PDO/PgSQL/Driver.php000064400000005610152062402520012102 0ustar00constructPdoDsn($safeParams), $params['user'] ?? '', $params['password'] ?? '', $driverOptions, ); } catch (PDOException $exception) { throw Exception::new($exception); } if ( ! isset($driverOptions[PDO::PGSQL_ATTR_DISABLE_PREPARES]) || $driverOptions[PDO::PGSQL_ATTR_DISABLE_PREPARES] === true ) { $pdo->setAttribute(PDO::PGSQL_ATTR_DISABLE_PREPARES, true); } $connection = new Connection($pdo); /* defining client_encoding via SET NAMES to avoid inconsistent DSN support * - passing client_encoding via the 'options' param breaks pgbouncer support */ if (isset($params['charset'])) { $connection->exec('SET NAMES \'' . $params['charset'] . '\''); } return $connection; } /** * Constructs the Postgres PDO DSN. * * @param array $params */ private function constructPdoDsn(array $params): string { $dsn = 'pgsql:'; if (isset($params['host']) && $params['host'] !== '') { $dsn .= 'host=' . $params['host'] . ';'; } if (isset($params['port']) && $params['port'] !== '') { $dsn .= 'port=' . $params['port'] . ';'; } if (isset($params['dbname'])) { $dsn .= 'dbname=' . $params['dbname'] . ';'; } if (isset($params['sslmode'])) { $dsn .= 'sslmode=' . $params['sslmode'] . ';'; } if (isset($params['sslrootcert'])) { $dsn .= 'sslrootcert=' . $params['sslrootcert'] . ';'; } if (isset($params['sslcert'])) { $dsn .= 'sslcert=' . $params['sslcert'] . ';'; } if (isset($params['sslkey'])) { $dsn .= 'sslkey=' . $params['sslkey'] . ';'; } if (isset($params['sslcrl'])) { $dsn .= 'sslcrl=' . $params['sslcrl'] . ';'; } if (isset($params['application_name'])) { $dsn .= 'application_name=' . $params['application_name'] . ';'; } return $dsn; } } src/Driver/PDO/Result.php000064400000004435152062402520011203 0ustar00fetch(PDO::FETCH_NUM); } public function fetchAssociative(): array|false { return $this->fetch(PDO::FETCH_ASSOC); } public function fetchOne(): mixed { return $this->fetch(PDO::FETCH_COLUMN); } /** * {@inheritDoc} */ public function fetchAllNumeric(): array { return $this->fetchAll(PDO::FETCH_NUM); } /** * {@inheritDoc} */ public function fetchAllAssociative(): array { return $this->fetchAll(PDO::FETCH_ASSOC); } /** * {@inheritDoc} */ public function fetchFirstColumn(): array { return $this->fetchAll(PDO::FETCH_COLUMN); } public function rowCount(): int { try { return $this->statement->rowCount(); } catch (PDOException $exception) { throw Exception::new($exception); } } public function columnCount(): int { try { return $this->statement->columnCount(); } catch (PDOException $exception) { throw Exception::new($exception); } } public function free(): void { $this->statement->closeCursor(); } /** * @psalm-param PDO::FETCH_* $mode * * @throws Exception */ private function fetch(int $mode): mixed { try { return $this->statement->fetch($mode); } catch (PDOException $exception) { throw Exception::new($exception); } } /** * @psalm-param PDO::FETCH_* $mode * * @return list * * @throws Exception */ private function fetchAll(int $mode): array { try { return $this->statement->fetchAll($mode); } catch (PDOException $exception) { throw Exception::new($exception); } } } src/Driver/PDO/SQLSrv/Connection.php000064400000001242152062402520013147 0ustar00connection->prepare($sql), ); } public function getNativeConnection(): PDO { return $this->connection->getNativeConnection(); } } src/Driver/PDO/SQLSrv/Driver.php000064400000005642152062402520012313 0ustar00 $value) { if (is_int($option)) { $driverOptions[$option] = $value; } else { $dsnOptions[$option] = $value; } } } if (! empty($params['persistent'])) { $driverOptions[PDO::ATTR_PERSISTENT] = true; } $safeParams = $params; unset($safeParams['password']); try { $pdo = new PDO( $this->constructDsn($safeParams, $dsnOptions), $params['user'] ?? '', $params['password'] ?? '', $driverOptions, ); } catch (\PDOException $exception) { throw PDOException::new($exception); } return new Connection(new PDOConnection($pdo)); } /** * Constructs the Sqlsrv PDO DSN. * * @param mixed[] $params * @param string[] $connectionOptions * * @throws Exception */ private function constructDsn(array $params, array $connectionOptions): string { $dsn = 'sqlsrv:server='; if (isset($params['host'])) { $dsn .= $params['host']; if (isset($params['port'])) { $dsn .= ',' . $params['port']; } } elseif (isset($params['port'])) { throw PortWithoutHost::new(); } if (isset($params['dbname'])) { $connectionOptions['Database'] = $params['dbname']; } if (isset($params['MultipleActiveResultSets'])) { $connectionOptions['MultipleActiveResultSets'] = $params['MultipleActiveResultSets'] ? 'true' : 'false'; } return $dsn . $this->getConnectionOptionsDsn($connectionOptions); } /** * Converts a connection options array to the DSN * * @param string[] $connectionOptions */ private function getConnectionOptionsDsn(array $connectionOptions): string { $connectionOptionsDsn = ''; foreach ($connectionOptions as $paramName => $paramValue) { $connectionOptionsDsn .= sprintf(';%s=%s', $paramName, $paramValue); } return $connectionOptionsDsn; } } src/Driver/PDO/SQLSrv/Statement.php000064400000002516152062402520013021 0ustar00statement->bindParamWithDriverOptions( $param, $value, $type, PDO::SQLSRV_ENCODING_BINARY, ); break; case ParameterType::ASCII: $this->statement->bindParamWithDriverOptions( $param, $value, ParameterType::STRING, PDO::SQLSRV_ENCODING_SYSTEM, ); break; default: $this->statement->bindValue($param, $value, $type); } } } src/Driver/PDO/SQLite/Driver.php000064400000002434152062402520012316 0ustar00constructPdoDsn(array_intersect_key($params, ['path' => true, 'memory' => true])), $params['user'] ?? '', $params['password'] ?? '', $params['driverOptions'] ?? [], ); } catch (PDOException $exception) { throw Exception::new($exception); } return new Connection($pdo); } /** * Constructs the Sqlite PDO DSN. * * @param array $params */ private function constructPdoDsn(array $params): string { $dsn = 'sqlite:'; if (isset($params['path'])) { $dsn .= $params['path']; } elseif (isset($params['memory'])) { $dsn .= ':memory:'; } return $dsn; } } src/Driver/PDO/Statement.php000064400000004232152062402520011664 0ustar00convertParamType($type); try { $this->stmt->bindValue($param, $value, $pdoType); } catch (PDOException $exception) { throw Exception::new($exception); } } /** * @internal Driver options can be only specified by a PDO-based driver. * * @throws ExceptionInterface */ public function bindParamWithDriverOptions( string|int $param, mixed &$variable, ParameterType $type, mixed $driverOptions, ): void { $pdoType = $this->convertParamType($type); try { $this->stmt->bindParam($param, $variable, $pdoType, 0, $driverOptions); } catch (PDOException $exception) { throw Exception::new($exception); } } public function execute(): Result { try { $this->stmt->execute(); } catch (PDOException $exception) { throw Exception::new($exception); } return new Result($this->stmt); } /** * Converts DBAL parameter type to PDO parameter type * * @psalm-return PDO::PARAM_* */ private function convertParamType(ParameterType $type): int { return match ($type) { ParameterType::NULL => PDO::PARAM_NULL, ParameterType::INTEGER => PDO::PARAM_INT, ParameterType::STRING, ParameterType::ASCII => PDO::PARAM_STR, ParameterType::BINARY, ParameterType::LARGE_OBJECT => PDO::PARAM_LOB, ParameterType::BOOLEAN => PDO::PARAM_BOOL, }; } } src/Driver/PgSQL/Connection.php000064400000006173152062402520012331 0ustar00parser = new Parser(false); } public function __destruct() { if (! isset($this->connection)) { return; } @pg_close($this->connection); } public function prepare(string $sql): Statement { $visitor = new ConvertParameters(); $this->parser->parse($sql, $visitor); $statementName = uniqid('dbal', true); if (@pg_send_prepare($this->connection, $statementName, $visitor->getSQL()) !== true) { throw new Exception(pg_last_error($this->connection)); } $result = @pg_get_result($this->connection); assert($result !== false); if ((bool) pg_result_error($result)) { throw Exception::fromResult($result); } return new Statement($this->connection, $statementName, $visitor->getParameterMap()); } public function query(string $sql): Result { if (@pg_send_query($this->connection, $sql) !== true) { throw new Exception(pg_last_error($this->connection)); } $result = @pg_get_result($this->connection); assert($result !== false); if ((bool) pg_result_error($result)) { throw Exception::fromResult($result); } return new Result($result); } /** {@inheritDoc} */ public function quote(string $value): string { $quotedValue = pg_escape_literal($this->connection, $value); assert($quotedValue !== false); return $quotedValue; } public function exec(string $sql): int { return $this->query($sql)->rowCount(); } /** {@inheritDoc} */ public function lastInsertId(): int|string { try { return $this->query('SELECT LASTVAL()')->fetchOne(); } catch (Exception $exception) { if ($exception->getSQLState() === '55000') { throw NoIdentityValue::new($exception); } throw $exception; } } public function beginTransaction(): void { $this->exec('BEGIN'); } public function commit(): void { $this->exec('COMMIT'); } public function rollBack(): void { $this->exec('ROLLBACK'); } public function getServerVersion(): string { return (string) pg_version($this->connection)['server']; } public function getNativeConnection(): PgSqlConnection { return $this->connection; } } src/Driver/PgSQL/ConvertParameters.php000064400000002223152062402520013666 0ustar00 */ private array $buffer = []; /** @var array */ private array $parameterMap = []; public function acceptPositionalParameter(string $sql): void { $position = count($this->parameterMap) + 1; $this->parameterMap[$position] = $position; $this->buffer[] = '$' . $position; } public function acceptNamedParameter(string $sql): void { $position = count($this->parameterMap) + 1; $this->parameterMap[$sql] = $position; $this->buffer[] = '$' . $position; } public function acceptOther(string $sql): void { $this->buffer[] = $sql; } public function getSQL(): string { return implode('', $this->buffer); } /** @return array */ public function getParameterMap(): array { return $this->parameterMap; } } src/Driver/PgSQL/Driver.php000064400000005050152062402520011456 0ustar00constructConnectionString($params), PGSQL_CONNECT_FORCE_NEW); } catch (ErrorException $e) { throw new Exception($e->getMessage(), '08006', 0, $e); } finally { restore_error_handler(); } if ($connection === false) { throw new Exception('Unable to connect to Postgres server.'); } $driverConnection = new Connection($connection); if (isset($params['application_name'])) { $driverConnection->exec('SET application_name = ' . $driverConnection->quote($params['application_name'])); } return $driverConnection; } /** * Constructs the Postgres connection string * * @param array $params */ private function constructConnectionString( #[SensitiveParameter] array $params, ): string { $components = array_filter( [ 'host' => $params['host'] ?? null, 'port' => $params['port'] ?? null, 'dbname' => $params['dbname'] ?? 'postgres', 'user' => $params['user'] ?? null, 'password' => $params['password'] ?? null, 'sslmode' => $params['sslmode'] ?? null, ], static fn (int|string|null $value) => $value !== '' && $value !== null, ); return implode(' ', array_map( static fn (int|string $value, string $key) => sprintf("%s='%s'", $key, addslashes((string) $value)), array_values($components), array_keys($components), )); } } src/Driver/PgSQL/Exception.php000064400000001254152062402520012163 0ustar00result = $result; } public function __destruct() { if (! isset($this->result)) { return; } $this->free(); } /** {@inheritDoc} */ public function fetchNumeric(): array|false { if ($this->result === null) { return false; } $row = pg_fetch_row($this->result); if ($row === false) { return false; } return $this->mapNumericRow($row, $this->fetchNumericColumnTypes()); } /** {@inheritDoc} */ public function fetchAssociative(): array|false { if ($this->result === null) { return false; } $row = pg_fetch_assoc($this->result); if ($row === false) { return false; } return $this->mapAssociativeRow($row, $this->fetchAssociativeColumnTypes()); } /** {@inheritDoc} */ public function fetchOne(): mixed { return FetchUtils::fetchOne($this); } /** {@inheritDoc} */ public function fetchAllNumeric(): array { if ($this->result === null) { return []; } $types = $this->fetchNumericColumnTypes(); return array_map( fn (array $row) => $this->mapNumericRow($row, $types), pg_fetch_all($this->result, PGSQL_NUM), ); } /** {@inheritDoc} */ public function fetchAllAssociative(): array { if ($this->result === null) { return []; } $types = $this->fetchAssociativeColumnTypes(); return array_map( fn (array $row) => $this->mapAssociativeRow($row, $types), pg_fetch_all($this->result, PGSQL_ASSOC), ); } /** {@inheritDoc} */ public function fetchFirstColumn(): array { if ($this->result === null) { return []; } $postgresType = pg_field_type($this->result, 0); return array_map( fn ($value) => $this->mapType($postgresType, $value), pg_fetch_all_columns($this->result), ); } public function rowCount(): int { if ($this->result === null) { return 0; } return pg_affected_rows($this->result); } public function columnCount(): int { if ($this->result === null) { return 0; } return pg_num_fields($this->result); } public function free(): void { if ($this->result === null) { return; } pg_free_result($this->result); $this->result = null; } /** @return array */ private function fetchNumericColumnTypes(): array { assert($this->result !== null); $types = []; $numFields = pg_num_fields($this->result); for ($i = 0; $i < $numFields; ++$i) { $types[$i] = pg_field_type($this->result, $i); } return $types; } /** @return array */ private function fetchAssociativeColumnTypes(): array { assert($this->result !== null); $types = []; $numFields = pg_num_fields($this->result); for ($i = 0; $i < $numFields; ++$i) { $types[pg_field_name($this->result, $i)] = pg_field_type($this->result, $i); } return $types; } /** * @param list $row * @param array $types * * @return list */ private function mapNumericRow(array $row, array $types): array { assert($this->result !== null); return array_map( fn ($value, $field) => $this->mapType($types[$field], $value), $row, array_keys($row), ); } /** * @param array $row * @param array $types * * @return array */ private function mapAssociativeRow(array $row, array $types): array { assert($this->result !== null); $mappedRow = []; foreach ($row as $field => $value) { $mappedRow[$field] = $this->mapType($types[$field], $value); } return $mappedRow; } private function mapType(string $postgresType, ?string $value): string|int|float|bool|null { if ($value === null) { return null; } return match ($postgresType) { 'bool' => match ($value) { 't' => true, 'f' => false, default => throw UnexpectedValue::new($value, $postgresType), }, 'bytea' => hex2bin(substr($value, 2)), 'float4', 'float8' => (float) $value, 'int2', 'int4' => (int) $value, 'int8' => PHP_INT_SIZE >= 8 ? (int) $value : $value, default => $value, }; } } src/Driver/PgSQL/Statement.php000064400000005167152062402520012200 0ustar00 */ private array $parameters = []; /** @psalm-var array */ private array $parameterTypes = []; /** @param array $parameterMap */ public function __construct( private readonly PgSqlConnection $connection, private readonly string $name, private readonly array $parameterMap, ) { } public function __destruct() { if (! isset($this->connection)) { return; } @pg_query( $this->connection, 'DEALLOCATE ' . pg_escape_identifier($this->connection, $this->name), ); } /** {@inheritDoc} */ public function bindValue(int|string $param, mixed $value, ParameterType $type = ParameterType::STRING): void { if (! isset($this->parameterMap[$param])) { throw UnknownParameter::new((string) $param); } $this->parameters[$this->parameterMap[$param]] = $value; $this->parameterTypes[$this->parameterMap[$param]] = $type; } /** {@inheritDoc} */ public function execute(): Result { ksort($this->parameters); $escapedParameters = []; foreach ($this->parameters as $parameter => $value) { $escapedParameters[] = match ($this->parameterTypes[$parameter]) { ParameterType::BINARY, ParameterType::LARGE_OBJECT => $value === null ? null : pg_escape_bytea($this->connection, is_resource($value) ? stream_get_contents($value) : $value), default => $value, }; } if (@pg_send_execute($this->connection, $this->name, $escapedParameters) !== true) { throw new Exception(pg_last_error($this->connection)); } $result = @pg_get_result($this->connection); assert($result !== false); if ((bool) pg_result_error($result)) { throw Exception::fromResult($result); } return new Result($result); } } src/Driver/Result.php000064400000005074152062402520010561 0ustar00|false * * @throws Exception */ public function fetchNumeric(): array|false; /** * Returns the next row of the result as an associative array or FALSE if there are no more rows. * * @return array|false * * @throws Exception */ public function fetchAssociative(): array|false; /** * Returns the first value of the next row of the result or FALSE if there are no more rows. * * @throws Exception */ public function fetchOne(): mixed; /** * Returns an array containing all of the result rows represented as numeric arrays. * * @return list> * * @throws Exception */ public function fetchAllNumeric(): array; /** * Returns an array containing all of the result rows represented as associative arrays. * * @return list> * * @throws Exception */ public function fetchAllAssociative(): array; /** * Returns an array containing the values of the first column of the result. * * @return list * * @throws Exception */ public function fetchFirstColumn(): array; /** * Returns the number of rows affected by the DELETE, INSERT, or UPDATE statement that produced the result. * * If the statement executed a SELECT query or a similar platform-specific SQL (e.g. DESCRIBE, SHOW, etc.), * some database drivers may return the number of rows returned by that query. However, this behaviour * is not guaranteed for all drivers and should not be relied on in portable applications. * * If the number of rows exceeds {@see PHP_INT_MAX}, it might be returned as string if the driver supports it. * * @return int|numeric-string * * @throws Exception */ public function rowCount(): int|string; /** * Returns the number of columns in the result * * @return int The number of columns in the result. If the columns cannot be counted, * this method must return 0. * * @throws Exception */ public function columnCount(): int; /** * Discards the non-fetched portion of the result, enabling the originating statement to be executed again. */ public function free(): void; } src/Driver/SQLSrv/Connection.php000064400000004604152062402520012532 0ustar00connection); return $serverInfo['SQLServerVersion']; } public function prepare(string $sql): Statement { return new Statement($this->connection, $sql); } public function query(string $sql): Result { return $this->prepare($sql)->execute(); } public function quote(string $value): string { return "'" . str_replace("'", "''", $value) . "'"; } public function exec(string $sql): int { $stmt = sqlsrv_query($this->connection, $sql); if ($stmt === false) { throw Error::new(); } $rowsAffected = sqlsrv_rows_affected($stmt); if ($rowsAffected === false) { throw Error::new(); } return $rowsAffected; } public function lastInsertId(): int|string { $result = $this->query('SELECT @@IDENTITY'); $lastInsertId = $result->fetchOne(); if ($lastInsertId === null) { throw NoIdentityValue::new(); } return $lastInsertId; } public function beginTransaction(): void { if (! sqlsrv_begin_transaction($this->connection)) { throw Error::new(); } } public function commit(): void { if (! sqlsrv_commit($this->connection)) { throw Error::new(); } } public function rollBack(): void { if (! sqlsrv_rollback($this->connection)) { throw Error::new(); } } /** @return resource */ public function getNativeConnection() { return $this->connection; } } src/Driver/SQLSrv/Driver.php000064400000003422152062402520011663 0ustar00fetch(SQLSRV_FETCH_NUMERIC); } public function fetchAssociative(): array|false { return $this->fetch(SQLSRV_FETCH_ASSOC); } public function fetchOne(): mixed { return FetchUtils::fetchOne($this); } /** * {@inheritDoc} */ public function fetchAllNumeric(): array { return FetchUtils::fetchAllNumeric($this); } /** * {@inheritDoc} */ public function fetchAllAssociative(): array { return FetchUtils::fetchAllAssociative($this); } /** * {@inheritDoc} */ public function fetchFirstColumn(): array { return FetchUtils::fetchFirstColumn($this); } public function rowCount(): int { $count = sqlsrv_rows_affected($this->statement); if ($count !== false) { return $count; } return 0; } public function columnCount(): int { $count = sqlsrv_num_fields($this->statement); if ($count !== false) { return $count; } return 0; } public function free(): void { // emulate it by fetching and discarding rows, similarly to what PDO does in this case // @link http://php.net/manual/en/pdostatement.closecursor.php // @link https://github.com/php/php-src/blob/php-7.0.11/ext/pdo/pdo_stmt.c#L2075 // deliberately do not consider multiple result sets, since doctrine/dbal doesn't support them while (sqlsrv_fetch($this->statement)) { } } private function fetch(int $fetchType): mixed { return sqlsrv_fetch_array($this->statement, $fetchType) ?? false; } } src/Driver/SQLSrv/Statement.php000064400000006654152062402520012406 0ustar00 */ private array $variables = []; /** * Bound parameter types. * * @var array */ private array $types = []; /** * Append to any INSERT query to retrieve the last insert id. */ private const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;'; /** * @internal The statement can be only instantiated by its driver connection. * * @param resource $conn */ public function __construct( private readonly mixed $conn, private string $sql, ) { if (stripos($sql, 'INSERT INTO ') !== 0) { return; } $this->sql .= self::LAST_INSERT_ID_SQL; } public function bindValue(int|string $param, mixed $value, ParameterType $type): void { assert(is_int($param)); $this->variables[$param] = $value; $this->types[$param] = $type; } public function execute(): Result { $this->stmt ??= $this->prepare(); if (! sqlsrv_execute($this->stmt)) { throw Error::new(); } return new Result($this->stmt); } /** * Prepares SQL Server statement resource * * @return resource * * @throws Exception */ private function prepare() { $params = []; foreach ($this->variables as $column => &$variable) { switch ($this->types[$column]) { case ParameterType::LARGE_OBJECT: $params[$column - 1] = [ &$variable, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY), SQLSRV_SQLTYPE_VARBINARY('max'), ]; break; case ParameterType::BINARY: $params[$column - 1] = [ &$variable, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY), ]; break; case ParameterType::ASCII: $params[$column - 1] = [ &$variable, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR), ]; break; default: $params[$column - 1] =& $variable; break; } } $stmt = sqlsrv_prepare($this->conn, $this->sql, $params); if ($stmt === false) { throw Error::new(); } return $stmt; } } src/Driver/SQLite3/Connection.php000064400000004646152062402520012632 0ustar00connection->prepare($sql); } catch (\Exception $e) { throw Exception::new($e); } assert($statement !== false); return new Statement($this->connection, $statement); } public function query(string $sql): Result { try { $result = $this->connection->query($sql); } catch (\Exception $e) { throw Exception::new($e); } assert($result !== false); return new Result($result, $this->connection->changes()); } public function quote(string $value): string { return sprintf('\'%s\'', SQLite3::escapeString($value)); } public function exec(string $sql): int { try { $this->connection->exec($sql); } catch (\Exception $e) { throw Exception::new($e); } return $this->connection->changes(); } public function lastInsertId(): int { $value = $this->connection->lastInsertRowID(); if ($value === 0) { throw NoIdentityValue::new(); } return $value; } public function beginTransaction(): void { try { $this->connection->exec('BEGIN'); } catch (\Exception $e) { throw Exception::new($e); } } public function commit(): void { try { $this->connection->exec('COMMIT'); } catch (\Exception $e) { throw Exception::new($e); } } public function rollBack(): void { try { $this->connection->exec('ROLLBACK'); } catch (\Exception $e) { throw Exception::new($e); } } public function getNativeConnection(): SQLite3 { return $this->connection; } public function getServerVersion(): string { return SQLite3::version()['versionString']; } } src/Driver/SQLite3/Driver.php000064400000002246152062402520011760 0ustar00enableExceptions(true); return new Connection($connection); } } src/Driver/SQLite3/Exception.php000064400000000601152062402520012454 0ustar00getMessage(), null, (int) $exception->getCode(), $exception); } } src/Driver/SQLite3/Result.php000064400000003506152062402520012003 0ustar00result = $result; } public function fetchNumeric(): array|false { if ($this->result === null) { return false; } return $this->result->fetchArray(SQLITE3_NUM); } public function fetchAssociative(): array|false { if ($this->result === null) { return false; } return $this->result->fetchArray(SQLITE3_ASSOC); } public function fetchOne(): mixed { return FetchUtils::fetchOne($this); } /** @inheritDoc */ public function fetchAllNumeric(): array { return FetchUtils::fetchAllNumeric($this); } /** @inheritDoc */ public function fetchAllAssociative(): array { return FetchUtils::fetchAllAssociative($this); } /** @inheritDoc */ public function fetchFirstColumn(): array { return FetchUtils::fetchFirstColumn($this); } public function rowCount(): int { return $this->changes; } public function columnCount(): int { if ($this->result === null) { return 0; } return $this->result->numColumns(); } public function free(): void { if ($this->result === null) { return; } $this->result->finalize(); $this->result = null; } } src/Driver/SQLite3/Statement.php000064400000003305152062402520012466 0ustar00statement->bindValue($param, $value, $this->convertParamType($type)); } public function execute(): Result { try { $result = $this->statement->execute(); } catch (\Exception $e) { throw Exception::new($e); } assert($result !== false); return new Result($result, $this->connection->changes()); } /** @psalm-return self::TYPE_* */ private function convertParamType(ParameterType $type): int { return match ($type) { ParameterType::NULL => self::TYPE_NULL, ParameterType::INTEGER, ParameterType::BOOLEAN => self::TYPE_INTEGER, ParameterType::STRING, ParameterType::ASCII => self::TYPE_TEXT, ParameterType::BINARY, ParameterType::LARGE_OBJECT => self::TYPE_BLOB, }; } } src/Driver/Statement.php000064400000002663152062402520011250 0ustar00, * driver?: key-of, * driverClass?: class-string, * driverOptions?: array, * host?: string, * memory?: bool, * password?: string, * path?: string, * persistent?: bool, * port?: int, * serverVersion?: string, * sessionMode?: int, * user?: string, * unix_socket?: string, * wrapperClass?: class-string, * } * @psalm-type Params = array{ * application_name?: string, * charset?: string, * dbname?: string, * defaultTableOptions?: array, * driver?: key-of, * driverClass?: class-string, * driverOptions?: array, * host?: string, * keepReplica?: bool, * memory?: bool, * password?: string, * path?: string, * persistent?: bool, * port?: int, * primary?: OverrideParams, * replica?: array, * serverVersion?: string, * sessionMode?: int, * user?: string, * wrapperClass?: class-string, * unix_socket?: string, * } */ final class DriverManager { /** * List of supported drivers and their mappings to the driver classes. * * To add your own driver use the 'driverClass' parameter to {@see DriverManager::getConnection()}. */ private const DRIVER_MAP = [ 'pdo_mysql' => PDO\MySQL\Driver::class, 'pdo_sqlite' => PDO\SQLite\Driver::class, 'pdo_pgsql' => PDO\PgSQL\Driver::class, 'pdo_oci' => PDO\OCI\Driver::class, 'oci8' => OCI8\Driver::class, 'ibm_db2' => IBMDB2\Driver::class, 'pdo_sqlsrv' => PDO\SQLSrv\Driver::class, 'mysqli' => Mysqli\Driver::class, 'pgsql' => PgSQL\Driver::class, 'sqlsrv' => SQLSrv\Driver::class, 'sqlite3' => SQLite3\Driver::class, ]; /** * Private constructor. This class cannot be instantiated. * * @codeCoverageIgnore */ private function __construct() { } /** * Creates a connection object based on the specified parameters. * This method returns a Doctrine\DBAL\Connection which wraps the underlying * driver connection. * * $params must contain at least one of the following. * * Either 'driver' with one of the array keys of {@see DRIVER_MAP}, * OR 'driverClass' that contains the full class name (with namespace) of the * driver class to instantiate. * * Other (optional) parameters: * * user (string): * The username to use when connecting. * * password (string): * The password to use when connecting. * * driverOptions (array): * Any additional driver-specific options for the driver. These are just passed * through to the driver. * * wrapperClass: * You may specify a custom wrapper class through the 'wrapperClass' * parameter but this class MUST inherit from Doctrine\DBAL\Connection. * * driverClass: * The driver class to use. * * @param Configuration|null $config The configuration to use. * @psalm-param Params $params * * @psalm-return ($params is array{wrapperClass: class-string} ? T : Connection) * * @template T of Connection */ public static function getConnection( #[SensitiveParameter] array $params, ?Configuration $config = null, ): Connection { $config ??= new Configuration(); $driver = self::createDriver($params['driver'] ?? null, $params['driverClass'] ?? null); foreach ($config->getMiddlewares() as $middleware) { $driver = $middleware->wrap($driver); } /** @var class-string $wrapperClass */ $wrapperClass = $params['wrapperClass'] ?? Connection::class; if (! is_a($wrapperClass, Connection::class, true)) { throw InvalidWrapperClass::new($wrapperClass); } return new $wrapperClass($params, $driver, $config); } /** * Returns the list of supported drivers. * * @return string[] * @psalm-return list> */ public static function getAvailableDrivers(): array { return array_keys(self::DRIVER_MAP); } /** * @param class-string|null $driverClass * @param key-of|null $driver */ private static function createDriver(?string $driver, ?string $driverClass): Driver { if ($driverClass === null) { if ($driver === null) { throw DriverRequired::new(); } if (! isset(self::DRIVER_MAP[$driver])) { throw UnknownDriver::new($driver, array_keys(self::DRIVER_MAP)); } $driverClass = self::DRIVER_MAP[$driver]; } elseif (! is_a($driverClass, Driver::class, true)) { throw InvalidDriverClass::new($driverClass); } return new $driverClass(); } } src/Exception.php000064400000000215152062402520007776 0ustar00getMessage(); } else { $message = 'An exception occurred in the driver: ' . $driverException->getMessage(); } parent::__construct($message, $driverException->getCode(), $driverException); } public function getSQLState(): ?string { $previous = $this->getPrevious(); assert($previous instanceof Driver\Exception); return $previous->getSQLState(); } public function getQuery(): ?Query { return $this->query; } } src/Exception/DriverRequired.php000064400000001572152062402520012741 0ustar00 */ private array $convertedSQL = []; /** @var list */ private array $convertedParameters = []; /** @var array,string|ParameterType|Type> */ private array $convertedTypes = []; /** * @param array|array $parameters * @psalm-param WrapperParameterTypeArray $types */ public function __construct( private readonly array $parameters, private readonly array $types, ) { } public function acceptPositionalParameter(string $sql): void { $index = $this->originalParameterIndex; if (! array_key_exists($index, $this->parameters)) { throw MissingPositionalParameter::new($index); } $this->acceptParameter($index, $this->parameters[$index]); $this->originalParameterIndex++; } public function acceptNamedParameter(string $sql): void { $name = substr($sql, 1); if (! array_key_exists($name, $this->parameters)) { throw MissingNamedParameter::new($name); } $this->acceptParameter($name, $this->parameters[$name]); } public function acceptOther(string $sql): void { $this->convertedSQL[] = $sql; } public function getSQL(): string { return implode('', $this->convertedSQL); } /** @return list */ public function getParameters(): array { return $this->convertedParameters; } private function acceptParameter(int|string $key, mixed $value): void { if (! isset($this->types[$key])) { $this->convertedSQL[] = '?'; $this->convertedParameters[] = $value; return; } $type = $this->types[$key]; if (! $type instanceof ArrayParameterType) { $this->appendTypedParameter([$value], $type); return; } if (count($value) === 0) { $this->convertedSQL[] = 'NULL'; return; } $this->appendTypedParameter($value, ArrayParameterType::toElementParameterType($type)); } /** @return array,string|ParameterType|Type> */ public function getTypes(): array { return $this->convertedTypes; } /** @param list $values */ private function appendTypedParameter(array $values, string|ParameterType|Type $type): void { $this->convertedSQL[] = implode(', ', array_fill(0, count($values), '?')); $index = count($this->convertedParameters); foreach ($values as $value) { $this->convertedParameters[] = $value; $this->convertedTypes[$index] = $type; $index++; } } } src/LockMode.php000064400000000326152062402520007540 0ustar00logger->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|string { $this->logger->debug('Executing statement: {sql}', ['sql' => $sql]); return parent::exec($sql); } public function beginTransaction(): void { $this->logger->debug('Beginning transaction'); parent::beginTransaction(); } public function commit(): void { $this->logger->debug('Committing transaction'); parent::commit(); } public function rollBack(): void { $this->logger->debug('Rolling back transaction'); parent::rollBack(); } } src/Logging/Driver.php000064400000002321152062402520010661 0ustar00logger->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( #[SensitiveParameter] array $params, ): array { if (isset($params['password'])) { $params['password'] = ''; } return $params; } } src/Logging/Middleware.php000064400000000735152062402520011512 0ustar00logger); } } src/Logging/Statement.php000064400000002572152062402520011402 0ustar00|array */ private array $params = []; /** @var array|array */ private array $types = []; /** @internal This statement can be only instantiated by its connection. */ public function __construct( StatementInterface $statement, private readonly LoggerInterface $logger, private readonly string $sql, ) { parent::__construct($statement); } public function bindValue(int|string $param, mixed $value, ParameterType $type): void { $this->params[$param] = $value; $this->types[$param] = $type; parent::bindValue($param, $value, $type); } public function execute(): ResultInterface { $this->logger->debug('Executing statement: {sql} (parameters: {params}, types: {types})', [ 'sql' => $this->sql, 'params' => $this->params, 'types' => $this->types, ]); return parent::execute(); } } src/ParameterType.php000064400000001266152062402520010631 0ustar00 0) { $query .= sprintf(' OFFSET %d', $offset); } } elseif ($offset > 0) { // 2^64-1 is the maximum of unsigned BIGINT, the biggest limit possible $query .= sprintf(' LIMIT 18446744073709551615 OFFSET %d', $offset); } return $query; } public function quoteSingleIdentifier(string $str): string { return '`' . str_replace('`', '``', $str) . '`'; } public function getRegexpExpression(): string { return 'RLIKE'; } public function getLocateExpression(string $string, string $substring, ?string $start = null): string { if ($start === null) { return sprintf('LOCATE(%s, %s)', $substring, $string); } return sprintf('LOCATE(%s, %s, %s)', $substring, $string, $start); } public function getConcatExpression(string ...$string): string { return sprintf('CONCAT(%s)', implode(', ', $string)); } protected function getDateArithmeticIntervalExpression( string $date, string $operator, string $interval, DateIntervalUnit $unit, ): string { $function = $operator === '+' ? 'DATE_ADD' : 'DATE_SUB'; return $function . '(' . $date . ', INTERVAL ' . $interval . ' ' . $unit->value . ')'; } public function getDateDiffExpression(string $date1, string $date2): string { return 'DATEDIFF(' . $date1 . ', ' . $date2 . ')'; } public function getCurrentDatabaseExpression(): string { return 'DATABASE()'; } public function getLengthExpression(string $string): string { return 'CHAR_LENGTH(' . $string . ')'; } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListDatabasesSQL(): string { return 'SHOW DATABASES'; } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListViewsSQL(string $database): string { return 'SELECT * FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ' . $this->quoteStringLiteral($database); } /** * {@inheritDoc} */ public function getJsonTypeDeclarationSQL(array $column): string { return 'JSON'; } /** * Gets the SQL snippet used to declare a CLOB column type. * TINYTEXT : 2 ^ 8 - 1 = 255 * TEXT : 2 ^ 16 - 1 = 65535 * MEDIUMTEXT : 2 ^ 24 - 1 = 16777215 * LONGTEXT : 2 ^ 32 - 1 = 4294967295 * * {@inheritDoc} */ public function getClobTypeDeclarationSQL(array $column): string { if (! empty($column['length']) && is_numeric($column['length'])) { $length = $column['length']; if ($length <= static::LENGTH_LIMIT_TINYTEXT) { return 'TINYTEXT'; } if ($length <= static::LENGTH_LIMIT_TEXT) { return 'TEXT'; } if ($length <= static::LENGTH_LIMIT_MEDIUMTEXT) { return 'MEDIUMTEXT'; } } return 'LONGTEXT'; } /** * {@inheritDoc} */ public function getDateTimeTypeDeclarationSQL(array $column): string { if (isset($column['version']) && $column['version'] === true) { return 'TIMESTAMP'; } return 'DATETIME'; } /** * {@inheritDoc} */ public function getDateTypeDeclarationSQL(array $column): string { return 'DATE'; } /** * {@inheritDoc} */ public function getTimeTypeDeclarationSQL(array $column): string { return 'TIME'; } /** * {@inheritDoc} */ public function getBooleanTypeDeclarationSQL(array $column): string { return 'TINYINT(1)'; } /** * {@inheritDoc} * * MySQL supports this through AUTO_INCREMENT columns. */ public function supportsIdentityColumns(): bool { return true; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function supportsInlineColumnComments(): bool { return true; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function supportsColumnCollation(): bool { return true; } /** * The SQL snippet required to elucidate a column type * * Returns a column type SELECT snippet string */ public function getColumnTypeSQLSnippet(string $tableAlias, string $databaseName): string { return $tableAlias . '.COLUMN_TYPE'; } /** * {@inheritDoc} */ protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array { $queryFields = $this->getColumnDeclarationListSQL($columns); if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { foreach ($options['uniqueConstraints'] as $definition) { $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($definition); } } // add all indexes if (isset($options['indexes']) && ! empty($options['indexes'])) { foreach ($options['indexes'] as $definition) { $queryFields .= ', ' . $this->getIndexDeclarationSQL($definition); } } // attach all primary keys if (isset($options['primary']) && ! empty($options['primary'])) { $keyColumns = array_unique(array_values($options['primary'])); $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; } $sql = ['CREATE']; if (! empty($options['temporary'])) { $sql[] = 'TEMPORARY'; } $sql[] = 'TABLE ' . $name . ' (' . $queryFields . ')'; $tableOptions = $this->buildTableOptions($options); if ($tableOptions !== '') { $sql[] = $tableOptions; } if (isset($options['partition_options'])) { $sql[] = $options['partition_options']; } $sql = [implode(' ', $sql)]; if (isset($options['foreignKeys'])) { foreach ($options['foreignKeys'] as $definition) { $sql[] = $this->getCreateForeignKeySQL($definition, $name); } } return $sql; } public function createSelectSQLBuilder(): SelectSQLBuilder { return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', null); } /** * Build SQL for table options * * @param mixed[] $options */ private function buildTableOptions(array $options): string { if (isset($options['table_options'])) { return $options['table_options']; } $tableOptions = []; if (isset($options['charset'])) { $tableOptions[] = sprintf('DEFAULT CHARACTER SET %s', $options['charset']); } if (isset($options['collation'])) { $tableOptions[] = $this->getColumnCollationDeclarationSQL($options['collation']); } if (isset($options['engine'])) { $tableOptions[] = sprintf('ENGINE = %s', $options['engine']); } // Auto increment if (isset($options['auto_increment'])) { $tableOptions[] = sprintf('AUTO_INCREMENT = %s', $options['auto_increment']); } // Comment if (isset($options['comment'])) { $tableOptions[] = sprintf('COMMENT = %s ', $this->quoteStringLiteral($options['comment'])); } // Row format if (isset($options['row_format'])) { $tableOptions[] = sprintf('ROW_FORMAT = %s', $options['row_format']); } return implode(' ', $tableOptions); } /** * {@inheritDoc} */ public function getAlterTableSQL(TableDiff $diff): array { $columnSql = []; $queryParts = []; foreach ($diff->getAddedColumns() as $column) { $columnProperties = array_merge($column->toArray(), [ 'comment' => $column->getComment(), ]); $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL( $column->getQuotedName($this), $columnProperties, ); } foreach ($diff->getDroppedColumns() as $column) { $queryParts[] = 'DROP ' . $column->getQuotedName($this); } foreach ($diff->getModifiedColumns() as $columnDiff) { $newColumn = $columnDiff->getNewColumn(); $newColumnProperties = array_merge($newColumn->toArray(), [ 'comment' => $newColumn->getComment(), ]); $oldColumn = $columnDiff->getOldColumn(); $queryParts[] = 'CHANGE ' . $oldColumn->getQuotedName($this) . ' ' . $this->getColumnDeclarationSQL($newColumn->getQuotedName($this), $newColumnProperties); } foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { $oldColumnName = new Identifier($oldColumnName); $columnProperties = array_merge($column->toArray(), [ 'comment' => $column->getComment(), ]); $queryParts[] = 'CHANGE ' . $oldColumnName->getQuotedName($this) . ' ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnProperties); } $addedIndexes = $this->indexAssetsByLowerCaseName($diff->getAddedIndexes()); $modifiedIndexes = $this->indexAssetsByLowerCaseName($diff->getModifiedIndexes()); $diffModified = false; if (isset($addedIndexes['primary'])) { $keyColumns = array_values(array_unique($addedIndexes['primary']->getColumns())); $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')'; unset($addedIndexes['primary']); $diffModified = true; } elseif (isset($modifiedIndexes['primary'])) { $addedColumns = $this->indexAssetsByLowerCaseName($diff->getAddedColumns()); // Necessary in case the new primary key includes a new auto_increment column foreach ($modifiedIndexes['primary']->getColumns() as $columnName) { if (isset($addedColumns[$columnName]) && $addedColumns[$columnName]->getAutoincrement()) { $keyColumns = array_values(array_unique($modifiedIndexes['primary']->getColumns())); $queryParts[] = 'DROP PRIMARY KEY'; $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')'; unset($modifiedIndexes['primary']); $diffModified = true; break; } } } if ($diffModified) { $diff = new TableDiff( $diff->getOldTable(), $diff->getAddedColumns(), $diff->getModifiedColumns(), $diff->getDroppedColumns(), $diff->getRenamedColumns(), array_values($addedIndexes), array_values($modifiedIndexes), $diff->getDroppedIndexes(), $diff->getRenamedIndexes(), $diff->getAddedForeignKeys(), $diff->getModifiedForeignKeys(), $diff->getDroppedForeignKeys(), ); } $sql = []; $tableSql = []; if (count($queryParts) > 0) { $sql[] = 'ALTER TABLE ' . $diff->getOldTable()->getQuotedName($this) . ' ' . implode(', ', $queryParts); } $sql = array_merge( $this->getPreAlterTableIndexForeignKeySQL($diff), $sql, $this->getPostAlterTableIndexForeignKeySQL($diff), ); return array_merge($sql, $tableSql, $columnSql); } /** * {@inheritDoc} */ protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff): array { $sql = []; $tableNameSQL = $diff->getOldTable()->getQuotedName($this); foreach ($diff->getModifiedIndexes() as $changedIndex) { $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $changedIndex)); } foreach ($diff->getDroppedIndexes() as $droppedIndex) { $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $droppedIndex)); foreach ($diff->getAddedIndexes() as $addedIndex) { if ($droppedIndex->getColumns() !== $addedIndex->getColumns()) { continue; } $indexClause = 'INDEX ' . $addedIndex->getName(); if ($addedIndex->isPrimary()) { $indexClause = 'PRIMARY KEY'; } elseif ($addedIndex->isUnique()) { $indexClause = 'UNIQUE INDEX ' . $addedIndex->getName(); } $query = 'ALTER TABLE ' . $tableNameSQL . ' DROP INDEX ' . $droppedIndex->getName() . ', '; $query .= 'ADD ' . $indexClause; $query .= ' (' . implode(', ', $addedIndex->getQuotedColumns($this)) . ')'; $sql[] = $query; $diff->unsetAddedIndex($addedIndex); $diff->unsetDroppedIndex($droppedIndex); break; } } return array_merge( $sql, $this->getPreAlterTableAlterIndexForeignKeySQL($diff), parent::getPreAlterTableIndexForeignKeySQL($diff), $this->getPreAlterTableRenameIndexForeignKeySQL($diff), ); } /** * @return list * * @throws Exception */ private function getPreAlterTableAlterPrimaryKeySQL(TableDiff $diff, Index $index): array { if (! $index->isPrimary()) { return []; } $table = $diff->getOldTable(); $sql = []; $tableNameSQL = $table->getQuotedName($this); // Dropping primary keys requires to unset autoincrement attribute on the particular column first. foreach ($index->getColumns() as $columnName) { if (! $table->hasColumn($columnName)) { continue; } $column = $table->getColumn($columnName); if (! $column->getAutoincrement()) { continue; } $column->setAutoincrement(false); $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' MODIFY ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); // original autoincrement information might be needed later on by other parts of the table alteration $column->setAutoincrement(true); } return $sql; } /** * @param TableDiff $diff The table diff to gather the SQL for. * * @return list * * @throws Exception */ private function getPreAlterTableAlterIndexForeignKeySQL(TableDiff $diff): array { $table = $diff->getOldTable(); $primaryKey = $table->getPrimaryKey(); if ($primaryKey === null) { return []; } $primaryKeyColumns = []; foreach ($primaryKey->getColumns() as $columnName) { if (! $table->hasColumn($columnName)) { continue; } $primaryKeyColumns[] = $table->getColumn($columnName); } if (count($primaryKeyColumns) === 0) { return []; } $sql = []; $tableNameSQL = $table->getQuotedName($this); foreach ($diff->getModifiedIndexes() as $changedIndex) { // Changed primary key if (! $changedIndex->isPrimary()) { continue; } foreach ($primaryKeyColumns as $column) { // Check if an autoincrement column was dropped from the primary key. if (! $column->getAutoincrement() || in_array($column->getName(), $changedIndex->getColumns(), true)) { continue; } // The autoincrement attribute needs to be removed from the dropped column // before we can drop and recreate the primary key. $column->setAutoincrement(false); $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' MODIFY ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); // Restore the autoincrement attribute as it might be needed later on // by other parts of the table alteration. $column->setAutoincrement(true); } } return $sql; } /** * @param TableDiff $diff The table diff to gather the SQL for. * * @return list */ protected function getPreAlterTableRenameIndexForeignKeySQL(TableDiff $diff): array { return []; } protected function getCreateIndexSQLFlags(Index $index): string { $type = ''; if ($index->isUnique()) { $type .= 'UNIQUE '; } elseif ($index->hasFlag('fulltext')) { $type .= 'FULLTEXT '; } elseif ($index->hasFlag('spatial')) { $type .= 'SPATIAL '; } return $type; } /** * {@inheritDoc} */ public function getIntegerTypeDeclarationSQL(array $column): string { return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ public function getBigIntTypeDeclarationSQL(array $column): string { return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ public function getSmallIntTypeDeclarationSQL(array $column): string { return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ public function getFloatDeclarationSQL(array $column): string { return 'DOUBLE PRECISION' . $this->getUnsignedDeclaration($column); } /** * {@inheritDoc} */ public function getDecimalTypeDeclarationSQL(array $column): string { return parent::getDecimalTypeDeclarationSQL($column) . $this->getUnsignedDeclaration($column); } /** * Get unsigned declaration for a column. * * @param mixed[] $columnDef */ private function getUnsignedDeclaration(array $columnDef): string { return ! empty($columnDef['unsigned']) ? ' UNSIGNED' : ''; } /** * {@inheritDoc} */ protected function _getCommonIntegerTypeDeclarationSQL(array $column): string { $autoinc = ''; if (! empty($column['autoincrement'])) { $autoinc = ' AUTO_INCREMENT'; } return $this->getUnsignedDeclaration($column) . $autoinc; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getColumnCharsetDeclarationSQL(string $charset): string { return 'CHARACTER SET ' . $charset; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey): string { $query = ''; if ($foreignKey->hasOption('match')) { $query .= ' MATCH ' . $foreignKey->getOption('match'); } $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey); return $query; } public function getDropIndexSQL(string $name, string $table): string { return 'DROP INDEX ' . $name . ' ON ' . $table; } /** * The `ALTER TABLE ... DROP CONSTRAINT` syntax is only available as of MySQL 8.0.19. * * @link https://dev.mysql.com/doc/refman/8.0/en/alter-table.html */ public function getDropUniqueConstraintSQL(string $name, string $tableName): string { return $this->getDropIndexSQL($name, $tableName); } public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string { return 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); } protected function initializeDoctrineTypeMappings(): void { $this->doctrineTypeMapping = [ 'bigint' => Types::BIGINT, 'binary' => Types::BINARY, 'blob' => Types::BLOB, 'char' => Types::STRING, 'date' => Types::DATE_MUTABLE, 'datetime' => Types::DATETIME_MUTABLE, 'decimal' => Types::DECIMAL, 'double' => Types::FLOAT, 'float' => Types::FLOAT, 'int' => Types::INTEGER, 'integer' => Types::INTEGER, 'json' => Types::JSON, 'longblob' => Types::BLOB, 'longtext' => Types::TEXT, 'mediumblob' => Types::BLOB, 'mediumint' => Types::INTEGER, 'mediumtext' => Types::TEXT, 'numeric' => Types::DECIMAL, 'real' => Types::FLOAT, 'set' => Types::SIMPLE_ARRAY, 'smallint' => Types::SMALLINT, 'string' => Types::STRING, 'text' => Types::TEXT, 'time' => Types::TIME_MUTABLE, 'timestamp' => Types::DATETIME_MUTABLE, 'tinyblob' => Types::BLOB, 'tinyint' => Types::BOOLEAN, 'tinytext' => Types::TEXT, 'varbinary' => Types::BINARY, 'varchar' => Types::STRING, 'year' => Types::DATE_MUTABLE, ]; } protected function createReservedKeywordsList(): KeywordList { return new MySQLKeywords(); } /** * {@inheritDoc} * * MySQL commits a transaction implicitly when DROP TABLE is executed, however not * if DROP TEMPORARY TABLE is executed. */ public function getDropTemporaryTableSQL(string $table): string { return 'DROP TEMPORARY TABLE ' . $table; } /** * Gets the SQL Snippet used to declare a BLOB column type. * TINYBLOB : 2 ^ 8 - 1 = 255 * BLOB : 2 ^ 16 - 1 = 65535 * MEDIUMBLOB : 2 ^ 24 - 1 = 16777215 * LONGBLOB : 2 ^ 32 - 1 = 4294967295 * * {@inheritDoc} */ public function getBlobTypeDeclarationSQL(array $column): string { if (! empty($column['length']) && is_numeric($column['length'])) { $length = $column['length']; if ($length <= static::LENGTH_LIMIT_TINYBLOB) { return 'TINYBLOB'; } if ($length <= static::LENGTH_LIMIT_BLOB) { return 'BLOB'; } if ($length <= static::LENGTH_LIMIT_MEDIUMBLOB) { return 'MEDIUMBLOB'; } } return 'LONGBLOB'; } public function quoteStringLiteral(string $str): string { // MySQL requires backslashes to be escaped as well. $str = str_replace('\\', '\\\\', $str); return parent::quoteStringLiteral($str); } public function getDefaultTransactionIsolationLevel(): TransactionIsolationLevel { return TransactionIsolationLevel::REPEATABLE_READ; } public function supportsColumnLengthIndexes(): bool { return true; } public function createSchemaManager(Connection $connection): MySQLSchemaManager { return new MySQLSchemaManager($connection, $this); } /** * @param array $assets * * @return array * * @template T of AbstractAsset */ private function indexAssetsByLowerCaseName(array $assets): array { $result = []; foreach ($assets as $asset) { $result[strtolower($asset->getName())] = $asset; } return $result; } } src/Platforms/AbstractPlatform.php000064400000215456152062402520013276 0ustar00initializeDoctrineTypeMappings(); foreach (Type::getTypesMap() as $typeName => $className) { foreach (Type::getType($typeName)->getMappedDatabaseTypes($this) as $dbType) { $this->doctrineTypeMapping[$dbType] = $typeName; } } } /** * Returns the SQL snippet used to declare a column that can * store characters in the ASCII character set * * @param array $column The column definition. */ public function getAsciiStringTypeDeclarationSQL(array $column): string { return $this->getStringTypeDeclarationSQL($column); } /** * Returns the SQL snippet used to declare a string column type. * * @param array $column The column definition. */ public function getStringTypeDeclarationSQL(array $column): string { $length = $column['length'] ?? null; if (empty($column['fixed'])) { try { return $this->getVarcharTypeDeclarationSQLSnippet($length); } catch (InvalidColumnType $e) { throw InvalidColumnDeclaration::fromInvalidColumnType($column['name'], $e); } } return $this->getCharTypeDeclarationSQLSnippet($length); } /** * Returns the SQL snippet used to declare a binary string column type. * * @param array $column The column definition. */ public function getBinaryTypeDeclarationSQL(array $column): string { $length = $column['length'] ?? null; try { if (empty($column['fixed'])) { return $this->getVarbinaryTypeDeclarationSQLSnippet($length); } return $this->getBinaryTypeDeclarationSQLSnippet($length); } catch (InvalidColumnType $e) { throw InvalidColumnDeclaration::fromInvalidColumnType($column['name'], $e); } } /** * Returns the SQL snippet to declare a GUID/UUID column. * * By default this maps directly to a CHAR(36) and only maps to more * special datatypes when the underlying databases support this datatype. * * @param array $column The column definition. */ public function getGuidTypeDeclarationSQL(array $column): string { $column['length'] = 36; $column['fixed'] = true; return $this->getStringTypeDeclarationSQL($column); } /** * Returns the SQL snippet to declare a JSON column. * * By default this maps directly to a CLOB and only maps to more * special datatypes when the underlying databases support this datatype. * * @param mixed[] $column */ public function getJsonTypeDeclarationSQL(array $column): string { return $this->getClobTypeDeclarationSQL($column); } /** * @param int|null $length The length of the column in characters * or NULL if the length should be omitted. */ protected function getCharTypeDeclarationSQLSnippet(?int $length): string { $sql = 'CHAR'; if ($length !== null) { $sql .= sprintf('(%d)', $length); } return $sql; } /** * @param int|null $length The length of the column in characters * or NULL if the length should be omitted. */ protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string { if ($length === null) { throw ColumnLengthRequired::new($this, 'VARCHAR'); } return sprintf('VARCHAR(%d)', $length); } /** * Returns the SQL snippet used to declare a fixed length binary column type. * * @param int|null $length The length of the column in bytes * or NULL if the length should be omitted. */ protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string { $sql = 'BINARY'; if ($length !== null) { $sql .= sprintf('(%d)', $length); } return $sql; } /** * Returns the SQL snippet used to declare a variable length binary column type. * * @param int|null $length The length of the column in bytes * or NULL if the length should be omitted. */ protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string { if ($length === null) { throw ColumnLengthRequired::new($this, 'VARBINARY'); } return sprintf('VARBINARY(%d)', $length); } /** * Returns the SQL snippet used to declare a CLOB column type. * * @param mixed[] $column */ abstract public function getClobTypeDeclarationSQL(array $column): string; /** * Returns the SQL Snippet used to declare a BLOB column type. * * @param mixed[] $column */ abstract public function getBlobTypeDeclarationSQL(array $column): string; /** * Registers a doctrine type to be used in conjunction with a column type of this platform. * * @throws Exception If the type is not found. */ public function registerDoctrineTypeMapping(string $dbType, string $doctrineType): void { if ($this->doctrineTypeMapping === null) { $this->initializeAllDoctrineTypeMappings(); } if (! Types\Type::hasType($doctrineType)) { throw TypeNotFound::new($doctrineType); } $dbType = strtolower($dbType); $this->doctrineTypeMapping[$dbType] = $doctrineType; } /** * Gets the Doctrine type that is mapped for the given database column type. */ public function getDoctrineTypeMapping(string $dbType): string { if ($this->doctrineTypeMapping === null) { $this->initializeAllDoctrineTypeMappings(); } $dbType = strtolower($dbType); if (! isset($this->doctrineTypeMapping[$dbType])) { throw new InvalidArgumentException(sprintf( 'Unknown database type "%s" requested, %s may not support it.', $dbType, static::class, )); } return $this->doctrineTypeMapping[$dbType]; } /** * Checks if a database type is currently supported by this platform. */ public function hasDoctrineTypeMappingFor(string $dbType): bool { if ($this->doctrineTypeMapping === null) { $this->initializeAllDoctrineTypeMappings(); } $dbType = strtolower($dbType); return isset($this->doctrineTypeMapping[$dbType]); } /** * Returns the regular expression operator. */ public function getRegexpExpression(): string { throw NotSupported::new(__METHOD__); } /** * Returns the SQL snippet to get the length of a text column in characters. * * @param string $string SQL expression producing the string. */ public function getLengthExpression(string $string): string { return 'LENGTH(' . $string . ')'; } /** * Returns the SQL snippet to get the remainder of the operation of division of dividend by divisor. * * @param string $dividend SQL expression producing the dividend. * @param string $divisor SQL expression producing the divisor. */ public function getModExpression(string $dividend, string $divisor): string { return 'MOD(' . $dividend . ', ' . $divisor . ')'; } /** * Returns the SQL snippet to trim a string. * * @param string $str The expression to apply the trim to. * @param TrimMode $mode The position of the trim. * @param string|null $char The char to trim, has to be quoted already. Defaults to space. */ public function getTrimExpression( string $str, TrimMode $mode = TrimMode::UNSPECIFIED, ?string $char = null, ): string { $tokens = []; switch ($mode) { case TrimMode::UNSPECIFIED: break; case TrimMode::LEADING: $tokens[] = 'LEADING'; break; case TrimMode::TRAILING: $tokens[] = 'TRAILING'; break; case TrimMode::BOTH: $tokens[] = 'BOTH'; break; } if ($char !== null) { $tokens[] = $char; } if (count($tokens) > 0) { $tokens[] = 'FROM'; } $tokens[] = $str; return sprintf('TRIM(%s)', implode(' ', $tokens)); } /** * Returns the SQL snippet to get the position of the first occurrence of the substring in the string. * * @param string $string SQL expression producing the string to locate the substring in. * @param string $substring SQL expression producing the substring to locate. * @param string|null $start SQL expression producing the position to start at. * Defaults to the beginning of the string. */ abstract public function getLocateExpression(string $string, string $substring, ?string $start = null): string; /** * Returns an SQL snippet to get a substring inside the string. * * Note: Not SQL92, but common functionality. * * @param string $string SQL expression producing the string from which a substring should be extracted. * @param string $start SQL expression producing the position to start at, * @param string|null $length SQL expression producing the length of the substring portion to be returned. * By default, the entire substring is returned. */ public function getSubstringExpression(string $string, string $start, ?string $length = null): string { if ($length === null) { return sprintf('SUBSTRING(%s FROM %s)', $string, $start); } return sprintf('SUBSTRING(%s FROM %s FOR %s)', $string, $start, $length); } /** * Returns a SQL snippet to concatenate the given strings. */ public function getConcatExpression(string ...$string): string { return implode(' || ', $string); } /** * Returns the SQL to calculate the difference in days between the two passed dates. * * Computes diff = date1 - date2. */ abstract public function getDateDiffExpression(string $date1, string $date2): string; /** * Returns the SQL to add the number of given seconds to a date. * * @param string $date SQL expression producing the date. * @param string $seconds SQL expression producing the number of seconds. */ public function getDateAddSecondsExpression(string $date, string $seconds): string { return $this->getDateArithmeticIntervalExpression($date, '+', $seconds, DateIntervalUnit::SECOND); } /** * Returns the SQL to subtract the number of given seconds from a date. * * @param string $date SQL expression producing the date. * @param string $seconds SQL expression producing the number of seconds. */ public function getDateSubSecondsExpression(string $date, string $seconds): string { return $this->getDateArithmeticIntervalExpression($date, '-', $seconds, DateIntervalUnit::SECOND); } /** * Returns the SQL to add the number of given minutes to a date. * * @param string $date SQL expression producing the date. * @param string $minutes SQL expression producing the number of minutes. */ public function getDateAddMinutesExpression(string $date, string $minutes): string { return $this->getDateArithmeticIntervalExpression($date, '+', $minutes, DateIntervalUnit::MINUTE); } /** * Returns the SQL to subtract the number of given minutes from a date. * * @param string $date SQL expression producing the date. * @param string $minutes SQL expression producing the number of minutes. */ public function getDateSubMinutesExpression(string $date, string $minutes): string { return $this->getDateArithmeticIntervalExpression($date, '-', $minutes, DateIntervalUnit::MINUTE); } /** * Returns the SQL to add the number of given hours to a date. * * @param string $date SQL expression producing the date. * @param string $hours SQL expression producing the number of hours. */ public function getDateAddHourExpression(string $date, string $hours): string { return $this->getDateArithmeticIntervalExpression($date, '+', $hours, DateIntervalUnit::HOUR); } /** * Returns the SQL to subtract the number of given hours to a date. * * @param string $date SQL expression producing the date. * @param string $hours SQL expression producing the number of hours. */ public function getDateSubHourExpression(string $date, string $hours): string { return $this->getDateArithmeticIntervalExpression($date, '-', $hours, DateIntervalUnit::HOUR); } /** * Returns the SQL to add the number of given days to a date. * * @param string $date SQL expression producing the date. * @param string $days SQL expression producing the number of days. */ public function getDateAddDaysExpression(string $date, string $days): string { return $this->getDateArithmeticIntervalExpression($date, '+', $days, DateIntervalUnit::DAY); } /** * Returns the SQL to subtract the number of given days to a date. * * @param string $date SQL expression producing the date. * @param string $days SQL expression producing the number of days. */ public function getDateSubDaysExpression(string $date, string $days): string { return $this->getDateArithmeticIntervalExpression($date, '-', $days, DateIntervalUnit::DAY); } /** * Returns the SQL to add the number of given weeks to a date. * * @param string $date SQL expression producing the date. * @param string $weeks SQL expression producing the number of weeks. */ public function getDateAddWeeksExpression(string $date, string $weeks): string { return $this->getDateArithmeticIntervalExpression($date, '+', $weeks, DateIntervalUnit::WEEK); } /** * Returns the SQL to subtract the number of given weeks from a date. * * @param string $date SQL expression producing the date. * @param string $weeks SQL expression producing the number of weeks. */ public function getDateSubWeeksExpression(string $date, string $weeks): string { return $this->getDateArithmeticIntervalExpression($date, '-', $weeks, DateIntervalUnit::WEEK); } /** * Returns the SQL to add the number of given months to a date. * * @param string $date SQL expression producing the date. * @param string $months SQL expression producing the number of months. */ public function getDateAddMonthExpression(string $date, string $months): string { return $this->getDateArithmeticIntervalExpression($date, '+', $months, DateIntervalUnit::MONTH); } /** * Returns the SQL to subtract the number of given months to a date. * * @param string $date SQL expression producing the date. * @param string $months SQL expression producing the number of months. */ public function getDateSubMonthExpression(string $date, string $months): string { return $this->getDateArithmeticIntervalExpression($date, '-', $months, DateIntervalUnit::MONTH); } /** * Returns the SQL to add the number of given quarters to a date. * * @param string $date SQL expression producing the date. * @param string $quarters SQL expression producing the number of quarters. */ public function getDateAddQuartersExpression(string $date, string $quarters): string { return $this->getDateArithmeticIntervalExpression($date, '+', $quarters, DateIntervalUnit::QUARTER); } /** * Returns the SQL to subtract the number of given quarters from a date. * * @param string $date SQL expression producing the date. * @param string $quarters SQL expression producing the number of quarters. */ public function getDateSubQuartersExpression(string $date, string $quarters): string { return $this->getDateArithmeticIntervalExpression($date, '-', $quarters, DateIntervalUnit::QUARTER); } /** * Returns the SQL to add the number of given years to a date. * * @param string $date SQL expression producing the date. * @param string $years SQL expression producing the number of years. */ public function getDateAddYearsExpression(string $date, string $years): string { return $this->getDateArithmeticIntervalExpression($date, '+', $years, DateIntervalUnit::YEAR); } /** * Returns the SQL to subtract the number of given years from a date. * * @param string $date SQL expression producing the date. * @param string $years SQL expression producing the number of years. */ public function getDateSubYearsExpression(string $date, string $years): string { return $this->getDateArithmeticIntervalExpression($date, '-', $years, DateIntervalUnit::YEAR); } /** * Returns the SQL for a date arithmetic expression. * * @param string $date SQL expression representing a date to perform the arithmetic operation on. * @param string $operator The arithmetic operator (+ or -). * @param string $interval SQL expression representing the value of the interval that shall be calculated * into the date. * @param DateIntervalUnit $unit The unit of the interval that shall be calculated into the date. */ abstract protected function getDateArithmeticIntervalExpression( string $date, string $operator, string $interval, DateIntervalUnit $unit, ): string; /** * Generates the SQL expression which represents the given date interval multiplied by a number * * @param string $interval SQL expression describing the interval value * @param int $multiplier Interval multiplier */ protected function multiplyInterval(string $interval, int $multiplier): string { return sprintf('(%s * %d)', $interval, $multiplier); } /** * Returns the SQL bit AND comparison expression. * * @param string $value1 SQL expression producing the first value. * @param string $value2 SQL expression producing the second value. */ public function getBitAndComparisonExpression(string $value1, string $value2): string { return '(' . $value1 . ' & ' . $value2 . ')'; } /** * Returns the SQL bit OR comparison expression. * * @param string $value1 SQL expression producing the first value. * @param string $value2 SQL expression producing the second value. */ public function getBitOrComparisonExpression(string $value1, string $value2): string { return '(' . $value1 . ' | ' . $value2 . ')'; } /** * Returns the SQL expression which represents the currently selected database. */ abstract public function getCurrentDatabaseExpression(): string; /** * Honors that some SQL vendors such as MsSql use table hints for locking instead of the * ANSI SQL FOR UPDATE specification. * * @param string $fromClause The FROM clause to append the hint for the given lock mode to */ public function appendLockHint(string $fromClause, LockMode $lockMode): string { return $fromClause; } /** * Returns the SQL snippet to drop an existing table. */ public function getDropTableSQL(string $table): string { return 'DROP TABLE ' . $table; } /** * Returns the SQL to safely drop a temporary table WITHOUT implicitly committing an open transaction. */ public function getDropTemporaryTableSQL(string $table): string { return $this->getDropTableSQL($table); } /** * Returns the SQL to drop an index from a table. */ public function getDropIndexSQL(string $name, string $table): string { return 'DROP INDEX ' . $name; } /** * Returns the SQL to drop a constraint. * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ protected function getDropConstraintSQL(string $name, string $table): string { return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $name; } /** * Returns the SQL to drop a foreign key. */ public function getDropForeignKeySQL(string $foreignKey, string $table): string { return 'ALTER TABLE ' . $table . ' DROP FOREIGN KEY ' . $foreignKey; } /** * Returns the SQL to drop a unique constraint. */ public function getDropUniqueConstraintSQL(string $name, string $tableName): string { return $this->getDropConstraintSQL($name, $tableName); } /** * Returns the SQL statement(s) to create a table with the specified name, columns and constraints * on this platform. * * @return list The list of SQL statements. */ public function getCreateTableSQL(Table $table): array { return $this->buildCreateTableSQL($table, true); } public function createSelectSQLBuilder(): SelectSQLBuilder { return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', 'SKIP LOCKED'); } /** * @internal * * @return list */ final protected function getCreateTableWithoutForeignKeysSQL(Table $table): array { return $this->buildCreateTableSQL($table, false); } /** @return list */ private function buildCreateTableSQL(Table $table, bool $createForeignKeys): array { if (count($table->getColumns()) === 0) { throw NoColumnsSpecifiedForTable::new($table->getName()); } $tableName = $table->getQuotedName($this); $options = $table->getOptions(); $options['uniqueConstraints'] = []; $options['indexes'] = []; $options['primary'] = []; foreach ($table->getIndexes() as $index) { if (! $index->isPrimary()) { $options['indexes'][$index->getQuotedName($this)] = $index; continue; } $options['primary'] = $index->getQuotedColumns($this); $options['primary_index'] = $index; } foreach ($table->getUniqueConstraints() as $uniqueConstraint) { $options['uniqueConstraints'][$uniqueConstraint->getQuotedName($this)] = $uniqueConstraint; } if ($createForeignKeys) { $options['foreignKeys'] = []; foreach ($table->getForeignKeys() as $fkConstraint) { $options['foreignKeys'][] = $fkConstraint; } } $columnSql = []; $columns = []; foreach ($table->getColumns() as $column) { $columnData = $this->columnToArray($column); if (in_array($column->getName(), $options['primary'], true)) { $columnData['primary'] = true; } $columns[] = $columnData; } $sql = $this->_getCreateTableSQL($tableName, $columns, $options); if ($this->supportsCommentOnStatement()) { if ($table->hasOption('comment')) { $sql[] = $this->getCommentOnTableSQL($tableName, $table->getOption('comment')); } foreach ($table->getColumns() as $column) { $comment = $column->getComment(); if ($comment === '') { continue; } $sql[] = $this->getCommentOnColumnSQL($tableName, $column->getQuotedName($this), $comment); } } return array_merge($sql, $columnSql); } /** * @param array $tables * * @return list */ public function getCreateTablesSQL(array $tables): array { $sql = []; foreach ($tables as $table) { $sql = array_merge($sql, $this->getCreateTableWithoutForeignKeysSQL($table)); } foreach ($tables as $table) { foreach ($table->getForeignKeys() as $foreignKey) { $sql[] = $this->getCreateForeignKeySQL( $foreignKey, $table->getQuotedName($this), ); } } return $sql; } /** * @param array
$tables * * @return list */ public function getDropTablesSQL(array $tables): array { $sql = []; foreach ($tables as $table) { foreach ($table->getForeignKeys() as $foreignKey) { $sql[] = $this->getDropForeignKeySQL( $foreignKey->getQuotedName($this), $table->getQuotedName($this), ); } } foreach ($tables as $table) { $sql[] = $this->getDropTableSQL($table->getQuotedName($this)); } return $sql; } protected function getCommentOnTableSQL(string $tableName, string $comment): string { $tableName = new Identifier($tableName); return sprintf( 'COMMENT ON TABLE %s IS %s', $tableName->getQuotedName($this), $this->quoteStringLiteral($comment), ); } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getCommentOnColumnSQL(string $tableName, string $columnName, string $comment): string { $tableName = new Identifier($tableName); $columnName = new Identifier($columnName); return sprintf( 'COMMENT ON COLUMN %s.%s IS %s', $tableName->getQuotedName($this), $columnName->getQuotedName($this), $this->quoteStringLiteral($comment), ); } /** * Returns the SQL to create inline comment on a column. * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getInlineColumnCommentSQL(string $comment): string { if (! $this->supportsInlineColumnComments()) { throw NotSupported::new(__METHOD__); } return 'COMMENT ' . $this->quoteStringLiteral($comment); } /** * Returns the SQL used to create a table. * * @param mixed[][] $columns * @param mixed[] $options * * @return array */ protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array { $columnListSql = $this->getColumnDeclarationListSQL($columns); if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { foreach ($options['uniqueConstraints'] as $definition) { $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($definition); } } if (isset($options['primary']) && ! empty($options['primary'])) { $columnListSql .= ', PRIMARY KEY(' . implode(', ', array_unique(array_values($options['primary']))) . ')'; } if (isset($options['indexes']) && ! empty($options['indexes'])) { foreach ($options['indexes'] as $index => $definition) { $columnListSql .= ', ' . $this->getIndexDeclarationSQL($definition); } } $query = 'CREATE TABLE ' . $name . ' (' . $columnListSql; $check = $this->getCheckDeclarationSQL($columns); if (! empty($check)) { $query .= ', ' . $check; } $query .= ')'; $sql = [$query]; if (isset($options['foreignKeys'])) { foreach ($options['foreignKeys'] as $definition) { $sql[] = $this->getCreateForeignKeySQL($definition, $name); } } return $sql; } public function getCreateTemporaryTableSnippetSQL(): string { return 'CREATE TEMPORARY TABLE'; } /** * Generates SQL statements that can be used to apply the diff. * * @return list */ public function getAlterSchemaSQL(SchemaDiff $diff): array { $sql = []; if ($this->supportsSchemas()) { foreach ($diff->getCreatedSchemas() as $schema) { $sql[] = $this->getCreateSchemaSQL($schema); } } if ($this->supportsSequences()) { foreach ($diff->getAlteredSequences() as $sequence) { $sql[] = $this->getAlterSequenceSQL($sequence); } foreach ($diff->getDroppedSequences() as $sequence) { $sql[] = $this->getDropSequenceSQL($sequence->getQuotedName($this)); } foreach ($diff->getCreatedSequences() as $sequence) { $sql[] = $this->getCreateSequenceSQL($sequence); } } $sql = array_merge( $sql, $this->getCreateTablesSQL( $diff->getCreatedTables(), ), $this->getDropTablesSQL( $diff->getDroppedTables(), ), ); foreach ($diff->getAlteredTables() as $tableDiff) { $sql = array_merge($sql, $this->getAlterTableSQL($tableDiff)); } return $sql; } /** * Returns the SQL to create a sequence on this platform. */ public function getCreateSequenceSQL(Sequence $sequence): string { throw NotSupported::new(__METHOD__); } /** * Returns the SQL to change a sequence on this platform. */ public function getAlterSequenceSQL(Sequence $sequence): string { throw NotSupported::new(__METHOD__); } /** * Returns the SQL snippet to drop an existing sequence. */ public function getDropSequenceSQL(string $name): string { if (! $this->supportsSequences()) { throw NotSupported::new(__METHOD__); } return 'DROP SEQUENCE ' . $name; } /** * Returns the SQL to create an index on a table on this platform. */ public function getCreateIndexSQL(Index $index, string $table): string { $name = $index->getQuotedName($this); $columns = $index->getColumns(); if (count($columns) === 0) { throw new InvalidArgumentException(sprintf( 'Incomplete or invalid index definition %s on table %s', $name, $table, )); } if ($index->isPrimary()) { return $this->getCreatePrimaryKeySQL($index, $table); } $query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table; $query .= ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index); return $query; } /** * Adds condition for partial index. */ protected function getPartialIndexSQL(Index $index): string { if ($this->supportsPartialIndexes() && $index->hasOption('where')) { return ' WHERE ' . $index->getOption('where'); } return ''; } /** * Adds additional flags for index generation. */ protected function getCreateIndexSQLFlags(Index $index): string { return $index->isUnique() ? 'UNIQUE ' : ''; } /** * Returns the SQL to create an unnamed primary key constraint. */ public function getCreatePrimaryKeySQL(Index $index, string $table): string { return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY (' . implode(', ', $index->getQuotedColumns($this)) . ')'; } /** * Returns the SQL to create a named schema. */ public function getCreateSchemaSQL(string $schemaName): string { if (! $this->supportsSchemas()) { throw NotSupported::new(__METHOD__); } return 'CREATE SCHEMA ' . $schemaName; } /** * Returns the SQL to create a unique constraint on a table on this platform. */ public function getCreateUniqueConstraintSQL(UniqueConstraint $constraint, string $tableName): string { return 'ALTER TABLE ' . $tableName . ' ADD CONSTRAINT ' . $constraint->getQuotedName($this) . ' UNIQUE' . ' (' . implode(', ', $constraint->getQuotedColumns($this)) . ')'; } /** * Returns the SQL snippet to drop a schema. */ public function getDropSchemaSQL(string $schemaName): string { if (! $this->supportsSchemas()) { throw NotSupported::new(__METHOD__); } return 'DROP SCHEMA ' . $schemaName; } /** * Quotes a string so that it can be safely used as a table or column name, * even if it is a reserved word of the platform. This also detects identifier * chains separated by dot and quotes them independently. * * NOTE: Just because you CAN use quoted identifiers doesn't mean * you SHOULD use them. In general, they end up causing way more * problems than they solve. * * @param string $identifier The identifier name to be quoted. * * @return string The quoted identifier string. */ public function quoteIdentifier(string $identifier): string { if (str_contains($identifier, '.')) { $parts = array_map($this->quoteSingleIdentifier(...), explode('.', $identifier)); return implode('.', $parts); } return $this->quoteSingleIdentifier($identifier); } /** * Quotes a single identifier (no dot chain separation). * * @param string $str The identifier name to be quoted. * * @return string The quoted identifier string. */ public function quoteSingleIdentifier(string $str): string { return '"' . str_replace('"', '""', $str) . '"'; } /** * Returns the SQL to create a new foreign key. * * @param ForeignKeyConstraint $foreignKey The foreign key constraint. * @param string $table The name of the table on which the foreign key is to be created. */ public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, string $table): string { return 'ALTER TABLE ' . $table . ' ADD ' . $this->getForeignKeyDeclarationSQL($foreignKey); } /** * Gets the SQL statements for altering an existing table. * * This method returns an array of SQL statements, since some platforms need several statements. * * @return list */ abstract public function getAlterTableSQL(TableDiff $diff): array; public function getRenameTableSQL(string $oldName, string $newName): string { return sprintf('ALTER TABLE %s RENAME TO %s', $oldName, $newName); } /** @return list */ protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff): array { $tableNameSQL = $diff->getOldTable()->getQuotedName($this); $sql = []; foreach ($diff->getDroppedForeignKeys() as $foreignKey) { $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableNameSQL); } foreach ($diff->getModifiedForeignKeys() as $foreignKey) { $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableNameSQL); } foreach ($diff->getDroppedIndexes() as $index) { $sql[] = $this->getDropIndexSQL($index->getQuotedName($this), $tableNameSQL); } foreach ($diff->getModifiedIndexes() as $index) { $sql[] = $this->getDropIndexSQL($index->getQuotedName($this), $tableNameSQL); } return $sql; } /** @return list */ protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff): array { $sql = []; $tableNameSQL = $diff->getOldTable()->getQuotedName($this); foreach ($diff->getAddedForeignKeys() as $foreignKey) { $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableNameSQL); } foreach ($diff->getModifiedForeignKeys() as $foreignKey) { $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableNameSQL); } foreach ($diff->getAddedIndexes() as $index) { $sql[] = $this->getCreateIndexSQL($index, $tableNameSQL); } foreach ($diff->getModifiedIndexes() as $index) { $sql[] = $this->getCreateIndexSQL($index, $tableNameSQL); } foreach ($diff->getRenamedIndexes() as $oldIndexName => $index) { $oldIndexName = new Identifier($oldIndexName); $sql = array_merge( $sql, $this->getRenameIndexSQL($oldIndexName->getQuotedName($this), $index, $tableNameSQL), ); } return $sql; } /** * Returns the SQL for renaming an index on a table. * * @param string $oldIndexName The name of the index to rename from. * @param Index $index The definition of the index to rename to. * @param string $tableName The table to rename the given index on. * * @return list The sequence of SQL statements for renaming the given index. */ protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array { return [ $this->getDropIndexSQL($oldIndexName, $tableName), $this->getCreateIndexSQL($index, $tableName), ]; } /** * Gets declaration of a number of columns in bulk. * * @param mixed[][] $columns A multidimensional array. * The first dimension determines the ordinal position of the column, * while the second dimension is keyed with the name of the properties * of the column being declared as array indexes. Currently, the types * of supported column properties are as follows: * * length * Integer value that determines the maximum length of the text * column. If this argument is missing the column should be * declared to have the longest length allowed by the DBMS. * default * Text value to be used as default for this column. * notnull * Boolean flag that indicates whether this column is constrained * to not be set to null. * charset * Text value with the default CHARACTER SET for this column. * collation * Text value with the default COLLATION for this column. */ public function getColumnDeclarationListSQL(array $columns): string { $declarations = []; foreach ($columns as $column) { $declarations[] = $this->getColumnDeclarationSQL($column['name'], $column); } return implode(', ', $declarations); } /** * Obtains DBMS specific SQL code portion needed to declare a generic type * column to be used in statements like CREATE TABLE. * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * * @param string $name The name the column to be declared. * @param mixed[] $column An associative array with the name of the properties * of the column being declared as array indexes. Currently, the types * of supported column properties are as follows: * * length * Integer value that determines the maximum length of the text * column. If this argument is missing the column should be * declared to have the longest length allowed by the DBMS. * default * Text value to be used as default for this column. * notnull * Boolean flag that indicates whether this column is constrained * to not be set to null. * charset * Text value with the default CHARACTER SET for this column. * collation * Text value with the default COLLATION for this column. * columnDefinition * a string that defines the complete column * * @return string DBMS specific SQL code portion that should be used to declare the column. */ public function getColumnDeclarationSQL(string $name, array $column): string { if (isset($column['columnDefinition'])) { $declaration = $column['columnDefinition']; } else { $default = $this->getDefaultValueDeclarationSQL($column); $charset = ! empty($column['charset']) ? ' ' . $this->getColumnCharsetDeclarationSQL($column['charset']) : ''; $collation = ! empty($column['collation']) ? ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : ''; $notnull = ! empty($column['notnull']) ? ' NOT NULL' : ''; $typeDecl = $column['type']->getSQLDeclaration($column, $this); $declaration = $typeDecl . $charset . $default . $notnull . $collation; if ($this->supportsInlineColumnComments() && isset($column['comment']) && $column['comment'] !== '') { $declaration .= ' ' . $this->getInlineColumnCommentSQL($column['comment']); } } return $name . ' ' . $declaration; } /** * Returns the SQL snippet that declares a floating point column of arbitrary precision. * * @param mixed[] $column */ public function getDecimalTypeDeclarationSQL(array $column): string { if (! isset($column['precision'])) { $e = ColumnPrecisionRequired::new(); } elseif (! isset($column['scale'])) { $e = ColumnScaleRequired::new(); } else { $e = null; } if ($e !== null) { throw InvalidColumnDeclaration::fromInvalidColumnType($column['name'], $e); } return 'NUMERIC(' . $column['precision'] . ', ' . $column['scale'] . ')'; } /** * Obtains DBMS specific SQL code portion needed to set a default value * declaration to be used in statements like CREATE TABLE. * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * * @param mixed[] $column The column definition array. * * @return string DBMS specific SQL code portion needed to set a default value. */ public function getDefaultValueDeclarationSQL(array $column): string { if (! isset($column['default'])) { return empty($column['notnull']) ? ' DEFAULT NULL' : ''; } $default = $column['default']; if (! isset($column['type'])) { return " DEFAULT '" . $default . "'"; } $type = $column['type']; if ($type instanceof Types\PhpIntegerMappingType) { return ' DEFAULT ' . $default; } if ($type instanceof Types\PhpDateTimeMappingType && $default === $this->getCurrentTimestampSQL()) { return ' DEFAULT ' . $this->getCurrentTimestampSQL(); } if ($type instanceof Types\PhpTimeMappingType && $default === $this->getCurrentTimeSQL()) { return ' DEFAULT ' . $this->getCurrentTimeSQL(); } if ($type instanceof Types\PhpDateMappingType && $default === $this->getCurrentDateSQL()) { return ' DEFAULT ' . $this->getCurrentDateSQL(); } if ($type instanceof Types\BooleanType) { return ' DEFAULT ' . $this->convertBooleans($default); } if (is_int($default) || is_float($default)) { return ' DEFAULT ' . $default; } return ' DEFAULT ' . $this->quoteStringLiteral($default); } /** * Obtains DBMS specific SQL code portion needed to set a CHECK constraint * declaration to be used in statements like CREATE TABLE. * * @param string[]|mixed[][] $definition The check definition. * * @return string DBMS specific SQL code portion needed to set a CHECK constraint. */ public function getCheckDeclarationSQL(array $definition): string { $constraints = []; foreach ($definition as $def) { if (is_string($def)) { $constraints[] = 'CHECK (' . $def . ')'; } else { if (isset($def['min'])) { $constraints[] = 'CHECK (' . $def['name'] . ' >= ' . $def['min'] . ')'; } if (! isset($def['max'])) { continue; } $constraints[] = 'CHECK (' . $def['name'] . ' <= ' . $def['max'] . ')'; } } return implode(', ', $constraints); } /** * Obtains DBMS specific SQL code portion needed to set a unique * constraint declaration to be used in statements like CREATE TABLE. * * @param UniqueConstraint $constraint The unique constraint definition. * * @return string DBMS specific SQL code portion needed to set a constraint. */ public function getUniqueConstraintDeclarationSQL(UniqueConstraint $constraint): string { $columns = $constraint->getColumns(); if (count($columns) === 0) { throw new InvalidArgumentException('Incomplete definition. "columns" required.'); } $chunks = ['CONSTRAINT']; if ($constraint->getName() !== '') { $chunks[] = $constraint->getQuotedName($this); } $chunks[] = 'UNIQUE'; if ($constraint->hasFlag('clustered')) { $chunks[] = 'CLUSTERED'; } $chunks[] = sprintf('(%s)', implode(', ', $columns)); return implode(' ', $chunks); } /** * Obtains DBMS specific SQL code portion needed to set an index * declaration to be used in statements like CREATE TABLE. * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * * @param Index $index The index definition. * * @return string DBMS specific SQL code portion needed to set an index. */ public function getIndexDeclarationSQL(Index $index): string { $columns = $index->getColumns(); if (count($columns) === 0) { throw new InvalidArgumentException('Incomplete definition. "columns" required.'); } return $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $index->getQuotedName($this) . ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index); } /** * Some vendors require temporary table names to be qualified specially. */ public function getTemporaryTableName(string $tableName): string { return $tableName; } /** * Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint * of a column declaration to be used in statements like CREATE TABLE. * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * * @return string DBMS specific SQL code portion needed to set the FOREIGN KEY constraint * of a column declaration. */ public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey): string { $sql = $this->getForeignKeyBaseDeclarationSQL($foreignKey); $sql .= $this->getAdvancedForeignKeyOptionsSQL($foreignKey); return $sql; } /** * Returns the FOREIGN KEY query section dealing with non-standard options * as MATCH, INITIALLY DEFERRED, ON UPDATE, ... * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * * @param ForeignKeyConstraint $foreignKey The foreign key definition. */ public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey): string { $query = ''; if ($foreignKey->hasOption('onUpdate')) { $query .= ' ON UPDATE ' . $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onUpdate')); } if ($foreignKey->hasOption('onDelete')) { $query .= ' ON DELETE ' . $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onDelete')); } return $query; } /** * Returns the given referential action in uppercase if valid, otherwise throws an exception. * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * * @param string $action The foreign key referential action. */ public function getForeignKeyReferentialActionSQL(string $action): string { $upper = strtoupper($action); return match ($upper) { 'CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT' => $upper, default => throw new InvalidArgumentException(sprintf('Invalid foreign key action "%s".', $upper)), }; } /** * Obtains DBMS specific SQL code portion needed to set the FOREIGN KEY constraint * of a column declaration to be used in statements like CREATE TABLE. */ public function getForeignKeyBaseDeclarationSQL(ForeignKeyConstraint $foreignKey): string { $sql = ''; if ($foreignKey->getName() !== '') { $sql .= 'CONSTRAINT ' . $foreignKey->getQuotedName($this) . ' '; } $sql .= 'FOREIGN KEY ('; if (count($foreignKey->getLocalColumns()) === 0) { throw new InvalidArgumentException('Incomplete definition. "local" required.'); } if (count($foreignKey->getForeignColumns()) === 0) { throw new InvalidArgumentException('Incomplete definition. "foreign" required.'); } if (strlen($foreignKey->getForeignTableName()) === 0) { throw new InvalidArgumentException('Incomplete definition. "foreignTable" required.'); } return $sql . implode(', ', $foreignKey->getQuotedLocalColumns($this)) . ') REFERENCES ' . $foreignKey->getQuotedForeignTableName($this) . ' (' . implode(', ', $foreignKey->getQuotedForeignColumns($this)) . ')'; } /** * Obtains DBMS specific SQL code portion needed to set the CHARACTER SET * of a column declaration to be used in statements like CREATE TABLE. * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * * @param string $charset The name of the charset. * * @return string DBMS specific SQL code portion needed to set the CHARACTER SET * of a column declaration. */ public function getColumnCharsetDeclarationSQL(string $charset): string { return ''; } /** * Obtains DBMS specific SQL code portion needed to set the COLLATION * of a column declaration to be used in statements like CREATE TABLE. * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. * * @param string $collation The name of the collation. * * @return string DBMS specific SQL code portion needed to set the COLLATION * of a column declaration. */ public function getColumnCollationDeclarationSQL(string $collation): string { return $this->supportsColumnCollation() ? 'COLLATE ' . $this->quoteSingleIdentifier($collation) : ''; } /** * Some platforms need the boolean values to be converted. * * The default conversion in this implementation converts to integers (false => 0, true => 1). * * Note: if the input is not a boolean the original input might be returned. * * There are two contexts when converting booleans: Literals and Prepared Statements. * This method should handle the literal case * * @param mixed $item A boolean or an array of them. * * @return mixed A boolean database value or an array of them. */ public function convertBooleans(mixed $item): mixed { if (is_array($item)) { foreach ($item as $k => $value) { if (! is_bool($value)) { continue; } $item[$k] = (int) $value; } } elseif (is_bool($item)) { $item = (int) $item; } return $item; } /** * Some platforms have boolean literals that needs to be correctly converted * * The default conversion tries to convert value into bool "(bool)$item" * * @param T $item * * @return (T is null ? null : bool) * * @template T */ public function convertFromBoolean(mixed $item): ?bool { if ($item === null) { return null; } return (bool) $item; } /** * This method should handle the prepared statements case. When there is no * distinction, it's OK to use the same method. * * Note: if the input is not a boolean the original input might be returned. * * @param mixed $item A boolean or an array of them. * * @return mixed A boolean database value or an array of them. */ public function convertBooleansToDatabaseValue(mixed $item): mixed { return $this->convertBooleans($item); } /** * Returns the SQL specific for the platform to get the current date. */ public function getCurrentDateSQL(): string { return 'CURRENT_DATE'; } /** * Returns the SQL specific for the platform to get the current time. */ public function getCurrentTimeSQL(): string { return 'CURRENT_TIME'; } /** * Returns the SQL specific for the platform to get the current timestamp */ public function getCurrentTimestampSQL(): string { return 'CURRENT_TIMESTAMP'; } /** * Returns the SQL for a given transaction isolation level Connection constant. */ protected function _getTransactionIsolationLevelSQL(TransactionIsolationLevel $level): string { return match ($level) { TransactionIsolationLevel::READ_UNCOMMITTED => 'READ UNCOMMITTED', TransactionIsolationLevel::READ_COMMITTED => 'READ COMMITTED', TransactionIsolationLevel::REPEATABLE_READ => 'REPEATABLE READ', TransactionIsolationLevel::SERIALIZABLE => 'SERIALIZABLE', }; } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListDatabasesSQL(): string { throw NotSupported::new(__METHOD__); } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListSequencesSQL(string $database): string { throw NotSupported::new(__METHOD__); } /** * Returns the SQL to list all views of a database or user. * * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ abstract public function getListViewsSQL(string $database): string; public function getCreateViewSQL(string $name, string $sql): string { return 'CREATE VIEW ' . $name . ' AS ' . $sql; } public function getDropViewSQL(string $name): string { return 'DROP VIEW ' . $name; } public function getSequenceNextValSQL(string $sequence): string { throw NotSupported::new(__METHOD__); } /** * Returns the SQL to create a new database. * * @param string $name The name of the database that should be created. */ public function getCreateDatabaseSQL(string $name): string { return 'CREATE DATABASE ' . $name; } /** * Returns the SQL snippet to drop an existing database. * * @param string $name The name of the database that should be dropped. */ public function getDropDatabaseSQL(string $name): string { return 'DROP DATABASE ' . $name; } /** * Returns the SQL to set the transaction isolation level. */ abstract public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string; /** * Obtains DBMS specific SQL to be used to create datetime columns in * statements like CREATE TABLE. * * @param mixed[] $column */ abstract public function getDateTimeTypeDeclarationSQL(array $column): string; /** * Obtains DBMS specific SQL to be used to create datetime with timezone offset columns. * * @param mixed[] $column */ public function getDateTimeTzTypeDeclarationSQL(array $column): string { return $this->getDateTimeTypeDeclarationSQL($column); } /** * Obtains DBMS specific SQL to be used to create date columns in statements * like CREATE TABLE. * * @param mixed[] $column */ abstract public function getDateTypeDeclarationSQL(array $column): string; /** * Obtains DBMS specific SQL to be used to create time columns in statements * like CREATE TABLE. * * @param mixed[] $column */ abstract public function getTimeTypeDeclarationSQL(array $column): string; /** @param mixed[] $column */ public function getFloatDeclarationSQL(array $column): string { return 'DOUBLE PRECISION'; } /** * Gets the default transaction isolation level of the platform. * * @return TransactionIsolationLevel The default isolation level. */ public function getDefaultTransactionIsolationLevel(): TransactionIsolationLevel { return TransactionIsolationLevel::READ_COMMITTED; } /* supports*() methods */ /** * Whether the platform supports sequences. */ public function supportsSequences(): bool { return false; } /** * Whether the platform supports identity columns. * * Identity columns are columns that receive an auto-generated value from the * database on insert of a row. */ public function supportsIdentityColumns(): bool { return false; } /** * Whether the platform supports partial indexes. * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function supportsPartialIndexes(): bool { return false; } /** * Whether the platform supports indexes with column length definitions. */ public function supportsColumnLengthIndexes(): bool { return false; } /** * Whether the platform supports savepoints. */ public function supportsSavepoints(): bool { return true; } /** * Whether the platform supports releasing savepoints. */ public function supportsReleaseSavepoints(): bool { return $this->supportsSavepoints(); } /** * Whether the platform supports database schemas. */ public function supportsSchemas(): bool { return false; } /** * Whether this platform support to add inline column comments as postfix. * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function supportsInlineColumnComments(): bool { return false; } /** * Whether this platform support the proprietary syntax "COMMENT ON asset". * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function supportsCommentOnStatement(): bool { return false; } /** * Does this platform support column collation? * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function supportsColumnCollation(): bool { return false; } /** * Gets the format string, as accepted by the date() function, that describes * the format of a stored datetime value of this platform. * * @return string The format string. */ public function getDateTimeFormatString(): string { return 'Y-m-d H:i:s'; } /** * Gets the format string, as accepted by the date() function, that describes * the format of a stored datetime with timezone value of this platform. * * @return string The format string. */ public function getDateTimeTzFormatString(): string { return 'Y-m-d H:i:s'; } /** * Gets the format string, as accepted by the date() function, that describes * the format of a stored date value of this platform. * * @return string The format string. */ public function getDateFormatString(): string { return 'Y-m-d'; } /** * Gets the format string, as accepted by the date() function, that describes * the format of a stored time value of this platform. * * @return string The format string. */ public function getTimeFormatString(): string { return 'H:i:s'; } /** * Adds an driver-specific LIMIT clause to the query. */ final public function modifyLimitQuery(string $query, ?int $limit, int $offset = 0): string { if ($offset < 0) { throw new InvalidArgumentException(sprintf( 'Offset must be a positive integer or zero, %d given.', $offset, )); } return $this->doModifyLimitQuery($query, $limit, $offset); } /** * Adds an platform-specific LIMIT clause to the query. */ protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string { if ($limit !== null) { $query .= sprintf(' LIMIT %d', $limit); } if ($offset > 0) { $query .= sprintf(' OFFSET %d', $offset); } return $query; } /** * Maximum length of any given database identifier, like tables or column names. */ public function getMaxIdentifierLength(): int { return 63; } /** * Returns the insert SQL for an empty insert statement. */ public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string { return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (null)'; } /** * Generates a Truncate Table SQL statement for a given table. * * Cascade is not supported on many platforms but would optionally cascade the truncate by * following the foreign keys. */ public function getTruncateTableSQL(string $tableName, bool $cascade = false): string { $tableIdentifier = new Identifier($tableName); return 'TRUNCATE ' . $tableIdentifier->getQuotedName($this); } /** * This is for test reasons, many vendors have special requirements for dummy statements. */ public function getDummySelectSQL(string $expression = '1'): string { return sprintf('SELECT %s', $expression); } /** * Returns the SQL to create a new savepoint. */ public function createSavePoint(string $savepoint): string { return 'SAVEPOINT ' . $savepoint; } /** * Returns the SQL to release a savepoint. */ public function releaseSavePoint(string $savepoint): string { return 'RELEASE SAVEPOINT ' . $savepoint; } /** * Returns the SQL to rollback a savepoint. */ public function rollbackSavePoint(string $savepoint): string { return 'ROLLBACK TO SAVEPOINT ' . $savepoint; } /** * Returns the keyword list instance of this platform. */ final public function getReservedKeywordsList(): KeywordList { // Store the instance so it doesn't need to be generated on every request. return $this->_keywords ??= $this->createReservedKeywordsList(); } /** * Creates an instance of the reserved keyword list of this platform. */ abstract protected function createReservedKeywordsList(): KeywordList; /** * Quotes a literal string. * This method is NOT meant to fix SQL injections! * It is only meant to escape this platform's string literal * quote character inside the given literal string. * * @param string $str The literal string to be quoted. * * @return string The quoted literal string. */ public function quoteStringLiteral(string $str): string { return "'" . str_replace("'", "''", $str) . "'"; } /** * Escapes metacharacters in a string intended to be used with a LIKE * operator. * * @param string $inputString a literal, unquoted string * @param string $escapeChar should be reused by the caller in the LIKE * expression. */ final public function escapeStringForLike(string $inputString, string $escapeChar): string { $sql = preg_replace( '~([' . preg_quote($this->getLikeWildcardCharacters() . $escapeChar, '~') . '])~u', addcslashes($escapeChar, '\\') . '$1', $inputString, ); assert(is_string($sql)); return $sql; } /** * @return array An associative array with the name of the properties * of the column being declared as array indexes. */ private function columnToArray(Column $column): array { return array_merge($column->toArray(), [ 'name' => $column->getQuotedName($this), 'version' => $column->hasPlatformOption('version') ? $column->getPlatformOption('version') : false, 'comment' => $column->getComment(), ]); } /** @internal */ public function createSQLParser(): Parser { return new Parser(false); } protected function getLikeWildcardCharacters(): string { return '%_'; } /** * Compares the definitions of the given columns in the context of this platform. */ public function columnsEqual(Column $column1, Column $column2): bool { $column1Array = $this->columnToArray($column1); $column2Array = $this->columnToArray($column2); // ignore explicit columnDefinition since it's not set on the Column generated by the SchemaManager unset($column1Array['columnDefinition']); unset($column2Array['columnDefinition']); if ( $this->getColumnDeclarationSQL('', $column1Array) !== $this->getColumnDeclarationSQL('', $column2Array) ) { return false; } // If the platform supports inline comments, all comparison is already done above if ($this->supportsInlineColumnComments()) { return true; } return $column1->getComment() === $column2->getComment(); } /** * Creates the schema manager that can be used to inspect and change the underlying * database schema according to the dialect of the platform. */ abstract public function createSchemaManager(Connection $connection): AbstractSchemaManager; } src/Platforms/DB2Platform.php000064400000043040152062402520012066 0ustar00doctrineTypeMapping = [ 'bigint' => Types::BIGINT, 'binary' => Types::BINARY, 'blob' => Types::BLOB, 'character' => Types::STRING, 'clob' => Types::TEXT, 'date' => Types::DATE_MUTABLE, 'decimal' => Types::DECIMAL, 'double' => Types::FLOAT, 'integer' => Types::INTEGER, 'real' => Types::FLOAT, 'smallint' => Types::SMALLINT, 'time' => Types::TIME_MUTABLE, 'timestamp' => Types::DATETIME_MUTABLE, 'varbinary' => Types::BINARY, 'varchar' => Types::STRING, ]; } protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string { return $this->getCharTypeDeclarationSQLSnippet($length) . ' FOR BIT DATA'; } protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string { return $this->getVarcharTypeDeclarationSQLSnippet($length) . ' FOR BIT DATA'; } /** * {@inheritDoc} */ public function getClobTypeDeclarationSQL(array $column): string { // todo clob(n) with $column['length']; return 'CLOB(1M)'; } /** * {@inheritDoc} */ public function getBooleanTypeDeclarationSQL(array $column): string { return 'SMALLINT'; } /** * {@inheritDoc} */ public function getIntegerTypeDeclarationSQL(array $column): string { return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ public function getBigIntTypeDeclarationSQL(array $column): string { return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ public function getSmallIntTypeDeclarationSQL(array $column): string { return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ protected function _getCommonIntegerTypeDeclarationSQL(array $column): string { $autoinc = ''; if (! empty($column['autoincrement'])) { $autoinc = ' GENERATED BY DEFAULT AS IDENTITY'; } return $autoinc; } public function getBitAndComparisonExpression(string $value1, string $value2): string { return 'BITAND(' . $value1 . ', ' . $value2 . ')'; } public function getBitOrComparisonExpression(string $value1, string $value2): string { return 'BITOR(' . $value1 . ', ' . $value2 . ')'; } protected function getDateArithmeticIntervalExpression( string $date, string $operator, string $interval, DateIntervalUnit $unit, ): string { switch ($unit) { case DateIntervalUnit::WEEK: $interval = $this->multiplyInterval($interval, 7); $unit = DateIntervalUnit::DAY; break; case DateIntervalUnit::QUARTER: $interval = $this->multiplyInterval($interval, 3); $unit = DateIntervalUnit::MONTH; break; } return $date . ' ' . $operator . ' ' . $interval . ' ' . $unit->value; } public function getDateDiffExpression(string $date1, string $date2): string { return 'DAYS(' . $date1 . ') - DAYS(' . $date2 . ')'; } /** * {@inheritDoc} */ public function getDateTimeTypeDeclarationSQL(array $column): string { if (isset($column['version']) && $column['version'] === true) { return 'TIMESTAMP(0) WITH DEFAULT'; } return 'TIMESTAMP(0)'; } /** * {@inheritDoc} */ public function getDateTypeDeclarationSQL(array $column): string { return 'DATE'; } /** * {@inheritDoc} */ public function getTimeTypeDeclarationSQL(array $column): string { return 'TIME'; } public function getTruncateTableSQL(string $tableName, bool $cascade = false): string { $tableIdentifier = new Identifier($tableName); return 'TRUNCATE ' . $tableIdentifier->getQuotedName($this) . ' IMMEDIATE'; } public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string { throw NotSupported::new(__METHOD__); } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListViewsSQL(string $database): string { return 'SELECT NAME, TEXT FROM SYSIBM.SYSVIEWS'; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function supportsCommentOnStatement(): bool { return true; } public function getCurrentDateSQL(): string { return 'CURRENT DATE'; } public function getCurrentTimeSQL(): string { return 'CURRENT TIME'; } public function getCurrentTimestampSQL(): string { return 'CURRENT TIMESTAMP'; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getIndexDeclarationSQL(Index $index): string { // Index declaration in statements like CREATE TABLE is not supported. throw NotSupported::new(__METHOD__); } /** * {@inheritDoc} */ protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array { $indexes = []; if (isset($options['indexes'])) { $indexes = $options['indexes']; } $options['indexes'] = []; $sqls = parent::_getCreateTableSQL($name, $columns, $options); foreach ($indexes as $definition) { $sqls[] = $this->getCreateIndexSQL($definition, $name); } return $sqls; } /** * {@inheritDoc} */ public function getAlterTableSQL(TableDiff $diff): array { $sql = []; $columnSql = []; $commentsSQL = []; $tableNameSQL = $diff->getOldTable()->getQuotedName($this); $queryParts = []; foreach ($diff->getAddedColumns() as $column) { $columnDef = $column->toArray(); $queryPart = 'ADD COLUMN ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef); // Adding non-nullable columns to a table requires a default value to be specified. if ( ! empty($columnDef['notnull']) && ! isset($columnDef['default']) && empty($columnDef['autoincrement']) ) { $queryPart .= ' WITH DEFAULT'; } $queryParts[] = $queryPart; $comment = $column->getComment(); if ($comment === '') { continue; } $commentsSQL[] = $this->getCommentOnColumnSQL( $tableNameSQL, $column->getQuotedName($this), $comment, ); } foreach ($diff->getDroppedColumns() as $column) { $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); } foreach ($diff->getModifiedColumns() as $columnDiff) { if ($columnDiff->hasCommentChanged()) { $newColumn = $columnDiff->getNewColumn(); $commentsSQL[] = $this->getCommentOnColumnSQL( $tableNameSQL, $newColumn->getQuotedName($this), $newColumn->getComment(), ); } $this->gatherAlterColumnSQL( $tableNameSQL, $columnDiff, $sql, $queryParts, ); } foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { $oldColumnName = new Identifier($oldColumnName); $queryParts[] = 'RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this); } if (count($queryParts) > 0) { $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . implode(' ', $queryParts); } // Some table alteration operations require a table reorganization. if (count($diff->getDroppedColumns()) > 0 || count($diff->getModifiedColumns()) > 0) { $sql[] = "CALL SYSPROC.ADMIN_CMD ('REORG TABLE " . $tableNameSQL . "')"; } $sql = array_merge( $this->getPreAlterTableIndexForeignKeySQL($diff), $sql, $commentsSQL, $this->getPostAlterTableIndexForeignKeySQL($diff), ); return array_merge($sql, $columnSql); } public function getRenameTableSQL(string $oldName, string $newName): string { return sprintf('RENAME TABLE %s TO %s', $oldName, $newName); } /** * Gathers the table alteration SQL for a given column diff. * * @param string $table The table to gather the SQL for. * @param ColumnDiff $columnDiff The column diff to evaluate. * @param list $sql The sequence of table alteration statements to fill. * @param list $queryParts The sequence of column alteration clauses to fill. */ private function gatherAlterColumnSQL( string $table, ColumnDiff $columnDiff, array &$sql, array &$queryParts, ): void { $alterColumnClauses = $this->getAlterColumnClausesSQL($columnDiff); if (empty($alterColumnClauses)) { return; } // If we have a single column alteration, we can append the clause to the main query. if (count($alterColumnClauses) === 1) { $queryParts[] = current($alterColumnClauses); return; } // We have multiple alterations for the same column, // so we need to trigger a complete ALTER TABLE statement // for each ALTER COLUMN clause. foreach ($alterColumnClauses as $alterColumnClause) { $sql[] = 'ALTER TABLE ' . $table . ' ' . $alterColumnClause; } } /** * Returns the ALTER COLUMN SQL clauses for altering a column described by the given column diff. * * @return string[] */ private function getAlterColumnClausesSQL(ColumnDiff $columnDiff): array { $newColumn = $columnDiff->getNewColumn()->toArray(); $alterClause = 'ALTER COLUMN ' . $columnDiff->getNewColumn()->getQuotedName($this); if ($newColumn['columnDefinition'] !== null) { return [$alterClause . ' ' . $newColumn['columnDefinition']]; } $clauses = []; if ( $columnDiff->hasTypeChanged() || $columnDiff->hasLengthChanged() || $columnDiff->hasPrecisionChanged() || $columnDiff->hasScaleChanged() || $columnDiff->hasFixedChanged() ) { $clauses[] = $alterClause . ' SET DATA TYPE ' . $newColumn['type']->getSQLDeclaration($newColumn, $this); } if ($columnDiff->hasNotNullChanged()) { $clauses[] = $newColumn['notnull'] ? $alterClause . ' SET NOT NULL' : $alterClause . ' DROP NOT NULL'; } if ($columnDiff->hasDefaultChanged()) { if (isset($newColumn['default'])) { $defaultClause = $this->getDefaultValueDeclarationSQL($newColumn); if ($defaultClause !== '') { $clauses[] = $alterClause . ' SET' . $defaultClause; } } else { $clauses[] = $alterClause . ' DROP DEFAULT'; } } return $clauses; } /** * {@inheritDoc} */ protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff): array { $sql = []; $tableNameSQL = $diff->getOldTable()->getQuotedName($this); foreach ($diff->getDroppedIndexes() as $droppedIndex) { foreach ($diff->getAddedIndexes() as $addedIndex) { if ($droppedIndex->getColumns() !== $addedIndex->getColumns()) { continue; } if ($droppedIndex->isPrimary()) { $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' DROP PRIMARY KEY'; } elseif ($droppedIndex->isUnique()) { $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' DROP UNIQUE ' . $droppedIndex->getQuotedName($this); } else { $sql[] = $this->getDropIndexSQL($droppedIndex->getQuotedName($this), $tableNameSQL); } $sql[] = $this->getCreateIndexSQL($addedIndex, $tableNameSQL); $diff->unsetAddedIndex($addedIndex); $diff->unsetDroppedIndex($droppedIndex); break; } } return array_merge($sql, parent::getPreAlterTableIndexForeignKeySQL($diff)); } /** * {@inheritDoc} */ protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array { if (str_contains($tableName, '.')) { [$schema] = explode('.', $tableName); $oldIndexName = $schema . '.' . $oldIndexName; } return ['RENAME INDEX ' . $oldIndexName . ' TO ' . $index->getQuotedName($this)]; } /** * {@inheritDoc} * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getDefaultValueDeclarationSQL(array $column): string { if (! empty($column['autoincrement'])) { return ''; } if (! empty($column['version'])) { if ((string) $column['type'] !== 'DateTime') { $column['default'] = '1'; } } return parent::getDefaultValueDeclarationSQL($column); } public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string { return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)'; } public function getCreateTemporaryTableSnippetSQL(): string { return 'DECLARE GLOBAL TEMPORARY TABLE'; } public function getTemporaryTableName(string $tableName): string { return 'SESSION.' . $tableName; } protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string { if ($offset > 0) { $query .= sprintf(' OFFSET %d ROWS', $offset); } if ($limit !== null) { $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit); } return $query; } public function getLocateExpression(string $string, string $substring, ?string $start = null): string { if ($start === null) { return sprintf('LOCATE(%s, %s)', $substring, $string); } return sprintf('LOCATE(%s, %s, %s)', $substring, $string, $start); } public function getSubstringExpression(string $string, string $start, ?string $length = null): string { if ($length === null) { return sprintf('SUBSTR(%s, %s)', $string, $start); } return sprintf('SUBSTR(%s, %s, %s)', $string, $start, $length); } public function getLengthExpression(string $string): string { return 'LENGTH(' . $string . ', CODEUNITS32)'; } public function getCurrentDatabaseExpression(): string { return 'CURRENT_USER'; } public function supportsIdentityColumns(): bool { return true; } public function createSelectSQLBuilder(): SelectSQLBuilder { return new DefaultSelectSQLBuilder($this, 'WITH RR USE AND KEEP UPDATE LOCKS', null); } public function getDummySelectSQL(string $expression = '1'): string { return sprintf('SELECT %s FROM sysibm.sysdummy1', $expression); } /** * {@inheritDoc} * * DB2 supports savepoints, but they work semantically different than on other vendor platforms. * * TODO: We have to investigate how to get DB2 up and running with savepoints. */ public function supportsSavepoints(): bool { return false; } protected function createReservedKeywordsList(): KeywordList { return new DB2Keywords(); } public function createSchemaManager(Connection $connection): DB2SchemaManager { return new DB2SchemaManager($connection, $this); } } src/Platforms/DateIntervalUnit.php000064400000000506152062402520013234 0ustar00keywords === null) { $this->initializeKeywords(); } return isset($this->keywords[strtoupper($word)]); } protected function initializeKeywords(): void { $this->keywords = array_flip(array_map('strtoupper', $this->getKeywords())); } /** * Returns the list of keywords. * * @return string[] */ abstract protected function getKeywords(): array; } src/Platforms/Keywords/MariaDBKeywords.php000064400000013473152062402520014617 0ustar00getQuotedName($this)]; } } src/Platforms/MariaDB1060Platform.php000064400000000644152062402520013270 0ustar00quoteStringLiteral($databaseName); // The check for `CONSTRAINT_SCHEMA = $databaseName` is mandatory here to prevent performance issues return <<getOldTable()->getQuotedName($this); $modifiedForeignKeys = $diff->getModifiedForeignKeys(); foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) { if (in_array($foreignKey, $modifiedForeignKeys, true)) { continue; } $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableName); } return $sql; } /** * {@inheritDoc} */ protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff): array { return array_merge( parent::getPostAlterTableIndexForeignKeySQL($diff), $this->getPostAlterTableRenameIndexForeignKeySQL($diff), ); } /** @return list */ private function getPostAlterTableRenameIndexForeignKeySQL(TableDiff $diff): array { $sql = []; $tableName = $diff->getOldTable()->getQuotedName($this); $modifiedForeignKeys = $diff->getModifiedForeignKeys(); foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) { if (in_array($foreignKey, $modifiedForeignKeys, true)) { continue; } $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName); } return $sql; } /** * Returns the remaining foreign key constraints that require one of the renamed indexes. * * "Remaining" here refers to the diff between the foreign keys currently defined in the associated * table and the foreign keys to be removed. * * @param TableDiff $diff The table diff to evaluate. * * @return ForeignKeyConstraint[] */ private function getRemainingForeignKeyConstraintsRequiringRenamedIndexes(TableDiff $diff): array { $renamedIndexes = $diff->getRenamedIndexes(); if (count($renamedIndexes) === 0) { return []; } $foreignKeys = []; $remainingForeignKeys = array_diff_key( $diff->getOldTable()->getForeignKeys(), $diff->getDroppedForeignKeys(), ); foreach ($remainingForeignKeys as $foreignKey) { foreach ($renamedIndexes as $index) { if ($foreignKey->intersectsIndexColumns($index)) { $foreignKeys[] = $foreignKey; break; } } } return $foreignKeys; } /** {@inheritDoc} */ public function getColumnDeclarationSQL(string $name, array $column): string { // MariaDb forces column collation to utf8mb4_bin where the column was declared as JSON so ignore // collation and character set for json columns as attempting to set them can cause an error. if ($this->getJsonTypeDeclarationSQL([]) === 'JSON' && ($column['type'] ?? null) instanceof JsonType) { unset($column['collation']); unset($column['charset']); } return parent::getColumnDeclarationSQL($name, $column); } protected function createReservedKeywordsList(): KeywordList { return new MariaDBKeywords(); } } src/Platforms/MySQL/CharsetMetadataProvider.php000064400000000314152062402520015521 0ustar00 */ private array $cache = []; public function __construct(private readonly CharsetMetadataProvider $charsetMetadataProvider) { } public function getDefaultCharsetCollation(string $charset): ?string { if (array_key_exists($charset, $this->cache)) { return $this->cache[$charset]; } return $this->cache[$charset] = $this->charsetMetadataProvider->getDefaultCharsetCollation($charset); } } src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php000064400000001571152062402520024314 0ustar00connection->fetchOne( <<<'SQL' SELECT DEFAULT_COLLATE_NAME FROM information_schema.CHARACTER_SETS WHERE CHARACTER_SET_NAME = ?; SQL , [$charset], ); if ($collation !== false) { return $collation; } return null; } } src/Platforms/MySQL/CollationMetadataProvider.php000064400000000311152062402520016051 0ustar00 */ private array $cache = []; public function __construct(private readonly CollationMetadataProvider $collationMetadataProvider) { } public function getCollationCharset(string $collation): ?string { if (array_key_exists($collation, $this->cache)) { return $this->cache[$collation]; } return $this->cache[$collation] = $this->collationMetadataProvider->getCollationCharset($collation); } } src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php000064400000001476152062402520025206 0ustar00connection->fetchOne( <<<'SQL' SELECT CHARACTER_SET_NAME FROM information_schema.COLLATIONS WHERE COLLATION_NAME = ?; SQL , [$collation], ); if ($charset !== false) { return $charset; } return null; } } src/Platforms/MySQL/Comparator.php000064400000006241152062402520013070 0ustar00normalizeTable($oldTable), $this->normalizeTable($newTable), ); } private function normalizeTable(Table $table): Table { $charset = $table->getOption('charset'); $collation = $table->getOption('collation'); if ($charset === null && $collation !== null) { $charset = $this->collationMetadataProvider->getCollationCharset($collation); } elseif ($charset !== null && $collation === null) { $collation = $this->charsetMetadataProvider->getDefaultCharsetCollation($charset); } elseif ($charset === null && $collation === null) { $charset = $this->defaultTableOptions->getCharset(); $collation = $this->defaultTableOptions->getCollation(); } $tableOptions = [ 'charset' => $charset, 'collation' => $collation, ]; $table = clone $table; foreach ($table->getColumns() as $column) { $originalOptions = $column->getPlatformOptions(); $normalizedOptions = $this->normalizeOptions($originalOptions); $overrideOptions = array_diff_assoc($normalizedOptions, $tableOptions); if ($overrideOptions === $originalOptions) { continue; } $column->setPlatformOptions($overrideOptions); } return $table; } /** * @param array $options * * @return array */ private function normalizeOptions(array $options): array { if (isset($options['charset']) && ! isset($options['collation'])) { $options['collation'] = $this->charsetMetadataProvider->getDefaultCharsetCollation($options['charset']); } elseif (isset($options['collation']) && ! isset($options['charset'])) { $options['charset'] = $this->collationMetadataProvider->getCollationCharset($options['collation']); } return $options; } } src/Platforms/MySQL/DefaultTableOptions.php000064400000000640152062402520014666 0ustar00charset; } public function getCollation(): string { return $this->collation; } } src/Platforms/MySQL80Platform.php000064400000001160152062402520012631 0ustar00getQuotedName($this)]; } protected function createReservedKeywordsList(): KeywordList { return new MySQLKeywords(); } } src/Platforms/OraclePlatform.php000064400000057723152062402520012741 0ustar00multiplyInterval($interval, 3); break; case DateIntervalUnit::YEAR: $interval = $this->multiplyInterval($interval, 12); break; } return 'ADD_MONTHS(' . $date . ', ' . $operator . $interval . ')'; default: $calculationClause = ''; switch ($unit) { case DateIntervalUnit::SECOND: $calculationClause = '/24/60/60'; break; case DateIntervalUnit::MINUTE: $calculationClause = '/24/60'; break; case DateIntervalUnit::HOUR: $calculationClause = '/24'; break; case DateIntervalUnit::WEEK: $calculationClause = '*7'; break; } return '(' . $date . $operator . $interval . $calculationClause . ')'; } } public function getDateDiffExpression(string $date1, string $date2): string { return sprintf('TRUNC(%s) - TRUNC(%s)', $date1, $date2); } public function getBitAndComparisonExpression(string $value1, string $value2): string { return 'BITAND(' . $value1 . ', ' . $value2 . ')'; } public function getCurrentDatabaseExpression(): string { return "SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')"; } public function getBitOrComparisonExpression(string $value1, string $value2): string { return '(' . $value1 . '-' . $this->getBitAndComparisonExpression($value1, $value2) . '+' . $value2 . ')'; } public function getCreatePrimaryKeySQL(Index $index, string $table): string { return 'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $index->getQuotedName($this) . ' PRIMARY KEY (' . implode(', ', $index->getQuotedColumns($this)) . ')'; } /** * {@inheritDoc} * * Need to specifiy minvalue, since start with is hidden in the system and MINVALUE <= START WITH. * Therefore we can use MINVALUE to be able to get a hint what START WITH was for later introspection * in {@see listSequences()} */ public function getCreateSequenceSQL(Sequence $sequence): string { return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . ' START WITH ' . $sequence->getInitialValue() . ' MINVALUE ' . $sequence->getInitialValue() . ' INCREMENT BY ' . $sequence->getAllocationSize() . $this->getSequenceCacheSQL($sequence); } public function getAlterSequenceSQL(Sequence $sequence): string { return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . ' INCREMENT BY ' . $sequence->getAllocationSize() . $this->getSequenceCacheSQL($sequence); } /** * Cache definition for sequences */ private function getSequenceCacheSQL(Sequence $sequence): string { if ($sequence->getCache() === 0) { return ' NOCACHE'; } if ($sequence->getCache() === 1) { return ' NOCACHE'; } if ($sequence->getCache() > 1) { return ' CACHE ' . $sequence->getCache(); } return ''; } public function getSequenceNextValSQL(string $sequence): string { return 'SELECT ' . $sequence . '.nextval FROM DUAL'; } public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string { return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); } protected function _getTransactionIsolationLevelSQL(TransactionIsolationLevel $level): string { return match ($level) { TransactionIsolationLevel::READ_UNCOMMITTED => 'READ UNCOMMITTED', TransactionIsolationLevel::READ_COMMITTED => 'READ COMMITTED', TransactionIsolationLevel::REPEATABLE_READ, TransactionIsolationLevel::SERIALIZABLE => 'SERIALIZABLE', }; } /** * {@inheritDoc} */ public function getBooleanTypeDeclarationSQL(array $column): string { return 'NUMBER(1)'; } /** * {@inheritDoc} */ public function getIntegerTypeDeclarationSQL(array $column): string { return 'NUMBER(10)'; } /** * {@inheritDoc} */ public function getBigIntTypeDeclarationSQL(array $column): string { return 'NUMBER(20)'; } /** * {@inheritDoc} */ public function getSmallIntTypeDeclarationSQL(array $column): string { return 'NUMBER(5)'; } /** * {@inheritDoc} */ public function getDateTimeTypeDeclarationSQL(array $column): string { return 'TIMESTAMP(0)'; } /** * {@inheritDoc} */ public function getDateTimeTzTypeDeclarationSQL(array $column): string { return 'TIMESTAMP(0) WITH TIME ZONE'; } /** * {@inheritDoc} */ public function getDateTypeDeclarationSQL(array $column): string { return 'DATE'; } /** * {@inheritDoc} */ public function getTimeTypeDeclarationSQL(array $column): string { return 'DATE'; } /** * {@inheritDoc} */ protected function _getCommonIntegerTypeDeclarationSQL(array $column): string { return ''; } protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string { if ($length === null) { throw ColumnLengthRequired::new($this, 'VARCHAR2'); } return sprintf('VARCHAR2(%d)', $length); } protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string { if ($length === null) { throw ColumnLengthRequired::new($this, 'RAW'); } return sprintf('RAW(%d)', $length); } protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string { return $this->getBinaryTypeDeclarationSQLSnippet($length); } /** * {@inheritDoc} */ public function getClobTypeDeclarationSQL(array $column): string { return 'CLOB'; } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListDatabasesSQL(): string { return 'SELECT username FROM all_users'; } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListSequencesSQL(string $database): string { return 'SELECT SEQUENCE_NAME, MIN_VALUE, INCREMENT_BY FROM SYS.ALL_SEQUENCES WHERE SEQUENCE_OWNER = ' . $this->quoteStringLiteral( $this->normalizeIdentifier($database)->getName(), ); } /** * {@inheritDoc} */ protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array { $indexes = $options['indexes'] ?? []; $options['indexes'] = []; $sql = parent::_getCreateTableSQL($name, $columns, $options); foreach ($columns as $column) { if (isset($column['sequence'])) { $sql[] = $this->getCreateSequenceSQL($column['sequence']); } if ( empty($column['autoincrement']) ) { continue; } $sql = array_merge($sql, $this->getCreateAutoincrementSql($column['name'], $name)); } foreach ($indexes as $index) { $sql[] = $this->getCreateIndexSQL($index, $name); } return $sql; } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListViewsSQL(string $database): string { return 'SELECT view_name, text FROM sys.user_views'; } /** @return array */ protected function getCreateAutoincrementSql(string $name, string $table, int $start = 1): array { $tableIdentifier = $this->normalizeIdentifier($table); $quotedTableName = $tableIdentifier->getQuotedName($this); $unquotedTableName = $tableIdentifier->getName(); $nameIdentifier = $this->normalizeIdentifier($name); $quotedName = $nameIdentifier->getQuotedName($this); $unquotedName = $nameIdentifier->getName(); $sql = []; $autoincrementIdentifierName = $this->getAutoincrementIdentifierName($tableIdentifier); $idx = new Index($autoincrementIdentifierName, [$quotedName], true, true); $sql[] = "DECLARE constraints_Count NUMBER; BEGIN SELECT COUNT(CONSTRAINT_NAME) INTO constraints_Count FROM USER_CONSTRAINTS WHERE TABLE_NAME = '" . $unquotedTableName . "' AND CONSTRAINT_TYPE = 'P'; IF constraints_Count = 0 OR constraints_Count = '' THEN EXECUTE IMMEDIATE '" . $this->getCreateIndexSQL($idx, $quotedTableName) . "'; END IF; END;"; $sequenceName = $this->getIdentitySequenceName( $tableIdentifier->isQuoted() ? $quotedTableName : $unquotedTableName, ); $sequence = new Sequence($sequenceName, $start); $sql[] = $this->getCreateSequenceSQL($sequence); $sql[] = 'CREATE TRIGGER ' . $autoincrementIdentifierName . ' BEFORE INSERT ON ' . $quotedTableName . ' FOR EACH ROW DECLARE last_Sequence NUMBER; last_InsertID NUMBER; BEGIN IF (:NEW.' . $quotedName . ' IS NULL OR :NEW.' . $quotedName . ' = 0) THEN SELECT ' . $sequenceName . '.NEXTVAL INTO :NEW.' . $quotedName . ' FROM DUAL; ELSE SELECT NVL(Last_Number, 0) INTO last_Sequence FROM User_Sequences WHERE Sequence_Name = \'' . $sequence->getName() . '\'; SELECT :NEW.' . $quotedName . ' INTO last_InsertID FROM DUAL; WHILE (last_InsertID > last_Sequence) LOOP SELECT ' . $sequenceName . '.NEXTVAL INTO last_Sequence FROM DUAL; END LOOP; SELECT ' . $sequenceName . '.NEXTVAL INTO last_Sequence FROM DUAL; END IF; END;'; return $sql; } /** * @internal The method should be only used from within the OracleSchemaManager class hierarchy. * * Returns the SQL statements to drop the autoincrement for the given table name. * * @param string $table The table name to drop the autoincrement for. * * @return string[] */ public function getDropAutoincrementSql(string $table): array { $table = $this->normalizeIdentifier($table); $autoincrementIdentifierName = $this->getAutoincrementIdentifierName($table); $identitySequenceName = $this->getIdentitySequenceName( $table->isQuoted() ? $table->getQuotedName($this) : $table->getName(), ); return [ 'DROP TRIGGER ' . $autoincrementIdentifierName, $this->getDropSequenceSQL($identitySequenceName), $this->getDropConstraintSQL($autoincrementIdentifierName, $table->getQuotedName($this)), ]; } /** * Normalizes the given identifier. * * Uppercases the given identifier if it is not quoted by intention * to reflect Oracle's internal auto uppercasing strategy of unquoted identifiers. * * @param string $name The identifier to normalize. */ private function normalizeIdentifier(string $name): Identifier { $identifier = new Identifier($name); return $identifier->isQuoted() ? $identifier : new Identifier(strtoupper($name)); } /** * Adds suffix to identifier, * * if the new string exceeds max identifier length, * keeps $suffix, cuts from $identifier as much as the part exceeding. */ private function addSuffix(string $identifier, string $suffix): string { $maxPossibleLengthWithoutSuffix = $this->getMaxIdentifierLength() - strlen($suffix); if (strlen($identifier) > $maxPossibleLengthWithoutSuffix) { $identifier = substr($identifier, 0, $maxPossibleLengthWithoutSuffix); } return $identifier . $suffix; } /** * Returns the autoincrement primary key identifier name for the given table identifier. * * Quotes the autoincrement primary key identifier name * if the given table name is quoted by intention. */ private function getAutoincrementIdentifierName(Identifier $table): string { $identifierName = $this->addSuffix($table->getName(), '_AI_PK'); return $table->isQuoted() ? $this->quoteSingleIdentifier($identifierName) : $identifierName; } public function getDropForeignKeySQL(string $foreignKey, string $table): string { return $this->getDropConstraintSQL($foreignKey, $table); } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey): string { $referentialAction = ''; if ($foreignKey->hasOption('onDelete')) { $referentialAction = $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onDelete')); } if ($referentialAction !== '') { return ' ON DELETE ' . $referentialAction; } return ''; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getForeignKeyReferentialActionSQL(string $action): string { $action = strtoupper($action); return match ($action) { 'RESTRICT', 'NO ACTION' => '', 'CASCADE', 'SET NULL' => $action, default => throw new InvalidArgumentException(sprintf('Invalid foreign key action "%s".', $action)), }; } public function getCreateDatabaseSQL(string $name): string { return 'CREATE USER ' . $name; } public function getDropDatabaseSQL(string $name): string { return 'DROP USER ' . $name . ' CASCADE'; } /** * {@inheritDoc} */ public function getAlterTableSQL(TableDiff $diff): array { $sql = []; $commentsSQL = []; $columnSql = []; $addColumnSQL = []; $tableNameSQL = $diff->getOldTable()->getQuotedName($this); foreach ($diff->getAddedColumns() as $column) { $addColumnSQL[] = $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); $comment = $column->getComment(); if ($comment === '') { continue; } $commentsSQL[] = $this->getCommentOnColumnSQL( $tableNameSQL, $column->getQuotedName($this), $comment, ); } if (count($addColumnSQL) > 0) { $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ADD (' . implode(', ', $addColumnSQL) . ')'; } $modifyColumnSQL = []; foreach ($diff->getModifiedColumns() as $columnDiff) { $newColumn = $columnDiff->getNewColumn(); $oldColumn = $columnDiff->getOldColumn(); $newColumnProperties = $newColumn->toArray(); $oldColumnProperties = $oldColumn->toArray(); $oldSQL = $this->getColumnDeclarationSQL('', $oldColumnProperties); $newSQL = $this->getColumnDeclarationSQL('', $newColumnProperties); if ($newSQL !== $oldSQL) { if (! $columnDiff->hasNotNullChanged()) { unset($newColumnProperties['notnull']); $newSQL = $this->getColumnDeclarationSQL('', $newColumnProperties); } $modifyColumnSQL[] = $newColumn->getQuotedName($this) . $newSQL; } if (! $columnDiff->hasCommentChanged()) { continue; } $commentsSQL[] = $this->getCommentOnColumnSQL( $tableNameSQL, $newColumn->getQuotedName($this), $newColumn->getComment(), ); } if (count($modifyColumnSQL) > 0) { $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' MODIFY (' . implode(', ', $modifyColumnSQL) . ')'; } foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { $oldColumnName = new Identifier($oldColumnName); $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this); } $dropColumnSQL = []; foreach ($diff->getDroppedColumns() as $column) { $dropColumnSQL[] = $column->getQuotedName($this); } if (count($dropColumnSQL) > 0) { $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' DROP (' . implode(', ', $dropColumnSQL) . ')'; } return array_merge( $this->getPreAlterTableIndexForeignKeySQL($diff), $sql, $commentsSQL, $this->getPostAlterTableIndexForeignKeySQL($diff), $columnSql, ); } /** * {@inheritDoc} * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getColumnDeclarationSQL(string $name, array $column): string { if (isset($column['columnDefinition'])) { $declaration = $column['columnDefinition']; } else { $default = $this->getDefaultValueDeclarationSQL($column); $notnull = ''; if (isset($column['notnull'])) { $notnull = $column['notnull'] ? ' NOT NULL' : ' NULL'; } $typeDecl = $column['type']->getSQLDeclaration($column, $this); $declaration = $typeDecl . $default . $notnull; } return $name . ' ' . $declaration; } /** * {@inheritDoc} */ protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array { if (str_contains($tableName, '.')) { [$schema] = explode('.', $tableName); $oldIndexName = $schema . '.' . $oldIndexName; } return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)]; } protected function getIdentitySequenceName(string $tableName): string { $table = new Identifier($tableName); // No usage of column name to preserve BC compatibility with <2.5 $identitySequenceName = $this->addSuffix($table->getName(), '_SEQ'); if ($table->isQuoted()) { $identitySequenceName = '"' . $identitySequenceName . '"'; } $identitySequenceIdentifier = $this->normalizeIdentifier($identitySequenceName); return $identitySequenceIdentifier->getQuotedName($this); } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function supportsCommentOnStatement(): bool { return true; } protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string { if ($offset > 0) { $query .= sprintf(' OFFSET %d ROWS', $offset); } if ($limit !== null) { $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit); } return $query; } public function getCreateTemporaryTableSnippetSQL(): string { return 'CREATE GLOBAL TEMPORARY TABLE'; } public function getDateTimeTzFormatString(): string { return 'Y-m-d H:i:sP'; } public function getDateFormatString(): string { return 'Y-m-d 00:00:00'; } public function getTimeFormatString(): string { return '1900-01-01 H:i:s'; } public function getMaxIdentifierLength(): int { return 128; } public function supportsSequences(): bool { return true; } public function supportsReleaseSavepoints(): bool { return false; } public function getTruncateTableSQL(string $tableName, bool $cascade = false): string { $tableIdentifier = new Identifier($tableName); return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this); } public function getDummySelectSQL(string $expression = '1'): string { return sprintf('SELECT %s FROM DUAL', $expression); } protected function initializeDoctrineTypeMappings(): void { $this->doctrineTypeMapping = [ 'binary_double' => Types::FLOAT, 'binary_float' => Types::FLOAT, 'binary_integer' => Types::BOOLEAN, 'blob' => Types::BLOB, 'char' => Types::STRING, 'clob' => Types::TEXT, 'date' => Types::DATE_MUTABLE, 'float' => Types::FLOAT, 'integer' => Types::INTEGER, 'long' => Types::STRING, 'long raw' => Types::BLOB, 'nchar' => Types::STRING, 'nclob' => Types::TEXT, 'number' => Types::INTEGER, 'nvarchar2' => Types::STRING, 'pls_integer' => Types::BOOLEAN, 'raw' => Types::BINARY, 'rowid' => Types::STRING, 'timestamp' => Types::DATETIME_MUTABLE, 'timestamptz' => Types::DATETIMETZ_MUTABLE, 'urowid' => Types::STRING, 'varchar' => Types::STRING, 'varchar2' => Types::STRING, ]; } public function releaseSavePoint(string $savepoint): string { return ''; } protected function createReservedKeywordsList(): KeywordList { return new OracleKeywords(); } /** * {@inheritDoc} */ public function getBlobTypeDeclarationSQL(array $column): string { return 'BLOB'; } public function createSchemaManager(Connection $connection): OracleSchemaManager { return new OracleSchemaManager($connection, $this); } } src/Platforms/PostgreSQLPlatform.php000064400000057321152062402520013531 0ustar00 [ 't', 'true', 'y', 'yes', 'on', '1', ], 'false' => [ 'f', 'false', 'n', 'no', 'off', '0', ], ]; /** * PostgreSQL has different behavior with some drivers * with regard to how booleans have to be handled. * * Enables use of 'true'/'false' or otherwise 1 and 0 instead. */ public function setUseBooleanTrueFalseStrings(bool $flag): void { $this->useBooleanTrueFalseStrings = $flag; } public function getRegexpExpression(): string { return 'SIMILAR TO'; } public function getLocateExpression(string $string, string $substring, ?string $start = null): string { if ($start !== null) { $string = $this->getSubstringExpression($string, $start); return 'CASE WHEN (POSITION(' . $substring . ' IN ' . $string . ') = 0) THEN 0' . ' ELSE (POSITION(' . $substring . ' IN ' . $string . ') + ' . $start . ' - 1) END'; } return sprintf('POSITION(%s IN %s)', $substring, $string); } protected function getDateArithmeticIntervalExpression( string $date, string $operator, string $interval, DateIntervalUnit $unit, ): string { if ($unit === DateIntervalUnit::QUARTER) { $interval = $this->multiplyInterval($interval, 3); $unit = DateIntervalUnit::MONTH; } return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit->value . "')::interval)"; } public function getDateDiffExpression(string $date1, string $date2): string { return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))'; } public function getCurrentDatabaseExpression(): string { return 'CURRENT_DATABASE()'; } public function supportsSequences(): bool { return true; } public function supportsSchemas(): bool { return true; } public function supportsIdentityColumns(): bool { return true; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function supportsPartialIndexes(): bool { return true; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function supportsCommentOnStatement(): bool { return true; } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListDatabasesSQL(): string { return 'SELECT datname FROM pg_database'; } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListSequencesSQL(string $database): string { return 'SELECT sequence_name AS relname, sequence_schema AS schemaname, minimum_value AS min_value, increment AS increment_by FROM information_schema.sequences WHERE sequence_catalog = ' . $this->quoteStringLiteral($database) . " AND sequence_schema NOT LIKE 'pg\_%' AND sequence_schema != 'information_schema'"; } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListViewsSQL(string $database): string { return 'SELECT quote_ident(table_name) AS viewname, table_schema AS schemaname, view_definition AS definition FROM information_schema.views WHERE view_definition IS NOT NULL'; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey): string { $query = ''; if ($foreignKey->hasOption('match')) { $query .= ' MATCH ' . $foreignKey->getOption('match'); } $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey); if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) { $query .= ' DEFERRABLE'; } else { $query .= ' NOT DEFERRABLE'; } if ( $foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false ) { $query .= ' INITIALLY DEFERRED'; } else { $query .= ' INITIALLY IMMEDIATE'; } return $query; } /** * {@inheritDoc} */ public function getAlterTableSQL(TableDiff $diff): array { $sql = []; $commentsSQL = []; $columnSql = []; $table = $diff->getOldTable(); $tableNameSQL = $table->getQuotedName($this); foreach ($diff->getAddedColumns() as $addedColumn) { $query = 'ADD ' . $this->getColumnDeclarationSQL( $addedColumn->getQuotedName($this), $addedColumn->toArray(), ); $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; $comment = $addedColumn->getComment(); if ($comment === '') { continue; } $commentsSQL[] = $this->getCommentOnColumnSQL( $tableNameSQL, $addedColumn->getQuotedName($this), $comment, ); } foreach ($diff->getDroppedColumns() as $droppedColumn) { $query = 'DROP ' . $droppedColumn->getQuotedName($this); $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; } foreach ($diff->getModifiedColumns() as $columnDiff) { $oldColumn = $columnDiff->getOldColumn(); $newColumn = $columnDiff->getNewColumn(); $oldColumnName = $oldColumn->getQuotedName($this); if ( $columnDiff->hasTypeChanged() || $columnDiff->hasPrecisionChanged() || $columnDiff->hasScaleChanged() || $columnDiff->hasFixedChanged() ) { $type = $newColumn->getType(); // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type $columnDefinition = $newColumn->toArray(); $columnDefinition['autoincrement'] = false; // here was a server version check before, but DBAL API does not support this anymore. $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this); $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; } if ($columnDiff->hasDefaultChanged()) { $defaultClause = $newColumn->getDefault() === null ? ' DROP DEFAULT' : ' SET' . $this->getDefaultValueDeclarationSQL($newColumn->toArray()); $query = 'ALTER ' . $oldColumnName . $defaultClause; $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; } if ($columnDiff->hasNotNullChanged()) { $query = 'ALTER ' . $oldColumnName . ' ' . ($newColumn->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL'; $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; } if ($columnDiff->hasAutoIncrementChanged()) { if ($newColumn->getAutoincrement()) { $query = 'ADD GENERATED BY DEFAULT AS IDENTITY'; } else { $query = 'DROP IDENTITY'; } $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ALTER ' . $oldColumnName . ' ' . $query; } $newComment = $newColumn->getComment(); $oldComment = $columnDiff->getOldColumn()->getComment(); if ($columnDiff->hasCommentChanged() || $oldComment !== $newComment) { $commentsSQL[] = $this->getCommentOnColumnSQL( $tableNameSQL, $newColumn->getQuotedName($this), $newComment, ); } if (! $columnDiff->hasLengthChanged()) { continue; } $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $newColumn->getType()->getSQLDeclaration($newColumn->toArray(), $this); $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; } foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { $oldColumnName = new Identifier($oldColumnName); $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . ' TO ' . $column->getQuotedName($this); } return array_merge( $this->getPreAlterTableIndexForeignKeySQL($diff), $sql, $commentsSQL, $this->getPostAlterTableIndexForeignKeySQL($diff), $columnSql, ); } /** * {@inheritDoc} */ protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array { if (str_contains($tableName, '.')) { [$schema] = explode('.', $tableName); $oldIndexName = $schema . '.' . $oldIndexName; } return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)]; } public function getCreateSequenceSQL(Sequence $sequence): string { return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . ' INCREMENT BY ' . $sequence->getAllocationSize() . ' MINVALUE ' . $sequence->getInitialValue() . ' START ' . $sequence->getInitialValue() . $this->getSequenceCacheSQL($sequence); } public function getAlterSequenceSQL(Sequence $sequence): string { return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . ' INCREMENT BY ' . $sequence->getAllocationSize() . $this->getSequenceCacheSQL($sequence); } /** * Cache definition for sequences */ private function getSequenceCacheSQL(Sequence $sequence): string { if ($sequence->getCache() > 1) { return ' CACHE ' . $sequence->getCache(); } return ''; } public function getDropSequenceSQL(string $name): string { return parent::getDropSequenceSQL($name) . ' CASCADE'; } public function getDropForeignKeySQL(string $foreignKey, string $table): string { return $this->getDropConstraintSQL($foreignKey, $table); } public function getDropIndexSQL(string $name, string $table): string { if ($name === '"primary"') { $constraintName = $table . '_pkey'; return $this->getDropConstraintSQL($constraintName, $table); } return parent::getDropIndexSQL($name, $table); } /** * {@inheritDoc} */ protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array { $queryFields = $this->getColumnDeclarationListSQL($columns); if (isset($options['primary']) && ! empty($options['primary'])) { $keyColumns = array_unique(array_values($options['primary'])); $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; } $unlogged = isset($options['unlogged']) && $options['unlogged'] === true ? ' UNLOGGED' : ''; $query = 'CREATE' . $unlogged . ' TABLE ' . $name . ' (' . $queryFields . ')'; $sql = [$query]; if (isset($options['indexes']) && ! empty($options['indexes'])) { foreach ($options['indexes'] as $index) { $sql[] = $this->getCreateIndexSQL($index, $name); } } if (isset($options['uniqueConstraints'])) { foreach ($options['uniqueConstraints'] as $uniqueConstraint) { $sql[] = $this->getCreateUniqueConstraintSQL($uniqueConstraint, $name); } } if (isset($options['foreignKeys'])) { foreach ($options['foreignKeys'] as $definition) { $sql[] = $this->getCreateForeignKeySQL($definition, $name); } } return $sql; } /** * Converts a single boolean value. * * First converts the value to its native PHP boolean type * and passes it to the given callback function to be reconverted * into any custom representation. * * @param mixed $value The value to convert. * @param callable $callback The callback function to use for converting the real boolean value. * * @throws UnexpectedValueException */ private function convertSingleBooleanValue(mixed $value, callable $callback): mixed { if ($value === null) { return $callback(null); } if (is_bool($value) || is_numeric($value)) { return $callback((bool) $value); } if (! is_string($value)) { return $callback(true); } /** * Better safe than sorry: http://php.net/in_array#106319 */ if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) { return $callback(false); } if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) { return $callback(true); } throw new UnexpectedValueException(sprintf( 'Unrecognized boolean literal, %s given.', $value, )); } /** * Converts one or multiple boolean values. * * First converts the value(s) to their native PHP boolean type * and passes them to the given callback function to be reconverted * into any custom representation. * * @param mixed $item The value(s) to convert. * @param callable $callback The callback function to use for converting the real boolean value(s). */ private function doConvertBooleans(mixed $item, callable $callback): mixed { if (is_array($item)) { foreach ($item as $key => $value) { $item[$key] = $this->convertSingleBooleanValue($value, $callback); } return $item; } return $this->convertSingleBooleanValue($item, $callback); } /** * {@inheritDoc} * * Postgres wants boolean values converted to the strings 'true'/'false'. */ public function convertBooleans(mixed $item): mixed { if (! $this->useBooleanTrueFalseStrings) { return parent::convertBooleans($item); } return $this->doConvertBooleans( $item, /** @param mixed $value */ static function ($value): string { if ($value === null) { return 'NULL'; } return $value === true ? 'true' : 'false'; }, ); } public function convertBooleansToDatabaseValue(mixed $item): mixed { if (! $this->useBooleanTrueFalseStrings) { return parent::convertBooleansToDatabaseValue($item); } return $this->doConvertBooleans( $item, /** @param mixed $value */ static function ($value): ?int { return $value === null ? null : (int) $value; }, ); } /** * @param T $item * * @return (T is null ? null : bool) * * @template T */ public function convertFromBoolean(mixed $item): ?bool { if (in_array($item, $this->booleanLiterals['false'], true)) { return false; } return parent::convertFromBoolean($item); } public function getSequenceNextValSQL(string $sequence): string { return "SELECT NEXTVAL('" . $sequence . "')"; } public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string { return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); } /** * {@inheritDoc} */ public function getBooleanTypeDeclarationSQL(array $column): string { return 'BOOLEAN'; } /** * {@inheritDoc} */ public function getIntegerTypeDeclarationSQL(array $column): string { return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ public function getBigIntTypeDeclarationSQL(array $column): string { return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ public function getSmallIntTypeDeclarationSQL(array $column): string { return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ public function getGuidTypeDeclarationSQL(array $column): string { return 'UUID'; } /** * {@inheritDoc} */ public function getDateTimeTypeDeclarationSQL(array $column): string { return 'TIMESTAMP(0) WITHOUT TIME ZONE'; } /** * {@inheritDoc} */ public function getDateTimeTzTypeDeclarationSQL(array $column): string { return 'TIMESTAMP(0) WITH TIME ZONE'; } /** * {@inheritDoc} */ public function getDateTypeDeclarationSQL(array $column): string { return 'DATE'; } /** * {@inheritDoc} */ public function getTimeTypeDeclarationSQL(array $column): string { return 'TIME(0) WITHOUT TIME ZONE'; } /** * {@inheritDoc} */ protected function _getCommonIntegerTypeDeclarationSQL(array $column): string { if (! empty($column['autoincrement'])) { return ' GENERATED BY DEFAULT AS IDENTITY'; } return ''; } protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string { $sql = 'VARCHAR'; if ($length !== null) { $sql .= sprintf('(%d)', $length); } return $sql; } protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string { return 'BYTEA'; } protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string { return 'BYTEA'; } /** * {@inheritDoc} */ public function getClobTypeDeclarationSQL(array $column): string { return 'TEXT'; } public function getDateTimeTzFormatString(): string { return 'Y-m-d H:i:sO'; } public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string { return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)'; } public function getTruncateTableSQL(string $tableName, bool $cascade = false): string { $tableIdentifier = new Identifier($tableName); $sql = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this); if ($cascade) { $sql .= ' CASCADE'; } return $sql; } protected function initializeDoctrineTypeMappings(): void { $this->doctrineTypeMapping = [ 'bigint' => Types::BIGINT, 'bigserial' => Types::BIGINT, 'bool' => Types::BOOLEAN, 'boolean' => Types::BOOLEAN, 'bpchar' => Types::STRING, 'bytea' => Types::BLOB, 'char' => Types::STRING, 'date' => Types::DATE_MUTABLE, 'datetime' => Types::DATETIME_MUTABLE, 'decimal' => Types::DECIMAL, 'double' => Types::FLOAT, 'double precision' => Types::FLOAT, 'float' => Types::FLOAT, 'float4' => Types::FLOAT, 'float8' => Types::FLOAT, 'inet' => Types::STRING, 'int' => Types::INTEGER, 'int2' => Types::SMALLINT, 'int4' => Types::INTEGER, 'int8' => Types::BIGINT, 'integer' => Types::INTEGER, 'interval' => Types::STRING, 'json' => Types::JSON, 'jsonb' => Types::JSON, 'money' => Types::DECIMAL, 'numeric' => Types::DECIMAL, 'serial' => Types::INTEGER, 'serial4' => Types::INTEGER, 'serial8' => Types::BIGINT, 'real' => Types::FLOAT, 'smallint' => Types::SMALLINT, 'text' => Types::TEXT, 'time' => Types::TIME_MUTABLE, 'timestamp' => Types::DATETIME_MUTABLE, 'timestamptz' => Types::DATETIMETZ_MUTABLE, 'timetz' => Types::TIME_MUTABLE, 'tsvector' => Types::TEXT, 'uuid' => Types::GUID, 'varchar' => Types::STRING, 'year' => Types::DATE_MUTABLE, '_varchar' => Types::STRING, ]; } protected function createReservedKeywordsList(): KeywordList { return new PostgreSQLKeywords(); } /** * {@inheritDoc} */ public function getBlobTypeDeclarationSQL(array $column): string { return 'BYTEA'; } /** * {@inheritDoc} * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getDefaultValueDeclarationSQL(array $column): string { if (isset($column['autoincrement']) && $column['autoincrement'] === true) { return ''; } return parent::getDefaultValueDeclarationSQL($column); } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function supportsColumnCollation(): bool { return true; } /** * {@inheritDoc} */ public function getJsonTypeDeclarationSQL(array $column): string { if (! empty($column['jsonb'])) { return 'JSONB'; } return 'JSON'; } public function createSchemaManager(Connection $connection): PostgreSQLSchemaManager { return new PostgreSQLSchemaManager($connection, $this); } } src/Platforms/SQLServer/Comparator.php000064400000002613152062402520013750 0ustar00normalizeColumns($oldTable), $this->normalizeColumns($newTable), ); } private function normalizeColumns(Table $table): Table { $table = clone $table; foreach ($table->getColumns() as $column) { $options = $column->getPlatformOptions(); if (! isset($options['collation']) || $options['collation'] !== $this->databaseCollation) { continue; } unset($options['collation']); $column->setPlatformOptions($options); } return $table; } } src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php000064400000004100152062402520020534 0ustar00isDistinct()) { $parts[] = 'DISTINCT'; } $parts[] = implode(', ', $query->getColumns()); $from = $query->getFrom(); if (count($from) > 0) { $parts[] = 'FROM ' . implode(', ', $from); } $forUpdate = $query->getForUpdate(); if ($forUpdate !== null) { $with = ['UPDLOCK', 'ROWLOCK']; if ($forUpdate->getConflictResolutionMode() === ConflictResolutionMode::SKIP_LOCKED) { $with[] = 'READPAST'; } $parts[] = 'WITH (' . implode(', ', $with) . ')'; } $where = $query->getWhere(); if ($where !== null) { $parts[] = 'WHERE ' . $where; } $groupBy = $query->getGroupBy(); if (count($groupBy) > 0) { $parts[] = 'GROUP BY ' . implode(', ', $groupBy); } $having = $query->getHaving(); if ($having !== null) { $parts[] = 'HAVING ' . $having; } $orderBy = $query->getOrderBy(); if (count($orderBy) > 0) { $parts[] = 'ORDER BY ' . implode(', ', $orderBy); } $sql = implode(' ', $parts); $limit = $query->getLimit(); if ($limit->isDefined()) { $sql = $this->platform->modifyLimitQuery($sql, $limit->getMaxResults(), $limit->getFirstResult()); } return $sql; } } src/Platforms/SQLServerPlatform.php000064400000122674152062402520013360 0ustar00getConvertExpression('date', 'GETDATE()'); } public function getCurrentTimeSQL(): string { return $this->getConvertExpression('time', 'GETDATE()'); } /** * Returns an expression that converts an expression of one data type to another. * * @param string $dataType The target native data type. Alias data types cannot be used. * @param string $expression The SQL expression to convert. */ private function getConvertExpression(string $dataType, string $expression): string { return sprintf('CONVERT(%s, %s)', $dataType, $expression); } protected function getDateArithmeticIntervalExpression( string $date, string $operator, string $interval, DateIntervalUnit $unit, ): string { $factorClause = ''; if ($operator === '-') { $factorClause = '-1 * '; } return 'DATEADD(' . $unit->value . ', ' . $factorClause . $interval . ', ' . $date . ')'; } public function getDateDiffExpression(string $date1, string $date2): string { return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')'; } /** * {@inheritDoc} * * Microsoft SQL Server supports this through AUTO_INCREMENT columns. */ public function supportsIdentityColumns(): bool { return true; } public function supportsReleaseSavepoints(): bool { return false; } public function supportsSchemas(): bool { return true; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function supportsColumnCollation(): bool { return true; } public function supportsSequences(): bool { return true; } public function getAlterSequenceSQL(Sequence $sequence): string { return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . ' INCREMENT BY ' . $sequence->getAllocationSize(); } public function getCreateSequenceSQL(Sequence $sequence): string { return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . ' START WITH ' . $sequence->getInitialValue() . ' INCREMENT BY ' . $sequence->getAllocationSize() . ' MINVALUE ' . $sequence->getInitialValue(); } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListSequencesSQL(string $database): string { return 'SELECT seq.name, CAST( seq.increment AS VARCHAR(MAX) ) AS increment, -- CAST avoids driver error for sql_variant type CAST( seq.start_value AS VARCHAR(MAX) ) AS start_value -- CAST avoids driver error for sql_variant type FROM sys.sequences AS seq'; } public function getSequenceNextValSQL(string $sequence): string { return 'SELECT NEXT VALUE FOR ' . $sequence; } public function getDropForeignKeySQL(string $foreignKey, string $table): string { return $this->getDropConstraintSQL($foreignKey, $table); } public function getDropIndexSQL(string $name, string $table): string { return 'DROP INDEX ' . $name . ' ON ' . $table; } /** * {@inheritDoc} */ protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array { $defaultConstraintsSql = []; $commentsSql = []; $tableComment = $options['comment'] ?? null; if ($tableComment !== null) { $commentsSql[] = $this->getCommentOnTableSQL($name, $tableComment); } // @todo does other code breaks because of this? // force primary keys to be not null foreach ($columns as &$column) { if (! empty($column['primary'])) { $column['notnull'] = true; } // Build default constraints SQL statements. if (isset($column['default'])) { $defaultConstraintsSql[] = 'ALTER TABLE ' . $name . ' ADD' . $this->getDefaultConstraintDeclarationSQL($column); } if (empty($column['comment']) && ! is_numeric($column['comment'])) { continue; } $commentsSql[] = $this->getCreateColumnCommentSQL($name, $column['name'], $column['comment']); } $columnListSql = $this->getColumnDeclarationListSQL($columns); if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { foreach ($options['uniqueConstraints'] as $definition) { $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($definition); } } if (isset($options['primary']) && ! empty($options['primary'])) { $flags = ''; if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) { $flags = ' NONCLUSTERED'; } $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')'; } $query = 'CREATE TABLE ' . $name . ' (' . $columnListSql; $check = $this->getCheckDeclarationSQL($columns); if (! empty($check)) { $query .= ', ' . $check; } $query .= ')'; $sql = [$query]; if (isset($options['indexes']) && ! empty($options['indexes'])) { foreach ($options['indexes'] as $index) { $sql[] = $this->getCreateIndexSQL($index, $name); } } if (isset($options['foreignKeys'])) { foreach ($options['foreignKeys'] as $definition) { $sql[] = $this->getCreateForeignKeySQL($definition, $name); } } return array_merge($sql, $commentsSql, $defaultConstraintsSql); } public function getCreatePrimaryKeySQL(Index $index, string $table): string { $sql = 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY'; if ($index->hasFlag('nonclustered')) { $sql .= ' NONCLUSTERED'; } return $sql . ' (' . implode(', ', $index->getQuotedColumns($this)) . ')'; } /** * Returns the SQL statement for creating a column comment. * * SQL Server does not support native column comments, * therefore the extended properties functionality is used * as a workaround to store them. * The property name used to store column comments is "MS_Description" * which provides compatibility with SQL Server Management Studio, * as column comments are stored in the same property there when * specifying a column's "Description" attribute. * * @param string $tableName The quoted table name to which the column belongs. * @param string $columnName The quoted column name to create the comment for. * @param string $comment The column's comment. */ protected function getCreateColumnCommentSQL(string $tableName, string $columnName, string $comment): string { if (str_contains($tableName, '.')) { [$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. * * @param mixed[] $column Column definition. */ protected function getDefaultConstraintDeclarationSQL(array $column): string { if (! isset($column['default'])) { throw new InvalidArgumentException('Incomplete column definition. "default" required.'); } $columnName = new Identifier($column['name']); return $this->getDefaultValueDeclarationSQL($column) . ' FOR ' . $columnName->getQuotedName($this); } public function getCreateIndexSQL(Index $index, string $table): string { $constraint = parent::getCreateIndexSQL($index, $table); if ($index->isUnique() && ! $index->isPrimary()) { $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index); } return $constraint; } protected function getCreateIndexSQLFlags(Index $index): string { $type = ''; if ($index->isUnique()) { $type .= 'UNIQUE '; } if ($index->hasFlag('clustered')) { $type .= 'CLUSTERED '; } elseif ($index->hasFlag('nonclustered')) { $type .= 'NONCLUSTERED '; } return $type; } /** * Extend unique key constraint with required filters */ private function _appendUniqueConstraintDefinition(string $sql, Index $index): string { $fields = []; foreach ($index->getQuotedColumns($this) as $field) { $fields[] = $field . ' IS NOT NULL'; } return $sql . ' WHERE ' . implode(' AND ', $fields); } /** * {@inheritDoc} */ public function getAlterTableSQL(TableDiff $diff): array { $queryParts = []; $sql = []; $columnSql = []; $commentsSql = []; $table = $diff->getOldTable(); $tableName = $table->getName(); foreach ($diff->getAddedColumns() as $column) { $columnProperties = $column->toArray(); $addColumnSql = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnProperties); if (isset($columnProperties['default'])) { $addColumnSql .= $this->getDefaultValueDeclarationSQL($columnProperties); } $queryParts[] = $addColumnSql; $comment = $column->getComment(); if ($comment === '') { continue; } $commentsSql[] = $this->getCreateColumnCommentSQL( $tableName, $column->getQuotedName($this), $comment, ); } foreach ($diff->getDroppedColumns() as $column) { if ($column->getDefault() !== null) { $queryParts[] = $this->getAlterTableDropDefaultConstraintClause($column); } $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); } foreach ($diff->getModifiedColumns() as $columnDiff) { $newColumn = $columnDiff->getNewColumn(); $newComment = $newColumn->getComment(); $hasNewComment = $newComment !== ''; $oldColumn = $columnDiff->getOldColumn(); $oldComment = $oldColumn->getComment(); $hasOldComment = $oldComment !== ''; if ($hasOldComment && $hasNewComment && $newComment !== $oldComment) { $commentsSql[] = $this->getAlterColumnCommentSQL( $tableName, $newColumn->getQuotedName($this), $newComment, ); } elseif ($hasOldComment && ! $hasNewComment) { $commentsSql[] = $this->getDropColumnCommentSQL( $tableName, $newColumn->getQuotedName($this), ); } elseif (! $hasOldComment && $hasNewComment) { $commentsSql[] = $this->getCreateColumnCommentSQL( $tableName, $newColumn->getQuotedName($this), $newComment, ); } $columnNameSQL = $newColumn->getQuotedName($this); $oldDeclarationSQL = $this->getColumnDeclarationSQL($columnNameSQL, $oldColumn->toArray()); $newDeclarationSQL = $this->getColumnDeclarationSQL($columnNameSQL, $newColumn->toArray()); $declarationSQLChanged = $newDeclarationSQL !== $oldDeclarationSQL; $defaultChanged = $columnDiff->hasDefaultChanged(); if (! $declarationSQLChanged && ! $defaultChanged) { continue; } $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff); if ($requireDropDefaultConstraint) { $queryParts[] = $this->getAlterTableDropDefaultConstraintClause($oldColumn); } if ($declarationSQLChanged) { $queryParts[] = 'ALTER COLUMN ' . $newDeclarationSQL; } if ( $newColumn->getDefault() === null || (! $requireDropDefaultConstraint && ! $defaultChanged) ) { continue; } $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($tableName, $newColumn); } $tableNameSQL = $table->getQuotedName($this); foreach ($diff->getRenamedColumns() as $oldColumnName => $newColumn) { $oldColumnName = new Identifier($oldColumnName); $sql[] = sprintf( "sp_rename '%s.%s', '%s', 'COLUMN'", $tableNameSQL, $oldColumnName->getQuotedName($this), $newColumn->getQuotedName($this), ); } foreach ($queryParts as $query) { $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; } return array_merge( $this->getPreAlterTableIndexForeignKeySQL($diff), $sql, $commentsSql, $this->getPostAlterTableIndexForeignKeySQL($diff), $columnSql, ); } public function getRenameTableSQL(string $oldName, string $newName): string { return sprintf( 'sp_rename %s, %s', $this->quoteStringLiteral($oldName), $this->quoteStringLiteral($newName), ); } /** * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement. * * @param string $tableName The name of the table to generate the clause for. * @param Column $column The column to generate the clause for. */ private function getAlterTableAddDefaultConstraintClause(string $tableName, Column $column): string { $columnDef = $column->toArray(); $columnDef['name'] = $column->getQuotedName($this); return 'ADD' . $this->getDefaultConstraintDeclarationSQL($columnDef); } /** * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement. */ private function getAlterTableDropDefaultConstraintClause(Column $column): string { if (! $column->hasPlatformOption(self::OPTION_DEFAULT_CONSTRAINT_NAME)) { throw new InvalidArgumentException( 'Column ' . $column->getName() . ' was not properly introspected as it has a default value' . ' but does not have the default constraint name.', ); } return 'DROP CONSTRAINT ' . $this->quoteIdentifier( $column->getPlatformOption(self::OPTION_DEFAULT_CONSTRAINT_NAME), ); } /** * Checks whether a column alteration requires dropping its default constraint first. * * Different to other database vendors SQL Server implements column default values * as constraints and therefore changes in a column's default value as well as changes * in a column's type require dropping the default constraint first before being to * alter the particular column to the new definition. */ private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff): bool { // We only need to drop an existing default constraint if we know the // column was defined with a default value before. if ($columnDiff->getOldColumn()->getDefault() === null) { return false; } // We need to drop an existing default constraint if the column was // defined with a default value before and it has changed. if ($columnDiff->hasDefaultChanged()) { return true; } // We need to drop an existing default constraint if the column was // defined with a default value before and the native column type has changed. return $columnDiff->hasTypeChanged() || $columnDiff->hasFixedChanged(); } /** * Returns the SQL statement for altering a column comment. * * SQL Server does not support native column comments, * therefore the extended properties functionality is used * as a workaround to store them. * The property name used to store column comments is "MS_Description" * which provides compatibility with SQL Server Management Studio, * as column comments are stored in the same property there when * specifying a column's "Description" attribute. * * @param string $tableName The quoted table name to which the column belongs. * @param string $columnName The quoted column name to alter the comment for. * @param string $comment The column's comment. */ protected function getAlterColumnCommentSQL(string $tableName, string $columnName, string $comment): string { if (str_contains($tableName, '.')) { [$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. */ protected function getDropColumnCommentSQL(string $tableName, string $columnName): string { if (str_contains($tableName, '.')) { [$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(string $oldIndexName, Index $index, string $tableName): array { return [sprintf( "EXEC sp_rename N'%s.%s', N'%s', N'INDEX'", $tableName, $oldIndexName, $index->getQuotedName($this), ), ]; } /** * Returns the SQL statement for adding an extended property to a database object. * * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx * * @param string $name The name of the property to add. * @param string|null $value The value of the property to add. * @param string|null $level0Type The type of the object at level 0 the property belongs to. * @param string|null $level0Name The name of the object at level 0 the property belongs to. * @param string|null $level1Type The type of the object at level 1 the property belongs to. * @param string|null $level1Name The name of the object at level 1 the property belongs to. * @param string|null $level2Type The type of the object at level 2 the property belongs to. * @param string|null $level2Name The name of the object at level 2 the property belongs to. */ protected function getAddExtendedPropertySQL( string $name, ?string $value = null, ?string $level0Type = null, ?string $level0Name = null, ?string $level1Type = null, ?string $level1Name = null, ?string $level2Type = null, ?string $level2Name = null, ): string { return 'EXEC sp_addextendedproperty ' . 'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((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. * * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx * * @param string $name The name of the property to drop. * @param string|null $level0Type The type of the object at level 0 the property belongs to. * @param string|null $level0Name The name of the object at level 0 the property belongs to. * @param string|null $level1Type The type of the object at level 1 the property belongs to. * @param string|null $level1Name The name of the object at level 1 the property belongs to. * @param string|null $level2Type The type of the object at level 2 the property belongs to. * @param string|null $level2Name The name of the object at level 2 the property belongs to. */ protected function getDropExtendedPropertySQL( string $name, ?string $level0Type = null, ?string $level0Name = null, ?string $level1Type = null, ?string $level1Name = null, ?string $level2Type = null, ?string $level2Name = null, ): string { return 'EXEC sp_dropextendedproperty ' . 'N' . $this->quoteStringLiteral($name) . ', ' . 'N' . $this->quoteStringLiteral((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. * * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx * * @param string $name The name of the property to update. * @param string|null $value The value of the property to update. * @param string|null $level0Type The type of the object at level 0 the property belongs to. * @param string|null $level0Name The name of the object at level 0 the property belongs to. * @param string|null $level1Type The type of the object at level 1 the property belongs to. * @param string|null $level1Name The name of the object at level 1 the property belongs to. * @param string|null $level2Type The type of the object at level 2 the property belongs to. * @param string|null $level2Name The name of the object at level 2 the property belongs to. */ protected function getUpdateExtendedPropertySQL( string $name, ?string $value = null, ?string $level0Type = null, ?string $level0Name = null, ?string $level1Type = null, ?string $level1Name = null, ?string $level2Type = null, ?string $level2Name = null, ): string { return 'EXEC sp_updateextendedproperty ' . 'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' . 'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' . 'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' . 'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name; } public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string { return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES'; } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListViewsSQL(string $database): string { return "SELECT name, definition FROM sysobjects INNER JOIN sys.sql_modules ON sysobjects.id = sys.sql_modules.object_id WHERE type = 'V' ORDER BY name"; } public function getLocateExpression(string $string, string $substring, ?string $start = null): string { if ($start === null) { return sprintf('CHARINDEX(%s, %s)', $substring, $string); } return sprintf('CHARINDEX(%s, %s, %s)', $substring, $string, $start); } public function getModExpression(string $dividend, string $divisor): string { return $dividend . ' % ' . $divisor; } public function getTrimExpression( string $str, TrimMode $mode = TrimMode::UNSPECIFIED, ?string $char = null, ): string { if ($char === null) { return match ($mode) { TrimMode::LEADING => 'LTRIM(' . $str . ')', TrimMode::TRAILING => 'RTRIM(' . $str . ')', default => 'LTRIM(RTRIM(' . $str . '))', }; } $pattern = "'%[^' + " . $char . " + ']%'"; if ($mode === TrimMode::LEADING) { return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)'; } if ($mode === TrimMode::TRAILING) { return 'reverse(stuff(reverse(' . $str . '), 1, ' . 'patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))'; } return 'reverse(stuff(reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)), 1, ' . 'patindex(' . $pattern . ', reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null))) - 1, null))'; } public function getConcatExpression(string ...$string): string { return sprintf('CONCAT(%s)', implode(', ', $string)); } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListDatabasesSQL(): string { return 'SELECT * FROM sys.databases'; } public function getSubstringExpression(string $string, string $start, ?string $length = null): string { if ($length === null) { return sprintf('SUBSTRING(%s, %s, LEN(%s) - %s + 1)', $string, $start, $string, $start); } return sprintf('SUBSTRING(%s, %s, %s)', $string, $start, $length); } public function getLengthExpression(string $string): string { return 'LEN(' . $string . ')'; } public function getCurrentDatabaseExpression(): string { return 'DB_NAME()'; } public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string { return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); } /** * {@inheritDoc} */ public function getIntegerTypeDeclarationSQL(array $column): string { return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ public function getBigIntTypeDeclarationSQL(array $column): string { return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ public function getSmallIntTypeDeclarationSQL(array $column): string { return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ public function getGuidTypeDeclarationSQL(array $column): string { return 'UNIQUEIDENTIFIER'; } /** * {@inheritDoc} */ public function getDateTimeTzTypeDeclarationSQL(array $column): string { return 'DATETIMEOFFSET(6)'; } protected function getCharTypeDeclarationSQLSnippet(?int $length): string { $sql = 'NCHAR'; if ($length !== null) { $sql .= sprintf('(%d)', $length); } return $sql; } protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string { if ($length === null) { throw ColumnLengthRequired::new($this, 'NVARCHAR'); } return sprintf('NVARCHAR(%d)', $length); } /** * {@inheritDoc} */ public function getAsciiStringTypeDeclarationSQL(array $column): string { $length = $column['length'] ?? null; if (empty($column['fixed'])) { return parent::getVarcharTypeDeclarationSQLSnippet($length); } return parent::getCharTypeDeclarationSQLSnippet($length); } /** * {@inheritDoc} */ public function getClobTypeDeclarationSQL(array $column): string { return 'VARCHAR(MAX)'; } /** * {@inheritDoc} */ protected function _getCommonIntegerTypeDeclarationSQL(array $column): string { return ! empty($column['autoincrement']) ? ' IDENTITY' : ''; } /** * {@inheritDoc} */ public function getDateTimeTypeDeclarationSQL(array $column): string { // 3 - microseconds precision length // http://msdn.microsoft.com/en-us/library/ms187819.aspx return 'DATETIME2(6)'; } /** * {@inheritDoc} */ public function getDateTypeDeclarationSQL(array $column): string { return 'DATE'; } /** * {@inheritDoc} */ public function getTimeTypeDeclarationSQL(array $column): string { return 'TIME(0)'; } /** * {@inheritDoc} */ public function getBooleanTypeDeclarationSQL(array $column): string { return 'BIT'; } protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string { if ($limit === null && $offset <= 0) { return $query; } if ($this->shouldAddOrderBy($query)) { if (preg_match('/^SELECT\s+DISTINCT/im', $query) > 0) { // SQL Server won't let us order by a non-selected column in a DISTINCT query, // so we have to do this madness. This says, order by the first column in the // result. SQL Server's docs say that a nonordered query's result order is non- // deterministic anyway, so this won't do anything that a bunch of update and // deletes to the table wouldn't do anyway. $query .= ' ORDER BY 1'; } else { // In another DBMS, we could do ORDER BY 0, but SQL Server gets angry if you // use constant expressions in the order by list. $query .= ' ORDER BY (SELECT 0)'; } } // This looks somewhat like MYSQL, but limit/offset are in inverse positions // Supposedly SQL:2008 core standard. // Per TSQL spec, FETCH NEXT n ROWS ONLY is not valid without OFFSET n ROWS. $query .= sprintf(' OFFSET %d ROWS', $offset); if ($limit !== null) { $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit); } return $query; } public function convertBooleans(mixed $item): mixed { if (is_array($item)) { foreach ($item as $key => $value) { if (! is_bool($value) && ! is_numeric($value)) { continue; } $item[$key] = (int) (bool) $value; } } elseif (is_bool($item) || is_numeric($item)) { $item = (int) (bool) $item; } return $item; } public function getCreateTemporaryTableSnippetSQL(): string { return 'CREATE TABLE'; } public function getTemporaryTableName(string $tableName): string { return '#' . $tableName; } public function getDateTimeFormatString(): string { return 'Y-m-d H:i:s.u'; } public function getDateFormatString(): string { return 'Y-m-d'; } public function getTimeFormatString(): string { return 'H:i:s'; } public function getDateTimeTzFormatString(): string { return 'Y-m-d H:i:s.u P'; } protected function initializeDoctrineTypeMappings(): void { $this->doctrineTypeMapping = [ 'bigint' => Types::BIGINT, 'binary' => Types::BINARY, 'bit' => Types::BOOLEAN, 'blob' => Types::BLOB, 'char' => Types::STRING, 'date' => Types::DATE_MUTABLE, 'datetime' => Types::DATETIME_MUTABLE, 'datetime2' => Types::DATETIME_MUTABLE, 'datetimeoffset' => Types::DATETIMETZ_MUTABLE, 'decimal' => Types::DECIMAL, 'double' => Types::FLOAT, 'double precision' => Types::FLOAT, 'float' => Types::FLOAT, 'image' => Types::BLOB, 'int' => Types::INTEGER, 'money' => Types::INTEGER, 'nchar' => Types::STRING, 'ntext' => Types::TEXT, 'numeric' => Types::DECIMAL, 'nvarchar' => Types::STRING, 'real' => Types::FLOAT, 'smalldatetime' => Types::DATETIME_MUTABLE, 'smallint' => Types::SMALLINT, 'smallmoney' => Types::INTEGER, 'text' => Types::TEXT, 'time' => Types::TIME_MUTABLE, 'tinyint' => Types::SMALLINT, 'uniqueidentifier' => Types::GUID, 'varbinary' => Types::BINARY, 'varchar' => Types::STRING, ]; } public function createSavePoint(string $savepoint): string { return 'SAVE TRANSACTION ' . $savepoint; } public function releaseSavePoint(string $savepoint): string { return ''; } public function rollbackSavePoint(string $savepoint): string { return 'ROLLBACK TRANSACTION ' . $savepoint; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getForeignKeyReferentialActionSQL(string $action): string { // RESTRICT is not supported, therefore falling back to NO ACTION. if (strtoupper($action) === 'RESTRICT') { return 'NO ACTION'; } return parent::getForeignKeyReferentialActionSQL($action); } public function appendLockHint(string $fromClause, LockMode $lockMode): string { return match ($lockMode) { LockMode::NONE, LockMode::OPTIMISTIC => $fromClause, LockMode::PESSIMISTIC_READ => $fromClause . ' WITH (HOLDLOCK, ROWLOCK)', LockMode::PESSIMISTIC_WRITE => $fromClause . ' WITH (UPDLOCK, ROWLOCK)', }; } protected function createReservedKeywordsList(): KeywordList { return new SQLServerKeywords(); } public function quoteSingleIdentifier(string $str): string { return '[' . str_replace(']', ']]', $str) . ']'; } public function getTruncateTableSQL(string $tableName, bool $cascade = false): string { $tableIdentifier = new Identifier($tableName); return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this); } /** * {@inheritDoc} */ public function getBlobTypeDeclarationSQL(array $column): string { return 'VARBINARY(MAX)'; } /** * {@inheritDoc} * * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getColumnDeclarationSQL(string $name, array $column): string { if (isset($column['columnDefinition'])) { $declaration = $column['columnDefinition']; } else { $collation = ! empty($column['collation']) ? ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : ''; $notnull = ! empty($column['notnull']) ? ' NOT NULL' : ''; $typeDecl = $column['type']->getSQLDeclaration($column, $this); $declaration = $typeDecl . $collation . $notnull; } return $name . ' ' . $declaration; } /** * SQL Server does not support quoting collation identifiers. */ public function getColumnCollationDeclarationSQL(string $collation): string { return 'COLLATE ' . $collation; } public function columnsEqual(Column $column1, Column $column2): bool { if (! parent::columnsEqual($column1, $column2)) { return false; } return $this->getDefaultValueDeclarationSQL($column1->toArray()) === $this->getDefaultValueDeclarationSQL($column2->toArray()); } protected function getLikeWildcardCharacters(): string { return parent::getLikeWildcardCharacters() . '[]^'; } protected function getCommentOnTableSQL(string $tableName, string $comment): string { return 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($comment), $this->quoteStringLiteral($tableName), ); } private function shouldAddOrderBy(string $query): bool { // Find the position of the last instance of ORDER BY and ensure it is not within a parenthetical statement // but can be in a newline $matches = []; $matchesCount = preg_match_all('/[\\s]+order\\s+by\\s/im', $query, $matches, PREG_OFFSET_CAPTURE); if ($matchesCount === 0) { return true; } // ORDER BY instance may be in a subquery after ORDER BY // e.g. SELECT col1 FROM test ORDER BY (SELECT col2 from test ORDER BY col2) // if in the searched query ORDER BY clause was found where // number of open parentheses after the occurrence of the clause is equal to // number of closed brackets after the occurrence of the clause, // it means that ORDER BY is included in the query being checked while ($matchesCount > 0) { $orderByPos = $matches[0][--$matchesCount][1]; $openBracketsCount = substr_count($query, '(', $orderByPos); $closedBracketsCount = substr_count($query, ')', $orderByPos); if ($openBracketsCount === $closedBracketsCount) { return false; } } return true; } public function createSchemaManager(Connection $connection): SQLServerSchemaManager { return new SQLServerSchemaManager($connection, $this); } } src/Platforms/SQLite/Comparator.php000064400000002544152062402520013266 0ustar00normalizeColumns($oldTable), $this->normalizeColumns($newTable), ); } private function normalizeColumns(Table $table): Table { $table = clone $table; foreach ($table->getColumns() as $column) { $options = $column->getPlatformOptions(); if (! isset($options['collation']) || strcasecmp($options['collation'], 'binary') !== 0) { continue; } unset($options['collation']); $column->setPlatformOptions($options); } return $table; } } src/Platforms/SQLitePlatform.php000064400000073205152062402520012666 0ustar00 'TRIM', TrimMode::LEADING => 'LTRIM', TrimMode::TRAILING => 'RTRIM', }; $arguments = [$str]; if ($char !== null) { $arguments[] = $char; } return sprintf('%s(%s)', $trimFn, implode(', ', $arguments)); } public function getSubstringExpression(string $string, string $start, ?string $length = null): string { if ($length === null) { return sprintf('SUBSTR(%s, %s)', $string, $start); } return sprintf('SUBSTR(%s, %s, %s)', $string, $start, $length); } public function getLocateExpression(string $string, string $substring, ?string $start = null): string { if ($start === null || $start === '1') { return sprintf('INSTR(%s, %s)', $string, $substring); } return sprintf( 'CASE WHEN INSTR(SUBSTR(%1$s, %3$s), %2$s) > 0 THEN INSTR(SUBSTR(%1$s, %3$s), %2$s) + %3$s - 1 ELSE 0 END', $string, $substring, $start, ); } protected function getDateArithmeticIntervalExpression( string $date, string $operator, string $interval, DateIntervalUnit $unit, ): string { switch ($unit) { case DateIntervalUnit::WEEK: $interval = $this->multiplyInterval($interval, 7); $unit = DateIntervalUnit::DAY; break; case DateIntervalUnit::QUARTER: $interval = $this->multiplyInterval($interval, 3); $unit = DateIntervalUnit::MONTH; break; } return 'DATETIME(' . $date . ',' . $this->getConcatExpression( $this->quoteStringLiteral($operator), $interval, $this->quoteStringLiteral(' ' . $unit->value), ) . ')'; } public function getDateDiffExpression(string $date1, string $date2): string { return sprintf("JULIANDAY(%s, 'start of day') - JULIANDAY(%s, 'start of day')", $date1, $date2); } /** * {@inheritDoc} * * The DBAL doesn't support databases on the SQLite platform. The expression here always returns a fixed string * as an indicator of an implicitly selected database. * * @link https://www.sqlite.org/lang_select.html * @see Connection::getDatabase() */ public function getCurrentDatabaseExpression(): string { return "'main'"; } /** @link https://www2.sqlite.org/cvstrac/wiki?p=UnsupportedSql */ public function createSelectSQLBuilder(): SelectSQLBuilder { return new DefaultSelectSQLBuilder($this, null, null); } protected function _getTransactionIsolationLevelSQL(TransactionIsolationLevel $level): string { return match ($level) { TransactionIsolationLevel::READ_UNCOMMITTED => '0', TransactionIsolationLevel::READ_COMMITTED, TransactionIsolationLevel::REPEATABLE_READ, TransactionIsolationLevel::SERIALIZABLE => '1', }; } public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string { return 'PRAGMA read_uncommitted = ' . $this->_getTransactionIsolationLevelSQL($level); } /** * {@inheritDoc} */ public function getBooleanTypeDeclarationSQL(array $column): string { return 'BOOLEAN'; } /** * {@inheritDoc} */ public function getIntegerTypeDeclarationSQL(array $column): string { return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ public function getBigIntTypeDeclarationSQL(array $column): string { // SQLite autoincrement is implicit for INTEGER PKs, but not for BIGINT fields. if (! empty($column['autoincrement'])) { return $this->getIntegerTypeDeclarationSQL($column); } return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ public function getSmallIntTypeDeclarationSQL(array $column): string { // SQLite autoincrement is implicit for INTEGER PKs, but not for SMALLINT fields. if (! empty($column['autoincrement'])) { return $this->getIntegerTypeDeclarationSQL($column); } return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); } /** * {@inheritDoc} */ public function getDateTimeTypeDeclarationSQL(array $column): string { return 'DATETIME'; } /** * {@inheritDoc} */ public function getDateTypeDeclarationSQL(array $column): string { return 'DATE'; } /** * {@inheritDoc} */ public function getTimeTypeDeclarationSQL(array $column): string { return 'TIME'; } /** * {@inheritDoc} */ protected function _getCommonIntegerTypeDeclarationSQL(array $column): string { // sqlite autoincrement is only possible for the primary key if (! empty($column['autoincrement'])) { return ' PRIMARY KEY AUTOINCREMENT'; } return ! empty($column['unsigned']) ? ' UNSIGNED' : ''; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey): string { return parent::getForeignKeyDeclarationSQL(new ForeignKeyConstraint( $foreignKey->getQuotedLocalColumns($this), $foreignKey->getQuotedForeignTableName($this), $foreignKey->getQuotedForeignColumns($this), $foreignKey->getName(), $foreignKey->getOptions(), )); } /** * {@inheritDoc} */ protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array { $queryFields = $this->getColumnDeclarationListSQL($columns); if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { foreach ($options['uniqueConstraints'] as $definition) { $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($definition); } } $queryFields .= $this->getNonAutoincrementPrimaryKeyDefinition($columns, $options); if (isset($options['foreignKeys'])) { foreach ($options['foreignKeys'] as $foreignKey) { $queryFields .= ', ' . $this->getForeignKeyDeclarationSQL($foreignKey); } } $tableComment = ''; if (isset($options['comment'])) { $comment = trim($options['comment'], " '"); $tableComment = $this->getInlineTableCommentSQL($comment); } $query = ['CREATE TABLE ' . $name . ' ' . $tableComment . '(' . $queryFields . ')']; if (isset($options['alter']) && $options['alter'] === true) { return $query; } if (isset($options['indexes']) && ! empty($options['indexes'])) { foreach ($options['indexes'] as $indexDef) { $query[] = $this->getCreateIndexSQL($indexDef, $name); } } if (isset($options['unique']) && ! empty($options['unique'])) { foreach ($options['unique'] as $indexDef) { $query[] = $this->getCreateIndexSQL($indexDef, $name); } } return $query; } /** * Generate a PRIMARY KEY definition if no autoincrement value is used * * @param mixed[][] $columns * @param mixed[] $options */ private function getNonAutoincrementPrimaryKeyDefinition(array $columns, array $options): string { if (empty($options['primary'])) { return ''; } $keyColumns = array_unique(array_values($options['primary'])); foreach ($keyColumns as $keyColumn) { foreach ($columns as $column) { if ($column['name'] === $keyColumn && ! empty($column['autoincrement'])) { return ''; } } } return ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; } protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string { return 'BLOB'; } protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string { $sql = 'VARCHAR'; if ($length !== null) { $sql .= sprintf('(%d)', $length); } return $sql; } protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string { return 'BLOB'; } /** * {@inheritDoc} */ public function getClobTypeDeclarationSQL(array $column): string { return 'CLOB'; } /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ public function getListViewsSQL(string $database): string { return "SELECT name, sql FROM sqlite_master WHERE type='view' AND sql NOT NULL"; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey): string { $query = parent::getAdvancedForeignKeyOptionsSQL($foreignKey); if (! $foreignKey->hasOption('deferrable') || $foreignKey->getOption('deferrable') === false) { $query .= ' NOT'; } $query .= ' DEFERRABLE'; $query .= ' INITIALLY'; if ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) { $query .= ' DEFERRED'; } else { $query .= ' IMMEDIATE'; } return $query; } public function supportsIdentityColumns(): bool { return true; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function supportsColumnCollation(): bool { return true; } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function supportsInlineColumnComments(): bool { return true; } public function getTruncateTableSQL(string $tableName, bool $cascade = false): string { $tableIdentifier = new Identifier($tableName); return 'DELETE FROM ' . $tableIdentifier->getQuotedName($this); } /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ public function getInlineColumnCommentSQL(string $comment): string { if ($comment === '') { return ''; } return '--' . str_replace("\n", "\n--", $comment) . "\n"; } private function getInlineTableCommentSQL(string $comment): string { return $this->getInlineColumnCommentSQL($comment); } protected function initializeDoctrineTypeMappings(): void { $this->doctrineTypeMapping = [ 'bigint' => 'bigint', 'bigserial' => 'bigint', 'blob' => 'blob', 'boolean' => 'boolean', 'char' => 'string', 'clob' => 'text', 'date' => 'date', 'datetime' => 'datetime', 'decimal' => 'decimal', 'double' => 'float', 'double precision' => 'float', 'float' => 'float', 'image' => 'string', 'int' => 'integer', 'integer' => 'integer', 'longtext' => 'text', 'longvarchar' => 'string', 'mediumint' => 'integer', 'mediumtext' => 'text', 'ntext' => 'string', 'numeric' => 'decimal', 'nvarchar' => 'string', 'real' => 'float', 'serial' => 'integer', 'smallint' => 'smallint', 'string' => 'string', 'text' => 'text', 'time' => 'time', 'timestamp' => 'datetime', 'tinyint' => 'boolean', 'tinytext' => 'text', 'varchar' => 'string', 'varchar2' => 'string', ]; } protected function createReservedKeywordsList(): KeywordList { return new SQLiteKeywords(); } /** * {@inheritDoc} */ protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff): array { return []; } /** * {@inheritDoc} */ protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff): array { $table = $diff->getOldTable(); $sql = []; foreach ($this->getIndexesInAlteredTable($diff, $table) as $index) { if ($index->isPrimary()) { continue; } $sql[] = $this->getCreateIndexSQL($index, $table->getQuotedName($this)); } return $sql; } protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string { if ($limit === null && $offset > 0) { $limit = -1; } return parent::doModifyLimitQuery($query, $limit, $offset); } /** * {@inheritDoc} */ public function getBlobTypeDeclarationSQL(array $column): string { return 'BLOB'; } public function getTemporaryTableName(string $tableName): string { return $tableName; } /** * {@inheritDoc} */ public function getCreateTablesSQL(array $tables): array { $sql = []; foreach ($tables as $table) { $sql = array_merge($sql, $this->getCreateTableSQL($table)); } return $sql; } /** {@inheritDoc} */ public function getCreateIndexSQL(Index $index, string $table): string { $name = $index->getQuotedName($this); $columns = $index->getColumns(); if (strpos($table, '.') !== false) { [$schema, $table] = explode('.', $table); $name = $schema . '.' . $name; } if (count($columns) === 0) { throw new InvalidArgumentException(sprintf( 'Incomplete or invalid index definition %s on table %s', $name, $table, )); } if ($index->isPrimary()) { return $this->getCreatePrimaryKeySQL($index, $table); } $query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table; $query .= ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index); return $query; } /** * {@inheritDoc} */ public function getDropTablesSQL(array $tables): array { $sql = []; foreach ($tables as $table) { $sql[] = $this->getDropTableSQL($table->getQuotedName($this)); } return $sql; } public function getCreatePrimaryKeySQL(Index $index, string $table): string { throw NotSupported::new(__METHOD__); } public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, string $table): string { throw NotSupported::new(__METHOD__); } public function getDropForeignKeySQL(string $foreignKey, string $table): string { throw NotSupported::new(__METHOD__); } /** * {@inheritDoc} */ public function getAlterTableSQL(TableDiff $diff): array { $sql = $this->getSimpleAlterTableSQL($diff); if ($sql !== false) { return $sql; } $table = $diff->getOldTable(); $columns = []; $oldColumnNames = []; $newColumnNames = []; $columnSql = []; foreach ($table->getColumns() as $column) { $columnName = strtolower($column->getName()); $columns[$columnName] = $column; $oldColumnNames[$columnName] = $newColumnNames[$columnName] = $column->getQuotedName($this); } foreach ($diff->getDroppedColumns() as $column) { $columnName = strtolower($column->getName()); if (! isset($columns[$columnName])) { continue; } unset( $columns[$columnName], $oldColumnNames[$columnName], $newColumnNames[$columnName], ); } foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { $oldColumnName = strtolower($oldColumnName); $columns = $this->replaceColumn( $table->getName(), $columns, $oldColumnName, $column, ); if (! isset($newColumnNames[$oldColumnName])) { continue; } $newColumnNames[$oldColumnName] = $column->getQuotedName($this); } foreach ($diff->getModifiedColumns() as $columnDiff) { $oldColumnName = strtolower($columnDiff->getOldColumn()->getName()); $newColumn = $columnDiff->getNewColumn(); $columns = $this->replaceColumn( $table->getName(), $columns, $oldColumnName, $newColumn, ); if (! isset($newColumnNames[$oldColumnName])) { continue; } $newColumnNames[$oldColumnName] = $newColumn->getQuotedName($this); } foreach ($diff->getAddedColumns() as $column) { $columns[strtolower($column->getName())] = $column; } $dataTable = new Table('__temp__' . $table->getName()); $newTable = new Table( $table->getQuotedName($this), $columns, $this->getPrimaryIndexInAlteredTable($diff, $table), [], $this->getForeignKeysInAlteredTable($diff, $table), $table->getOptions(), ); $newTable->addOption('alter', true); $sql = $this->getPreAlterTableIndexForeignKeySQL($diff); $sql[] = sprintf( 'CREATE TEMPORARY TABLE %s AS SELECT %s FROM %s', $dataTable->getQuotedName($this), implode(', ', $oldColumnNames), $table->getQuotedName($this), ); $sql[] = $this->getDropTableSQL($table->getQuotedName($this)); $sql = array_merge($sql, $this->getCreateTableSQL($newTable)); $sql[] = sprintf( 'INSERT INTO %s (%s) SELECT %s FROM %s', $newTable->getQuotedName($this), implode(', ', $newColumnNames), implode(', ', $oldColumnNames), $dataTable->getQuotedName($this), ); $sql[] = $this->getDropTableSQL($dataTable->getQuotedName($this)); return array_merge($sql, $this->getPostAlterTableIndexForeignKeySQL($diff), $columnSql); } /** * Replace the column with the given name with the new column. * * @param array $columns * * @return array * * @throws Exception */ private function replaceColumn(string $tableName, array $columns, string $columnName, Column $column): array { $keys = array_keys($columns); $index = array_search($columnName, $keys, true); if ($index === false) { throw ColumnDoesNotExist::new($columnName, $tableName); } $values = array_values($columns); $keys[$index] = strtolower($column->getName()); $values[$index] = $column; return array_combine($keys, $values); } /** * @return list|false * * @throws Exception */ private function getSimpleAlterTableSQL(TableDiff $diff): array|false { if ( count($diff->getModifiedColumns()) > 0 || count($diff->getDroppedColumns()) > 0 || count($diff->getRenamedColumns()) > 0 || count($diff->getAddedIndexes()) > 0 || count($diff->getModifiedIndexes()) > 0 || count($diff->getDroppedIndexes()) > 0 || count($diff->getRenamedIndexes()) > 0 || count($diff->getAddedForeignKeys()) > 0 || count($diff->getModifiedForeignKeys()) > 0 || count($diff->getDroppedForeignKeys()) > 0 ) { return false; } $table = $diff->getOldTable(); $sql = []; $columnSql = []; foreach ($diff->getAddedColumns() as $column) { $definition = array_merge([ 'unique' => null, 'autoincrement' => null, 'default' => null, ], $column->toArray()); $type = $definition['type']; switch (true) { case isset($definition['columnDefinition']) || $definition['autoincrement'] || $definition['unique']: case $type instanceof Types\DateTimeType && $definition['default'] === $this->getCurrentTimestampSQL(): case $type instanceof Types\DateType && $definition['default'] === $this->getCurrentDateSQL(): case $type instanceof Types\TimeType && $definition['default'] === $this->getCurrentTimeSQL(): return false; } $definition['name'] = $column->getQuotedName($this); $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' ADD COLUMN ' . $this->getColumnDeclarationSQL($definition['name'], $definition); } return array_merge($sql, $columnSql); } /** @return string[] */ private function getColumnNamesInAlteredTable(TableDiff $diff, Table $oldTable): array { $columns = []; foreach ($oldTable->getColumns() as $column) { $columnName = $column->getName(); $columns[strtolower($columnName)] = $columnName; } foreach ($diff->getDroppedColumns() as $column) { $columnName = strtolower($column->getName()); if (! isset($columns[$columnName])) { continue; } unset($columns[$columnName]); } foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { $columnName = $column->getName(); $columns[strtolower($oldColumnName)] = $columnName; $columns[strtolower($columnName)] = $columnName; } foreach ($diff->getModifiedColumns() as $columnDiff) { $oldColumnName = $columnDiff->getOldColumn()->getName(); $newColumnName = $columnDiff->getNewColumn()->getName(); $columns[strtolower($oldColumnName)] = $newColumnName; $columns[strtolower($newColumnName)] = $newColumnName; } foreach ($diff->getAddedColumns() as $column) { $columnName = $column->getName(); $columns[strtolower($columnName)] = $columnName; } return $columns; } /** @return Index[] */ private function getIndexesInAlteredTable(TableDiff $diff, Table $oldTable): array { $indexes = $oldTable->getIndexes(); $columnNames = $this->getColumnNamesInAlteredTable($diff, $oldTable); foreach ($indexes as $key => $index) { foreach ($diff->getRenamedIndexes() as $oldIndexName => $renamedIndex) { if (strtolower($key) !== strtolower($oldIndexName)) { continue; } unset($indexes[$key]); } $changed = false; $indexColumns = []; foreach ($index->getColumns() as $columnName) { $normalizedColumnName = strtolower($columnName); if (! isset($columnNames[$normalizedColumnName])) { unset($indexes[$key]); continue 2; } $indexColumns[] = $columnNames[$normalizedColumnName]; if ($columnName === $columnNames[$normalizedColumnName]) { continue; } $changed = true; } if (! $changed) { continue; } $indexes[$key] = new Index( $index->getName(), $indexColumns, $index->isUnique(), $index->isPrimary(), $index->getFlags(), ); } foreach ($diff->getDroppedIndexes() as $index) { $indexName = $index->getName(); if ($indexName === '') { continue; } unset($indexes[strtolower($indexName)]); } foreach ( array_merge( $diff->getModifiedIndexes(), $diff->getAddedIndexes(), $diff->getRenamedIndexes(), ) as $index ) { $indexName = $index->getName(); if ($indexName !== '') { $indexes[strtolower($indexName)] = $index; } else { $indexes[] = $index; } } return $indexes; } /** @return ForeignKeyConstraint[] */ private function getForeignKeysInAlteredTable(TableDiff $diff, Table $oldTable): array { $foreignKeys = $oldTable->getForeignKeys(); $columnNames = $this->getColumnNamesInAlteredTable($diff, $oldTable); foreach ($foreignKeys as $key => $constraint) { $changed = false; $localColumns = []; foreach ($constraint->getLocalColumns() as $columnName) { $normalizedColumnName = strtolower($columnName); if (! isset($columnNames[$normalizedColumnName])) { unset($foreignKeys[$key]); continue 2; } $localColumns[] = $columnNames[$normalizedColumnName]; if ($columnName === $columnNames[$normalizedColumnName]) { continue; } $changed = true; } if (! $changed) { continue; } $foreignKeys[$key] = new ForeignKeyConstraint( $localColumns, $constraint->getForeignTableName(), $constraint->getForeignColumns(), $constraint->getName(), $constraint->getOptions(), ); } foreach ($diff->getDroppedForeignKeys() as $constraint) { $constraintName = $constraint->getName(); if ($constraintName === '') { continue; } unset($foreignKeys[strtolower($constraintName)]); } foreach (array_merge($diff->getModifiedForeignKeys(), $diff->getAddedForeignKeys()) as $constraint) { $constraintName = $constraint->getName(); if ($constraintName !== '') { $foreignKeys[strtolower($constraintName)] = $constraint; } else { $foreignKeys[] = $constraint; } } return $foreignKeys; } /** @return Index[] */ private function getPrimaryIndexInAlteredTable(TableDiff $diff, Table $oldTable): array { $primaryIndex = []; foreach ($this->getIndexesInAlteredTable($diff, $oldTable) as $index) { if (! $index->isPrimary()) { continue; } $primaryIndex = [$index->getName() => $index]; } return $primaryIndex; } public function createSchemaManager(Connection $connection): SQLiteSchemaManager { return new SQLiteSchemaManager($connection, $this); } } src/Platforms/TrimMode.php000064400000000241152062402520011526 0ustar00converter, ); } public function query(string $sql): Result { return new Result( parent::query($sql), $this->converter, ); } } src/Portability/Converter.php000064400000020012152062402520012306 0ustar00createConvertValue($convertEmptyStringToNull, $rightTrimString); $convertNumeric = $this->createConvertRow($convertValue, null); $convertAssociative = $this->createConvertRow($convertValue, $case); $this->convertNumeric = $this->createConvert($convertNumeric); $this->convertAssociative = $this->createConvert($convertAssociative); $this->convertOne = $this->createConvert($convertValue); $this->convertAllNumeric = $this->createConvertAll($convertNumeric); $this->convertAllAssociative = $this->createConvertAll($convertAssociative); $this->convertFirstColumn = $this->createConvertAll($convertValue); } /** * @param array|false $row * * @return list|false */ public function convertNumeric(array|false $row): array|false { return ($this->convertNumeric)($row); } /** * @param array|false $row * * @return array|false */ public function convertAssociative(array|false $row): array|false { return ($this->convertAssociative)($row); } public function convertOne(mixed $value): mixed { return ($this->convertOne)($value); } /** * @param list> $data * * @return list> */ public function convertAllNumeric(array $data): array { return ($this->convertAllNumeric)($data); } /** * @param list> $data * * @return list> */ public function convertAllAssociative(array $data): array { return ($this->convertAllAssociative)($data); } /** * @param list $data * * @return list */ public function convertFirstColumn(array $data): array { return ($this->convertFirstColumn)($data); } /** * @param T $value * * @return T * * @template T */ private static function id(mixed $value): mixed { return $value; } /** * @param T $value * * @return T|null * * @template T */ private static function convertEmptyStringToNull(mixed $value): mixed { if ($value === '') { return null; } return $value; } /** * @param T $value * * @return T|string * @psalm-return (T is string ? string : T) * * @template T */ private static function rightTrimString(mixed $value): mixed { if (! is_string($value)) { return $value; } return rtrim($value); } /** * Creates a function that will convert each individual value retrieved from the database * * @param bool $convertEmptyStringToNull Whether each empty string should be converted to NULL * @param bool $rightTrimString Whether each string should right-trimmed * * @return Closure|null The resulting function or NULL if no conversion is needed */ private function createConvertValue(bool $convertEmptyStringToNull, bool $rightTrimString): ?Closure { $functions = []; if ($convertEmptyStringToNull) { $functions[] = self::convertEmptyStringToNull(...); } if ($rightTrimString) { $functions[] = self::rightTrimString(...); } return $this->compose(...$functions); } /** * Creates a function that will convert each array-row retrieved from the database * * @param Closure|null $function The function that will convert each value * @param self::CASE_LOWER|self::CASE_UPPER|null $case Column name case * * @return Closure|null The resulting function or NULL if no conversion is needed */ private function createConvertRow(?Closure $function, ?int $case): ?Closure { $functions = []; if ($function !== null) { $functions[] = $this->createMapper($function); } if ($case !== null) { $functions[] = static fn (array $row): array => array_change_key_case($row, $case); } return $this->compose(...$functions); } /** * Creates a function that will be applied to the return value of Statement::fetch*() * or an identity function if no conversion is needed * * @param Closure|null $function The function that will convert each tow */ private function createConvert(?Closure $function): Closure { if ($function === null) { return self::id(...); } return /** * @param T $value * * @psalm-return (T is false ? false : T) * * @template T */ static function (mixed $value) use ($function): mixed { if ($value === false) { return false; } return $function($value); }; } /** * Creates a function that will be applied to the return value of Statement::fetchAll*() * or an identity function if no transformation is required * * @param Closure|null $function The function that will transform each value */ private function createConvertAll(?Closure $function): Closure { if ($function === null) { return self::id(...); } return $this->createMapper($function); } /** * Creates a function that maps each value of the array using the given function * * @param Closure $function The function that maps each value of the array * * @return Closure(array):array */ private function createMapper(Closure $function): Closure { return static fn (array $array): array => array_map($function, $array); } /** * Creates a composition of the given set of functions * * @param Closure(T):T ...$functions The functions to compose * * @return Closure(T):T|null * * @template T */ private function compose(Closure ...$functions): ?Closure { return array_reduce($functions, static function (?Closure $carry, Closure $item): Closure { if ($carry === null) { return $item; } return /** * @param T $value * * @return T * * @template T */ static fn (mixed $value): mixed => $item($carry($value)); }); } } src/Portability/Driver.php000064400000003651152062402520011604 0ustar00getDatabasePlatform($connection), $this->mode, ); $case = null; if ($this->case !== null && ($portability & Connection::PORTABILITY_FIX_CASE) !== 0) { $nativeConnection = $connection->getNativeConnection(); $case = match ($this->case) { ColumnCase::LOWER => CASE_LOWER, ColumnCase::UPPER => CASE_UPPER, }; if ($nativeConnection instanceof PDO) { $portability &= ~Connection::PORTABILITY_FIX_CASE; $nativeConnection->setAttribute(PDO::ATTR_CASE, $case); } } $convertEmptyStringToNull = ($portability & Connection::PORTABILITY_EMPTY_TO_NULL) !== 0; $rightTrimString = ($portability & Connection::PORTABILITY_RTRIM) !== 0; if (! $convertEmptyStringToNull && ! $rightTrimString && $case === null) { return $connection; } return new Connection( $connection, new Converter($convertEmptyStringToNull, $rightTrimString, $case), ); } } src/Portability/Middleware.php000064400000001113152062402520012415 0ustar00mode !== 0) { return new Driver($driver, $this->mode, $this->case); } return $driver; } } src/Portability/OptimizeFlags.php000064400000002265152062402520013126 0ustar00 */ private static array $platforms = [ DB2Platform::class => 0, OraclePlatform::class => Connection::PORTABILITY_EMPTY_TO_NULL, PostgreSQLPlatform::class => 0, SQLitePlatform::class => 0, SQLServerPlatform::class => 0, ]; public function __invoke(AbstractPlatform $platform, int $flags): int { foreach (self::$platforms as $class => $mask) { if ($platform instanceof $class) { $flags &= ~$mask; break; } } return $flags; } } src/Portability/Result.php000064400000003030152062402520011616 0ustar00converter->convertNumeric( parent::fetchNumeric(), ); } public function fetchAssociative(): array|false { return $this->converter->convertAssociative( parent::fetchAssociative(), ); } public function fetchOne(): mixed { return $this->converter->convertOne( parent::fetchOne(), ); } /** * {@inheritDoc} */ public function fetchAllNumeric(): array { return $this->converter->convertAllNumeric( parent::fetchAllNumeric(), ); } /** * {@inheritDoc} */ public function fetchAllAssociative(): array { return $this->converter->convertAllAssociative( parent::fetchAllAssociative(), ); } /** * {@inheritDoc} */ public function fetchFirstColumn(): array { return $this->converter->convertFirstColumn( parent::fetchFirstColumn(), ); } } src/Portability/Statement.php000064400000001341152062402520012307 0ustar00Statement and applies portability measures. */ public function __construct(DriverStatement $stmt, private readonly Converter $converter) { parent::__construct($stmt); } public function execute(): ResultInterface { return new Result( parent::execute(), $this->converter, ); } } src/Query.php000064400000001550152062402520007150 0ustar00 $params * @psalm-param array $types * * @psalm-suppress ImpurePropertyAssignment */ public function __construct( private readonly string $sql, private readonly array $params, private readonly array $types, ) { } public function getSQL(): string { return $this->sql; } /** @return array */ public function getParams(): array { return $this->params; } /** @psalm-return array */ public function getTypes(): array { return $this->types; } } src/Query/Exception/NonUniqueAlias.php000064400000001242152062402520013777 0ustar00 */ private array $parts; /** @internal Use the and() / or() factory methods. */ public function __construct( private readonly string $type, self|string $part, self|string ...$parts, ) { $this->parts = array_merge([$part], array_values($parts)); } public static function and(self|string $part, self|string ...$parts): self { return new self(self::TYPE_AND, $part, ...$parts); } public static function or(self|string $part, self|string ...$parts): self { return new self(self::TYPE_OR, $part, ...$parts); } /** * Returns a new CompositeExpression with the given parts added. */ public function with(self|string $part, self|string ...$parts): self { $that = clone $this; $that->parts = array_merge($that->parts, [$part], array_values($parts)); return $that; } /** * Retrieves the amount of expressions on composite expression. * * @psalm-return int<0, max> */ public function count(): int { return count($this->parts); } /** * Retrieves the string representation of this composite expression. */ public function __toString(): string { if ($this->count() === 1) { return (string) $this->parts[0]; } return '(' . implode(') ' . $this->type . ' (', $this->parts) . ')'; } /** * Returns the type of this composite expression (AND/OR). */ public function getType(): string { return $this->type; } } src/Query/Expression/ExpressionBuilder.php000064400000017363152062402520014766 0ustar00'; final public const LT = '<'; final public const LTE = '<='; final public const GT = '>'; final public const GTE = '>='; /** * Initializes a new ExpressionBuilder. * * @param Connection $connection The DBAL Connection. */ public function __construct(private readonly Connection $connection) { } /** * Creates a conjunction of the given expressions. */ public function and( string|CompositeExpression $expression, string|CompositeExpression ...$expressions, ): CompositeExpression { return CompositeExpression::and($expression, ...$expressions); } /** * Creates a disjunction of the given expressions. */ public function or( string|CompositeExpression $expression, string|CompositeExpression ...$expressions, ): CompositeExpression { return CompositeExpression::or($expression, ...$expressions); } /** * Creates a comparison expression. * * @param string $x The left expression. * @param string $operator The comparison operator. * @param string $y The right expression. */ public function comparison(string $x, string $operator, string $y): string { return $x . ' ' . $operator . ' ' . $y; } /** * Creates an equality comparison expression with the given arguments. * * First argument is considered the left expression and the second is the right expression. * When converted to string, it will generated a = . Example: * * [php] * // u.id = ? * $expr->eq('u.id', '?'); * * @param string $x The left expression. * @param string $y The right expression. */ public function eq(string $x, string $y): string { return $this->comparison($x, self::EQ, $y); } /** * Creates a non equality comparison expression with the given arguments. * First argument is considered the left expression and the second is the right expression. * When converted to string, it will generated a <> . Example: * * [php] * // u.id <> 1 * $q->where($q->expr()->neq('u.id', '1')); * * @param string $x The left expression. * @param string $y The right expression. */ public function neq(string $x, string $y): string { return $this->comparison($x, self::NEQ, $y); } /** * Creates a lower-than comparison expression with the given arguments. * First argument is considered the left expression and the second is the right expression. * When converted to string, it will generated a < . Example: * * [php] * // u.id < ? * $q->where($q->expr()->lt('u.id', '?')); * * @param string $x The left expression. * @param string $y The right expression. */ public function lt(string $x, string $y): string { return $this->comparison($x, self::LT, $y); } /** * Creates a lower-than-equal comparison expression with the given arguments. * First argument is considered the left expression and the second is the right expression. * When converted to string, it will generated a <= . Example: * * [php] * // u.id <= ? * $q->where($q->expr()->lte('u.id', '?')); * * @param string $x The left expression. * @param string $y The right expression. */ public function lte(string $x, string $y): string { return $this->comparison($x, self::LTE, $y); } /** * Creates a greater-than comparison expression with the given arguments. * First argument is considered the left expression and the second is the right expression. * When converted to string, it will generated a > . Example: * * [php] * // u.id > ? * $q->where($q->expr()->gt('u.id', '?')); * * @param string $x The left expression. * @param string $y The right expression. */ public function gt(string $x, string $y): string { return $this->comparison($x, self::GT, $y); } /** * Creates a greater-than-equal comparison expression with the given arguments. * First argument is considered the left expression and the second is the right expression. * When converted to string, it will generated a >= . Example: * * [php] * // u.id >= ? * $q->where($q->expr()->gte('u.id', '?')); * * @param string $x The left expression. * @param string $y The right expression. */ public function gte(string $x, string $y): string { return $this->comparison($x, self::GTE, $y); } /** * Creates an IS NULL expression with the given arguments. * * @param string $x The expression to be restricted by IS NULL. */ public function isNull(string $x): string { return $x . ' IS NULL'; } /** * Creates an IS NOT NULL expression with the given arguments. * * @param string $x The expression to be restricted by IS NOT NULL. */ public function isNotNull(string $x): string { return $x . ' IS NOT NULL'; } /** * Creates a LIKE comparison expression. * * @param string $expression The expression to be inspected by the LIKE comparison * @param string $pattern The pattern to compare against */ public function like(string $expression, string $pattern, ?string $escapeChar = null): string { return $this->comparison($expression, 'LIKE', $pattern) . ($escapeChar !== null ? sprintf(' ESCAPE %s', $escapeChar) : ''); } /** * Creates a NOT LIKE comparison expression * * @param string $expression The expression to be inspected by the NOT LIKE comparison * @param string $pattern The pattern to compare against */ public function notLike(string $expression, string $pattern, ?string $escapeChar = null): string { return $this->comparison($expression, 'NOT LIKE', $pattern) . ($escapeChar !== null ? sprintf(' ESCAPE %s', $escapeChar) : ''); } /** * Creates an IN () comparison expression with the given arguments. * * @param string $x The SQL expression to be matched against the set. * @param string|string[] $y The SQL expression or an array of SQL expressions representing the set. */ public function in(string $x, string|array $y): string { return $this->comparison($x, 'IN', '(' . implode(', ', (array) $y) . ')'); } /** * Creates a NOT IN () comparison expression with the given arguments. * * @param string $x The SQL expression to be matched against the set. * @param string|string[] $y The SQL expression or an array of SQL expressions representing the set. */ public function notIn(string $x, string|array $y): string { return $this->comparison($x, 'NOT IN', '(' . implode(', ', (array) $y) . ')'); } /** * Creates an SQL literal expression from the string. * * The usage of this method is discouraged. Use prepared statements * or {@see AbstractPlatform::quoteStringLiteral()} instead. */ public function literal(string $input): string { return $this->connection->quote($input); } } src/Query/ForUpdate.php000064400000000642152062402520011042 0ustar00conflictResolutionMode; } } src/Query/ForUpdate/ConflictResolutionMode.php000064400000000400152062402520015464 0ustar00maxResults !== null || $this->firstResult !== 0; } public function getMaxResults(): ?int { return $this->maxResults; } public function getFirstResult(): int { return $this->firstResult; } } src/Query/QueryBuilder.php000064400000121362152062402520011570 0ustar00|array */ private array $params = []; /** * The parameter type map of this query. * * @psalm-var WrapperParameterTypeArray */ private array $types = []; /** * The type of query this is. Can be select, update or delete. */ private QueryType $type = QueryType::SELECT; /** * The index of the first result to retrieve. */ private int $firstResult = 0; /** * The maximum number of results to retrieve or NULL to retrieve all results. */ private ?int $maxResults = null; /** * The counter of bound parameters used with {@see bindValue). * * @var int<0, max> */ private int $boundCounter = 0; /** * The SELECT parts of the query. * * @var string[] */ private array $select = []; /** * Whether this is a SELECT DISTINCT query. */ private bool $distinct = false; /** * The FROM parts of a SELECT query. * * @var From[] */ private array $from = []; /** * The table name for an INSERT, UPDATE or DELETE query. */ private ?string $table = null; /** * The list of joins, indexed by from alias. * * @var array */ private array $join = []; /** * The SET parts of an UPDATE query. * * @var string[] */ private array $set = []; /** * The WHERE part of a SELECT, UPDATE or DELETE query. */ private string|CompositeExpression|null $where = null; /** * The GROUP BY part of a SELECT query. * * @var string[] */ private array $groupBy = []; /** * The HAVING part of a SELECT query. */ private string|CompositeExpression|null $having = null; /** * The ORDER BY parts of a SELECT query. * * @var string[] */ private array $orderBy = []; private ?ForUpdate $forUpdate = null; /** * The values of an INSERT query. * * @var array */ private array $values = []; /** * The query cache profile used for caching results. */ private ?QueryCacheProfile $resultCacheProfile = null; /** * Initializes a new QueryBuilder. * * @param Connection $connection The DBAL Connection. */ public function __construct(private readonly Connection $connection) { } /** * Gets an ExpressionBuilder used for object-oriented construction of query expressions. * This producer method is intended for convenient inline usage. Example: * * * $qb = $conn->createQueryBuilder() * ->select('u') * ->from('users', 'u') * ->where($qb->expr()->eq('u.id', 1)); * * * For more complex expression construction, consider storing the expression * builder object in a local variable. */ public function expr(): ExpressionBuilder { return $this->connection->createExpressionBuilder(); } /** * Prepares and executes an SQL query and returns the first row of the result * as an associative array. * * @return array|false False is returned if no rows are found. * * @throws Exception */ public function fetchAssociative(): array|false { return $this->executeQuery()->fetchAssociative(); } /** * Prepares and executes an SQL query and returns the first row of the result * as a numerically indexed array. * * @return array|false False is returned if no rows are found. * * @throws Exception */ public function fetchNumeric(): array|false { return $this->executeQuery()->fetchNumeric(); } /** * Prepares and executes an SQL query and returns the value of a single column * of the first row of the result. * * @return mixed|false False is returned if no rows are found. * * @throws Exception */ public function fetchOne(): mixed { return $this->executeQuery()->fetchOne(); } /** * Prepares and executes an SQL query and returns the result as an array of numeric arrays. * * @return array> * * @throws Exception */ public function fetchAllNumeric(): array { return $this->executeQuery()->fetchAllNumeric(); } /** * Prepares and executes an SQL query and returns the result as an array of associative arrays. * * @return array> * * @throws Exception */ public function fetchAllAssociative(): array { return $this->executeQuery()->fetchAllAssociative(); } /** * Prepares and executes an SQL query and returns the result as an associative array with the keys * mapped to the first column and the values mapped to the second column. * * @return array * * @throws Exception */ public function fetchAllKeyValue(): array { return $this->executeQuery()->fetchAllKeyValue(); } /** * Prepares and executes an SQL query and returns the result as an associative array with the keys mapped * to the first column and the values being an associative array representing the rest of the columns * and their values. * * @return array> * * @throws Exception */ public function fetchAllAssociativeIndexed(): array { return $this->executeQuery()->fetchAllAssociativeIndexed(); } /** * Prepares and executes an SQL query and returns the result as an array of the first column values. * * @return array * * @throws Exception */ public function fetchFirstColumn(): array { return $this->executeQuery()->fetchFirstColumn(); } /** * Executes an SQL query (SELECT) and returns a Result. * * @throws Exception */ public function executeQuery(): Result { return $this->connection->executeQuery( $this->getSQL(), $this->params, $this->types, $this->resultCacheProfile, ); } /** * Executes an SQL statement and returns the number of affected rows. * * Should be used for INSERT, UPDATE and DELETE * * @return int|numeric-string The number of affected rows. * * @throws Exception */ public function executeStatement(): int|string { return $this->connection->executeStatement($this->getSQL(), $this->params, $this->types); } /** * Gets the complete SQL string formed by the current specifications of this QueryBuilder. * * * $qb = $em->createQueryBuilder() * ->select('u') * ->from('User', 'u') * echo $qb->getSQL(); // SELECT u FROM User u * * * @return string The SQL query string. */ public function getSQL(): string { return $this->sql ??= match ($this->type) { QueryType::INSERT => $this->getSQLForInsert(), QueryType::DELETE => $this->getSQLForDelete(), QueryType::UPDATE => $this->getSQLForUpdate(), QueryType::SELECT => $this->getSQLForSelect(), }; } /** * Sets a query parameter for the query being constructed. * * * $qb = $conn->createQueryBuilder() * ->select('u') * ->from('users', 'u') * ->where('u.id = :user_id') * ->setParameter('user_id', 1); * * * @param int<0, max>|string $key Parameter position or name * * @return $this This QueryBuilder instance. */ public function setParameter( int|string $key, mixed $value, string|ParameterType|Type|ArrayParameterType $type = ParameterType::STRING, ): self { $this->params[$key] = $value; $this->types[$key] = $type; return $this; } /** * Sets a collection of query parameters for the query being constructed. * * * $qb = $conn->createQueryBuilder() * ->select('u') * ->from('users', 'u') * ->where('u.id = :user_id1 OR u.id = :user_id2') * ->setParameters(array( * 'user_id1' => 1, * 'user_id2' => 2 * )); * * * @param list|array $params * @psalm-param WrapperParameterTypeArray $types * * @return $this This QueryBuilder instance. */ public function setParameters(array $params, array $types = []): self { $this->params = $params; $this->types = $types; return $this; } /** * Gets all defined query parameters for the query being constructed indexed by parameter index or name. * * @return list|array The currently defined query parameters */ public function getParameters(): array { return $this->params; } /** * Gets a (previously set) query parameter of the query being constructed. * * @param string|int $key The key (index or name) of the bound parameter. * * @return mixed The value of the bound parameter. */ public function getParameter(string|int $key): mixed { return $this->params[$key] ?? null; } /** * Gets all defined query parameter types for the query being constructed indexed by parameter index or name. * * @psalm-return WrapperParameterTypeArray */ public function getParameterTypes(): array { return $this->types; } /** * Gets a (previously set) query parameter type of the query being constructed. * * @param int|string $key The key of the bound parameter type */ public function getParameterType(int|string $key): string|ParameterType|Type|ArrayParameterType { return $this->types[$key] ?? ParameterType::STRING; } /** * Sets the position of the first result to retrieve (the "offset"). * * @param int $firstResult The first result to return. * * @return $this This QueryBuilder instance. */ public function setFirstResult(int $firstResult): self { $this->firstResult = $firstResult; $this->sql = null; return $this; } /** * Gets the position of the first result the query object was set to retrieve (the "offset"). * * @return int The position of the first result. */ public function getFirstResult(): int { return $this->firstResult; } /** * Sets the maximum number of results to retrieve (the "limit"). * * @param int|null $maxResults The maximum number of results to retrieve or NULL to retrieve all results. * * @return $this This QueryBuilder instance. */ public function setMaxResults(?int $maxResults): self { $this->maxResults = $maxResults; $this->sql = null; return $this; } /** * Gets the maximum number of results the query object was set to retrieve (the "limit"). * Returns NULL if all results will be returned. * * @return int|null The maximum number of results. */ public function getMaxResults(): ?int { return $this->maxResults; } /** * Locks the queried rows for a subsequent update. * * @return $this */ public function forUpdate(ConflictResolutionMode $conflictResolutionMode = ConflictResolutionMode::ORDINARY): self { $this->forUpdate = new ForUpdate($conflictResolutionMode); $this->sql = null; return $this; } /** * Specifies an item that is to be returned in the query result. * Replaces any previously specified selections, if any. * * * $qb = $conn->createQueryBuilder() * ->select('u.id', 'p.id') * ->from('users', 'u') * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); * * * @param string ...$expressions The selection expressions. * * @return $this This QueryBuilder instance. */ public function select(string ...$expressions): self { $this->type = QueryType::SELECT; $this->select = $expressions; $this->sql = null; return $this; } /** * Adds or removes DISTINCT to/from the query. * * * $qb = $conn->createQueryBuilder() * ->select('u.id') * ->distinct() * ->from('users', 'u') * * * @return $this This QueryBuilder instance. */ public function distinct(bool $distinct = true): self { $this->distinct = $distinct; $this->sql = null; return $this; } /** * Adds an item that is to be returned in the query result. * * * $qb = $conn->createQueryBuilder() * ->select('u.id') * ->addSelect('p.id') * ->from('users', 'u') * ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id'); * * * @param string $expression The selection expression. * @param string ...$expressions Additional selection expressions. * * @return $this This QueryBuilder instance. */ public function addSelect(string $expression, string ...$expressions): self { $this->type = QueryType::SELECT; $this->select = array_merge($this->select, [$expression], $expressions); $this->sql = null; return $this; } /** * Turns the query being built into a bulk delete query that ranges over * a certain table. * * * $qb = $conn->createQueryBuilder() * ->delete('users', 'u') * ->where('u.id = :user_id') * ->setParameter(':user_id', 1); * * * @param string $table The table whose rows are subject to the deletion. * * @return $this This QueryBuilder instance. */ public function delete(string $table): self { $this->type = QueryType::DELETE; $this->table = $table; $this->sql = null; return $this; } /** * Turns the query being built into a bulk update query that ranges over * a certain table * * * $qb = $conn->createQueryBuilder() * ->update('counters', 'c') * ->set('c.value', 'c.value + 1') * ->where('c.id = ?'); * * * @param string $table The table whose rows are subject to the update. * * @return $this This QueryBuilder instance. */ public function update(string $table): self { $this->type = QueryType::UPDATE; $this->table = $table; $this->sql = null; return $this; } /** * Turns the query being built into an insert query that inserts into * a certain table * * * $qb = $conn->createQueryBuilder() * ->insert('users') * ->values( * array( * 'name' => '?', * 'password' => '?' * ) * ); * * * @param string $table The table into which the rows should be inserted. * * @return $this This QueryBuilder instance. */ public function insert(string $table): self { $this->type = QueryType::INSERT; $this->table = $table; $this->sql = null; return $this; } /** * Creates and adds a query root corresponding to the table identified by the * given alias, forming a cartesian product with any existing query roots. * * * $qb = $conn->createQueryBuilder() * ->select('u.id') * ->from('users', 'u') * * * @param string $table The table. * @param string|null $alias The alias of the table. * * @return $this This QueryBuilder instance. */ public function from(string $table, ?string $alias = null): self { $this->from[] = new From($table, $alias); $this->sql = null; return $this; } /** * Creates and adds a join to the query. * * * $qb = $conn->createQueryBuilder() * ->select('u.name') * ->from('users', 'u') * ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1'); * * * @param string $fromAlias The alias that points to a from clause. * @param string $join The table name to join. * @param string $alias The alias of the join table. * @param string $condition The condition for the join. * * @return $this This QueryBuilder instance. */ public function join(string $fromAlias, string $join, string $alias, ?string $condition = null): self { return $this->innerJoin($fromAlias, $join, $alias, $condition); } /** * Creates and adds a join to the query. * * * $qb = $conn->createQueryBuilder() * ->select('u.name') * ->from('users', 'u') * ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); * * * @param string $fromAlias The alias that points to a from clause. * @param string $join The table name to join. * @param string $alias The alias of the join table. * @param string $condition The condition for the join. * * @return $this This QueryBuilder instance. */ public function innerJoin(string $fromAlias, string $join, string $alias, ?string $condition = null): self { $this->join[$fromAlias][] = Join::inner($join, $alias, $condition); $this->sql = null; return $this; } /** * Creates and adds a left join to the query. * * * $qb = $conn->createQueryBuilder() * ->select('u.name') * ->from('users', 'u') * ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); * * * @param string $fromAlias The alias that points to a from clause. * @param string $join The table name to join. * @param string $alias The alias of the join table. * @param string $condition The condition for the join. * * @return $this This QueryBuilder instance. */ public function leftJoin(string $fromAlias, string $join, string $alias, ?string $condition = null): self { $this->join[$fromAlias][] = Join::left($join, $alias, $condition); $this->sql = null; return $this; } /** * Creates and adds a right join to the query. * * * $qb = $conn->createQueryBuilder() * ->select('u.name') * ->from('users', 'u') * ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); * * * @param string $fromAlias The alias that points to a from clause. * @param string $join The table name to join. * @param string $alias The alias of the join table. * @param string $condition The condition for the join. * * @return $this This QueryBuilder instance. */ public function rightJoin(string $fromAlias, string $join, string $alias, ?string $condition = null): self { $this->join[$fromAlias][] = Join::right($join, $alias, $condition); $this->sql = null; return $this; } /** * Sets a new value for a column in a bulk update query. * * * $qb = $conn->createQueryBuilder() * ->update('counters', 'c') * ->set('c.value', 'c.value + 1') * ->where('c.id = ?'); * * * @param string $key The column to set. * @param string $value The value, expression, placeholder, etc. * * @return $this This QueryBuilder instance. */ public function set(string $key, string $value): self { $this->set[] = $key . ' = ' . $value; $this->sql = null; return $this; } /** * Specifies one or more restrictions to the query result. * Replaces any previously specified restrictions, if any. * * * $qb = $conn->createQueryBuilder() * ->select('c.value') * ->from('counters', 'c') * ->where('c.id = ?'); * * // You can optionally programmatically build and/or expressions * $qb = $conn->createQueryBuilder(); * * $or = $qb->expr()->orx(); * $or->add($qb->expr()->eq('c.id', 1)); * $or->add($qb->expr()->eq('c.id', 2)); * * $qb->update('counters', 'c') * ->set('c.value', 'c.value + 1') * ->where($or); * * * @param string|CompositeExpression $predicate The WHERE clause predicate. * @param string|CompositeExpression ...$predicates Additional WHERE clause predicates. * * @return $this This QueryBuilder instance. */ public function where(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self { $this->where = $this->createPredicate($predicate, ...$predicates); $this->sql = null; return $this; } /** * Adds one or more restrictions to the query results, forming a logical * conjunction with any previously specified restrictions. * * * $qb = $conn->createQueryBuilder() * ->select('u') * ->from('users', 'u') * ->where('u.username LIKE ?') * ->andWhere('u.is_active = 1'); * * * @see where() * * @param string|CompositeExpression $predicate The predicate to append. * @param string|CompositeExpression ...$predicates Additional predicates to append. * * @return $this This QueryBuilder instance. */ public function andWhere(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self { $this->where = $this->appendToPredicate( $this->where, CompositeExpression::TYPE_AND, $predicate, ...$predicates, ); $this->sql = null; return $this; } /** * Adds one or more restrictions to the query results, forming a logical * disjunction with any previously specified restrictions. * * * $qb = $em->createQueryBuilder() * ->select('u.name') * ->from('users', 'u') * ->where('u.id = 1') * ->orWhere('u.id = 2'); * * * @see where() * * @param string|CompositeExpression $predicate The predicate to append. * @param string|CompositeExpression ...$predicates Additional predicates to append. * * @return $this This QueryBuilder instance. */ public function orWhere(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self { $this->where = $this->appendToPredicate($this->where, CompositeExpression::TYPE_OR, $predicate, ...$predicates); $this->sql = null; return $this; } /** * Specifies one or more grouping expressions over the results of the query. * Replaces any previously specified groupings, if any. * * * $qb = $conn->createQueryBuilder() * ->select('u.name') * ->from('users', 'u') * ->groupBy('u.id'); * * * @param string $expression The grouping expression * @param string ...$expressions Additional grouping expressions * * @return $this This QueryBuilder instance. */ public function groupBy(string $expression, string ...$expressions): self { $this->groupBy = array_merge([$expression], $expressions); $this->sql = null; return $this; } /** * Adds one or more grouping expressions to the query. * * * $qb = $conn->createQueryBuilder() * ->select('u.name') * ->from('users', 'u') * ->groupBy('u.lastLogin') * ->addGroupBy('u.createdAt'); * * * @param string $expression The grouping expression * @param string ...$expressions Additional grouping expressions * * @return $this This QueryBuilder instance. */ public function addGroupBy(string $expression, string ...$expressions): self { $this->groupBy = array_merge($this->groupBy, [$expression], $expressions); $this->sql = null; return $this; } /** * Sets a value for a column in an insert query. * * * $qb = $conn->createQueryBuilder() * ->insert('users') * ->values( * array( * 'name' => '?' * ) * ) * ->setValue('password', '?'); * * * @param string $column The column into which the value should be inserted. * @param string $value The value that should be inserted into the column. * * @return $this This QueryBuilder instance. */ public function setValue(string $column, string $value): self { $this->values[$column] = $value; return $this; } /** * Specifies values for an insert query indexed by column names. * Replaces any previous values, if any. * * * $qb = $conn->createQueryBuilder() * ->insert('users') * ->values( * array( * 'name' => '?', * 'password' => '?' * ) * ); * * * @param array $values The values to specify for the insert query indexed by column names. * * @return $this This QueryBuilder instance. */ public function values(array $values): self { $this->values = $values; $this->sql = null; return $this; } /** * Specifies a restriction over the groups of the query. * Replaces any previous having restrictions, if any. * * @param string|CompositeExpression $predicate The HAVING clause predicate. * @param string|CompositeExpression ...$predicates Additional HAVING clause predicates. * * @return $this This QueryBuilder instance. */ public function having(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self { $this->having = $this->createPredicate($predicate, ...$predicates); $this->sql = null; return $this; } /** * Adds a restriction over the groups of the query, forming a logical * conjunction with any existing having restrictions. * * @param string|CompositeExpression $predicate The predicate to append. * @param string|CompositeExpression ...$predicates Additional predicates to append. * * @return $this This QueryBuilder instance. */ public function andHaving(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self { $this->having = $this->appendToPredicate( $this->having, CompositeExpression::TYPE_AND, $predicate, ...$predicates, ); $this->sql = null; return $this; } /** * Adds a restriction over the groups of the query, forming a logical * disjunction with any existing having restrictions. * * @param string|CompositeExpression $predicate The predicate to append. * @param string|CompositeExpression ...$predicates Additional predicates to append. * * @return $this This QueryBuilder instance. */ public function orHaving(string|CompositeExpression $predicate, string|CompositeExpression ...$predicates): self { $this->having = $this->appendToPredicate( $this->having, CompositeExpression::TYPE_OR, $predicate, ...$predicates, ); $this->sql = null; return $this; } /** * Creates a CompositeExpression from one or more predicates combined by the AND logic. */ private function createPredicate( string|CompositeExpression $predicate, string|CompositeExpression ...$predicates, ): string|CompositeExpression { if (count($predicates) === 0) { return $predicate; } return new CompositeExpression(CompositeExpression::TYPE_AND, $predicate, ...$predicates); } /** * Appends the given predicates combined by the given type of logic to the current predicate. */ private function appendToPredicate( string|CompositeExpression|null $currentPredicate, string $type, string|CompositeExpression ...$predicates, ): string|CompositeExpression { if ($currentPredicate instanceof CompositeExpression && $currentPredicate->getType() === $type) { return $currentPredicate->with(...$predicates); } if ($currentPredicate !== null) { array_unshift($predicates, $currentPredicate); } elseif (count($predicates) === 1) { return $predicates[0]; } return new CompositeExpression($type, ...$predicates); } /** * Specifies an ordering for the query results. * Replaces any previously specified orderings, if any. * * @param string $sort The ordering expression. * @param string $order The ordering direction. * * @return $this This QueryBuilder instance. */ public function orderBy(string $sort, ?string $order = null): self { $orderBy = $sort; if ($order !== null) { $orderBy .= ' ' . $order; } $this->orderBy = [$orderBy]; $this->sql = null; return $this; } /** * Adds an ordering to the query results. * * @param string $sort The ordering expression. * @param string $order The ordering direction. * * @return $this This QueryBuilder instance. */ public function addOrderBy(string $sort, ?string $order = null): self { $orderBy = $sort; if ($order !== null) { $orderBy .= ' ' . $order; } $this->orderBy[] = $orderBy; $this->sql = null; return $this; } /** * Resets the WHERE conditions for the query. * * @return $this This QueryBuilder instance. */ public function resetWhere(): self { $this->where = null; $this->sql = null; return $this; } /** * Resets the grouping for the query. * * @return $this This QueryBuilder instance. */ public function resetGroupBy(): self { $this->groupBy = []; $this->sql = null; return $this; } /** * Resets the HAVING conditions for the query. * * @return $this This QueryBuilder instance. */ public function resetHaving(): self { $this->having = null; $this->sql = null; return $this; } /** * Resets the ordering for the query. * * @return $this This QueryBuilder instance. */ public function resetOrderBy(): self { $this->orderBy = []; $this->sql = null; return $this; } /** @throws Exception */ private function getSQLForSelect(): string { if (count($this->select) === 0) { throw new QueryException('No SELECT expressions given. Please use select() or addSelect().'); } return $this->connection->getDatabasePlatform() ->createSelectSQLBuilder() ->buildSQL( new SelectQuery( $this->distinct, $this->select, $this->getFromClauses(), $this->where !== null ? (string) $this->where : null, $this->groupBy, $this->having !== null ? (string) $this->having : null, $this->orderBy, new Limit($this->maxResults, $this->firstResult), $this->forUpdate, ), ); } /** * @return array * * @throws QueryException */ private function getFromClauses(): array { $fromClauses = []; $knownAliases = []; foreach ($this->from as $from) { if ($from->alias === null || $from->alias === $from->table) { $tableSql = $from->table; $tableReference = $from->table; } else { $tableSql = $from->table . ' ' . $from->alias; $tableReference = $from->alias; } $knownAliases[$tableReference] = true; $fromClauses[$tableReference] = $tableSql . $this->getSQLForJoins($tableReference, $knownAliases); } $this->verifyAllAliasesAreKnown($knownAliases); return $fromClauses; } /** * @param array $knownAliases * * @throws QueryException */ private function verifyAllAliasesAreKnown(array $knownAliases): void { foreach ($this->join as $fromAlias => $joins) { if (! isset($knownAliases[$fromAlias])) { throw UnknownAlias::new($fromAlias, array_keys($knownAliases)); } } } /** * Converts this instance into an INSERT string in SQL. */ private function getSQLForInsert(): string { return 'INSERT INTO ' . $this->table . ' (' . implode(', ', array_keys($this->values)) . ')' . ' VALUES(' . implode(', ', $this->values) . ')'; } /** * Converts this instance into an UPDATE string in SQL. */ private function getSQLForUpdate(): string { $query = 'UPDATE ' . $this->table . ' SET ' . implode(', ', $this->set); if ($this->where !== null) { $query .= ' WHERE ' . $this->where; } return $query; } /** * Converts this instance into a DELETE string in SQL. */ private function getSQLForDelete(): string { $query = 'DELETE FROM ' . $this->table; if ($this->where !== null) { $query .= ' WHERE ' . $this->where; } return $query; } /** * Gets a string representation of this QueryBuilder which corresponds to * the final SQL query being constructed. * * @return string The string representation of this QueryBuilder. */ public function __toString(): string { return $this->getSQL(); } /** * Creates a new named parameter and bind the value $value to it. * * This method provides a shortcut for {@see Statement::bindValue()} * when using prepared statements. * * The parameter $value specifies the value that you want to bind. If * $placeholder is not provided createNamedParameter() will automatically * create a placeholder for you. An automatic placeholder will be of the * name ':dcValue1', ':dcValue2' etc. * * Example: * * $value = 2; * $q->eq( 'id', $q->createNamedParameter( $value ) ); * $stmt = $q->executeQuery(); // executed with 'id = 2' * * * @link http://www.zetacomponents.org * * @param string|null $placeHolder The name to bind with. The string must start with a colon ':'. * * @return string the placeholder name used. */ public function createNamedParameter( mixed $value, string|ParameterType|Type|ArrayParameterType $type = ParameterType::STRING, ?string $placeHolder = null, ): string { if ($placeHolder === null) { $this->boundCounter++; $placeHolder = ':dcValue' . $this->boundCounter; } $this->setParameter(substr($placeHolder, 1), $value, $type); return $placeHolder; } /** * Creates a new positional parameter and bind the given value to it. * * Attention: If you are using positional parameters with the query builder you have * to be very careful to bind all parameters in the order they appear in the SQL * statement , otherwise they get bound in the wrong order which can lead to serious * bugs in your code. * * Example: * * $qb = $conn->createQueryBuilder(); * $qb->select('u.*') * ->from('users', 'u') * ->where('u.username = ' . $qb->createPositionalParameter('Foo', ParameterType::STRING)) * ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', ParameterType::STRING)) * */ public function createPositionalParameter( mixed $value, string|ParameterType|Type|ArrayParameterType $type = ParameterType::STRING, ): string { $this->setParameter($this->boundCounter, $value, $type); $this->boundCounter++; return '?'; } /** * @param array $knownAliases * * @throws QueryException */ private function getSQLForJoins(string $fromAlias, array &$knownAliases): string { $sql = ''; if (! isset($this->join[$fromAlias])) { return $sql; } foreach ($this->join[$fromAlias] as $join) { if (array_key_exists($join->alias, $knownAliases)) { throw NonUniqueAlias::new($join->alias, array_keys($knownAliases)); } $sql .= ' ' . $join->type . ' JOIN ' . $join->table . ' ' . $join->alias; if ($join->condition !== null) { $sql .= ' ON ' . $join->condition; } $knownAliases[$join->alias] = true; } foreach ($this->join[$fromAlias] as $join) { $sql .= $this->getSQLForJoins($join->alias, $knownAliases); } return $sql; } /** * Deep clone of all expression objects in the SQL parts. */ public function __clone() { foreach ($this->from as $key => $from) { $this->from[$key] = clone $from; } foreach ($this->join as $fromAlias => $joins) { foreach ($joins as $key => $join) { $this->join[$fromAlias][$key] = clone $join; } } if (is_object($this->where)) { $this->where = clone $this->where; } if (is_object($this->having)) { $this->having = clone $this->having; } foreach ($this->params as $name => $param) { if (! is_object($param)) { continue; } $this->params[$name] = clone $param; } } /** * Enables caching of the results of this query, for given amount of seconds * and optionally specified witch key to use for the cache entry. * * @return $this */ public function enableResultCache(QueryCacheProfile $cacheProfile): self { $this->resultCacheProfile = $cacheProfile; return $this; } /** * Disables caching of the results of this query. * * @return $this */ public function disableResultCache(): self { $this->resultCacheProfile = null; return $this; } } src/Query/QueryException.php000064400000000270152062402520012132 0ustar00distinct; } /** @return string[] */ public function getColumns(): array { return $this->columns; } /** @return string[] */ public function getFrom(): array { return $this->from; } public function getWhere(): ?string { return $this->where; } /** @return string[] */ public function getGroupBy(): array { return $this->groupBy; } public function getHaving(): ?string { return $this->having; } /** @return string[] */ public function getOrderBy(): array { return $this->orderBy; } public function getLimit(): Limit { return $this->limit; } public function getForUpdate(): ?ForUpdate { return $this->forUpdate; } } src/Result.php000064400000015415152062402520007326 0ustar00|false * * @throws Exception */ public function fetchNumeric(): array|false { try { return $this->result->fetchNumeric(); } catch (DriverException $e) { throw $this->connection->convertException($e); } } /** * Returns the next row of the result as an associative array or FALSE if there are no more rows. * * @return array|false * * @throws Exception */ public function fetchAssociative(): array|false { try { return $this->result->fetchAssociative(); } catch (DriverException $e) { throw $this->connection->convertException($e); } } /** * Returns the first value of the next row of the result or FALSE if there are no more rows. * * @throws Exception */ public function fetchOne(): mixed { try { return $this->result->fetchOne(); } catch (DriverException $e) { throw $this->connection->convertException($e); } } /** * Returns an array containing all of the result rows represented as numeric arrays. * * @return list> * * @throws Exception */ public function fetchAllNumeric(): array { try { return $this->result->fetchAllNumeric(); } catch (DriverException $e) { throw $this->connection->convertException($e); } } /** * Returns an array containing all of the result rows represented as associative arrays. * * @return list> * * @throws Exception */ public function fetchAllAssociative(): array { try { return $this->result->fetchAllAssociative(); } catch (DriverException $e) { throw $this->connection->convertException($e); } } /** * Returns an array containing the values of the first column of the result. * * @return array * * @throws Exception */ public function fetchAllKeyValue(): array { $this->ensureHasKeyValue(); $data = []; foreach ($this->fetchAllNumeric() as $row) { assert(count($row) >= 2); [$key, $value] = $row; $data[$key] = $value; } return $data; } /** * Returns an associative array with the keys mapped to the first column and the values being * an associative array representing the rest of the columns and their values. * * @return array> * * @throws Exception */ public function fetchAllAssociativeIndexed(): array { $data = []; foreach ($this->fetchAllAssociative() as $row) { $data[array_shift($row)] = $row; } return $data; } /** * @return list * * @throws Exception */ public function fetchFirstColumn(): array { try { return $this->result->fetchFirstColumn(); } catch (DriverException $e) { throw $this->connection->convertException($e); } } /** * @return Traversable> * * @throws Exception */ public function iterateNumeric(): Traversable { while (($row = $this->fetchNumeric()) !== false) { yield $row; } } /** * @return Traversable> * * @throws Exception */ public function iterateAssociative(): Traversable { while (($row = $this->fetchAssociative()) !== false) { yield $row; } } /** * @return Traversable * * @throws Exception */ public function iterateKeyValue(): Traversable { $this->ensureHasKeyValue(); foreach ($this->iterateNumeric() as $row) { assert(count($row) >= 2); [$key, $value] = $row; yield $key => $value; } } /** * Returns an iterator over the result set with the keys mapped to the first column and the values being * an associative array representing the rest of the columns and their values. * * @return Traversable> * * @throws Exception */ public function iterateAssociativeIndexed(): Traversable { foreach ($this->iterateAssociative() as $row) { yield array_shift($row) => $row; } } /** * @return Traversable * * @throws Exception */ public function iterateColumn(): Traversable { while (($value = $this->fetchOne()) !== false) { yield $value; } } /** * Returns the number of rows affected by the DELETE, INSERT, or UPDATE statement that produced the result. * * If the statement executed a SELECT query or a similar platform-specific SQL (e.g. DESCRIBE, SHOW, etc.), * some database drivers may return the number of rows returned by that query. However, this behaviour * is not guaranteed for all drivers and should not be relied on in portable applications. * * If the number of rows exceeds {@see PHP_INT_MAX}, it might be returned as string if the driver supports it. * * @return int|numeric-string * * @throws Exception */ public function rowCount(): int|string { try { return $this->result->rowCount(); } catch (DriverException $e) { throw $this->connection->convertException($e); } } /** @throws Exception */ public function columnCount(): int { try { return $this->result->columnCount(); } catch (DriverException $e) { throw $this->connection->convertException($e); } } public function free(): void { $this->result->free(); } /** @throws Exception */ private function ensureHasKeyValue(): void { $columnCount = $this->columnCount(); if ($columnCount < 2) { throw NoKeyValue::fromColumnCount($columnCount); } } } src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php000064400000003314152062402520015615 0ustar00 */ public function buildSQL(Schema $schema): array { return array_merge( $this->buildNamespaceStatements($schema->getNamespaces()), $this->buildSequenceStatements($schema->getSequences()), $this->buildTableStatements($schema->getTables()), ); } /** * @param list $namespaces * * @return list */ private function buildNamespaceStatements(array $namespaces): array { $statements = []; if ($this->platform->supportsSchemas()) { foreach ($namespaces as $namespace) { $statements[] = $this->platform->getCreateSchemaSQL($namespace); } } return $statements; } /** * @param list
$tables * * @return list */ private function buildTableStatements(array $tables): array { return $this->platform->getCreateTablesSQL($tables); } /** * @param list $sequences * * @return list */ private function buildSequenceStatements(array $sequences): array { $statements = []; foreach ($sequences as $sequence) { $statements[] = $this->platform->getCreateSequenceSQL($sequence); } return $statements; } } src/SQL/Builder/DefaultSelectSQLBuilder.php000064400000004623152062402520014507 0ustar00isDistinct()) { $parts[] = 'DISTINCT'; } $parts[] = implode(', ', $query->getColumns()); $from = $query->getFrom(); if (count($from) > 0) { $parts[] = 'FROM ' . implode(', ', $from); } $where = $query->getWhere(); if ($where !== null) { $parts[] = 'WHERE ' . $where; } $groupBy = $query->getGroupBy(); if (count($groupBy) > 0) { $parts[] = 'GROUP BY ' . implode(', ', $groupBy); } $having = $query->getHaving(); if ($having !== null) { $parts[] = 'HAVING ' . $having; } $orderBy = $query->getOrderBy(); if (count($orderBy) > 0) { $parts[] = 'ORDER BY ' . implode(', ', $orderBy); } $sql = implode(' ', $parts); $limit = $query->getLimit(); if ($limit->isDefined()) { $sql = $this->platform->modifyLimitQuery($sql, $limit->getMaxResults(), $limit->getFirstResult()); } $forUpdate = $query->getForUpdate(); if ($forUpdate !== null) { if ($this->forUpdateSQL === null) { throw NotSupported::new('FOR UPDATE'); } $sql .= ' ' . $this->forUpdateSQL; if ($forUpdate->getConflictResolutionMode() === ConflictResolutionMode::SKIP_LOCKED) { if ($this->skipLockedSQL === null) { throw NotSupported::new('SKIP LOCKED'); } $sql .= ' ' . $this->skipLockedSQL; } } return $sql; } } src/SQL/Builder/DropSchemaObjectsSQLBuilder.php000064400000002353152062402520015320 0ustar00 */ public function buildSQL(Schema $schema): array { return array_merge( $this->buildSequenceStatements($schema->getSequences()), $this->buildTableStatements($schema->getTables()), ); } /** * @param list
$tables * * @return list */ private function buildTableStatements(array $tables): array { return $this->platform->getDropTablesSQL($tables); } /** * @param list $sequences * * @return list */ private function buildSequenceStatements(array $sequences): array { $statements = []; foreach ($sequences as $sequence) { $statements[] = $this->platform->getDropSequenceSQL($sequence->getQuotedName($this->platform)); } return $statements; } } src/SQL/Builder/SelectSQLBuilder.php000064400000000400152062402520013167 0ustar00getMySQLStringLiteralPattern("'"), $this->getMySQLStringLiteralPattern('"'), ]; } else { $patterns = [ $this->getAnsiSQLStringLiteralPattern("'"), $this->getAnsiSQLStringLiteralPattern('"'), ]; } $patterns = array_merge($patterns, [ self::BACKTICK_IDENTIFIER, self::BRACKET_IDENTIFIER, self::MULTICHAR, self::ONE_LINE_COMMENT, self::MULTI_LINE_COMMENT, self::OTHER, ]); $this->sqlPattern = sprintf('(%s)', implode('|', $patterns)); } /** * Parses the given SQL statement * * @throws Exception */ public function parse(string $sql, Visitor $visitor): void { /** @var array $patterns */ $patterns = [ self::NAMED_PARAMETER => static function (string $sql) use ($visitor): void { $visitor->acceptNamedParameter($sql); }, self::POSITIONAL_PARAMETER => static function (string $sql) use ($visitor): void { $visitor->acceptPositionalParameter($sql); }, $this->sqlPattern => static function (string $sql) use ($visitor): void { $visitor->acceptOther($sql); }, self::SPECIAL => static function (string $sql) use ($visitor): void { $visitor->acceptOther($sql); }, ]; $offset = 0; while (($handler = current($patterns)) !== false) { if (preg_match('~\G' . key($patterns) . '~s', $sql, $matches, 0, $offset) === 1) { $handler($matches[0]); reset($patterns); $offset += strlen($matches[0]); } elseif (preg_last_error() !== PREG_NO_ERROR) { // @codeCoverageIgnoreStart throw RegularExpressionError::new(); // @codeCoverageIgnoreEnd } else { next($patterns); } } assert($offset === strlen($sql)); } private function getMySQLStringLiteralPattern(string $delimiter): string { return $delimiter . '((\\\\.)|(?![' . $delimiter . '\\\\]).)*' . $delimiter; } private function getAnsiSQLStringLiteralPattern(string $delimiter): string { return $delimiter . '[^' . $delimiter . ']*' . $delimiter; } } src/SQL/Parser/Exception.php000064400000000200152062402520011663 0ustar00 Table($tableName)); if you want to rename the table, you have to make sure */ abstract class AbstractAsset { protected string $_name = ''; /** * Namespace of the asset. If none isset the default namespace is assumed. */ protected ?string $_namespace = null; protected bool $_quoted = false; /** * Sets the name of this asset. */ protected function _setName(string $name): void { if ($this->isIdentifierQuoted($name)) { $this->_quoted = true; $name = $this->trimQuotes($name); } if (str_contains($name, '.')) { $parts = explode('.', $name); $this->_namespace = $parts[0]; $name = $parts[1]; } $this->_name = $name; } /** * Is this asset in the default namespace? */ public function isInDefaultNamespace(string $defaultNamespaceName): bool { return $this->_namespace === $defaultNamespaceName || $this->_namespace === null; } /** * Gets the namespace name of this asset. * * If NULL is returned this means the default namespace is used. */ public function getNamespaceName(): ?string { return $this->_namespace; } /** * The shortest name is stripped of the default namespace. All other * namespaced elements are returned as full-qualified names. */ public function getShortestName(?string $defaultNamespaceName): string { $shortestName = $this->getName(); if ($this->_namespace === $defaultNamespaceName) { $shortestName = $this->_name; } return strtolower($shortestName); } /** * Checks if this asset's name is quoted. */ public function isQuoted(): bool { return $this->_quoted; } /** * Checks if this identifier is quoted. */ protected function isIdentifierQuoted(string $identifier): bool { return isset($identifier[0]) && ($identifier[0] === '`' || $identifier[0] === '"' || $identifier[0] === '['); } /** * Trim quotes from the identifier. */ protected function trimQuotes(string $identifier): string { return str_replace(['`', '"', '[', ']'], '', $identifier); } /** * Returns the name of this schema asset. */ public function getName(): string { if ($this->_namespace !== null) { return $this->_namespace . '.' . $this->_name; } return $this->_name; } /** * Gets the quoted representation of this asset but only if it was defined with one. Otherwise * return the plain unquoted value as inserted. */ public function getQuotedName(AbstractPlatform $platform): string { $keywords = $platform->getReservedKeywordsList(); $parts = explode('.', $this->getName()); foreach ($parts as $k => $v) { $parts[$k] = $this->_quoted || $keywords->isKeyword($v) ? $platform->quoteIdentifier($v) : $v; } return implode('.', $parts); } /** * Generates an identifier from a list of column names obeying a certain string length. * * This is especially important for Oracle, since it does not allow identifiers larger than 30 chars, * however building idents automatically for foreign keys, composite keys or such can easily create * very long names. * * @param array $columnNames */ protected function _generateIdentifierName(array $columnNames, string $prefix = '', int $maxSize = 30): string { $hash = implode('', array_map(static function ($column): string { return dechex(crc32($column)); }, $columnNames)); return strtoupper(substr($prefix . '_' . $hash, 0, $maxSize)); } } src/Schema/AbstractSchemaManager.php000064400000057303152062402520013431 0ustar00 * * @throws Exception */ public function listDatabases(): array { return array_map(function (array $row): string { return $this->_getPortableDatabaseDefinition($row); }, $this->connection->fetchAllAssociative( $this->platform->getListDatabasesSQL(), )); } /** * Returns a list of the names of all schemata in the current database. * * @return list * * @throws Exception */ public function listSchemaNames(): array { throw NotSupported::new(__METHOD__); } /** * Lists the available sequences for this connection. * * @return array * * @throws Exception */ public function listSequences(): array { return $this->filterAssetNames( array_map(function (array $row): Sequence { return $this->_getPortableSequenceDefinition($row); }, $this->connection->fetchAllAssociative( $this->platform->getListSequencesSQL( $this->getDatabase(__METHOD__), ), )), ); } /** * Lists the columns for a given table. * * In contrast to other libraries and to the old version of Doctrine, * this column definition does try to contain the 'primary' column for * the reason that it is not portable across different RDBMS. Use * {@see listTableIndexes($tableName)} to retrieve the primary key * of a table. Where a RDBMS specifies more details, these are held * in the platformDetails array. * * @return array * * @throws Exception */ public function listTableColumns(string $table): array { $database = $this->getDatabase(__METHOD__); return $this->_getPortableTableColumnList( $table, $database, $this->selectTableColumns($database, $this->normalizeName($table)) ->fetchAllAssociative(), ); } /** * Lists the indexes for a given table returning an array of Index instances. * * Keys of the portable indexes list are all lower-cased. * * @return array * * @throws Exception */ public function listTableIndexes(string $table): array { $database = $this->getDatabase(__METHOD__); $table = $this->normalizeName($table); return $this->_getPortableTableIndexesList( $this->selectIndexColumns( $database, $table, )->fetchAllAssociative(), $table, ); } /** * Returns true if all the given tables exist. * * @param array $names * * @throws Exception */ public function tablesExist(array $names): bool { $names = array_map('strtolower', $names); return count($names) === count(array_intersect($names, array_map('strtolower', $this->listTableNames()))); } public function tableExists(string $tableName): bool { return $this->tablesExist([$tableName]); } /** * Returns a list of all tables in the current database. * * @return array * * @throws Exception */ public function listTableNames(): array { return $this->filterAssetNames( array_map(function (array $row): string { return $this->_getPortableTableDefinition($row); }, $this->selectTableNames( $this->getDatabase(__METHOD__), )->fetchAllAssociative()), ); } /** * Filters asset names if they are configured to return only a subset of all * the found elements. * * @param array $assetNames * * @return array */ private function filterAssetNames(array $assetNames): array { $filter = $this->connection->getConfiguration()->getSchemaAssetsFilter(); return array_values(array_filter($assetNames, $filter)); } /** * Lists the tables for this connection. * * @return list
* * @throws Exception */ public function listTables(): array { $database = $this->getDatabase(__METHOD__); $tableColumnsByTable = $this->fetchTableColumnsByTable($database); $indexColumnsByTable = $this->fetchIndexColumnsByTable($database); $foreignKeyColumnsByTable = $this->fetchForeignKeyColumnsByTable($database); $tableOptionsByTable = $this->fetchTableOptionsByTable($database); $filter = $this->connection->getConfiguration()->getSchemaAssetsFilter(); $tables = []; foreach ($tableColumnsByTable as $tableName => $tableColumns) { if (! $filter($tableName)) { continue; } $tables[] = new Table( $tableName, $this->_getPortableTableColumnList($tableName, $database, $tableColumns), $this->_getPortableTableIndexesList($indexColumnsByTable[$tableName] ?? [], $tableName), [], $this->_getPortableTableForeignKeysList($foreignKeyColumnsByTable[$tableName] ?? []), $tableOptionsByTable[$tableName] ?? [], ); } return $tables; } /** * An extension point for those platforms where case sensitivity of the object name depends on whether it's quoted. * * Such platforms should convert a possibly quoted name into a value of the corresponding case. */ protected function normalizeName(string $name): string { $identifier = new Identifier($name); return $identifier->getName(); } /** * Selects names of tables in the specified database. * * @throws Exception */ abstract protected function selectTableNames(string $databaseName): Result; /** * Selects definitions of table columns in the specified database. If the table name is specified, narrows down * the selection to this table. * * @throws Exception */ abstract protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result; /** * Selects definitions of index columns in the specified database. If the table name is specified, narrows down * the selection to this table. * * @throws Exception */ abstract protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result; /** * Selects definitions of foreign key columns in the specified database. If the table name is specified, * narrows down the selection to this table. * * @throws Exception */ abstract protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result; /** * Fetches definitions of table columns in the specified database and returns them grouped by table name. * * @return array>> * * @throws Exception */ protected function fetchTableColumnsByTable(string $databaseName): array { return $this->fetchAllAssociativeGrouped($this->selectTableColumns($databaseName)); } /** * Fetches definitions of index columns in the specified database and returns them grouped by table name. * * @return array>> * * @throws Exception */ protected function fetchIndexColumnsByTable(string $databaseName): array { return $this->fetchAllAssociativeGrouped($this->selectIndexColumns($databaseName)); } /** * Fetches definitions of foreign key columns in the specified database and returns them grouped by table name. * * @return array>> * * @throws Exception */ protected function fetchForeignKeyColumnsByTable(string $databaseName): array { return $this->fetchAllAssociativeGrouped( $this->selectForeignKeyColumns($databaseName), ); } /** * Fetches table options for the tables in the specified database and returns them grouped by table name. * If the table name is specified, narrows down the selection to this table. * * @return array> * * @throws Exception */ abstract protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array; /** * Introspects the table with the given name. * * @throws Exception */ public function introspectTable(string $name): Table { $columns = $this->listTableColumns($name); if ($columns === []) { throw TableDoesNotExist::new($name); } return new Table( $name, $columns, $this->listTableIndexes($name), [], $this->listTableForeignKeys($name), $this->getTableOptions($name), ); } /** * Lists the views this connection has. * * @return list * * @throws Exception */ public function listViews(): array { return array_map(function (array $row): View { return $this->_getPortableViewDefinition($row); }, $this->connection->fetchAllAssociative( $this->platform->getListViewsSQL( $this->getDatabase(__METHOD__), ), )); } /** * Lists the foreign keys for the given table. * * @return array * * @throws Exception */ public function listTableForeignKeys(string $table): array { $database = $this->getDatabase(__METHOD__); return $this->_getPortableTableForeignKeysList( $this->selectForeignKeyColumns( $database, $this->normalizeName($table), )->fetchAllAssociative(), ); } /** * @return array * * @throws Exception */ private function getTableOptions(string $name): array { $normalizedName = $this->normalizeName($name); return $this->fetchTableOptionsByTable( $this->getDatabase(__METHOD__), $normalizedName, )[$normalizedName] ?? []; } /* drop*() Methods */ /** * Drops a database. * * NOTE: You can not drop the database this SchemaManager is currently connected to. * * @throws Exception */ public function dropDatabase(string $database): void { $this->connection->executeStatement( $this->platform->getDropDatabaseSQL($database), ); } /** * Drops a schema. * * @throws Exception */ public function dropSchema(string $schemaName): void { $this->connection->executeStatement( $this->platform->getDropSchemaSQL($schemaName), ); } /** * Drops the given table. * * @throws Exception */ public function dropTable(string $name): void { $this->connection->executeStatement( $this->platform->getDropTableSQL($name), ); } /** * Drops the index from the given table. * * @throws Exception */ public function dropIndex(string $index, string $table): void { $this->connection->executeStatement( $this->platform->getDropIndexSQL($index, $table), ); } /** * Drops a foreign key from a table. * * @throws Exception */ public function dropForeignKey(string $name, string $table): void { $this->connection->executeStatement( $this->platform->getDropForeignKeySQL($name, $table), ); } /** * Drops a sequence with a given name. * * @throws Exception */ public function dropSequence(string $name): void { $this->connection->executeStatement( $this->platform->getDropSequenceSQL($name), ); } /** * Drops the unique constraint from the given table. * * @throws Exception */ public function dropUniqueConstraint(string $name, string $tableName): void { $this->connection->executeStatement( $this->platform->getDropUniqueConstraintSQL($name, $tableName), ); } /** * Drops a view. * * @throws Exception */ public function dropView(string $name): void { $this->connection->executeStatement( $this->platform->getDropViewSQL($name), ); } /* create*() Methods */ /** @throws Exception */ public function createSchemaObjects(Schema $schema): void { $this->executeStatements($schema->toSql($this->platform)); } /** * Creates a new database. * * @throws Exception */ public function createDatabase(string $database): void { $this->connection->executeStatement( $this->platform->getCreateDatabaseSQL($database), ); } /** * Creates a new table. * * @throws Exception */ public function createTable(Table $table): void { $this->executeStatements($this->platform->getCreateTableSQL($table)); } /** * Creates a new sequence. * * @throws Exception */ public function createSequence(Sequence $sequence): void { $this->connection->executeStatement( $this->platform->getCreateSequenceSQL($sequence), ); } /** * Creates a new index on a table. * * @param string $table The name of the table on which the index is to be created. * * @throws Exception */ public function createIndex(Index $index, string $table): void { $this->connection->executeStatement( $this->platform->getCreateIndexSQL($index, $table), ); } /** * Creates a new foreign key. * * @param ForeignKeyConstraint $foreignKey The ForeignKey instance. * @param string $table The name of the table on which the foreign key is to be created. * * @throws Exception */ public function createForeignKey(ForeignKeyConstraint $foreignKey, string $table): void { $this->connection->executeStatement( $this->platform->getCreateForeignKeySQL($foreignKey, $table), ); } /** * Creates a unique constraint on a table. * * @throws Exception */ public function createUniqueConstraint(UniqueConstraint $uniqueConstraint, string $tableName): void { $this->connection->executeStatement( $this->platform->getCreateUniqueConstraintSQL($uniqueConstraint, $tableName), ); } /** * Creates a new view. * * @throws Exception */ public function createView(View $view): void { $this->connection->executeStatement( $this->platform->getCreateViewSQL( $view->getQuotedName($this->platform), $view->getSql(), ), ); } /** @throws Exception */ public function dropSchemaObjects(Schema $schema): void { $this->executeStatements($schema->toDropSql($this->platform)); } /** * Alters an existing schema. * * @throws Exception */ public function alterSchema(SchemaDiff $schemaDiff): void { $this->executeStatements($this->platform->getAlterSchemaSQL($schemaDiff)); } /** * Migrates an existing schema to a new schema. * * @throws Exception */ public function migrateSchema(Schema $newSchema): void { $schemaDiff = $this->createComparator() ->compareSchemas($this->introspectSchema(), $newSchema); $this->alterSchema($schemaDiff); } /* alterTable() Methods */ /** * Alters an existing tables schema. * * @throws Exception */ public function alterTable(TableDiff $tableDiff): void { $this->executeStatements($this->platform->getAlterTableSQL($tableDiff)); } /** * Renames a given table to another name. * * @throws Exception */ public function renameTable(string $name, string $newName): void { $this->connection->executeStatement( $this->platform->getRenameTableSQL($name, $newName), ); } /** * Methods for filtering return values of list*() methods to convert * the native DBMS data definition to a portable Doctrine definition */ /** @param array $database */ protected function _getPortableDatabaseDefinition(array $database): string { throw NotSupported::new(__METHOD__); } /** @param array $sequence */ protected function _getPortableSequenceDefinition(array $sequence): Sequence { throw NotSupported::new(__METHOD__); } /** * Independent of the database the keys of the column list result are lowercased. * * The name of the created column instance however is kept in its case. * * @param array> $tableColumns * * @return array * * @throws Exception */ protected function _getPortableTableColumnList(string $table, string $database, array $tableColumns): array { $list = []; foreach ($tableColumns as $tableColumn) { $column = $this->_getPortableTableColumnDefinition($tableColumn); $name = strtolower($column->getQuotedName($this->platform)); $list[$name] = $column; } return $list; } /** * Gets Table Column Definition. * * @param array $tableColumn * * @throws Exception */ abstract protected function _getPortableTableColumnDefinition(array $tableColumn): Column; /** * Aggregates and groups the index results according to the required data result. * * @param array> $tableIndexes * * @return array * * @throws Exception */ protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array { $result = []; foreach ($tableIndexes as $tableIndex) { $indexName = $keyName = $tableIndex['key_name']; if ($tableIndex['primary']) { $keyName = 'primary'; } $keyName = strtolower($keyName); if (! isset($result[$keyName])) { $options = [ 'lengths' => [], ]; if (isset($tableIndex['where'])) { $options['where'] = $tableIndex['where']; } $result[$keyName] = [ 'name' => $indexName, 'columns' => [], 'unique' => ! $tableIndex['non_unique'], 'primary' => $tableIndex['primary'], 'flags' => $tableIndex['flags'] ?? [], 'options' => $options, ]; } $result[$keyName]['columns'][] = $tableIndex['column_name']; $result[$keyName]['options']['lengths'][] = $tableIndex['length'] ?? null; } $indexes = []; foreach ($result as $indexKey => $data) { $indexes[$indexKey] = new Index( $data['name'], $data['columns'], $data['unique'], $data['primary'], $data['flags'], $data['options'], ); } return $indexes; } /** @param array $table */ abstract protected function _getPortableTableDefinition(array $table): string; /** @param array $view */ abstract protected function _getPortableViewDefinition(array $view): View; /** * @param array> $tableForeignKeys * * @return array */ protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array { $list = []; foreach ($tableForeignKeys as $value) { $list[] = $this->_getPortableTableForeignKeyDefinition($value); } return $list; } /** @param array $tableForeignKey */ abstract protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint; /** * @param array $sql * * @throws Exception */ private function executeStatements(array $sql): void { foreach ($sql as $query) { $this->connection->executeStatement($query); } } /** * Returns a {@see Schema} instance representing the current database schema. * * @throws Exception */ public function introspectSchema(): Schema { $schemaNames = []; if ($this->platform->supportsSchemas()) { $schemaNames = $this->listSchemaNames(); } $sequences = []; if ($this->platform->supportsSequences()) { $sequences = $this->listSequences(); } $tables = $this->listTables(); return new Schema($tables, $sequences, $this->createSchemaConfig(), $schemaNames); } /** * Creates the configuration for this schema. * * @throws Exception */ public function createSchemaConfig(): SchemaConfig { $schemaConfig = new SchemaConfig(); $schemaConfig->setMaxIdentifierLength($this->platform->getMaxIdentifierLength()); $params = $this->connection->getParams(); if (! isset($params['defaultTableOptions'])) { $params['defaultTableOptions'] = []; } if (! isset($params['defaultTableOptions']['charset']) && isset($params['charset'])) { $params['defaultTableOptions']['charset'] = $params['charset']; } $schemaConfig->setDefaultTableOptions($params['defaultTableOptions']); return $schemaConfig; } /** @throws Exception */ private function getDatabase(string $methodName): string { $database = $this->connection->getDatabase(); if ($database === null) { throw DatabaseRequired::new($methodName); } return $database; } public function createComparator(): Comparator { return new Comparator($this->platform); } /** * @return array>> * * @throws Exception */ private function fetchAllAssociativeGrouped(Result $result): array { $data = []; foreach ($result->fetchAllAssociative() as $row) { $tableName = $this->_getPortableTableDefinition($row); $data[$tableName][] = $row; } return $data; } } src/Schema/Column.php000064400000012256152062402520010505 0ustar00 */ protected array $_platformOptions = []; protected ?string $_columnDefinition = null; protected string $_comment = ''; /** * Creates a new Column. * * @param array $options */ public function __construct(string $name, Type $type, array $options = []) { $this->_setName($name); $this->setType($type); $this->setOptions($options); } /** @param array $options */ public function setOptions(array $options): self { foreach ($options as $name => $value) { $method = 'set' . $name; if (! method_exists($this, $method)) { throw UnknownColumnOption::new($name); } $this->$method($value); } return $this; } public function setType(Type $type): self { $this->_type = $type; return $this; } public function setLength(?int $length): self { $this->_length = $length; return $this; } public function setPrecision(?int $precision): self { $this->_precision = $precision; return $this; } public function setScale(int $scale): self { $this->_scale = $scale; return $this; } public function setUnsigned(bool $unsigned): self { $this->_unsigned = $unsigned; return $this; } public function setFixed(bool $fixed): self { $this->_fixed = $fixed; return $this; } public function setNotnull(bool $notnull): self { $this->_notnull = $notnull; return $this; } public function setDefault(mixed $default): self { $this->_default = $default; return $this; } /** @param array $platformOptions */ public function setPlatformOptions(array $platformOptions): self { $this->_platformOptions = $platformOptions; return $this; } public function setPlatformOption(string $name, mixed $value): self { $this->_platformOptions[$name] = $value; return $this; } public function setColumnDefinition(?string $value): self { $this->_columnDefinition = $value; return $this; } public function getType(): Type { return $this->_type; } public function getLength(): ?int { return $this->_length; } public function getPrecision(): ?int { return $this->_precision; } public function getScale(): int { return $this->_scale; } public function getUnsigned(): bool { return $this->_unsigned; } public function getFixed(): bool { return $this->_fixed; } public function getNotnull(): bool { return $this->_notnull; } public function getDefault(): mixed { return $this->_default; } /** @return array */ public function getPlatformOptions(): array { return $this->_platformOptions; } public function hasPlatformOption(string $name): bool { return isset($this->_platformOptions[$name]); } public function getPlatformOption(string $name): mixed { return $this->_platformOptions[$name]; } public function getColumnDefinition(): ?string { return $this->_columnDefinition; } public function getAutoincrement(): bool { return $this->_autoincrement; } public function setAutoincrement(bool $flag): self { $this->_autoincrement = $flag; return $this; } public function setComment(string $comment): self { $this->_comment = $comment; return $this; } public function getComment(): string { return $this->_comment; } /** @return array */ public function toArray(): array { return array_merge([ 'name' => $this->_name, 'type' => $this->_type, 'default' => $this->_default, 'notnull' => $this->_notnull, 'length' => $this->_length, 'precision' => $this->_precision, 'scale' => $this->_scale, 'fixed' => $this->_fixed, 'unsigned' => $this->_unsigned, 'autoincrement' => $this->_autoincrement, 'columnDefinition' => $this->_columnDefinition, 'comment' => $this->_comment, ], $this->_platformOptions); } } src/Schema/ColumnDiff.php000064400000005535152062402520011300 0ustar00oldColumn; } public function getNewColumn(): Column { return $this->newColumn; } public function hasTypeChanged(): bool { return $this->newColumn->getType()::class !== $this->oldColumn->getType()::class; } public function hasLengthChanged(): bool { return $this->hasPropertyChanged(static function (Column $column): ?int { return $column->getLength(); }); } public function hasPrecisionChanged(): bool { return $this->hasPropertyChanged(static function (Column $column): ?int { return $column->getPrecision(); }); } public function hasScaleChanged(): bool { return $this->hasPropertyChanged(static function (Column $column): int { return $column->getScale(); }); } public function hasUnsignedChanged(): bool { return $this->hasPropertyChanged(static function (Column $column): bool { return $column->getUnsigned(); }); } public function hasFixedChanged(): bool { return $this->hasPropertyChanged(static function (Column $column): bool { return $column->getFixed(); }); } public function hasNotNullChanged(): bool { return $this->hasPropertyChanged(static function (Column $column): bool { return $column->getNotnull(); }); } public function hasDefaultChanged(): bool { $oldDefault = $this->oldColumn->getDefault(); $newDefault = $this->newColumn->getDefault(); // Null values need to be checked additionally as they tell whether to create or drop a default value. // null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation. if (($newDefault === null) xor ($oldDefault === null)) { return true; } return $newDefault != $oldDefault; } public function hasAutoIncrementChanged(): bool { return $this->hasPropertyChanged(static function (Column $column): bool { return $column->getAutoincrement(); }); } public function hasCommentChanged(): bool { return $this->hasPropertyChanged(static function (Column $column): string { return $column->getComment(); }); } private function hasPropertyChanged(callable $property): bool { return $property($this->newColumn) !== $property($this->oldColumn); } } src/Schema/Comparator.php000064400000032712152062402520011356 0ustar00getNamespaces() as $newNamespace) { if ($oldSchema->hasNamespace($newNamespace)) { continue; } $createdSchemas[] = $newNamespace; } foreach ($oldSchema->getNamespaces() as $oldNamespace) { if ($newSchema->hasNamespace($oldNamespace)) { continue; } $droppedSchemas[] = $oldNamespace; } foreach ($newSchema->getTables() as $newTable) { $newTableName = $newTable->getShortestName($newSchema->getName()); if (! $oldSchema->hasTable($newTableName)) { $createdTables[] = $newSchema->getTable($newTableName); } else { $tableDiff = $this->compareTables( $oldSchema->getTable($newTableName), $newSchema->getTable($newTableName), ); if (! $tableDiff->isEmpty()) { $alteredTables[] = $tableDiff; } } } // Check if there are tables removed foreach ($oldSchema->getTables() as $oldTable) { $oldTableName = $oldTable->getShortestName($oldSchema->getName()); $oldTable = $oldSchema->getTable($oldTableName); if ($newSchema->hasTable($oldTableName)) { continue; } $droppedTables[] = $oldTable; } foreach ($newSchema->getSequences() as $newSequence) { $newSequenceName = $newSequence->getShortestName($newSchema->getName()); if (! $oldSchema->hasSequence($newSequenceName)) { if (! $this->isAutoIncrementSequenceInSchema($oldSchema, $newSequence)) { $createdSequences[] = $newSequence; } } else { if ($this->diffSequence($newSequence, $oldSchema->getSequence($newSequenceName))) { $alteredSequences[] = $newSchema->getSequence($newSequenceName); } } } foreach ($oldSchema->getSequences() as $oldSequence) { if ($this->isAutoIncrementSequenceInSchema($newSchema, $oldSequence)) { continue; } $oldSequenceName = $oldSequence->getShortestName($oldSchema->getName()); if ($newSchema->hasSequence($oldSequenceName)) { continue; } $droppedSequences[] = $oldSequence; } return new SchemaDiff( $createdSchemas, $droppedSchemas, $createdTables, $alteredTables, $droppedTables, $createdSequences, $alteredSequences, $droppedSequences, ); } private function isAutoIncrementSequenceInSchema(Schema $schema, Sequence $sequence): bool { foreach ($schema->getTables() as $table) { if ($sequence->isAutoIncrementsFor($table)) { return true; } } return false; } public function diffSequence(Sequence $sequence1, Sequence $sequence2): bool { if ($sequence1->getAllocationSize() !== $sequence2->getAllocationSize()) { return true; } return $sequence1->getInitialValue() !== $sequence2->getInitialValue(); } /** * Compares the tables and returns the difference between them. */ public function compareTables(Table $oldTable, Table $newTable): TableDiff { $addedColumns = []; $modifiedColumns = []; $droppedColumns = []; $addedIndexes = []; $modifiedIndexes = []; $droppedIndexes = []; $addedForeignKeys = []; $modifiedForeignKeys = []; $droppedForeignKeys = []; $oldColumns = $oldTable->getColumns(); $newColumns = $newTable->getColumns(); // See if all the columns in the old table exist in the new table foreach ($newColumns as $newColumn) { $newColumnName = strtolower($newColumn->getName()); if ($oldTable->hasColumn($newColumnName)) { continue; } $addedColumns[$newColumnName] = $newColumn; } // See if there are any removed columns in the new table foreach ($oldColumns as $oldColumn) { $oldColumnName = strtolower($oldColumn->getName()); // See if column is removed in the new table. if (! $newTable->hasColumn($oldColumnName)) { $droppedColumns[$oldColumnName] = $oldColumn; continue; } $newColumn = $newTable->getColumn($oldColumnName); if ($this->columnsEqual($oldColumn, $newColumn)) { continue; } $modifiedColumns[] = new ColumnDiff($oldColumn, $newColumn); } $renamedColumns = $this->detectRenamedColumns($addedColumns, $droppedColumns); $oldIndexes = $oldTable->getIndexes(); $newIndexes = $newTable->getIndexes(); // See if all the indexes from the old table exist in the new one foreach ($newIndexes as $newIndexName => $newIndex) { if (($newIndex->isPrimary() && $oldTable->getPrimaryKey() !== null) || $oldTable->hasIndex($newIndexName)) { continue; } $addedIndexes[$newIndexName] = $newIndex; } // See if there are any removed indexes in the new table foreach ($oldIndexes as $oldIndexName => $oldIndex) { // See if the index is removed in the new table. if ( ($oldIndex->isPrimary() && $newTable->getPrimaryKey() === null) || ! $oldIndex->isPrimary() && ! $newTable->hasIndex($oldIndexName) ) { $droppedIndexes[$oldIndexName] = $oldIndex; continue; } // See if index has changed in the new table. $newIndex = $oldIndex->isPrimary() ? $newTable->getPrimaryKey() : $newTable->getIndex($oldIndexName); assert($newIndex instanceof Index); if (! $this->diffIndex($oldIndex, $newIndex)) { continue; } $modifiedIndexes[] = $newIndex; } $renamedIndexes = $this->detectRenamedIndexes($addedIndexes, $droppedIndexes); $oldForeignKeys = $oldTable->getForeignKeys(); $newForeignKeys = $newTable->getForeignKeys(); foreach ($oldForeignKeys as $oldKey => $oldForeignKey) { foreach ($newForeignKeys as $newKey => $newForeignKey) { if ($this->diffForeignKey($oldForeignKey, $newForeignKey) === false) { unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]); } else { if (strtolower($oldForeignKey->getName()) === strtolower($newForeignKey->getName())) { $modifiedForeignKeys[] = $newForeignKey; unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]); } } } } foreach ($oldForeignKeys as $oldForeignKey) { $droppedForeignKeys[] = $oldForeignKey; } foreach ($newForeignKeys as $newForeignKey) { $addedForeignKeys[] = $newForeignKey; } return new TableDiff( $oldTable, $addedColumns, $modifiedColumns, $droppedColumns, $renamedColumns, $addedIndexes, $modifiedIndexes, $droppedIndexes, $renamedIndexes, $addedForeignKeys, $modifiedForeignKeys, $droppedForeignKeys, ); } /** * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop * however ambiguities between different possibilities should not lead to renaming at all. * * @param array $addedColumns * @param array $removedColumns * * @return array */ private function detectRenamedColumns(array &$addedColumns, array &$removedColumns): array { $candidatesByName = []; foreach ($addedColumns as $addedColumnName => $addedColumn) { foreach ($removedColumns as $removedColumn) { if (! $this->columnsEqual($addedColumn, $removedColumn)) { continue; } $candidatesByName[$addedColumn->getName()][] = [$removedColumn, $addedColumn, $addedColumnName]; } } $renamedColumns = []; foreach ($candidatesByName as $candidates) { if (count($candidates) !== 1) { continue; } [$removedColumn, $addedColumn] = $candidates[0]; $removedColumnName = $removedColumn->getName(); $addedColumnName = strtolower($addedColumn->getName()); if (isset($renamedColumns[$removedColumnName])) { continue; } $renamedColumns[$removedColumnName] = $addedColumn; unset( $addedColumns[$addedColumnName], $removedColumns[strtolower($removedColumnName)], ); } return $renamedColumns; } /** * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop * however ambiguities between different possibilities should not lead to renaming at all. * * @param array $addedIndexes * @param array $removedIndexes * * @return array */ private function detectRenamedIndexes(array &$addedIndexes, array &$removedIndexes): array { $candidatesByName = []; // Gather possible rename candidates by comparing each added and removed index based on semantics. foreach ($addedIndexes as $addedIndexName => $addedIndex) { foreach ($removedIndexes as $removedIndex) { if ($this->diffIndex($addedIndex, $removedIndex)) { continue; } $candidatesByName[$addedIndex->getName()][] = [$removedIndex, $addedIndex, $addedIndexName]; } } $renamedIndexes = []; foreach ($candidatesByName as $candidates) { // If the current rename candidate contains exactly one semantically equal index, // we can safely rename it. // Otherwise, it is unclear if a rename action is really intended, // therefore we let those ambiguous indexes be added/dropped. if (count($candidates) !== 1) { continue; } [$removedIndex, $addedIndex] = $candidates[0]; $removedIndexName = strtolower($removedIndex->getName()); $addedIndexName = strtolower($addedIndex->getName()); if (isset($renamedIndexes[$removedIndexName])) { continue; } $renamedIndexes[$removedIndexName] = $addedIndex; unset( $addedIndexes[$addedIndexName], $removedIndexes[$removedIndexName], ); } return $renamedIndexes; } protected function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2): bool { if ( array_map('strtolower', $key1->getUnquotedLocalColumns()) !== array_map('strtolower', $key2->getUnquotedLocalColumns()) ) { return true; } if ( array_map('strtolower', $key1->getUnquotedForeignColumns()) !== array_map('strtolower', $key2->getUnquotedForeignColumns()) ) { return true; } if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) { return true; } if ($key1->onUpdate() !== $key2->onUpdate()) { return true; } return $key1->onDelete() !== $key2->onDelete(); } /** * Compares the definitions of the given columns */ protected function columnsEqual(Column $column1, Column $column2): bool { return $this->platform->columnsEqual($column1, $column2); } /** * Finds the difference between the indexes $index1 and $index2. * * Compares $index1 with $index2 and returns true if there are any * differences or false in case there are no differences. */ protected function diffIndex(Index $index1, Index $index2): bool { return ! ($index1->isFulfilledBy($index2) && $index2->isFulfilledBy($index1)); } } src/Schema/DB2SchemaManager.php000064400000025566152062402520012243 0ustar00 */ class DB2SchemaManager extends AbstractSchemaManager { /** * {@inheritDoc} * * @throws Exception */ protected function _getPortableTableColumnDefinition(array $tableColumn): Column { $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); $length = $precision = $default = null; $scale = 0; $fixed = false; if ($tableColumn['default'] !== null && $tableColumn['default'] !== 'NULL') { $default = $tableColumn['default']; if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) { $default = str_replace("''", "'", $matches[1]); } } $type = $this->platform->getDoctrineTypeMapping($tableColumn['typename']); switch (strtolower($tableColumn['typename'])) { case 'varchar': if ($tableColumn['codepage'] === 0) { $type = Types::BINARY; } $length = $tableColumn['length']; break; case 'character': if ($tableColumn['codepage'] === 0) { $type = Types::BINARY; } $length = $tableColumn['length']; $fixed = true; break; case 'clob': $length = $tableColumn['length']; break; case 'decimal': case 'double': case 'real': $scale = $tableColumn['scale']; $precision = $tableColumn['length']; break; } $options = [ 'length' => $length, 'unsigned' => false, 'fixed' => $fixed, 'default' => $default, 'autoincrement' => (bool) $tableColumn['autoincrement'], 'notnull' => $tableColumn['nulls'] === 'N', 'platformOptions' => [], ]; if (isset($tableColumn['comment'])) { $options['comment'] = $tableColumn['comment']; } if ($scale !== null && $precision !== null) { $options['scale'] = $scale; $options['precision'] = $precision; } return new Column($tableColumn['colname'], Type::getType($type), $options); } /** * {@inheritDoc} */ protected function _getPortableTableDefinition(array $table): string { $table = array_change_key_case($table, CASE_LOWER); return $table['name']; } /** * {@inheritDoc} */ protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array { foreach ($tableIndexes as &$tableIndexRow) { $tableIndexRow = array_change_key_case($tableIndexRow, CASE_LOWER); $tableIndexRow['primary'] = (bool) $tableIndexRow['primary']; } return parent::_getPortableTableIndexesList($tableIndexes, $tableName); } /** * {@inheritDoc} */ protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint { return new ForeignKeyConstraint( $tableForeignKey['local_columns'], $tableForeignKey['foreign_table'], $tableForeignKey['foreign_columns'], $tableForeignKey['name'], $tableForeignKey['options'], ); } /** * {@inheritDoc} */ protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array { $foreignKeys = []; foreach ($tableForeignKeys as $tableForeignKey) { $tableForeignKey = array_change_key_case($tableForeignKey, CASE_LOWER); if (! isset($foreignKeys[$tableForeignKey['index_name']])) { $foreignKeys[$tableForeignKey['index_name']] = [ 'local_columns' => [$tableForeignKey['local_column']], 'foreign_table' => $tableForeignKey['foreign_table'], 'foreign_columns' => [$tableForeignKey['foreign_column']], 'name' => $tableForeignKey['index_name'], 'options' => [ 'onUpdate' => $tableForeignKey['on_update'], 'onDelete' => $tableForeignKey['on_delete'], ], ]; } else { $foreignKeys[$tableForeignKey['index_name']]['local_columns'][] = $tableForeignKey['local_column']; $foreignKeys[$tableForeignKey['index_name']]['foreign_columns'][] = $tableForeignKey['foreign_column']; } } return parent::_getPortableTableForeignKeysList($foreignKeys); } /** * {@inheritDoc} */ protected function _getPortableViewDefinition(array $view): View { $view = array_change_key_case($view, CASE_LOWER); $sql = ''; $pos = strpos($view['text'], ' AS '); if ($pos !== false) { $sql = substr($view['text'], $pos + 4); } return new View($view['name'], $sql); } protected function normalizeName(string $name): string { $identifier = new Identifier($name); return $identifier->isQuoted() ? $identifier->getName() : strtoupper($name); } protected function selectTableNames(string $databaseName): Result { $sql = <<<'SQL' SELECT NAME FROM SYSIBM.SYSTABLES WHERE TYPE = 'T' AND CREATOR = ? SQL; return $this->connection->executeQuery($sql, [$databaseName]); } protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result { $sql = 'SELECT'; if ($tableName === null) { $sql .= ' C.TABNAME AS NAME,'; } $sql .= <<<'SQL' C.COLNAME, C.TYPENAME, C.CODEPAGE, C.NULLS, C.LENGTH, C.SCALE, C.REMARKS AS COMMENT, CASE WHEN C.GENERATED = 'D' THEN 1 ELSE 0 END AS AUTOINCREMENT, C.DEFAULT FROM SYSCAT.COLUMNS C JOIN SYSCAT.TABLES AS T ON T.TABSCHEMA = C.TABSCHEMA AND T.TABNAME = C.TABNAME SQL; $conditions = ['C.TABSCHEMA = ?', "T.TYPE = 'T'"]; $params = [$databaseName]; if ($tableName !== null) { $conditions[] = 'C.TABNAME = ?'; $params[] = $tableName; } $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY C.TABNAME, C.COLNO'; return $this->connection->executeQuery($sql, $params); } protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result { $sql = 'SELECT'; if ($tableName === null) { $sql .= ' IDX.TABNAME AS NAME,'; } $sql .= <<<'SQL' IDX.INDNAME AS KEY_NAME, IDXCOL.COLNAME AS COLUMN_NAME, CASE WHEN IDX.UNIQUERULE = 'P' THEN 1 ELSE 0 END AS PRIMARY, CASE WHEN IDX.UNIQUERULE = 'D' THEN 1 ELSE 0 END AS NON_UNIQUE FROM SYSCAT.INDEXES AS IDX JOIN SYSCAT.TABLES AS T ON IDX.TABSCHEMA = T.TABSCHEMA AND IDX.TABNAME = T.TABNAME JOIN SYSCAT.INDEXCOLUSE AS IDXCOL ON IDX.INDSCHEMA = IDXCOL.INDSCHEMA AND IDX.INDNAME = IDXCOL.INDNAME SQL; $conditions = ['IDX.TABSCHEMA = ?', "T.TYPE = 'T'"]; $params = [$databaseName]; if ($tableName !== null) { $conditions[] = 'IDX.TABNAME = ?'; $params[] = $tableName; } $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY IDX.INDNAME, IDXCOL.COLSEQ'; return $this->connection->executeQuery($sql, $params); } protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result { $sql = 'SELECT'; if ($tableName === null) { $sql .= ' R.TABNAME AS NAME,'; } $sql .= <<<'SQL' FKCOL.COLNAME AS LOCAL_COLUMN, R.REFTABNAME AS FOREIGN_TABLE, PKCOL.COLNAME AS FOREIGN_COLUMN, R.CONSTNAME AS INDEX_NAME, CASE WHEN R.UPDATERULE = 'R' THEN 'RESTRICT' END AS ON_UPDATE, CASE WHEN R.DELETERULE = 'C' THEN 'CASCADE' WHEN R.DELETERULE = 'N' THEN 'SET NULL' WHEN R.DELETERULE = 'R' THEN 'RESTRICT' END AS ON_DELETE FROM SYSCAT.REFERENCES AS R JOIN SYSCAT.TABLES AS T ON T.TABSCHEMA = R.TABSCHEMA AND T.TABNAME = R.TABNAME JOIN SYSCAT.KEYCOLUSE AS FKCOL ON FKCOL.CONSTNAME = R.CONSTNAME AND FKCOL.TABSCHEMA = R.TABSCHEMA AND FKCOL.TABNAME = R.TABNAME JOIN SYSCAT.KEYCOLUSE AS PKCOL ON PKCOL.CONSTNAME = R.REFKEYNAME AND PKCOL.TABSCHEMA = R.REFTABSCHEMA AND PKCOL.TABNAME = R.REFTABNAME AND PKCOL.COLSEQ = FKCOL.COLSEQ SQL; $conditions = ['R.TABSCHEMA = ?', "T.TYPE = 'T'"]; $params = [$databaseName]; if ($tableName !== null) { $conditions[] = 'R.TABNAME = ?'; $params[] = $tableName; } $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY R.CONSTNAME, FKCOL.COLSEQ'; return $this->connection->executeQuery($sql, $params); } /** * {@inheritDoc} */ protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array { $sql = 'SELECT NAME, REMARKS'; $conditions = []; $params = []; if ($tableName !== null) { $conditions[] = 'NAME = ?'; $params[] = $tableName; } $sql .= ' FROM SYSIBM.SYSTABLES'; if ($conditions !== []) { $sql .= ' WHERE ' . implode(' AND ', $conditions); } /** @var array> $metadata */ $metadata = $this->connection->executeQuery($sql, $params) ->fetchAllAssociativeIndexed(); $tableOptions = []; foreach ($metadata as $table => $data) { $data = array_change_key_case($data, CASE_LOWER); $tableOptions[$table] = ['comment' => $data['remarks']]; } return $tableOptions; } } src/Schema/DefaultSchemaManagerFactory.php000064400000001102152062402520014564 0ustar00getDatabasePlatform()->createSchemaManager($connection); } } src/Schema/Exception/ColumnAlreadyExists.php000064400000000724152062402520015142 0ustar00 */ protected array $_localColumnNames; /** * Table or asset identifier instance of the referenced table name the foreign key constraint is associated with. */ protected Identifier $_foreignTableName; /** * Asset identifier instances of the referenced table column names the foreign key constraint is associated with. * * @var array */ protected array $_foreignColumnNames; /** * Initializes the foreign key constraint. * * @param array $localColumnNames Names of the referencing table columns. * @param string $foreignTableName Referenced table. * @param array $foreignColumnNames Names of the referenced table columns. * @param string $name Name of the foreign key constraint. * @param array $options Options associated with the foreign key constraint. */ public function __construct( array $localColumnNames, string $foreignTableName, array $foreignColumnNames, string $name = '', protected array $options = [], ) { $this->_setName($name); $this->_localColumnNames = $this->createIdentifierMap($localColumnNames); $this->_foreignTableName = new Identifier($foreignTableName); $this->_foreignColumnNames = $this->createIdentifierMap($foreignColumnNames); } /** * @param array $names * * @return array */ private function createIdentifierMap(array $names): array { $identifiers = []; foreach ($names as $name) { $identifiers[$name] = new Identifier($name); } return $identifiers; } /** * Returns the names of the referencing table columns * the foreign key constraint is associated with. * * @return array */ public function getLocalColumns(): array { return array_keys($this->_localColumnNames); } /** * Returns the quoted representation of the referencing table column names * the foreign key constraint is associated with. * * But only if they were defined with one or the referencing table column name * is a keyword reserved by the platform. * Otherwise the plain unquoted value as inserted is returned. * * @param AbstractPlatform $platform The platform to use for quotation. * * @return array */ public function getQuotedLocalColumns(AbstractPlatform $platform): array { $columns = []; foreach ($this->_localColumnNames as $column) { $columns[] = $column->getQuotedName($platform); } return $columns; } /** * Returns unquoted representation of local table column names for comparison with other FK * * @return array */ public function getUnquotedLocalColumns(): array { return array_map($this->trimQuotes(...), $this->getLocalColumns()); } /** * Returns unquoted representation of foreign table column names for comparison with other FK * * @return array */ public function getUnquotedForeignColumns(): array { return array_map($this->trimQuotes(...), $this->getForeignColumns()); } /** * Returns the name of the referenced table * the foreign key constraint is associated with. */ public function getForeignTableName(): string { return $this->_foreignTableName->getName(); } /** * Returns the non-schema qualified foreign table name. */ public function getUnqualifiedForeignTableName(): string { $name = $this->_foreignTableName->getName(); $position = strrpos($name, '.'); if ($position !== false) { $name = substr($name, $position + 1); } return strtolower($name); } /** * Returns the quoted representation of the referenced table name * the foreign key constraint is associated with. * * But only if it was defined with one or the referenced table name * is a keyword reserved by the platform. * Otherwise the plain unquoted value as inserted is returned. * * @param AbstractPlatform $platform The platform to use for quotation. */ public function getQuotedForeignTableName(AbstractPlatform $platform): string { return $this->_foreignTableName->getQuotedName($platform); } /** * Returns the names of the referenced table columns * the foreign key constraint is associated with. * * @return array */ public function getForeignColumns(): array { return array_keys($this->_foreignColumnNames); } /** * Returns the quoted representation of the referenced table column names * the foreign key constraint is associated with. * * But only if they were defined with one or the referenced table column name * is a keyword reserved by the platform. * Otherwise the plain unquoted value as inserted is returned. * * @param AbstractPlatform $platform The platform to use for quotation. * * @return array */ public function getQuotedForeignColumns(AbstractPlatform $platform): array { $columns = []; foreach ($this->_foreignColumnNames as $column) { $columns[] = $column->getQuotedName($platform); } return $columns; } /** * Returns whether or not a given option * is associated with the foreign key constraint. */ public function hasOption(string $name): bool { return isset($this->options[$name]); } /** * Returns an option associated with the foreign key constraint. */ public function getOption(string $name): mixed { return $this->options[$name]; } /** * Returns the options associated with the foreign key constraint. * * @return array */ public function getOptions(): array { return $this->options; } /** * Returns the referential action for UPDATE operations * on the referenced table the foreign key constraint is associated with. */ public function onUpdate(): ?string { return $this->onEvent('onUpdate'); } /** * Returns the referential action for DELETE operations * on the referenced table the foreign key constraint is associated with. */ public function onDelete(): ?string { return $this->onEvent('onDelete'); } /** * Returns the referential action for a given database operation * on the referenced table the foreign key constraint is associated with. * * @param string $event Name of the database operation/event to return the referential action for. */ private function onEvent(string $event): ?string { if (isset($this->options[$event])) { $onEvent = strtoupper($this->options[$event]); if ($onEvent !== 'NO ACTION' && $onEvent !== 'RESTRICT') { return $onEvent; } } return null; } /** * Checks whether this foreign key constraint intersects the given index columns. * * Returns `true` if at least one of this foreign key's local columns * matches one of the given index's columns, `false` otherwise. * * @param Index $index The index to be checked against. */ public function intersectsIndexColumns(Index $index): bool { foreach ($index->getColumns() as $indexColumn) { foreach ($this->_localColumnNames as $localColumn) { if (strtolower($indexColumn) === strtolower($localColumn->getName())) { return true; } } } return false; } } src/Schema/Identifier.php000064400000001300152062402520011316 0ustar00_setName($identifier); if (! $quote || $this->_quoted) { return; } $this->_setName('"' . $this->getName() . '"'); } } src/Schema/Index.php000064400000020402152062402520010307 0ustar00 */ protected array $_columns = []; protected bool $_isUnique = false; protected bool $_isPrimary = false; /** * Platform specific flags for indexes. * * @var array */ protected array $_flags = []; /** * @param array $columns * @param array $flags * @param array $options */ public function __construct( ?string $name, array $columns, bool $isUnique = false, bool $isPrimary = false, array $flags = [], private readonly array $options = [], ) { $isUnique = $isUnique || $isPrimary; if ($name !== null) { $this->_setName($name); } $this->_isUnique = $isUnique; $this->_isPrimary = $isPrimary; foreach ($columns as $column) { $this->_addColumn($column); } foreach ($flags as $flag) { $this->addFlag($flag); } } protected function _addColumn(string $column): void { $this->_columns[$column] = new Identifier($column); } /** * Returns the names of the referencing table columns the constraint is associated with. * * @return list */ public function getColumns(): array { return array_keys($this->_columns); } /** * Returns the quoted representation of the column names the constraint is associated with. * * But only if they were defined with one or a column name * is a keyword reserved by the platform. * Otherwise, the plain unquoted value as inserted is returned. * * @param AbstractPlatform $platform The platform to use for quotation. * * @return list */ public function getQuotedColumns(AbstractPlatform $platform): array { $subParts = $platform->supportsColumnLengthIndexes() && $this->hasOption('lengths') ? $this->getOption('lengths') : []; $columns = []; foreach ($this->_columns as $column) { $length = array_shift($subParts); $quotedColumn = $column->getQuotedName($platform); if ($length !== null) { $quotedColumn .= '(' . $length . ')'; } $columns[] = $quotedColumn; } return $columns; } /** @return array */ public function getUnquotedColumns(): array { return array_map($this->trimQuotes(...), $this->getColumns()); } /** * Is the index neither unique nor primary key? */ public function isSimpleIndex(): bool { return ! $this->_isPrimary && ! $this->_isUnique; } public function isUnique(): bool { return $this->_isUnique; } public function isPrimary(): bool { return $this->_isPrimary; } public function hasColumnAtPosition(string $name, int $pos = 0): bool { $name = $this->trimQuotes(strtolower($name)); $indexColumns = array_map('strtolower', $this->getUnquotedColumns()); return array_search($name, $indexColumns, true) === $pos; } /** * Checks if this index exactly spans the given column names in the correct order. * * @param array $columnNames */ public function spansColumns(array $columnNames): bool { $columns = $this->getColumns(); $numberOfColumns = count($columns); $sameColumns = true; for ($i = 0; $i < $numberOfColumns; $i++) { if ( isset($columnNames[$i]) && $this->trimQuotes(strtolower($columns[$i])) === $this->trimQuotes(strtolower($columnNames[$i])) ) { continue; } $sameColumns = false; } return $sameColumns; } /** * Checks if the other index already fulfills all the indexing and constraint needs of the current one. */ public function isFulfilledBy(Index $other): bool { // allow the other index to be equally large only. It being larger is an option // but it creates a problem with scenarios of the kind PRIMARY KEY(foo,bar) UNIQUE(foo) if (count($other->getColumns()) !== count($this->getColumns())) { return false; } // Check if columns are the same, and even in the same order $sameColumns = $this->spansColumns($other->getColumns()); if ($sameColumns) { if (! $this->samePartialIndex($other)) { return false; } if (! $this->hasSameColumnLengths($other)) { return false; } if (! $this->isUnique() && ! $this->isPrimary()) { // this is a special case: If the current key is neither primary or unique, any unique or // primary key will always have the same effect for the index and there cannot be any constraint // overlaps. This means a primary or unique index can always fulfill the requirements of just an // index that has no constraints. return true; } if ($other->isPrimary() !== $this->isPrimary()) { return false; } return $other->isUnique() === $this->isUnique(); } return false; } /** * Detects if the other index is a non-unique, non primary index that can be overwritten by this one. */ public function overrules(Index $other): bool { if ($other->isPrimary()) { return false; } if ($this->isSimpleIndex() && $other->isUnique()) { return false; } return $this->spansColumns($other->getColumns()) && ($this->isPrimary() || $this->isUnique()) && $this->samePartialIndex($other); } /** * Returns platform specific flags for indexes. * * @return array */ public function getFlags(): array { return array_keys($this->_flags); } /** * Adds Flag for an index that translates to platform specific handling. * * @example $index->addFlag('CLUSTERED') */ public function addFlag(string $flag): self { $this->_flags[strtolower($flag)] = true; return $this; } /** * Does this index have a specific flag? */ public function hasFlag(string $flag): bool { return isset($this->_flags[strtolower($flag)]); } /** * Removes a flag. */ public function removeFlag(string $flag): void { unset($this->_flags[strtolower($flag)]); } public function hasOption(string $name): bool { return isset($this->options[strtolower($name)]); } public function getOption(string $name): mixed { return $this->options[strtolower($name)]; } /** @return array */ public function getOptions(): array { return $this->options; } /** * Return whether the two indexes have the same partial index */ private function samePartialIndex(Index $other): bool { if ( $this->hasOption('where') && $other->hasOption('where') && $this->getOption('where') === $other->getOption('where') ) { return true; } return ! $this->hasOption('where') && ! $other->hasOption('where'); } /** * Returns whether the index has the same column lengths as the other */ private function hasSameColumnLengths(self $other): bool { $filter = static function (?int $length): bool { return $length !== null; }; return array_filter($this->options['lengths'] ?? [], $filter) === array_filter($other->options['lengths'] ?? [], $filter); } } src/Schema/MySQLSchemaManager.php000064400000042002152062402520012621 0ustar00 */ class MySQLSchemaManager extends AbstractSchemaManager { /** @see https://mariadb.com/kb/en/library/string-literals/#escape-sequences */ private const MARIADB_ESCAPE_SEQUENCES = [ '\\0' => "\0", "\\'" => "'", '\\"' => '"', '\\b' => "\b", '\\n' => "\n", '\\r' => "\r", '\\t' => "\t", '\\Z' => "\x1a", '\\\\' => '\\', '\\%' => '%', '\\_' => '_', // Internally, MariaDB escapes single quotes using the standard syntax "''" => "'", ]; private ?DefaultTableOptions $defaultTableOptions = null; /** * {@inheritDoc} */ protected function _getPortableTableDefinition(array $table): string { return $table['TABLE_NAME']; } /** * {@inheritDoc} */ protected function _getPortableViewDefinition(array $view): View { return new View($view['TABLE_NAME'], $view['VIEW_DEFINITION']); } /** * {@inheritDoc} */ protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array { foreach ($tableIndexes as $k => $v) { $v = array_change_key_case($v, CASE_LOWER); if ($v['key_name'] === 'PRIMARY') { $v['primary'] = true; } else { $v['primary'] = false; } if (str_contains($v['index_type'], 'FULLTEXT')) { $v['flags'] = ['FULLTEXT']; } elseif (str_contains($v['index_type'], 'SPATIAL')) { $v['flags'] = ['SPATIAL']; } // Ignore prohibited prefix `length` for spatial index if (! str_contains($v['index_type'], 'SPATIAL')) { $v['length'] = isset($v['sub_part']) ? (int) $v['sub_part'] : null; } $tableIndexes[$k] = $v; } return parent::_getPortableTableIndexesList($tableIndexes, $tableName); } /** * {@inheritDoc} */ protected function _getPortableDatabaseDefinition(array $database): string { return $database['Database']; } /** * {@inheritDoc} */ protected function _getPortableTableColumnDefinition(array $tableColumn): Column { $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); $dbType = strtolower($tableColumn['type']); $dbType = strtok($dbType, '(), '); assert(is_string($dbType)); $length = $tableColumn['length'] ?? strtok('(), '); $fixed = false; if (! isset($tableColumn['name'])) { $tableColumn['name'] = ''; } $scale = 0; $precision = null; $type = $this->platform->getDoctrineTypeMapping($dbType); switch ($dbType) { case 'char': case 'binary': $fixed = true; break; case 'float': case 'double': case 'real': case 'numeric': case 'decimal': if ( preg_match( '([A-Za-z]+\(([0-9]+),([0-9]+)\))', $tableColumn['type'], $match, ) === 1 ) { $precision = (int) $match[1]; $scale = (int) $match[2]; $length = null; } break; case 'tinytext': $length = AbstractMySQLPlatform::LENGTH_LIMIT_TINYTEXT; break; case 'text': $length = AbstractMySQLPlatform::LENGTH_LIMIT_TEXT; break; case 'mediumtext': $length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMTEXT; break; case 'tinyblob': $length = AbstractMySQLPlatform::LENGTH_LIMIT_TINYBLOB; break; case 'blob': $length = AbstractMySQLPlatform::LENGTH_LIMIT_BLOB; break; case 'mediumblob': $length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMBLOB; break; case 'tinyint': case 'smallint': case 'mediumint': case 'int': case 'integer': case 'bigint': case 'year': $length = null; break; } if ($this->platform instanceof MariaDBPlatform) { $columnDefault = $this->getMariaDBColumnDefault($this->platform, $tableColumn['default']); } else { $columnDefault = $tableColumn['default']; } $options = [ 'length' => $length !== null ? (int) $length : null, 'unsigned' => str_contains($tableColumn['type'], 'unsigned'), 'fixed' => $fixed, 'default' => $columnDefault, 'notnull' => $tableColumn['null'] !== 'YES', 'scale' => $scale, 'precision' => $precision, 'autoincrement' => str_contains($tableColumn['extra'], 'auto_increment'), ]; if (isset($tableColumn['comment'])) { $options['comment'] = $tableColumn['comment']; } $column = new Column($tableColumn['field'], Type::getType($type), $options); if (isset($tableColumn['characterset'])) { $column->setPlatformOption('charset', $tableColumn['characterset']); } if (isset($tableColumn['collation'])) { $column->setPlatformOption('collation', $tableColumn['collation']); } return $column; } /** * Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers. * * - Since MariaDb 10.2.7 column defaults stored in information_schema are now quoted * to distinguish them from expressions (see MDEV-10134). * - CURRENT_TIMESTAMP, CURRENT_TIME, CURRENT_DATE are stored in information_schema * as current_timestamp(), currdate(), currtime() * - Quoted 'NULL' is not enforced by Maria, it is technically possible to have * null in some circumstances (see https://jira.mariadb.org/browse/MDEV-14053) * - \' is always stored as '' in information_schema (normalized) * * @link https://mariadb.com/kb/en/library/information-schema-columns-table/ * @link https://jira.mariadb.org/browse/MDEV-13132 * * @param string|null $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7 */ private function getMariaDBColumnDefault(MariaDBPlatform $platform, ?string $columnDefault): ?string { if ($columnDefault === 'NULL' || $columnDefault === null) { return null; } if (preg_match('/^\'(.*)\'$/', $columnDefault, $matches) === 1) { return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES); } return match ($columnDefault) { 'current_timestamp()' => $platform->getCurrentTimestampSQL(), 'curdate()' => $platform->getCurrentDateSQL(), 'curtime()' => $platform->getCurrentTimeSQL(), default => $columnDefault, }; } /** * {@inheritDoc} */ protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array { $list = []; foreach ($tableForeignKeys as $value) { $value = array_change_key_case($value, CASE_LOWER); if (! isset($list[$value['constraint_name']])) { if (! isset($value['delete_rule']) || $value['delete_rule'] === 'RESTRICT') { $value['delete_rule'] = null; } if (! isset($value['update_rule']) || $value['update_rule'] === 'RESTRICT') { $value['update_rule'] = null; } $list[$value['constraint_name']] = [ 'name' => $value['constraint_name'], 'local' => [], 'foreign' => [], 'foreignTable' => $value['referenced_table_name'], 'onDelete' => $value['delete_rule'], 'onUpdate' => $value['update_rule'], ]; } $list[$value['constraint_name']]['local'][] = $value['column_name']; $list[$value['constraint_name']]['foreign'][] = $value['referenced_column_name']; } return parent::_getPortableTableForeignKeysList($list); } /** * {@inheritDoc} */ protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint { return new ForeignKeyConstraint( $tableForeignKey['local'], $tableForeignKey['foreignTable'], $tableForeignKey['foreign'], $tableForeignKey['name'], [ 'onDelete' => $tableForeignKey['onDelete'], 'onUpdate' => $tableForeignKey['onUpdate'], ], ); } /** @throws Exception */ public function createComparator(): Comparator { return new MySQL\Comparator( $this->platform, new CachingCharsetMetadataProvider( new ConnectionCharsetMetadataProvider($this->connection), ), new CachingCollationMetadataProvider( new ConnectionCollationMetadataProvider($this->connection), ), $this->getDefaultTableOptions(), ); } protected function selectTableNames(string $databaseName): Result { $sql = <<<'SQL' SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME SQL; return $this->connection->executeQuery($sql, [$databaseName]); } protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result { $columnTypeSQL = $this->platform->getColumnTypeSQLSnippet('c', $databaseName); $sql = 'SELECT'; if ($tableName === null) { $sql .= ' c.TABLE_NAME,'; } $sql .= <<connection->executeQuery($sql, $params); } protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result { $sql = 'SELECT'; if ($tableName === null) { $sql .= ' TABLE_NAME,'; } $sql .= <<<'SQL' NON_UNIQUE AS Non_Unique, INDEX_NAME AS Key_name, COLUMN_NAME AS Column_Name, SUB_PART AS Sub_Part, INDEX_TYPE AS Index_Type FROM information_schema.STATISTICS SQL; $conditions = ['TABLE_SCHEMA = ?']; $params = [$databaseName]; if ($tableName !== null) { $conditions[] = 'TABLE_NAME = ?'; $params[] = $tableName; } $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY SEQ_IN_INDEX'; return $this->connection->executeQuery($sql, $params); } protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result { $sql = 'SELECT DISTINCT'; if ($tableName === null) { $sql .= ' k.TABLE_NAME,'; } $sql .= <<<'SQL' k.CONSTRAINT_NAME, k.COLUMN_NAME, k.REFERENCED_TABLE_NAME, k.REFERENCED_COLUMN_NAME, k.ORDINAL_POSITION, c.UPDATE_RULE, c.DELETE_RULE FROM information_schema.key_column_usage k INNER JOIN information_schema.referential_constraints c ON c.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND c.TABLE_NAME = k.TABLE_NAME SQL; $conditions = ['k.TABLE_SCHEMA = ?']; $params = [$databaseName]; if ($tableName !== null) { $conditions[] = 'k.TABLE_NAME = ?'; $params[] = $tableName; } // The schema name is passed multiple times in the WHERE clause instead of using a JOIN condition // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions // caused by https://bugs.mysql.com/bug.php?id=81347 $conditions[] = 'c.CONSTRAINT_SCHEMA = ?'; $params[] = $databaseName; $conditions[] = 'k.REFERENCED_COLUMN_NAME IS NOT NULL'; $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY k.ORDINAL_POSITION'; return $this->connection->executeQuery($sql, $params); } /** * {@inheritDoc} */ protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array { $sql = <<<'SQL' SELECT t.TABLE_NAME, 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 SQL; $conditions = ['t.TABLE_SCHEMA = ?']; $params = [$databaseName]; if ($tableName !== null) { $conditions[] = 't.TABLE_NAME = ?'; $params[] = $tableName; } $conditions[] = "t.TABLE_TYPE = 'BASE TABLE'"; $sql .= ' WHERE ' . implode(' AND ', $conditions); /** @var array> $metadata */ $metadata = $this->connection->executeQuery($sql, $params) ->fetchAllAssociativeIndexed(); $tableOptions = []; foreach ($metadata as $table => $data) { $data = array_change_key_case($data, CASE_LOWER); $tableOptions[$table] = [ 'engine' => $data['engine'], 'collation' => $data['table_collation'], 'charset' => $data['character_set_name'], 'autoincrement' => $data['auto_increment'], 'comment' => $data['table_comment'], 'create_options' => $this->parseCreateOptions($data['create_options']), ]; } return $tableOptions; } /** @return array|array */ private function parseCreateOptions(?string $string): array { $options = []; if ($string === null || $string === '') { return $options; } foreach (explode(' ', $string) as $pair) { $parts = explode('=', $pair, 2); $options[$parts[0]] = $parts[1] ?? true; } return $options; } /** @throws Exception */ private function getDefaultTableOptions(): DefaultTableOptions { if ($this->defaultTableOptions === null) { $row = $this->connection->fetchNumeric( 'SELECT @@character_set_database, @@collation_database', ); assert($row !== false); $this->defaultTableOptions = new DefaultTableOptions(...$row); } return $this->defaultTableOptions; } } src/Schema/OracleSchemaManager.php000064400000035311152062402520013066 0ustar00 */ class OracleSchemaManager extends AbstractSchemaManager { /** * {@inheritDoc} */ protected function _getPortableViewDefinition(array $view): View { $view = array_change_key_case($view, CASE_LOWER); return new View($this->getQuotedIdentifierName($view['view_name']), $view['text']); } /** * {@inheritDoc} */ protected function _getPortableTableDefinition(array $table): string { $table = array_change_key_case($table, CASE_LOWER); return $this->getQuotedIdentifierName($table['table_name']); } /** * {@inheritDoc} * * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html */ protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array { $indexBuffer = []; foreach ($tableIndexes as $tableIndex) { $tableIndex = array_change_key_case($tableIndex, CASE_LOWER); $keyName = strtolower($tableIndex['name']); $buffer = []; if ($tableIndex['is_primary'] === 'P') { $keyName = 'primary'; $buffer['primary'] = true; $buffer['non_unique'] = false; } else { $buffer['primary'] = false; $buffer['non_unique'] = ! $tableIndex['is_unique']; } $buffer['key_name'] = $keyName; $buffer['column_name'] = $this->getQuotedIdentifierName($tableIndex['column_name']); $indexBuffer[] = $buffer; } return parent::_getPortableTableIndexesList($indexBuffer, $tableName); } /** * {@inheritDoc} */ protected function _getPortableTableColumnDefinition(array $tableColumn): Column { $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); $dbType = strtolower($tableColumn['data_type']); if (str_starts_with($dbType, 'timestamp(')) { if (str_contains($dbType, 'with time zone')) { $dbType = 'timestamptz'; } else { $dbType = 'timestamp'; } } $length = $precision = null; $scale = 0; $fixed = false; if (! isset($tableColumn['column_name'])) { $tableColumn['column_name'] = ''; } assert(array_key_exists('data_default', $tableColumn)); // Default values returned from database sometimes have trailing spaces. if (is_string($tableColumn['data_default'])) { $tableColumn['data_default'] = trim($tableColumn['data_default']); } if ($tableColumn['data_default'] === '' || $tableColumn['data_default'] === 'NULL') { $tableColumn['data_default'] = null; } if ($tableColumn['data_default'] !== null) { // Default values returned from database are represented as literal expressions if (preg_match('/^\'(.*)\'$/s', $tableColumn['data_default'], $matches) === 1) { $tableColumn['data_default'] = str_replace("''", "'", $matches[1]); } } if ($tableColumn['data_precision'] !== null) { $precision = (int) $tableColumn['data_precision']; } if ($tableColumn['data_scale'] !== null) { $scale = (int) $tableColumn['data_scale']; } $type = $this->platform->getDoctrineTypeMapping($dbType); switch ($dbType) { case 'number': if ($precision === 20 && $scale === 0) { $type = 'bigint'; } elseif ($precision === 5 && $scale === 0) { $type = 'smallint'; } elseif ($precision === 1 && $scale === 0) { $type = 'boolean'; } elseif ($scale > 0) { $type = 'decimal'; } break; case 'varchar': case 'varchar2': case 'nvarchar2': $length = (int) $tableColumn['char_length']; break; case 'raw': $length = (int) $tableColumn['data_length']; $fixed = true; break; case 'char': case 'nchar': $length = (int) $tableColumn['char_length']; $fixed = true; break; } $options = [ 'notnull' => $tableColumn['nullable'] === 'N', 'fixed' => $fixed, 'default' => $tableColumn['data_default'], 'length' => $length, 'precision' => $precision, 'scale' => $scale, ]; if (isset($tableColumn['comments'])) { $options['comment'] = $tableColumn['comments']; } return new Column($this->getQuotedIdentifierName($tableColumn['column_name']), Type::getType($type), $options); } /** * {@inheritDoc} */ protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array { $list = []; foreach ($tableForeignKeys as $value) { $value = array_change_key_case($value, CASE_LOWER); if (! isset($list[$value['constraint_name']])) { if ($value['delete_rule'] === 'NO ACTION') { $value['delete_rule'] = null; } $list[$value['constraint_name']] = [ 'name' => $this->getQuotedIdentifierName($value['constraint_name']), 'local' => [], 'foreign' => [], 'foreignTable' => $value['references_table'], 'onDelete' => $value['delete_rule'], ]; } $localColumn = $this->getQuotedIdentifierName($value['local_column']); $foreignColumn = $this->getQuotedIdentifierName($value['foreign_column']); $list[$value['constraint_name']]['local'][$value['position']] = $localColumn; $list[$value['constraint_name']]['foreign'][$value['position']] = $foreignColumn; } return parent::_getPortableTableForeignKeysList($list); } /** * {@inheritDoc} */ protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint { return new ForeignKeyConstraint( array_values($tableForeignKey['local']), $this->getQuotedIdentifierName($tableForeignKey['foreignTable']), array_values($tableForeignKey['foreign']), $this->getQuotedIdentifierName($tableForeignKey['name']), ['onDelete' => $tableForeignKey['onDelete']], ); } /** * {@inheritDoc} */ protected function _getPortableSequenceDefinition(array $sequence): Sequence { $sequence = array_change_key_case($sequence, CASE_LOWER); return new Sequence( $this->getQuotedIdentifierName($sequence['sequence_name']), (int) $sequence['increment_by'], (int) $sequence['min_value'], ); } /** * {@inheritDoc} */ protected function _getPortableDatabaseDefinition(array $database): string { $database = array_change_key_case($database, CASE_LOWER); return $database['username']; } public function createDatabase(string $database): void { $statement = $this->platform->getCreateDatabaseSQL($database); $params = $this->connection->getParams(); if (isset($params['password'])) { $statement .= ' IDENTIFIED BY ' . $params['password']; } $this->connection->executeStatement($statement); $statement = 'GRANT DBA TO ' . $database; $this->connection->executeStatement($statement); } /** @throws Exception */ protected function dropAutoincrement(string $table): bool { $sql = $this->platform->getDropAutoincrementSql($table); foreach ($sql as $query) { $this->connection->executeStatement($query); } return true; } public function dropTable(string $name): void { try { $this->dropAutoincrement($name); } catch (DatabaseObjectNotFoundException) { } parent::dropTable($name); } /** * Returns the quoted representation of the given identifier name. * * Quotes non-uppercase identifiers explicitly to preserve case * and thus make references to the particular identifier work. */ private function getQuotedIdentifierName(string $identifier): string { if (preg_match('/[a-z]/', $identifier) === 1) { return $this->platform->quoteIdentifier($identifier); } return $identifier; } protected function selectTableNames(string $databaseName): Result { $sql = <<<'SQL' SELECT TABLE_NAME FROM ALL_TABLES WHERE OWNER = :OWNER ORDER BY TABLE_NAME SQL; return $this->connection->executeQuery($sql, ['OWNER' => $databaseName]); } protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result { $sql = 'SELECT'; if ($tableName === null) { $sql .= ' C.TABLE_NAME,'; } $sql .= <<<'SQL' C.COLUMN_NAME, C.DATA_TYPE, C.DATA_DEFAULT, C.DATA_PRECISION, C.DATA_SCALE, C.CHAR_LENGTH, C.DATA_LENGTH, C.NULLABLE, D.COMMENTS FROM ALL_TAB_COLUMNS C INNER JOIN ALL_TABLES T ON T.OWNER = C.OWNER AND T.TABLE_NAME = C.TABLE_NAME LEFT JOIN ALL_COL_COMMENTS D ON D.OWNER = C.OWNER AND D.TABLE_NAME = C.TABLE_NAME AND D.COLUMN_NAME = C.COLUMN_NAME SQL; $conditions = ['C.OWNER = :OWNER']; $params = ['OWNER' => $databaseName]; if ($tableName !== null) { $conditions[] = 'C.TABLE_NAME = :TABLE_NAME'; $params['TABLE_NAME'] = $tableName; } $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY C.COLUMN_ID'; return $this->connection->executeQuery($sql, $params); } protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result { $sql = 'SELECT'; if ($tableName === null) { $sql .= ' IND_COL.TABLE_NAME,'; } $sql .= <<<'SQL' IND_COL.INDEX_NAME AS NAME, IND.INDEX_TYPE AS TYPE, DECODE(IND.UNIQUENESS, 'NONUNIQUE', 0, 'UNIQUE', 1) AS IS_UNIQUE, IND_COL.COLUMN_NAME, IND_COL.COLUMN_POSITION AS COLUMN_POS, CON.CONSTRAINT_TYPE AS IS_PRIMARY FROM ALL_IND_COLUMNS IND_COL LEFT JOIN ALL_INDEXES IND ON IND.OWNER = IND_COL.INDEX_OWNER AND IND.INDEX_NAME = IND_COL.INDEX_NAME LEFT JOIN ALL_CONSTRAINTS CON ON CON.OWNER = IND_COL.INDEX_OWNER AND CON.INDEX_NAME = IND_COL.INDEX_NAME SQL; $conditions = ['IND_COL.INDEX_OWNER = :OWNER']; $params = ['OWNER' => $databaseName]; if ($tableName !== null) { $conditions[] = 'IND_COL.TABLE_NAME = :TABLE_NAME'; $params['TABLE_NAME'] = $tableName; } $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY IND_COL.TABLE_NAME, IND_COL.INDEX_NAME' . ', IND_COL.COLUMN_POSITION'; return $this->connection->executeQuery($sql, $params); } protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result { $sql = 'SELECT'; if ($tableName === null) { $sql .= ' COLS.TABLE_NAME,'; } $sql .= <<<'SQL' ALC.CONSTRAINT_NAME, ALC.DELETE_RULE, COLS.COLUMN_NAME LOCAL_COLUMN, COLS.POSITION, R_COLS.TABLE_NAME REFERENCES_TABLE, R_COLS.COLUMN_NAME FOREIGN_COLUMN FROM ALL_CONS_COLUMNS COLS LEFT JOIN ALL_CONSTRAINTS ALC ON ALC.OWNER = COLS.OWNER AND ALC.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME LEFT JOIN ALL_CONS_COLUMNS R_COLS ON R_COLS.OWNER = ALC.R_OWNER AND R_COLS.CONSTRAINT_NAME = ALC.R_CONSTRAINT_NAME AND R_COLS.POSITION = COLS.POSITION SQL; $conditions = ["ALC.CONSTRAINT_TYPE = 'R'", 'COLS.OWNER = :OWNER']; $params = ['OWNER' => $databaseName]; if ($tableName !== null) { $conditions[] = 'COLS.TABLE_NAME = :TABLE_NAME'; $params['TABLE_NAME'] = $tableName; } $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY COLS.TABLE_NAME, COLS.CONSTRAINT_NAME' . ', COLS.POSITION'; return $this->connection->executeQuery($sql, $params); } /** * {@inheritDoc} */ protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array { $sql = 'SELECT TABLE_NAME, COMMENTS'; $conditions = ['OWNER = :OWNER']; $params = ['OWNER' => $databaseName]; if ($tableName !== null) { $conditions[] = 'TABLE_NAME = :TABLE_NAME'; $params['TABLE_NAME'] = $tableName; } $sql .= ' FROM ALL_TAB_COMMENTS WHERE ' . implode(' AND ', $conditions); /** @var array> $metadata */ $metadata = $this->connection->executeQuery($sql, $params) ->fetchAllAssociativeIndexed(); $tableOptions = []; foreach ($metadata as $table => $data) { $data = array_change_key_case($data, CASE_LOWER); $tableOptions[$table] = [ 'comment' => $data['comments'], ]; } return $tableOptions; } protected function normalizeName(string $name): string { $identifier = new Identifier($name); return $identifier->isQuoted() ? $identifier->getName() : strtoupper($name); } } src/Schema/PostgreSQLSchemaManager.php000064400000042277152062402520013675 0ustar00 */ class PostgreSQLSchemaManager extends AbstractSchemaManager { private ?string $currentSchema = null; /** * {@inheritDoc} */ public function listSchemaNames(): array { return $this->connection->fetchFirstColumn( <<<'SQL' SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT LIKE 'pg\_%' AND schema_name != 'information_schema' SQL, ); } public function createSchemaConfig(): SchemaConfig { $config = parent::createSchemaConfig(); $config->setName($this->getCurrentSchema()); return $config; } /** * Returns the name of the current schema. * * @throws Exception */ protected function getCurrentSchema(): ?string { return $this->currentSchema ??= $this->determineCurrentSchema(); } /** * Determines the name of the current schema. * * @throws Exception */ protected function determineCurrentSchema(): string { $currentSchema = $this->connection->fetchOne('SELECT current_schema()'); assert(is_string($currentSchema)); return $currentSchema; } /** * {@inheritDoc} */ protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint { $onUpdate = null; $onDelete = null; if ( preg_match( '(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match, ) === 1 ) { $onUpdate = $match[1]; } if ( preg_match( '(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match, ) === 1 ) { $onDelete = $match[1]; } $result = preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values); assert($result === 1); // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get // the idea to trim them here. $localColumns = array_map('trim', explode(',', $values[1])); $foreignColumns = array_map('trim', explode(',', $values[3])); $foreignTable = $values[2]; return new ForeignKeyConstraint( $localColumns, $foreignTable, $foreignColumns, $tableForeignKey['conname'], ['onUpdate' => $onUpdate, 'onDelete' => $onDelete], ); } /** * {@inheritDoc} */ protected function _getPortableViewDefinition(array $view): View { return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']); } /** * {@inheritDoc} */ protected function _getPortableTableDefinition(array $table): string { $currentSchema = $this->getCurrentSchema(); if ($table['schema_name'] === $currentSchema) { return $table['table_name']; } return $table['schema_name'] . '.' . $table['table_name']; } /** * {@inheritDoc} * * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html */ protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array { $buffer = []; foreach ($tableIndexes as $row) { $colNumbers = array_map('intval', explode(' ', $row['indkey'])); $columnNameSql = sprintf( 'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC', $row['indrelid'], implode(' ,', $colNumbers), ); $indexColumns = $this->connection->fetchAllAssociative($columnNameSql); // required for getting the order of the columns right. foreach ($colNumbers as $colNum) { foreach ($indexColumns as $colRow) { if ($colNum !== $colRow['attnum']) { continue; } $buffer[] = [ 'key_name' => $row['relname'], 'column_name' => trim($colRow['attname']), 'non_unique' => ! $row['indisunique'], 'primary' => $row['indisprimary'], 'where' => $row['where'], ]; } } } return parent::_getPortableTableIndexesList($buffer, $tableName); } /** * {@inheritDoc} */ protected function _getPortableDatabaseDefinition(array $database): string { return $database['datname']; } /** * {@inheritDoc} */ protected function _getPortableSequenceDefinition(array $sequence): Sequence { if ($sequence['schemaname'] !== 'public') { $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname']; } else { $sequenceName = $sequence['relname']; } return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']); } /** * {@inheritDoc} */ protected function _getPortableTableColumnDefinition(array $tableColumn): Column { $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); $length = null; if ( in_array(strtolower($tableColumn['type']), ['varchar', 'bpchar'], true) && preg_match('/\((\d*)\)/', $tableColumn['complete_type'], $matches) === 1 ) { $length = (int) $matches[1]; } $autoincrement = $tableColumn['attidentity'] === 'd'; $matches = []; assert(array_key_exists('default', $tableColumn)); assert(array_key_exists('complete_type', $tableColumn)); if ($tableColumn['default'] !== null) { if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches) === 1) { $tableColumn['default'] = $matches[1]; } elseif (preg_match('/^NULL::/', $tableColumn['default']) === 1) { $tableColumn['default'] = null; } } if ($length === -1 && isset($tableColumn['atttypmod'])) { $length = $tableColumn['atttypmod'] - 4; } if ((int) $length <= 0) { $length = null; } $fixed = false; if (! isset($tableColumn['name'])) { $tableColumn['name'] = ''; } $precision = null; $scale = 0; $jsonb = null; $dbType = strtolower($tableColumn['type']); if ( $tableColumn['domain_type'] !== null && $tableColumn['domain_type'] !== '' && ! $this->platform->hasDoctrineTypeMappingFor($tableColumn['type']) ) { $dbType = strtolower($tableColumn['domain_type']); $tableColumn['complete_type'] = $tableColumn['domain_complete_type']; } $type = $this->platform->getDoctrineTypeMapping($dbType); switch ($dbType) { case 'smallint': case 'int2': case 'int': case 'int4': case 'integer': case 'bigint': case 'int8': $length = null; break; case 'bool': case 'boolean': if ($tableColumn['default'] === 'true') { $tableColumn['default'] = true; } if ($tableColumn['default'] === 'false') { $tableColumn['default'] = false; } $length = null; break; case 'text': case '_varchar': case 'varchar': $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']); break; case 'char': case 'bpchar': $fixed = true; break; case 'float': case 'float4': case 'float8': case 'double': case 'double precision': case 'real': case 'decimal': case 'money': case 'numeric': if ( preg_match( '([A-Za-z]+\(([0-9]+),([0-9]+)\))', $tableColumn['complete_type'], $match, ) === 1 ) { $precision = (int) $match[1]; $scale = (int) $match[2]; $length = null; } break; case 'year': $length = null; break; // PostgreSQL 9.4+ only case 'jsonb': $jsonb = true; break; } if ( is_string($tableColumn['default']) && preg_match( "('([^']+)'::)", $tableColumn['default'], $match, ) === 1 ) { $tableColumn['default'] = $match[1]; } $options = [ 'length' => $length, 'notnull' => (bool) $tableColumn['isnotnull'], 'default' => $tableColumn['default'], 'precision' => $precision, 'scale' => $scale, 'fixed' => $fixed, 'autoincrement' => $autoincrement, ]; if (isset($tableColumn['comment'])) { $options['comment'] = $tableColumn['comment']; } $column = new Column($tableColumn['field'], Type::getType($type), $options); if (! empty($tableColumn['collation'])) { $column->setPlatformOption('collation', $tableColumn['collation']); } if ($column->getType() instanceof JsonType) { $column->setPlatformOption('jsonb', $jsonb); } return $column; } /** * Parses a default value expression as given by PostgreSQL */ private function parseDefaultExpression(?string $default): ?string { if ($default === null) { return $default; } return str_replace("''", "'", $default); } protected function selectTableNames(string $databaseName): Result { $sql = <<<'SQL' SELECT quote_ident(table_name) AS table_name, table_schema AS schema_name FROM information_schema.tables WHERE table_catalog = ? AND table_schema NOT LIKE 'pg\_%' AND table_schema != 'information_schema' AND table_name != 'geometry_columns' AND table_name != 'spatial_ref_sys' AND table_type = 'BASE TABLE' SQL; return $this->connection->executeQuery($sql, [$databaseName]); } protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result { $sql = 'SELECT'; if ($tableName === null) { $sql .= ' c.relname AS table_name, n.nspname AS schema_name,'; } $sql .= <<<'SQL' a.attnum, quote_ident(a.attname) AS field, t.typname AS type, format_type(a.atttypid, a.atttypmod) AS complete_type, (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation, (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type, (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type, a.attnotnull AS isnotnull, a.attidentity, (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 INNER JOIN pg_class c ON c.oid = a.attrelid INNER JOIN pg_type t ON t.oid = a.atttypid INNER JOIN pg_namespace n ON n.oid = c.relnamespace LEFT JOIN pg_depend d ON d.objid = c.oid AND d.deptype = 'e' AND d.classid = (SELECT oid FROM pg_class WHERE relname = 'pg_class') SQL; $conditions = array_merge([ 'a.attnum > 0', "c.relkind = 'r'", 'd.refobjid IS NULL', ], $this->buildQueryConditions($tableName)); $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY a.attnum'; return $this->connection->executeQuery($sql); } protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result { $sql = 'SELECT'; if ($tableName === null) { $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,'; } $sql .= <<<'SQL' quote_ident(ic.relname) AS relname, i.indisunique, i.indisprimary, i.indkey, i.indrelid, pg_get_expr(indpred, indrelid) AS "where" FROM pg_index i JOIN pg_class AS tc ON tc.oid = i.indrelid JOIN pg_namespace tn ON tn.oid = tc.relnamespace JOIN pg_class AS ic ON ic.oid = i.indexrelid WHERE ic.oid IN ( SELECT indexrelid FROM pg_index i, pg_class c, pg_namespace n SQL; $conditions = array_merge([ 'c.oid = i.indrelid', 'c.relnamespace = n.oid', ], $this->buildQueryConditions($tableName)); $sql .= ' WHERE ' . implode(' AND ', $conditions) . ')'; return $this->connection->executeQuery($sql); } protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result { $sql = 'SELECT'; if ($tableName === null) { $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,'; } $sql .= <<<'SQL' quote_ident(r.conname) as conname, pg_get_constraintdef(r.oid, true) as condef FROM pg_constraint r JOIN pg_class AS tc ON tc.oid = r.conrelid JOIN pg_namespace tn ON tn.oid = tc.relnamespace WHERE r.conrelid IN ( SELECT c.oid FROM pg_class c, pg_namespace n SQL; $conditions = array_merge(['n.oid = c.relnamespace'], $this->buildQueryConditions($tableName)); $sql .= ' WHERE ' . implode(' AND ', $conditions) . ") AND r.contype = 'f'"; return $this->connection->executeQuery($sql); } /** * {@inheritDoc} */ protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array { $sql = <<<'SQL' SELECT c.relname, CASE c.relpersistence WHEN 'u' THEN true ELSE false END as unlogged, obj_description(c.oid, 'pg_class') AS comment FROM pg_class c INNER JOIN pg_namespace n ON n.oid = c.relnamespace SQL; $conditions = array_merge(["c.relkind = 'r'"], $this->buildQueryConditions($tableName)); $sql .= ' WHERE ' . implode(' AND ', $conditions); return $this->connection->fetchAllAssociativeIndexed($sql); } /** @return list */ private function buildQueryConditions(?string $tableName): array { $conditions = []; if ($tableName !== null) { if (str_contains($tableName, '.')) { [$schemaName, $tableName] = explode('.', $tableName); $conditions[] = 'n.nspname = ' . $this->platform->quoteStringLiteral($schemaName); } else { $conditions[] = 'n.nspname = ANY(current_schemas(false))'; } $identifier = new Identifier($tableName); $conditions[] = 'c.relname = ' . $this->platform->quoteStringLiteral($identifier->getName()); } $conditions[] = "n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')"; return $conditions; } } src/Schema/SQLServerSchemaManager.php000064400000037515152062402520013517 0ustar00 */ class SQLServerSchemaManager extends AbstractSchemaManager { private ?string $databaseCollation = null; /** * {@inheritDoc} */ public function listSchemaNames(): array { return $this->connection->fetchFirstColumn( <<<'SQL' SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys') SQL, ); } /** * {@inheritDoc} */ protected function _getPortableSequenceDefinition(array $sequence): Sequence { return new Sequence($sequence['name'], (int) $sequence['increment'], (int) $sequence['start_value']); } /** * {@inheritDoc} */ protected function _getPortableTableColumnDefinition(array $tableColumn): Column { $dbType = strtok($tableColumn['type'], '(), '); assert(is_string($dbType)); $length = (int) $tableColumn['length']; $precision = null; $scale = 0; $fixed = false; if (! isset($tableColumn['name'])) { $tableColumn['name'] = ''; } if ($tableColumn['scale'] !== null) { $scale = (int) $tableColumn['scale']; } if ($tableColumn['precision'] !== null) { $precision = (int) $tableColumn['precision']; } switch ($dbType) { case 'nchar': case 'ntext': // Unicode data requires 2 bytes per character $length /= 2; break; case 'nvarchar': if ($length === -1) { break; } // Unicode data requires 2 bytes per character $length /= 2; break; case 'varchar': // TEXT type is returned as VARCHAR(MAX) with a length of -1 if ($length === -1) { $dbType = 'text'; } break; case 'varbinary': if ($length === -1) { $dbType = 'blob'; } break; } if ($dbType === 'char' || $dbType === 'nchar' || $dbType === 'binary') { $fixed = true; } $type = $this->platform->getDoctrineTypeMapping($dbType); $options = [ 'fixed' => $fixed, 'notnull' => (bool) $tableColumn['notnull'], 'scale' => $scale, 'precision' => $precision, 'autoincrement' => (bool) $tableColumn['autoincrement'], ]; if (isset($tableColumn['comment'])) { $options['comment'] = $tableColumn['comment']; } if ($length !== 0 && ($type === 'text' || $type === 'string' || $type === 'binary')) { $options['length'] = $length; } $column = new Column($tableColumn['name'], Type::getType($type), $options); if ($tableColumn['default'] !== null) { $default = $this->parseDefaultExpression($tableColumn['default']); $column->setDefault($default); $column->setPlatformOption( SQLServerPlatform::OPTION_DEFAULT_CONSTRAINT_NAME, $tableColumn['df_name'], ); } if (isset($tableColumn['collation']) && $tableColumn['collation'] !== 'NULL') { $column->setPlatformOption('collation', $tableColumn['collation']); } return $column; } private function parseDefaultExpression(string $value): ?string { while (preg_match('/^\((.*)\)$/s', $value, $matches)) { $value = $matches[1]; } if ($value === 'NULL') { return null; } if (preg_match('/^\'(.*)\'$/s', $value, $matches) === 1) { $value = str_replace("''", "'", $matches[1]); } if ($value === 'getdate()') { return $this->platform->getCurrentTimestampSQL(); } return $value; } /** * {@inheritDoc} */ protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array { $foreignKeys = []; foreach ($tableForeignKeys as $tableForeignKey) { $name = $tableForeignKey['ForeignKey']; if (! isset($foreignKeys[$name])) { $foreignKeys[$name] = [ 'local_columns' => [$tableForeignKey['ColumnName']], 'foreign_table' => $tableForeignKey['ReferenceTableName'], 'foreign_columns' => [$tableForeignKey['ReferenceColumnName']], 'name' => $name, 'options' => [ 'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']), 'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']), ], ]; } else { $foreignKeys[$name]['local_columns'][] = $tableForeignKey['ColumnName']; $foreignKeys[$name]['foreign_columns'][] = $tableForeignKey['ReferenceColumnName']; } } return parent::_getPortableTableForeignKeysList($foreignKeys); } /** * {@inheritDoc} */ protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array { foreach ($tableIndexes as &$tableIndex) { $tableIndex['non_unique'] = (bool) $tableIndex['non_unique']; $tableIndex['primary'] = (bool) $tableIndex['primary']; $tableIndex['flags'] = $tableIndex['flags'] ? [$tableIndex['flags']] : null; } return parent::_getPortableTableIndexesList($tableIndexes, $tableName); } /** * {@inheritDoc} */ protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint { return new ForeignKeyConstraint( $tableForeignKey['local_columns'], $tableForeignKey['foreign_table'], $tableForeignKey['foreign_columns'], $tableForeignKey['name'], $tableForeignKey['options'], ); } /** * {@inheritDoc} */ protected function _getPortableTableDefinition(array $table): string { if ($table['schema_name'] !== 'dbo') { return $table['schema_name'] . '.' . $table['table_name']; } return $table['table_name']; } /** * {@inheritDoc} */ protected function _getPortableDatabaseDefinition(array $database): string { return $database['name']; } /** * {@inheritDoc} */ protected function _getPortableViewDefinition(array $view): View { return new View($view['name'], $view['definition']); } /** @throws Exception */ public function createComparator(): Comparator { return new SQLServer\Comparator($this->platform, $this->getDatabaseCollation()); } /** @throws Exception */ private function getDatabaseCollation(): string { if ($this->databaseCollation === null) { $databaseCollation = $this->connection->fetchOne( 'SELECT collation_name FROM sys.databases WHERE name = ' . $this->platform->getCurrentDatabaseExpression(), ); // a database is always selected, even if omitted in the connection parameters assert(is_string($databaseCollation)); $this->databaseCollation = $databaseCollation; } return $this->databaseCollation; } protected function selectTableNames(string $databaseName): Result { // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams $sql = <<<'SQL' SELECT name AS table_name, SCHEMA_NAME(schema_id) AS schema_name FROM sys.objects WHERE type = 'U' AND name != 'sysdiagrams' ORDER BY name SQL; return $this->connection->executeQuery($sql); } protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result { $sql = 'SELECT'; if ($tableName === null) { $sql .= ' obj.name AS table_name, scm.name AS schema_name,'; } $sql .= <<<'SQL' col.name, type.name AS type, col.max_length AS length, ~col.is_nullable AS notnull, def.definition AS [default], def.name AS df_name, col.scale, col.precision, col.is_identity AS autoincrement, col.collation_name AS collation, -- CAST avoids driver error for sql_variant type CAST(prop.value AS NVARCHAR(MAX)) AS comment 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' SQL; // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams $conditions = ["obj.type = 'U'", "obj.name != 'sysdiagrams'"]; $params = []; if ($tableName !== null) { $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'obj.name'); } $sql .= ' WHERE ' . implode(' AND ', $conditions); return $this->connection->executeQuery($sql, $params); } protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result { $sql = 'SELECT'; if ($tableName === null) { $sql .= ' tbl.name AS table_name, scm.name AS schema_name,'; } $sql .= <<<'SQL' 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 SQL; $conditions = []; $params = []; if ($tableName !== null) { $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'tbl.name'); $sql .= ' WHERE ' . implode(' AND ', $conditions); } $sql .= ' ORDER BY idx.index_id, idxcol.key_ordinal'; return $this->connection->executeQuery($sql, $params); } protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result { $sql = 'SELECT'; if ($tableName === null) { $sql .= ' OBJECT_NAME (f.parent_object_id) AS table_name, SCHEMA_NAME(f.schema_id) AS schema_name,'; } $sql .= <<<'SQL' 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 SQL; $conditions = []; $params = []; if ($tableName !== null) { $conditions[] = $this->getTableWhereClause( $tableName, 'SCHEMA_NAME(f.schema_id)', 'OBJECT_NAME(f.parent_object_id)', ); $sql .= ' WHERE ' . implode(' AND ', $conditions); } $sql .= ' ORDER BY fc.constraint_column_id'; return $this->connection->executeQuery($sql, $params); } /** * {@inheritDoc} */ protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array { $sql = <<<'SQL' SELECT tbl.name, 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 SQL; $conditions = ["SCHEMA_NAME(tbl.schema_id) = N'dbo'", "p.name = N'MS_Description'"]; $params = []; if ($tableName !== null) { $conditions[] = "tbl.name = N'" . $tableName . "'"; } $sql .= ' WHERE ' . implode(' AND ', $conditions); /** @var array> $metadata */ $metadata = $this->connection->executeQuery($sql, $params) ->fetchAllAssociativeIndexed(); $tableOptions = []; foreach ($metadata as $table => $data) { $data = array_change_key_case($data, CASE_LOWER); $tableOptions[$table] = [ 'comment' => $data['table_comment'], ]; } return $tableOptions; } /** * 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(string $table, string $schemaColumn, string $tableColumn): string { if (str_contains($table, '.')) { [$schema, $table] = explode('.', $table); $schema = $this->platform->quoteStringLiteral($schema); $table = $this->platform->quoteStringLiteral($table); } else { $schema = 'SCHEMA_NAME()'; $table = $this->platform->quoteStringLiteral($table); } return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema); } } src/Schema/SQLiteSchemaManager.php000064400000044322152062402520013024 0ustar00 */ class SQLiteSchemaManager extends AbstractSchemaManager { /** * {@inheritDoc} */ protected function fetchForeignKeyColumnsByTable(string $databaseName): array { $columnsByTable = parent::fetchForeignKeyColumnsByTable($databaseName); if (count($columnsByTable) > 0) { foreach ($columnsByTable as $table => $columns) { $columnsByTable[$table] = $this->addDetailsToTableForeignKeyColumns($table, $columns); } } return $columnsByTable; } public function createForeignKey(ForeignKeyConstraint $foreignKey, string $table): void { $table = $this->introspectTable($table); $this->alterTable(new TableDiff($table, [], [], [], [], [], [], [], [], [$foreignKey], [], [])); } public function dropForeignKey(string $name, string $table): void { $table = $this->introspectTable($table); $foreignKey = $table->getForeignKey($name); $this->alterTable(new TableDiff($table, [], [], [], [], [], [], [], [], [], [], [$foreignKey])); } /** * {@inheritDoc} */ public function listTableForeignKeys(string $table): array { $table = $this->normalizeName($table); $columns = $this->selectForeignKeyColumns('', $table) ->fetchAllAssociative(); if (count($columns) > 0) { $columns = $this->addDetailsToTableForeignKeyColumns($table, $columns); } return $this->_getPortableTableForeignKeysList($columns); } /** * {@inheritDoc} */ protected function _getPortableTableDefinition(array $table): string { return $table['table_name']; } /** * {@inheritDoc} * * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html */ protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array { $indexBuffer = []; // fetch primary $indexArray = $this->connection->fetchAllAssociative('SELECT * FROM PRAGMA_TABLE_INFO (?)', [$tableName]); usort( $indexArray, /** * @param array $a * @param array $b */ static function (array $a, array $b): int { if ($a['pk'] === $b['pk']) { return $a['cid'] - $b['cid']; } return $a['pk'] - $b['pk']; }, ); foreach ($indexArray as $indexColumnRow) { if ($indexColumnRow['pk'] === 0 || $indexColumnRow['pk'] === '0') { continue; } $indexBuffer[] = [ 'key_name' => 'primary', 'primary' => true, 'non_unique' => false, 'column_name' => $indexColumnRow['name'], ]; } // fetch regular indexes foreach ($tableIndexes as $tableIndex) { // Ignore indexes with reserved names, e.g. autoindexes if (str_starts_with($tableIndex['name'], 'sqlite_')) { continue; } $keyName = $tableIndex['name']; $idx = []; $idx['key_name'] = $keyName; $idx['primary'] = false; $idx['non_unique'] = ! $tableIndex['unique']; $indexArray = $this->connection->fetchAllAssociative('SELECT * FROM PRAGMA_INDEX_INFO (?)', [$keyName]); foreach ($indexArray as $indexColumnRow) { $idx['column_name'] = $indexColumnRow['name']; $indexBuffer[] = $idx; } } return parent::_getPortableTableIndexesList($indexBuffer, $tableName); } /** * {@inheritDoc} */ protected function _getPortableTableColumnList(string $table, string $database, array $tableColumns): array { $list = parent::_getPortableTableColumnList($table, $database, $tableColumns); // find column with autoincrement $autoincrementColumn = null; $autoincrementCount = 0; foreach ($tableColumns as $tableColumn) { if ($tableColumn['pk'] === 0 || $tableColumn['pk'] === '0') { continue; } $autoincrementCount++; if ($autoincrementColumn !== null || strtolower($tableColumn['type']) !== 'integer') { continue; } $autoincrementColumn = $tableColumn['name']; } if ($autoincrementCount === 1 && $autoincrementColumn !== null) { foreach ($list as $column) { if ($autoincrementColumn !== $column->getName()) { continue; } $column->setAutoincrement(true); } } // inspect column collation and comments $createSql = $this->getCreateTableSQL($table); foreach ($list as $columnName => $column) { $type = $column->getType(); if ($type instanceof StringType || $type instanceof TextType) { $column->setPlatformOption( 'collation', $this->parseColumnCollationFromSQL($columnName, $createSql) ?? 'BINARY', ); } $comment = $this->parseColumnCommentFromSQL($columnName, $createSql); $column->setComment($comment); } return $list; } /** * {@inheritDoc} */ protected function _getPortableTableColumnDefinition(array $tableColumn): Column { preg_match('/^([^()]*)\\s*(\\(((\\d+)(,\\s*(\\d+))?)\\))?/', $tableColumn['type'], $matches); $dbType = trim(strtolower($matches[1])); $length = $precision = $unsigned = null; $fixed = $unsigned = false; $scale = 0; if (count($matches) >= 6) { $precision = (int) $matches[4]; $scale = (int) $matches[6]; } elseif (count($matches) >= 4) { $length = (int) $matches[4]; } if (str_contains($dbType, ' unsigned')) { $dbType = str_replace(' unsigned', '', $dbType); $unsigned = true; } $type = $this->platform->getDoctrineTypeMapping($dbType); $default = $tableColumn['dflt_value']; if ($default === 'NULL') { $default = null; } if ($default !== null) { // SQLite returns the default value as a literal expression, so we need to parse it if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) { $default = str_replace("''", "'", $matches[1]); } } $notnull = (bool) $tableColumn['notnull']; if (! isset($tableColumn['name'])) { $tableColumn['name'] = ''; } if ($dbType === 'char') { $fixed = true; } $options = [ 'length' => $length, 'unsigned' => $unsigned, 'fixed' => $fixed, 'notnull' => $notnull, 'default' => $default, 'precision' => $precision, 'scale' => $scale, ]; return new Column($tableColumn['name'], Type::getType($type), $options); } /** * {@inheritDoc} */ protected function _getPortableViewDefinition(array $view): View { return new View($view['name'], $view['sql']); } /** * {@inheritDoc} */ protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array { $list = []; foreach ($tableForeignKeys as $value) { $value = array_change_key_case($value, CASE_LOWER); $id = $value['id']; if (! isset($list[$id])) { if (! isset($value['on_delete']) || $value['on_delete'] === 'RESTRICT') { $value['on_delete'] = null; } if (! isset($value['on_update']) || $value['on_update'] === 'RESTRICT') { $value['on_update'] = null; } $list[$id] = [ 'name' => $value['constraint_name'], 'local' => [], 'foreign' => [], 'foreignTable' => $value['table'], 'onDelete' => $value['on_delete'], 'onUpdate' => $value['on_update'], 'deferrable' => $value['deferrable'], 'deferred' => $value['deferred'], ]; } $list[$id]['local'][] = $value['from']; if ($value['to'] === null) { // Inferring a shorthand form for the foreign key constraint, where the "to" field is empty. // @see https://www.sqlite.org/foreignkeys.html#fk_indexes. $foreignTableIndexes = $this->_getPortableTableIndexesList([], $value['table']); if (! isset($foreignTableIndexes['primary'])) { continue; } $list[$id]['foreign'] = [...$list[$id]['foreign'], ...$foreignTableIndexes['primary']->getColumns()]; continue; } $list[$id]['foreign'][] = $value['to']; } return parent::_getPortableTableForeignKeysList($list); } /** * {@inheritDoc} */ protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint { return new ForeignKeyConstraint( $tableForeignKey['local'], $tableForeignKey['foreignTable'], $tableForeignKey['foreign'], $tableForeignKey['name'], [ 'onDelete' => $tableForeignKey['onDelete'], 'onUpdate' => $tableForeignKey['onUpdate'], 'deferrable' => $tableForeignKey['deferrable'], 'deferred' => $tableForeignKey['deferred'], ], ); } private function parseColumnCollationFromSQL(string $column, string $sql): ?string { $pattern = '{(?:\W' . preg_quote($column) . '\W|\W' . preg_quote($this->platform->quoteSingleIdentifier($column)) . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is'; if (preg_match($pattern, $sql, $match) !== 1) { return null; } return $match[1]; } private function parseTableCommentFromSQL(string $table, string $sql): ?string { $pattern = '/\s* # Allow whitespace characters at start of line CREATE\sTABLE # Match "CREATE TABLE" (?:\W"' . preg_quote($this->platform->quoteSingleIdentifier($table), '/') . '"\W|\W' . preg_quote($table, '/') . '\W) # Match table name (quoted and unquoted) ( # Start capture (?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s) )/ix'; if (preg_match($pattern, $sql, $match) !== 1) { return null; } $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n")); return $comment === '' ? null : $comment; } private function parseColumnCommentFromSQL(string $column, string $sql): string { $pattern = '{[\s(,](?:\W' . preg_quote($this->platform->quoteSingleIdentifier($column)) . '\W|\W' . preg_quote($column) . '\W)(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i'; if (preg_match($pattern, $sql, $match) !== 1) { return ''; } $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n")); assert(is_string($comment)); return $comment; } /** @throws Exception */ private function getCreateTableSQL(string $table): string { $sql = $this->connection->fetchOne( <<<'SQL' SELECT sql FROM ( SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master ) WHERE type = 'table' AND name = ? SQL , [$table], ); if ($sql !== false) { return $sql; } return ''; } /** * @param list> $columns * * @return list> * * @throws Exception */ private function addDetailsToTableForeignKeyColumns(string $table, array $columns): array { $foreignKeyDetails = $this->getForeignKeyDetails($table); $foreignKeyCount = count($foreignKeyDetails); foreach ($columns as $i => $column) { // SQLite identifies foreign keys in reverse order of appearance in SQL $columns[$i] = array_merge($column, $foreignKeyDetails[$foreignKeyCount - $column['id'] - 1]); } return $columns; } /** * @return list> * * @throws Exception */ private function getForeignKeyDetails(string $table): array { $createSql = $this->getCreateTableSQL($table); if ( preg_match_all( '# (?:CONSTRAINT\s+(\S+)\s+)? (?:FOREIGN\s+KEY[^)]+\)\s*)? REFERENCES\s+\S+\s*(?:\([^)]+\))? (?: [^,]*? (NOT\s+DEFERRABLE|DEFERRABLE) (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))? )?#isx', $createSql, $match, ) === 0 ) { return []; } $names = $match[1]; $deferrable = $match[2]; $deferred = $match[3]; $details = []; for ($i = 0, $count = count($match[0]); $i < $count; $i++) { $details[] = [ 'constraint_name' => $names[$i] ?? '', 'deferrable' => isset($deferrable[$i]) && strcasecmp($deferrable[$i], 'deferrable') === 0, 'deferred' => isset($deferred[$i]) && strcasecmp($deferred[$i], 'deferred') === 0, ]; } return $details; } public function createComparator(): Comparator { return new SQLite\Comparator($this->platform); } protected function selectTableNames(string $databaseName): Result { $sql = <<<'SQL' SELECT name AS table_name FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence' AND name != 'geometry_columns' AND name != 'spatial_ref_sys' UNION ALL SELECT name FROM sqlite_temp_master WHERE type = 'table' ORDER BY name SQL; return $this->connection->executeQuery($sql); } protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result { $sql = <<<'SQL' SELECT t.name AS table_name, c.* FROM sqlite_master t JOIN pragma_table_info(t.name) c SQL; $conditions = [ "t.type = 'table'", "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')", ]; $params = []; if ($tableName !== null) { $conditions[] = 't.name = ?'; $params[] = str_replace('.', '__', $tableName); } $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, c.cid'; return $this->connection->executeQuery($sql, $params); } protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result { $sql = <<<'SQL' SELECT t.name AS table_name, i.* FROM sqlite_master t JOIN pragma_index_list(t.name) i SQL; $conditions = [ "t.type = 'table'", "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')", ]; $params = []; if ($tableName !== null) { $conditions[] = 't.name = ?'; $params[] = str_replace('.', '__', $tableName); } $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, i.seq'; return $this->connection->executeQuery($sql, $params); } protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result { $sql = <<<'SQL' SELECT t.name AS table_name, p.* FROM sqlite_master t JOIN pragma_foreign_key_list(t.name) p ON p."seq" != "-1" SQL; $conditions = [ "t.type = 'table'", "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')", ]; $params = []; if ($tableName !== null) { $conditions[] = 't.name = ?'; $params[] = str_replace('.', '__', $tableName); } $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, p.id DESC, p.seq'; return $this->connection->executeQuery($sql, $params); } /** * {@inheritDoc} */ protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array { if ($tableName === null) { $tables = $this->listTableNames(); } else { $tables = [$tableName]; } $tableOptions = []; foreach ($tables as $table) { $comment = $this->parseTableCommentFromSQL($table, $this->getCreateTableSQL($table)); if ($comment === null) { continue; } $tableOptions[$table]['comment'] = $comment; } return $tableOptions; } } src/Schema/Schema.php000064400000023741152062402520010451 0ustar00 */ private array $namespaces = []; /** @var array */ protected array $_tables = []; /** @var array */ protected array $_sequences = []; protected SchemaConfig $_schemaConfig; /** * @param array
$tables * @param array $sequences * @param array $namespaces */ public function __construct( array $tables = [], array $sequences = [], ?SchemaConfig $schemaConfig = null, array $namespaces = [], ) { $schemaConfig ??= new SchemaConfig(); $this->_schemaConfig = $schemaConfig; $name = $schemaConfig->getName(); if ($name !== null) { $this->_setName($name); } foreach ($namespaces as $namespace) { $this->createNamespace($namespace); } foreach ($tables as $table) { $this->_addTable($table); } foreach ($sequences as $sequence) { $this->_addSequence($sequence); } } protected function _addTable(Table $table): void { $namespaceName = $table->getNamespaceName(); $tableName = $this->normalizeName($table); if (isset($this->_tables[$tableName])) { throw TableAlreadyExists::new($tableName); } if ( $namespaceName !== null && ! $table->isInDefaultNamespace($this->getName()) && ! $this->hasNamespace($namespaceName) ) { $this->createNamespace($namespaceName); } $this->_tables[$tableName] = $table; $table->setSchemaConfig($this->_schemaConfig); } protected function _addSequence(Sequence $sequence): void { $namespaceName = $sequence->getNamespaceName(); $seqName = $this->normalizeName($sequence); if (isset($this->_sequences[$seqName])) { throw SequenceAlreadyExists::new($seqName); } if ( $namespaceName !== null && ! $sequence->isInDefaultNamespace($this->getName()) && ! $this->hasNamespace($namespaceName) ) { $this->createNamespace($namespaceName); } $this->_sequences[$seqName] = $sequence; } /** * Returns the namespaces of this schema. * * @return list A list of namespace names. */ public function getNamespaces(): array { return array_values($this->namespaces); } /** * Gets all tables of this schema. * * @return list
*/ public function getTables(): array { return array_values($this->_tables); } public function getTable(string $name): Table { $name = $this->getFullQualifiedAssetName($name); if (! isset($this->_tables[$name])) { throw TableDoesNotExist::new($name); } return $this->_tables[$name]; } private function getFullQualifiedAssetName(string $name): string { $name = $this->getUnquotedAssetName($name); if (! str_contains($name, '.')) { $name = $this->getName() . '.' . $name; } return strtolower($name); } /** * The normalized name is qualified and lower-cased. Lower-casing is * actually wrong, but we have to do it to keep our sanity. If you are * using database objects that only differentiate in the casing (FOO vs * Foo) then you will NOT be able to use Doctrine Schema abstraction. * * Every non-namespaced element is prefixed with this schema name. */ private function normalizeName(AbstractAsset $asset): string { $name = $asset->getName(); if ($asset->getNamespaceName() === null) { $name = $this->getName() . '.' . $name; } return strtolower($name); } /** * Returns the unquoted representation of a given asset name. */ private function getUnquotedAssetName(string $assetName): string { if ($this->isIdentifierQuoted($assetName)) { return $this->trimQuotes($assetName); } return $assetName; } /** * Does this schema have a namespace with the given name? */ public function hasNamespace(string $name): bool { $name = strtolower($this->getUnquotedAssetName($name)); return isset($this->namespaces[$name]); } /** * Does this schema have a table with the given name? */ public function hasTable(string $name): bool { $name = $this->getFullQualifiedAssetName($name); return isset($this->_tables[$name]); } public function hasSequence(string $name): bool { $name = $this->getFullQualifiedAssetName($name); return isset($this->_sequences[$name]); } public function getSequence(string $name): Sequence { $name = $this->getFullQualifiedAssetName($name); if (! $this->hasSequence($name)) { throw SequenceDoesNotExist::new($name); } return $this->_sequences[$name]; } /** @return list */ public function getSequences(): array { return array_values($this->_sequences); } /** * Creates a new namespace. * * @return $this */ public function createNamespace(string $name): self { $unquotedName = strtolower($this->getUnquotedAssetName($name)); if (isset($this->namespaces[$unquotedName])) { throw NamespaceAlreadyExists::new($unquotedName); } $this->namespaces[$unquotedName] = $name; return $this; } /** * Creates a new table. */ public function createTable(string $name): Table { $table = new Table($name); $this->_addTable($table); foreach ($this->_schemaConfig->getDefaultTableOptions() as $option => $value) { $table->addOption($option, $value); } return $table; } /** * Renames a table. * * @return $this */ public function renameTable(string $oldName, string $newName): self { $table = $this->getTable($oldName); $table->_setName($newName); $this->dropTable($oldName); $this->_addTable($table); return $this; } /** * Drops a table from the schema. * * @return $this */ public function dropTable(string $name): self { $name = $this->getFullQualifiedAssetName($name); $this->getTable($name); unset($this->_tables[$name]); return $this; } /** * Creates a new sequence. */ public function createSequence(string $name, int $allocationSize = 1, int $initialValue = 1): Sequence { $seq = new Sequence($name, $allocationSize, $initialValue); $this->_addSequence($seq); return $seq; } /** @return $this */ public function dropSequence(string $name): self { $name = $this->getFullQualifiedAssetName($name); unset($this->_sequences[$name]); return $this; } /** * Returns an array of necessary SQL queries to create the schema on the given platform. * * @return list * * @throws Exception */ public function toSql(AbstractPlatform $platform): array { $builder = new CreateSchemaObjectsSQLBuilder($platform); return $builder->buildSQL($this); } /** * Return an array of necessary SQL queries to drop the schema on the given platform. * * @return list */ public function toDropSql(AbstractPlatform $platform): array { $builder = new DropSchemaObjectsSQLBuilder($platform); return $builder->buildSQL($this); } /** * Cloning a Schema triggers a deep clone of all related assets. */ public function __clone() { foreach ($this->_tables as $k => $table) { $this->_tables[$k] = clone $table; } foreach ($this->_sequences as $k => $sequence) { $this->_sequences[$k] = clone $sequence; } } } src/Schema/SchemaConfig.php000064400000002451152062402520011572 0ustar00 */ protected array $defaultTableOptions = []; public function setMaxIdentifierLength(int $length): void { $this->maxIdentifierLength = $length; } public function getMaxIdentifierLength(): int { return $this->maxIdentifierLength; } /** * Gets the default namespace of schema objects. */ public function getName(): ?string { return $this->name; } /** * Sets the default namespace name of schema objects. */ public function setName(?string $name): void { $this->name = $name; } /** * Gets the default options that are passed to Table instances created with * Schema#createTable(). * * @return array */ public function getDefaultTableOptions(): array { return $this->defaultTableOptions; } /** @param array $defaultTableOptions */ public function setDefaultTableOptions(array $defaultTableOptions): void { $this->defaultTableOptions = $defaultTableOptions; } } src/Schema/SchemaDiff.php000064400000005600152062402520011234 0ustar00 */ private readonly array $alteredTables; /** * Constructs an SchemaDiff object. * * @internal The diff can be only instantiated by a {@see Comparator}. * * @param array $createdSchemas * @param array $droppedSchemas * @param array
$createdTables * @param array $alteredTables * @param array
$droppedTables * @param array $createdSequences * @param array $alteredSequences * @param array $droppedSequences */ public function __construct( private readonly array $createdSchemas, private readonly array $droppedSchemas, private readonly array $createdTables, array $alteredTables, private readonly array $droppedTables, private readonly array $createdSequences, private readonly array $alteredSequences, private readonly array $droppedSequences, ) { $this->alteredTables = array_filter($alteredTables, static function (TableDiff $diff): bool { return ! $diff->isEmpty(); }); } /** @return array */ public function getCreatedSchemas(): array { return $this->createdSchemas; } /** @return array */ public function getDroppedSchemas(): array { return $this->droppedSchemas; } /** @return array
*/ public function getCreatedTables(): array { return $this->createdTables; } /** @return array */ public function getAlteredTables(): array { return $this->alteredTables; } /** @return array
*/ public function getDroppedTables(): array { return $this->droppedTables; } /** @return array */ public function getCreatedSequences(): array { return $this->createdSequences; } /** @return array */ public function getAlteredSequences(): array { return $this->alteredSequences; } /** @return array */ public function getDroppedSequences(): array { return $this->droppedSequences; } /** * Returns whether the diff is empty (contains no changes). */ public function isEmpty(): bool { return count($this->createdSchemas) === 0 && count($this->droppedSchemas) === 0 && count($this->createdTables) === 0 && count($this->alteredTables) === 0 && count($this->droppedTables) === 0 && count($this->createdSequences) === 0 && count($this->alteredSequences) === 0 && count($this->droppedSequences) === 0; } } src/Schema/SchemaException.php000064400000000250152062402520012316 0ustar00_setName($name); $this->setAllocationSize($allocationSize); $this->setInitialValue($initialValue); } public function getAllocationSize(): int { return $this->allocationSize; } public function getInitialValue(): int { return $this->initialValue; } public function getCache(): ?int { return $this->cache; } public function setAllocationSize(int $allocationSize): self { $this->allocationSize = $allocationSize; return $this; } public function setInitialValue(int $initialValue): self { $this->initialValue = $initialValue; return $this; } public function setCache(int $cache): self { $this->cache = $cache; return $this; } /** * Checks if this sequence is an autoincrement sequence for a given table. * * This is used inside the comparator to not report sequences as missing, * when the "from" schema implicitly creates the sequences. */ public function isAutoIncrementsFor(Table $table): bool { $primaryKey = $table->getPrimaryKey(); if ($primaryKey === null) { return false; } $pkColumns = $primaryKey->getColumns(); if (count($pkColumns) !== 1) { return false; } $column = $table->getColumn($pkColumns[0]); if (! $column->getAutoincrement()) { return false; } $sequenceName = $this->getShortestName($table->getNamespaceName()); $tableName = $table->getShortestName($table->getNamespaceName()); $tableSequenceName = sprintf('%s_%s_seq', $tableName, $column->getShortestName($table->getNamespaceName())); return $tableSequenceName === $sequenceName; } } src/Schema/Table.php000064400000052115152062402520010275 0ustar00 [], ]; protected ?SchemaConfig $_schemaConfig = null; /** * @param array $columns * @param array $indexes * @param array $uniqueConstraints * @param array $fkConstraints * @param array $options */ public function __construct( string $name, array $columns = [], array $indexes = [], array $uniqueConstraints = [], array $fkConstraints = [], array $options = [], ) { if ($name === '') { throw InvalidTableName::new($name); } $this->_setName($name); foreach ($columns as $column) { $this->_addColumn($column); } foreach ($indexes as $idx) { $this->_addIndex($idx); } foreach ($uniqueConstraints as $uniqueConstraint) { $this->_addUniqueConstraint($uniqueConstraint); } foreach ($fkConstraints as $fkConstraint) { $this->_addForeignKeyConstraint($fkConstraint); } $this->_options = array_merge($this->_options, $options); } public function setSchemaConfig(SchemaConfig $schemaConfig): void { $this->_schemaConfig = $schemaConfig; } /** * Sets the Primary Key. * * @param array $columnNames */ public function setPrimaryKey(array $columnNames, ?string $indexName = null): self { if ($indexName === null) { $indexName = 'primary'; } $this->_addIndex($this->_createIndex($columnNames, $indexName, true, true)); foreach ($columnNames as $columnName) { $column = $this->getColumn($columnName); $column->setNotnull(true); } return $this; } /** * @param array $columnNames * @param array $flags * @param array $options */ public function addUniqueConstraint( array $columnNames, ?string $indexName = null, array $flags = [], array $options = [], ): self { $indexName ??= $this->_generateIdentifierName( array_merge([$this->getName()], $columnNames), 'uniq', $this->_getMaxIdentifierLength(), ); return $this->_addUniqueConstraint($this->_createUniqueConstraint($columnNames, $indexName, $flags, $options)); } /** * @param array $columnNames * @param array $flags * @param array $options */ public function addIndex( array $columnNames, ?string $indexName = null, array $flags = [], array $options = [], ): self { $indexName ??= $this->_generateIdentifierName( array_merge([$this->getName()], $columnNames), 'idx', $this->_getMaxIdentifierLength(), ); return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options)); } /** * Drops the primary key from this table. */ public function dropPrimaryKey(): void { if ($this->_primaryKeyName === null) { return; } $this->dropIndex($this->_primaryKeyName); $this->_primaryKeyName = null; } /** * Drops an index from this table. */ public function dropIndex(string $name): void { $name = $this->normalizeIdentifier($name); if (! $this->hasIndex($name)) { throw IndexDoesNotExist::new($name, $this->_name); } unset($this->_indexes[$name]); } /** * @param array $columnNames * @param array $options */ public function addUniqueIndex(array $columnNames, ?string $indexName = null, array $options = []): self { $indexName ??= $this->_generateIdentifierName( array_merge([$this->getName()], $columnNames), 'uniq', $this->_getMaxIdentifierLength(), ); return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options)); } /** * Renames an index. * * @param string $oldName The name of the index to rename from. * @param string|null $newName The name of the index to rename to. If null is given, the index name * will be auto-generated. */ public function renameIndex(string $oldName, ?string $newName = null): self { $oldName = $this->normalizeIdentifier($oldName); $normalizedNewName = $this->normalizeIdentifier($newName); if ($oldName === $normalizedNewName) { return $this; } if (! $this->hasIndex($oldName)) { throw IndexDoesNotExist::new($oldName, $this->_name); } if ($this->hasIndex($normalizedNewName)) { throw IndexAlreadyExists::new($normalizedNewName, $this->_name); } $oldIndex = $this->_indexes[$oldName]; if ($oldIndex->isPrimary()) { $this->dropPrimaryKey(); return $this->setPrimaryKey($oldIndex->getColumns(), $newName ?? null); } unset($this->_indexes[$oldName]); if ($oldIndex->isUnique()) { return $this->addUniqueIndex($oldIndex->getColumns(), $newName, $oldIndex->getOptions()); } return $this->addIndex($oldIndex->getColumns(), $newName, $oldIndex->getFlags(), $oldIndex->getOptions()); } /** * Checks if an index begins in the order of the given columns. * * @param array $columnNames */ public function columnsAreIndexed(array $columnNames): bool { foreach ($this->getIndexes() as $index) { if ($index->spansColumns($columnNames)) { return true; } } return false; } /** @param array $options */ public function addColumn(string $name, string $typeName, array $options = []): Column { $column = new Column($name, Type::getType($typeName), $options); $this->_addColumn($column); return $column; } /** @param array $options */ public function modifyColumn(string $name, array $options): self { $column = $this->getColumn($name); $column->setOptions($options); return $this; } /** * Drops a Column from the Table. */ public function dropColumn(string $name): self { $name = $this->normalizeIdentifier($name); unset($this->_columns[$name]); return $this; } /** * Adds a foreign key constraint. * * Name is inferred from the local columns. * * @param array $localColumnNames * @param array $foreignColumnNames * @param array $options */ public function addForeignKeyConstraint( string $foreignTableName, array $localColumnNames, array $foreignColumnNames, array $options = [], ?string $name = null, ): self { $name ??= $this->_generateIdentifierName( array_merge([$this->getName()], $localColumnNames), 'fk', $this->_getMaxIdentifierLength(), ); foreach ($localColumnNames as $columnName) { if (! $this->hasColumn($columnName)) { throw ColumnDoesNotExist::new($columnName, $this->_name); } } $constraint = new ForeignKeyConstraint( $localColumnNames, $foreignTableName, $foreignColumnNames, $name, $options, ); return $this->_addForeignKeyConstraint($constraint); } public function addOption(string $name, mixed $value): self { $this->_options[$name] = $value; return $this; } /** * Returns whether this table has a foreign key constraint with the given name. */ public function hasForeignKey(string $name): bool { $name = $this->normalizeIdentifier($name); return isset($this->_fkConstraints[$name]); } /** * Returns the foreign key constraint with the given name. */ public function getForeignKey(string $name): ForeignKeyConstraint { $name = $this->normalizeIdentifier($name); if (! $this->hasForeignKey($name)) { throw ForeignKeyDoesNotExist::new($name, $this->_name); } return $this->_fkConstraints[$name]; } /** * Removes the foreign key constraint with the given name. */ public function removeForeignKey(string $name): void { $name = $this->normalizeIdentifier($name); if (! $this->hasForeignKey($name)) { throw ForeignKeyDoesNotExist::new($name, $this->_name); } unset($this->_fkConstraints[$name]); } /** * Returns whether this table has a unique constraint with the given name. */ public function hasUniqueConstraint(string $name): bool { $name = $this->normalizeIdentifier($name); return isset($this->uniqueConstraints[$name]); } /** * Returns the unique constraint with the given name. */ public function getUniqueConstraint(string $name): UniqueConstraint { $name = $this->normalizeIdentifier($name); if (! $this->hasUniqueConstraint($name)) { throw UniqueConstraintDoesNotExist::new($name, $this->_name); } return $this->uniqueConstraints[$name]; } /** * Removes the unique constraint with the given name. */ public function removeUniqueConstraint(string $name): void { $name = $this->normalizeIdentifier($name); if (! $this->hasUniqueConstraint($name)) { throw UniqueConstraintDoesNotExist::new($name, $this->_name); } unset($this->uniqueConstraints[$name]); } /** * Returns the list of table columns. * * @return list */ public function getColumns(): array { return array_values($this->_columns); } /** * Returns whether this table has a Column with the given name. */ public function hasColumn(string $name): bool { $name = $this->normalizeIdentifier($name); return isset($this->_columns[$name]); } /** * Returns the Column with the given name. */ public function getColumn(string $name): Column { $name = $this->normalizeIdentifier($name); if (! $this->hasColumn($name)) { throw ColumnDoesNotExist::new($name, $this->_name); } return $this->_columns[$name]; } /** * Returns the primary key. */ public function getPrimaryKey(): ?Index { if ($this->_primaryKeyName !== null) { return $this->getIndex($this->_primaryKeyName); } return null; } /** * Returns whether this table has an Index with the given name. */ public function hasIndex(string $name): bool { $name = $this->normalizeIdentifier($name); return isset($this->_indexes[$name]); } /** * Returns the Index with the given name. */ public function getIndex(string $name): Index { $name = $this->normalizeIdentifier($name); if (! $this->hasIndex($name)) { throw IndexDoesNotExist::new($name, $this->_name); } return $this->_indexes[$name]; } /** @return array */ public function getIndexes(): array { return $this->_indexes; } /** * Returns the unique constraints. * * @return array */ public function getUniqueConstraints(): array { return $this->uniqueConstraints; } /** * Returns the foreign key constraints. * * @return array */ public function getForeignKeys(): array { return $this->_fkConstraints; } public function hasOption(string $name): bool { return isset($this->_options[$name]); } public function getOption(string $name): mixed { return $this->_options[$name] ?? null; } /** @return array */ public function getOptions(): array { return $this->_options; } /** * Clone of a Table triggers a deep clone of all affected assets. */ public function __clone() { foreach ($this->_columns as $k => $column) { $this->_columns[$k] = clone $column; } foreach ($this->_indexes as $k => $index) { $this->_indexes[$k] = clone $index; } foreach ($this->_fkConstraints as $k => $fk) { $this->_fkConstraints[$k] = clone $fk; } } protected function _getMaxIdentifierLength(): int { return $this->_schemaConfig instanceof SchemaConfig ? $this->_schemaConfig->getMaxIdentifierLength() : 63; } protected function _addColumn(Column $column): void { $columnName = $column->getName(); $columnName = $this->normalizeIdentifier($columnName); if (isset($this->_columns[$columnName])) { throw ColumnAlreadyExists::new($this->getName(), $columnName); } $this->_columns[$columnName] = $column; } /** * Adds an index to the table. */ protected function _addIndex(Index $indexCandidate): self { $indexName = $indexCandidate->getName(); $indexName = $this->normalizeIdentifier($indexName); $replacedImplicitIndexes = []; foreach ($this->implicitIndexes as $name => $implicitIndex) { if (! $implicitIndex->isFulfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) { continue; } $replacedImplicitIndexes[] = $name; } if ( (isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) || ($this->_primaryKeyName !== null && $indexCandidate->isPrimary()) ) { throw IndexAlreadyExists::new($indexName, $this->_name); } foreach ($replacedImplicitIndexes as $name) { unset($this->_indexes[$name], $this->implicitIndexes[$name]); } if ($indexCandidate->isPrimary()) { $this->_primaryKeyName = $indexName; } $this->_indexes[$indexName] = $indexCandidate; return $this; } protected function _addUniqueConstraint(UniqueConstraint $constraint): self { $name = $constraint->getName() !== '' ? $constraint->getName() : $this->_generateIdentifierName( array_merge((array) $this->getName(), $constraint->getColumns()), 'fk', $this->_getMaxIdentifierLength(), ); $name = $this->normalizeIdentifier($name); $this->uniqueConstraints[$name] = $constraint; // If there is already an index that fulfills this requirements drop the request. In the case of __construct // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates. // This creates computation overhead in this case, however no duplicate indexes are ever added (column based). $indexName = $this->_generateIdentifierName( array_merge([$this->getName()], $constraint->getColumns()), 'idx', $this->_getMaxIdentifierLength(), ); $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false); foreach ($this->_indexes as $existingIndex) { if ($indexCandidate->isFulfilledBy($existingIndex)) { return $this; } } $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate; return $this; } protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint): self { $name = $constraint->getName() !== '' ? $constraint->getName() : $this->_generateIdentifierName( array_merge((array) $this->getName(), $constraint->getLocalColumns()), 'fk', $this->_getMaxIdentifierLength(), ); $name = $this->normalizeIdentifier($name); $this->_fkConstraints[$name] = $constraint; // add an explicit index on the foreign key columns. // If there is already an index that fulfills this requirements drop the request. In the case of __construct // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates. // This creates computation overhead in this case, however no duplicate indexes are ever added (column based). $indexName = $this->_generateIdentifierName( array_merge([$this->getName()], $constraint->getLocalColumns()), 'idx', $this->_getMaxIdentifierLength(), ); $indexCandidate = $this->_createIndex($constraint->getLocalColumns(), $indexName, false, false); foreach ($this->_indexes as $existingIndex) { if ($indexCandidate->isFulfilledBy($existingIndex)) { return $this; } } $this->_addIndex($indexCandidate); $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate; return $this; } /** * Normalizes a given identifier. * * Trims quotes and lowercases the given identifier. */ private function normalizeIdentifier(?string $identifier): string { if ($identifier === null) { return ''; } return $this->trimQuotes(strtolower($identifier)); } public function setComment(string $comment): self { // For keeping backward compatibility with MySQL in previous releases, table comments are stored as options. $this->addOption('comment', $comment); return $this; } public function getComment(): ?string { return $this->_options['comment'] ?? null; } /** * @param array $columns * @param array $flags * @param array $options */ private function _createUniqueConstraint( array $columns, string $indexName, array $flags = [], array $options = [], ): UniqueConstraint { if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) { throw IndexNameInvalid::new($indexName); } foreach ($columns as $index => $value) { if (is_string($index)) { $columnName = $index; } else { $columnName = $value; } if (! $this->hasColumn($columnName)) { throw ColumnDoesNotExist::new($columnName, $this->_name); } } return new UniqueConstraint($indexName, $columns, $flags, $options); } /** * @param array $columns * @param array $flags * @param array $options */ private function _createIndex( array $columns, string $indexName, bool $isUnique, bool $isPrimary, array $flags = [], array $options = [], ): Index { if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) { throw IndexNameInvalid::new($indexName); } foreach ($columns as $columnName) { if (! $this->hasColumn($columnName)) { throw ColumnDoesNotExist::new($columnName, $this->_name); } } return new Index($indexName, $columns, $isUnique, $isPrimary, $flags, $options); } } src/Schema/TableDiff.php000064400000011301152062402520011056 0ustar00 $droppedForeignKeys * @param array $addedColumns * @param array $modifiedColumns * @param array $droppedColumns * @param array $renamedColumns * @param array $addedIndexes * @param array $modifiedIndexes * @param array $droppedIndexes * @param array $renamedIndexes * @param array $addedForeignKeys * @param array $modifiedForeignKeys */ public function __construct( private readonly Table $oldTable, private readonly array $addedColumns, private readonly array $modifiedColumns, private readonly array $droppedColumns, private readonly array $renamedColumns, private array $addedIndexes, private readonly array $modifiedIndexes, private array $droppedIndexes, private readonly array $renamedIndexes, private readonly array $addedForeignKeys, private readonly array $modifiedForeignKeys, private readonly array $droppedForeignKeys, ) { } public function getOldTable(): Table { return $this->oldTable; } /** @return array */ public function getAddedColumns(): array { return $this->addedColumns; } /** @return array */ public function getModifiedColumns(): array { return $this->modifiedColumns; } /** @return array */ public function getDroppedColumns(): array { return $this->droppedColumns; } /** @return array */ public function getRenamedColumns(): array { return $this->renamedColumns; } /** @return array */ public function getAddedIndexes(): array { return $this->addedIndexes; } /** * @internal This method exists only for compatibility with the current implementation of schema managers * that modify the diff while processing it. */ public function unsetAddedIndex(Index $index): void { $this->addedIndexes = array_filter( $this->addedIndexes, static function (Index $addedIndex) use ($index): bool { return $addedIndex !== $index; }, ); } /** @return array */ public function getModifiedIndexes(): array { return $this->modifiedIndexes; } /** @return array */ public function getDroppedIndexes(): array { return $this->droppedIndexes; } /** * @internal This method exists only for compatibility with the current implementation of schema managers * that modify the diff while processing it. */ public function unsetDroppedIndex(Index $index): void { $this->droppedIndexes = array_filter( $this->droppedIndexes, static function (Index $droppedIndex) use ($index): bool { return $droppedIndex !== $index; }, ); } /** @return array */ public function getRenamedIndexes(): array { return $this->renamedIndexes; } /** @return array */ public function getAddedForeignKeys(): array { return $this->addedForeignKeys; } /** @return array */ public function getModifiedForeignKeys(): array { return $this->modifiedForeignKeys; } /** @return array */ public function getDroppedForeignKeys(): array { return $this->droppedForeignKeys; } /** * Returns whether the diff is empty (contains no changes). */ public function isEmpty(): bool { return count($this->addedColumns) === 0 && count($this->modifiedColumns) === 0 && count($this->droppedColumns) === 0 && count($this->renamedColumns) === 0 && count($this->addedIndexes) === 0 && count($this->modifiedIndexes) === 0 && count($this->droppedIndexes) === 0 && count($this->renamedIndexes) === 0 && count($this->addedForeignKeys) === 0 && count($this->modifiedForeignKeys) === 0 && count($this->droppedForeignKeys) === 0; } } src/Schema/UniqueConstraint.php000064400000006732152062402520012565 0ustar00 */ protected array $columns = []; /** * Platform specific flags * * @var array */ protected array $flags = []; /** * @param array $columns * @param array $flags * @param array $options */ public function __construct( string $name, array $columns, array $flags = [], private readonly array $options = [], ) { $this->_setName($name); foreach ($columns as $column) { $this->addColumn($column); } foreach ($flags as $flag) { $this->addFlag($flag); } } /** * Returns the names of the referencing table columns the constraint is associated with. * * @return list */ public function getColumns(): array { return array_keys($this->columns); } /** * Returns the quoted representation of the column names the constraint is associated with. * * But only if they were defined with one or a column name * is a keyword reserved by the platform. * Otherwise, the plain unquoted value as inserted is returned. * * @param AbstractPlatform $platform The platform to use for quotation. * * @return list */ public function getQuotedColumns(AbstractPlatform $platform): array { $columns = []; foreach ($this->columns as $column) { $columns[] = $column->getQuotedName($platform); } return $columns; } /** @return array */ public function getUnquotedColumns(): array { return array_map($this->trimQuotes(...), $this->getColumns()); } /** * Returns platform specific flags for unique constraint. * * @return array */ public function getFlags(): array { return array_keys($this->flags); } /** * Adds flag for a unique constraint that translates to platform specific handling. * * @return $this * * @example $uniqueConstraint->addFlag('CLUSTERED') */ public function addFlag(string $flag): self { $this->flags[strtolower($flag)] = true; return $this; } /** * Does this unique constraint have a specific flag? */ public function hasFlag(string $flag): bool { return isset($this->flags[strtolower($flag)]); } /** * Removes a flag. */ public function removeFlag(string $flag): void { unset($this->flags[strtolower($flag)]); } public function hasOption(string $name): bool { return isset($this->options[strtolower($name)]); } public function getOption(string $name): mixed { return $this->options[strtolower($name)]; } /** @return array */ public function getOptions(): array { return $this->options; } protected function addColumn(string $column): void { $this->columns[$column] = new Identifier($column); } } src/Schema/View.php000064400000000532152062402520010154 0ustar00_setName($name); } public function getSql(): string { return $this->sql; } } src/ServerVersionProvider.php000064400000000312152062402520012365 0ustar00Statement for the given SQL and Connection. * * @internal The statement can be only instantiated by {@see Connection}. * * @param Connection $conn The connection for handling statement errors. * @param Driver\Statement $stmt The underlying driver-level statement. * @param string $sql The SQL of the statement. * * @throws Exception */ public function __construct( protected Connection $conn, protected Driver\Statement $stmt, protected string $sql, ) { $this->platform = $conn->getDatabasePlatform(); } /** * Binds a parameter value to the statement. * * The value can optionally be bound with a DBAL mapping type. * If bound with a DBAL mapping type, the binding type is derived from the mapping * type and the value undergoes the conversion routines of the mapping type before * being bound. * * @param string|int $param Parameter identifier. For a prepared statement using named placeholders, * this will be a parameter name of the form :name. For a prepared statement * using question mark placeholders, this will be the 1-indexed position * of the parameter. * @param mixed $value The value to bind to the parameter. * @param ParameterType|string|Type $type Either a {@see \Doctrine\DBAL\ParameterType} or a DBAL mapping type name * or instance. * * @throws Exception */ public function bindValue( string|int $param, mixed $value, string|ParameterType|Type $type = ParameterType::STRING, ): void { $this->params[$param] = $value; $this->types[$param] = $type; if (is_string($type)) { $type = Type::getType($type); } if ($type instanceof Type) { $value = $type->convertToDatabaseValue($value, $this->platform); $bindingType = $type->getBindingType(); } else { $bindingType = $type; } try { $this->stmt->bindValue($param, $value, $bindingType); } catch (Driver\Exception $e) { throw $this->conn->convertException($e); } } /** @throws Exception */ private function execute(): Result { try { return new Result( $this->stmt->execute(), $this->conn, ); } catch (Driver\Exception $ex) { throw $this->conn->convertExceptionDuringQuery($ex, $this->sql, $this->params, $this->types); } } /** * Executes the statement with the currently bound parameters and return result. * * @throws Exception */ public function executeQuery(): Result { return $this->execute(); } /** * Executes the statement with the currently bound parameters and return affected rows. * * If the number of rows exceeds {@see PHP_INT_MAX}, it might be returned as string if the driver supports it. * * @return int|numeric-string * * @throws Exception */ public function executeStatement(): int|string { return $this->execute()->rowCount(); } /** * Gets the wrapped driver statement. */ public function getWrappedStatement(): Driver\Statement { return $this->stmt; } } src/Tools/Console/Command/RunSqlCommand.php000064400000007206152062402520014652 0ustar00setName('dbal:run-sql') ->setDescription('Executes arbitrary SQL directly from the command line.') ->setDefinition([ new InputOption('connection', null, InputOption::VALUE_REQUIRED, 'The named database connection'), new InputArgument('sql', InputArgument::REQUIRED, 'The SQL statement to execute.'), new InputOption('depth', null, InputOption::VALUE_REQUIRED, 'Dumping depth of result set (deprecated).'), new InputOption('force-fetch', null, InputOption::VALUE_NONE, 'Forces fetching the result.'), ]) ->setHelp(<<<'EOT' The %command.name% command executes the given SQL query and outputs the results: php %command.full_name% "SELECT * FROM users" EOT); } /** * {@inheritDoc} * * @throws Exception */ protected function execute(InputInterface $input, OutputInterface $output): int { $conn = $this->getConnection($input); $io = new SymfonyStyle($input, $output); $sql = $input->getArgument('sql'); if ($sql === null) { throw new RuntimeException('Argument "sql" is required in order to execute this command correctly.'); } assert(is_string($sql)); if ($input->getOption('depth') !== null) { $io->warning('Parameter "depth" is deprecated and has no effect anymore.'); } $forceFetch = $input->getOption('force-fetch'); assert(is_bool($forceFetch)); if (stripos($sql, 'select') === 0 || $forceFetch) { $this->runQuery($io, $conn, $sql); } else { $this->runStatement($io, $conn, $sql); } return 0; } private function getConnection(InputInterface $input): Connection { $connectionName = $input->getOption('connection'); assert(is_string($connectionName) || $connectionName === null); if ($connectionName !== null) { return $this->connectionProvider->getConnection($connectionName); } return $this->connectionProvider->getDefaultConnection(); } /** @throws Exception */ private function runQuery(SymfonyStyle $io, Connection $conn, string $sql): void { $resultSet = $conn->fetchAllAssociative($sql); if ($resultSet === []) { $io->success('The query yielded an empty result set.'); return; } $io->table(array_keys($resultSet[0]), $resultSet); } /** @throws Exception */ private function runStatement(SymfonyStyle $io, Connection $conn, string $sql): void { $io->success(sprintf('%d rows affected.', $conn->executeStatement($sql))); } } src/Tools/Console/ConnectionNotFound.php000064400000000244152062402520014320 0ustar00connection; } public function getConnection(string $name): Connection { if ($name !== $this->defaultConnectionName) { throw new ConnectionNotFound(sprintf('Connection with name "%s" does not exist.', $name)); } return $this->connection; } } src/Tools/DsnParser.php000064400000014767152062402520011062 0ustar00> $schemeMapping An array used to map DSN schemes to DBAL drivers */ public function __construct( private readonly array $schemeMapping = [], ) { } /** * @psalm-return Params * * @throws MalformedDsnException */ public function parse( #[SensitiveParameter] string $dsn, ): array { // (pdo-)?sqlite3?:///... => (pdo-)?sqlite3?://localhost/... or else the URL will be invalid $url = preg_replace('#^((?:pdo-)?sqlite3?):///#', '$1://localhost/', $dsn); assert($url !== null); $url = parse_url($url); if ($url === false) { throw MalformedDsnException::new(); } foreach ($url as $param => $value) { if (! is_string($value)) { continue; } $url[$param] = rawurldecode($value); } $params = []; if (isset($url['scheme'])) { $params['driver'] = $this->parseDatabaseUrlScheme($url['scheme']); } if (isset($url['host'])) { $params['host'] = $url['host']; } if (isset($url['port'])) { $params['port'] = $url['port']; } if (isset($url['user'])) { $params['user'] = $url['user']; } if (isset($url['pass'])) { $params['password'] = $url['pass']; } if (isset($params['driver']) && is_a($params['driver'], Driver::class, true)) { $params['driverClass'] = $params['driver']; unset($params['driver']); } $params = $this->parseDatabaseUrlPath($url, $params); $params = $this->parseDatabaseUrlQuery($url, $params); return $params; } /** * Parses the given connection URL and resolves the given connection parameters. * * Assumes that the connection URL scheme is already parsed and resolved into the given connection parameters * via {@see parseDatabaseUrlScheme}. * * @see parseDatabaseUrlScheme * * @param mixed[] $url The URL parts to evaluate. * @param mixed[] $params The connection parameters to resolve. * * @return mixed[] The resolved connection parameters. */ private function parseDatabaseUrlPath(array $url, array $params): array { if (! isset($url['path'])) { return $params; } $url['path'] = $this->normalizeDatabaseUrlPath($url['path']); // If we do not have a known DBAL driver, we do not know any connection URL path semantics to evaluate // and therefore treat the path as a regular DBAL connection URL path. if (! isset($params['driver'])) { return $this->parseRegularDatabaseUrlPath($url, $params); } if (strpos($params['driver'], 'sqlite') !== false) { return $this->parseSqliteDatabaseUrlPath($url, $params); } return $this->parseRegularDatabaseUrlPath($url, $params); } /** * Normalizes the given connection URL path. * * @return string The normalized connection URL path */ private function normalizeDatabaseUrlPath(string $urlPath): string { // Trim leading slash from URL path. return substr($urlPath, 1); } /** * Parses the query part of the given connection URL and resolves the given connection parameters. * * @param mixed[] $url The connection URL parts to evaluate. * @param mixed[] $params The connection parameters to resolve. * * @return mixed[] The resolved connection parameters. */ private function parseDatabaseUrlQuery(array $url, array $params): array { if (! isset($url['query'])) { return $params; } $query = []; parse_str($url['query'], $query); // simply ingest query as extra params, e.g. charset or sslmode return array_merge($params, $query); // parse_str wipes existing array elements } /** * Parses the given regular connection URL and resolves the given connection parameters. * * Assumes that the "path" URL part is already normalized via {@see normalizeDatabaseUrlPath}. * * @see normalizeDatabaseUrlPath * * @param mixed[] $url The regular connection URL parts to evaluate. * @param mixed[] $params The connection parameters to resolve. * * @return mixed[] The resolved connection parameters. */ private function parseRegularDatabaseUrlPath(array $url, array $params): array { $params['dbname'] = $url['path']; return $params; } /** * Parses the given SQLite connection URL and resolves the given connection parameters. * * Assumes that the "path" URL part is already normalized via {@see normalizeDatabaseUrlPath}. * * @see normalizeDatabaseUrlPath * * @param mixed[] $url The SQLite connection URL parts to evaluate. * @param mixed[] $params The connection parameters to resolve. * * @return mixed[] The resolved connection parameters. */ private function parseSqliteDatabaseUrlPath(array $url, array $params): array { if ($url['path'] === ':memory:') { $params['memory'] = true; return $params; } $params['path'] = $url['path']; // pdo_sqlite driver uses 'path' instead of 'dbname' key return $params; } /** * Parses the scheme part from given connection URL and resolves the given connection parameters. * * @return string The resolved driver. */ private function parseDatabaseUrlScheme(string $scheme): string { // URL schemes must not contain underscores, but dashes are ok $driver = str_replace('-', '_', $scheme); // If the driver is an alias (e.g. "postgres"), map it to the actual name ("pdo-pgsql"). // Otherwise, let checkParams decide later if the driver exists. return $this->schemeMapping[$driver] ?? $driver; } } src/TransactionIsolationLevel.php000064400000000303152062402520013175 0ustar00getAsciiStringTypeDeclarationSQL($column); } public function getBindingType(): ParameterType { return ParameterType::ASCII; } } src/Types/BigIntType.php000064400000002623152062402520011167 0ustar00getBigIntTypeDeclarationSQL($column); } public function getBindingType(): ParameterType { return ParameterType::STRING; } /** * @param T $value * * @return (T is null ? null : int|string) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): int|string|null { if ($value === null || is_int($value)) { return $value; } if ($value > PHP_INT_MIN && $value < PHP_INT_MAX) { return (int) $value; } assert( is_string($value), 'DBAL assumes values outside of the integer range to be returned as string by the database driver.', ); return $value; } } src/Types/BinaryType.php000064400000002115152062402520011233 0ustar00getBinaryTypeDeclarationSQL($column); } public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return null; } if (is_resource($value)) { $value = stream_get_contents($value); } if (! is_string($value)) { throw ValueNotConvertible::new($value, Types::BINARY); } return $value; } public function getBindingType(): ParameterType { return ParameterType::BINARY; } } src/Types/BlobType.php000064400000002347152062402520010674 0ustar00getBlobTypeDeclarationSQL($column); } public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed { if ($value === null) { return null; } if (is_string($value)) { $fp = fopen('php://temp', 'rb+'); assert(is_resource($fp)); fwrite($fp, $value); fseek($fp, 0); $value = $fp; } if (! is_resource($value)) { throw ValueNotConvertible::new($value, Types::BLOB); } return $value; } public function getBindingType(): ParameterType { return ParameterType::LARGE_OBJECT; } } src/Types/BooleanType.php000064400000001730152062402520011370 0ustar00getBooleanTypeDeclarationSQL($column); } public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed { return $platform->convertBooleansToDatabaseValue($value); } /** * @param T $value * * @return (T is null ? null : bool) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?bool { return $platform->convertFromBoolean($value); } public function getBindingType(): ParameterType { return ParameterType::BOOLEAN; } } src/Types/ConversionException.php000064400000000421152062402520013147 0ustar00getDateTypeDeclarationSQL($column); } /** * @param T $value * * @return (T is null ? null : string) * * @template T */ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return $value; } if ($value instanceof DateTimeImmutable) { return $value->format($platform->getDateFormatString()); } throw InvalidType::new( $value, static::class, ['null', DateTimeImmutable::class], ); } /** * @param T $value * * @return (T is null ? null : DateTimeImmutable) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTimeImmutable { if ($value === null || $value instanceof DateTimeImmutable) { return $value; } $dateTime = DateTimeImmutable::createFromFormat('!' . $platform->getDateFormatString(), $value); if ($dateTime === false) { throw InvalidFormat::new( $value, static::class, $platform->getDateFormatString(), ); } return $dateTime; } } src/Types/DateIntervalType.php000064400000003720152062402520012374 0ustar00getStringTypeDeclarationSQL($column); } /** * @param T $value * * @return (T is null ? null : string) * * @template T */ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return null; } if ($value instanceof DateInterval) { return $value->format(self::FORMAT); } throw InvalidType::new($value, static::class, ['null', DateInterval::class]); } /** * @param T $value * * @return (T is null ? null : DateInterval) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateInterval { if ($value === null || $value instanceof DateInterval) { return $value; } $negative = false; if (isset($value[0]) && ($value[0] === '+' || $value[0] === '-')) { $negative = $value[0] === '-'; $value = substr($value, 1); } try { $interval = new DateInterval($value); if ($negative) { $interval->invert = 1; } return $interval; } catch (Throwable $exception) { throw InvalidFormat::new($value, static::class, self::FORMAT, $exception); } } } src/Types/DateTimeImmutableType.php000064400000003647152062402520013356 0ustar00getDateTimeTypeDeclarationSQL($column); } /** * @param T $value * * @return (T is null ? null : string) * * @template T */ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return $value; } if ($value instanceof DateTimeImmutable) { return $value->format($platform->getDateTimeFormatString()); } throw InvalidType::new( $value, static::class, ['null', DateTimeImmutable::class], ); } /** * @param T $value * * @return (T is null ? null : DateTimeImmutable) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTimeImmutable { if ($value === null || $value instanceof DateTimeImmutable) { return $value; } $dateTime = DateTimeImmutable::createFromFormat($platform->getDateTimeFormatString(), $value); if ($dateTime !== false) { return $dateTime; } try { return new DateTimeImmutable($value); } catch (Exception $e) { throw InvalidFormat::new( $value, static::class, $platform->getDateTimeFormatString(), $e, ); } } } src/Types/DateTimeType.php000064400000003562152062402520011512 0ustar00getDateTimeTypeDeclarationSQL($column); } /** * @param T $value * * @return (T is null ? null : string) * * @template T */ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return $value; } if ($value instanceof DateTime) { return $value->format($platform->getDateTimeFormatString()); } throw InvalidType::new( $value, static::class, ['null', DateTime::class], ); } /** * @param T $value * * @return (T is null ? null : DateTime) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTime { if ($value === null || $value instanceof DateTime) { return $value; } $dateTime = DateTime::createFromFormat($platform->getDateTimeFormatString(), $value); if ($dateTime !== false) { return $dateTime; } try { return new DateTime($value); } catch (Exception $e) { throw InvalidFormat::new( $value, static::class, $platform->getDateTimeFormatString(), $e, ); } } } src/Types/DateTimeTzImmutableType.php000064400000003427152062402520013670 0ustar00getDateTimeTzTypeDeclarationSQL($column); } /** * @psalm-param T $value * * @return (T is null ? null : string) * * @template T */ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return $value; } if ($value instanceof DateTimeImmutable) { return $value->format($platform->getDateTimeTzFormatString()); } throw InvalidType::new( $value, static::class, ['null', DateTimeImmutable::class], ); } /** * @param T $value * * @return (T is null ? null : DateTimeImmutable) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTimeImmutable { if ($value === null || $value instanceof DateTimeImmutable) { return $value; } $dateTime = DateTimeImmutable::createFromFormat($platform->getDateTimeTzFormatString(), $value); if ($dateTime !== false) { return $dateTime; } throw InvalidFormat::new( $value, static::class, $platform->getDateTimeTzFormatString(), ); } } src/Types/DateTimeTzType.php000064400000005023152062402520012022 0ustar00getDateTimeTzTypeDeclarationSQL($column); } /** * @param T $value * * @return (T is null ? null : string) * * @template T */ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return $value; } if ($value instanceof DateTime) { return $value->format($platform->getDateTimeTzFormatString()); } throw InvalidType::new( $value, static::class, ['null', DateTime::class], ); } /** * @param T $value * * @return (T is null ? null : DateTime) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTime { if ($value === null || $value instanceof DateTime) { return $value; } $dateTime = DateTime::createFromFormat($platform->getDateTimeTzFormatString(), $value); if ($dateTime !== false) { return $dateTime; } throw InvalidFormat::new( $value, static::class, $platform->getDateTimeTzFormatString(), ); } } src/Types/DateType.php000064400000003211152062402520010662 0ustar00getDateTypeDeclarationSQL($column); } /** * @psalm-param T $value * * @return (T is null ? null : string) * * @template T */ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed { if ($value === null) { return $value; } if ($value instanceof DateTime) { return $value->format($platform->getDateFormatString()); } throw InvalidType::new($value, static::class, ['null', DateTime::class]); } /** * @param T $value * * @return (T is null ? null : DateTime) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTime { if ($value === null || $value instanceof DateTime) { return $value; } $dateTime = DateTime::createFromFormat('!' . $platform->getDateFormatString(), $value); if ($dateTime !== false) { return $dateTime; } throw InvalidFormat::new( $value, static::class, $platform->getDateFormatString(), ); } } src/Types/DecimalType.php000064400000001437152062402520011353 0ustar00getDecimalTypeDeclarationSQL($column); } public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?string { // Some drivers can represent decimals as float/int // See also: https://github.com/doctrine/dbal/pull/4818 if (is_float($value) || is_int($value)) { return (string) $value; } return $value; } } src/Types/Exception/InvalidFormat.php000064400000001673152062402520013652 0ustar00 32 ? substr($value, 0, 20) . '...' : $value, $toType, $expectedFormat ?? '', ), 0, $previous, ); } } src/Types/Exception/InvalidType.php000064400000002600152062402520013332 0ustar00 32 ? substr($value, 0, 20) . '...' : $value, $toType, ); } return new self($message, 0, $previous); } } src/Types/FloatType.php000064400000001145152062402520011056 0ustar00getFloatDeclarationSQL($column); } /** * @param T $value * * @return (T is null ? null : float) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?float { return $value === null ? null : (float) $value; } } src/Types/GuidType.php000064400000000667152062402520010711 0ustar00getGuidTypeDeclarationSQL($column); } } src/Types/IntegerType.php000064400000001510152062402520011402 0ustar00getIntegerTypeDeclarationSQL($column); } /** * @param T $value * * @return (T is null ? null : int) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?int { return $value === null ? null : (int) $value; } public function getBindingType(): ParameterType { return ParameterType::INTEGER; } } src/Types/JsonType.php000064400000003272152062402520010725 0ustar00getJsonTypeDeclarationSQL($column); } /** * @param T $value * * @return (T is null ? null : string) * * @template T */ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return null; } try { return json_encode($value, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION); } catch (JsonException $e) { throw SerializationFailed::new($value, 'json', $e->getMessage(), $e); } } public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed { if ($value === null || $value === '') { return null; } if (is_resource($value)) { $value = stream_get_contents($value); } try { return json_decode($value, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $e) { throw ValueNotConvertible::new($value, 'json', $e->getMessage(), $e); } } } src/Types/PhpDateMappingType.php000064400000000315152062402520012650 0ustar00getClobTypeDeclarationSQL($column); } public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if (! is_array($value) || count($value) === 0) { return null; } return implode(',', $value); } /** @return list */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): array { if ($value === null) { return []; } $value = is_resource($value) ? stream_get_contents($value) : $value; return explode(',', $value); } } src/Types/SmallIntType.php000064400000001523152062402520011534 0ustar00getSmallIntTypeDeclarationSQL($column); } /** * @param T $value * * @return (T is null ? null : int) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?int { return $value === null ? null : (int) $value; } public function getBindingType(): ParameterType { return ParameterType::INTEGER; } } src/Types/StringType.php000064400000000626152062402520011262 0ustar00getStringTypeDeclarationSQL($column); } } src/Types/TextType.php000064400000001173152062402520010736 0ustar00getClobTypeDeclarationSQL($column); } public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed { return is_resource($value) ? stream_get_contents($value) : $value; } } src/Types/TimeImmutableType.php000064400000003357152062402520012556 0ustar00getTimeTypeDeclarationSQL($column); } /** * @param T $value * * @return (T is null ? null : string) * * @template T */ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return $value; } if ($value instanceof DateTimeImmutable) { return $value->format($platform->getTimeFormatString()); } throw InvalidType::new( $value, static::class, ['null', DateTimeImmutable::class], ); } /** * @param T $value * * @return (T is null ? null : DateTimeImmutable) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTimeImmutable { if ($value === null || $value instanceof DateTimeImmutable) { return $value; } $dateTime = DateTimeImmutable::createFromFormat('!' . $platform->getTimeFormatString(), $value); if ($dateTime !== false) { return $dateTime; } throw InvalidFormat::new( $value, static::class, $platform->getTimeFormatString(), ); } } src/Types/TimeType.php000064400000003211152062402520010703 0ustar00getTimeTypeDeclarationSQL($column); } /** * @param T $value * * @return (T is null ? null : string) * * @template T */ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { if ($value === null) { return $value; } if ($value instanceof DateTime) { return $value->format($platform->getTimeFormatString()); } throw InvalidType::new($value, static::class, ['null', DateTime::class]); } /** * @param T $value * * @return (T is null ? null : DateTime) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTime { if ($value === null || $value instanceof DateTime) { return $value; } $dateTime = DateTime::createFromFormat('!' . $platform->getTimeFormatString(), $value); if ($dateTime !== false) { return $dateTime; } throw InvalidFormat::new( $value, static::class, $platform->getTimeFormatString(), ); } } src/Types/Type.php000064400000015131152062402520010070 0ustar00 AsciiStringType::class, Types::BIGINT => BigIntType::class, Types::BINARY => BinaryType::class, Types::BLOB => BlobType::class, Types::BOOLEAN => BooleanType::class, Types::DATE_MUTABLE => DateType::class, Types::DATE_IMMUTABLE => DateImmutableType::class, Types::DATEINTERVAL => DateIntervalType::class, Types::DATETIME_MUTABLE => DateTimeType::class, Types::DATETIME_IMMUTABLE => DateTimeImmutableType::class, Types::DATETIMETZ_MUTABLE => DateTimeTzType::class, Types::DATETIMETZ_IMMUTABLE => DateTimeTzImmutableType::class, Types::DECIMAL => DecimalType::class, Types::FLOAT => FloatType::class, Types::GUID => GuidType::class, Types::INTEGER => IntegerType::class, Types::JSON => JsonType::class, Types::SIMPLE_ARRAY => SimpleArrayType::class, Types::SMALLINT => SmallIntType::class, Types::STRING => StringType::class, Types::TEXT => TextType::class, Types::TIME_MUTABLE => TimeType::class, Types::TIME_IMMUTABLE => TimeImmutableType::class, ]; private static ?TypeRegistry $typeRegistry = null; /** @internal Do not instantiate directly - use {@see Type::addType()} method instead. */ final public function __construct() { } /** * Converts a value from its PHP representation to its database representation * of this type. * * @param mixed $value The value to convert. * @param AbstractPlatform $platform The currently used database platform. * * @return mixed The database representation of the value. * * @throws ConversionException */ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed { return $value; } /** * Converts a value from its database representation to its PHP representation * of this type. * * @param mixed $value The value to convert. * @param AbstractPlatform $platform The currently used database platform. * * @return mixed The PHP representation of the value. * * @throws ConversionException */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed { return $value; } /** * Gets the SQL declaration snippet for a column of this type. * * @param array $column The column definition * @param AbstractPlatform $platform The currently used database platform. */ abstract public function getSQLDeclaration(array $column, AbstractPlatform $platform): string; final public static function getTypeRegistry(): TypeRegistry { return self::$typeRegistry ??= self::createTypeRegistry(); } private static function createTypeRegistry(): TypeRegistry { $instances = []; foreach (self::BUILTIN_TYPES_MAP as $name => $class) { $instances[$name] = new $class(); } return new TypeRegistry($instances); } /** * Factory method to create type instances. * * @param string $name The name of the type. * * @throws Exception */ public static function getType(string $name): self { return self::getTypeRegistry()->get($name); } /** * Finds a name for the given type. * * @throws Exception */ public static function lookupName(self $type): string { return self::getTypeRegistry()->lookupName($type); } /** * Adds a custom type to the type map. * * @param string $name The name of the type. * @param class-string $className The class name of the custom type. * * @throws Exception */ public static function addType(string $name, string $className): void { self::getTypeRegistry()->register($name, new $className()); } /** * Checks if exists support for a type. * * @param string $name The name of the type. * * @return bool TRUE if type is supported; FALSE otherwise. */ public static function hasType(string $name): bool { return self::getTypeRegistry()->has($name); } /** * Overrides an already defined type to use a different implementation. * * @param class-string $className * * @throws Exception */ public static function overrideType(string $name, string $className): void { self::getTypeRegistry()->override($name, new $className()); } /** * Gets the (preferred) binding type for values of this type that * can be used when binding parameters to prepared statements. */ public function getBindingType(): ParameterType { return ParameterType::STRING; } /** * Gets the types array map which holds all registered types and the corresponding * type class * * @return array */ public static function getTypesMap(): array { return array_map( static function (Type $type): string { return $type::class; }, self::getTypeRegistry()->getMap(), ); } /** * Modifies the SQL expression (identifier, parameter) to convert to a database value. */ public function convertToDatabaseValueSQL(string $sqlExpr, AbstractPlatform $platform): string { return $sqlExpr; } /** * Modifies the SQL expression (identifier, parameter) to convert to a PHP value. */ public function convertToPHPValueSQL(string $sqlExpr, AbstractPlatform $platform): string { return $sqlExpr; } /** * Gets an array of database types that map to this Doctrine type. * * @return array */ public function getMappedDatabaseTypes(AbstractPlatform $platform): array { return []; } } src/Types/TypeRegistry.php000064400000006501152062402520011622 0ustar00 Map of type names and their corresponding flyweight objects. */ private array $instances; /** @var array */ private array $instancesReverseIndex; /** @param array $instances */ public function __construct(array $instances = []) { $this->instances = []; $this->instancesReverseIndex = []; foreach ($instances as $name => $type) { $this->register($name, $type); } } /** * Finds a type by the given name. * * @throws Exception */ public function get(string $name): Type { $type = $this->instances[$name] ?? null; if ($type === null) { throw UnknownColumnType::new($name); } return $type; } /** * Finds a name for the given type. * * @throws Exception */ public function lookupName(Type $type): string { $name = $this->findTypeName($type); if ($name === null) { throw TypeNotRegistered::new($type); } return $name; } /** * Checks if there is a type of the given name. */ public function has(string $name): bool { return isset($this->instances[$name]); } /** * Registers a custom type to the type map. * * @throws Exception */ public function register(string $name, Type $type): void { if (isset($this->instances[$name])) { throw TypesAlreadyExists::new($name); } if ($this->findTypeName($type) !== null) { throw TypeAlreadyRegistered::new($type); } $this->instances[$name] = $type; $this->instancesReverseIndex[spl_object_id($type)] = $name; } /** * Overrides an already defined type to use a different implementation. * * @throws Exception */ public function override(string $name, Type $type): void { $origType = $this->instances[$name] ?? null; if ($origType === null) { throw TypeNotFound::new($name); } if (($this->findTypeName($type) ?? $name) !== $name) { throw TypeAlreadyRegistered::new($type); } unset($this->instancesReverseIndex[spl_object_id($origType)]); $this->instances[$name] = $type; $this->instancesReverseIndex[spl_object_id($type)] = $name; } /** * Gets the map of all registered types and their corresponding type instances. * * @internal * * @return array */ public function getMap(): array { return $this->instances; } private function findTypeName(Type $type): ?string { return $this->instancesReverseIndex[spl_object_id($type)] ?? null; } } src/Types/Types.php000064400000002633152062402520010256 0ustar00format($platform->getDateTimeFormatString()); } throw InvalidType::new( $value, static::class, ['null', DateTimeImmutable::class], ); } /** * @param T $value * * @return (T is null ? null : DateTimeImmutable) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTimeImmutable { if ($value === null || $value instanceof DateTimeImmutable) { return $value; } try { $dateTime = new DateTimeImmutable($value); } catch (Exception $e) { throw ValueNotConvertible::new($value, DateTimeImmutable::class, $e->getMessage(), $e); } return $dateTime; } } src/Types/VarDateTimeType.php000064400000002135152062402520012156 0ustar00 0 it is necessary to use this type. */ class VarDateTimeType extends DateTimeType { /** * @param T $value * * @return (T is null ? null : DateTime) * * @template T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTime { if ($value === null || $value instanceof DateTime) { return $value; } try { $dateTime = new DateTime($value); } catch (Exception $e) { throw ValueNotConvertible::new($value, DateTime::class, $e->getMessage(), $e); } return $dateTime; } }