diff --git a/engine/app/model/MusicModel.php b/engine/app/model/MusicModel.php index 66a8814..7c0debe 100644 --- a/engine/app/model/MusicModel.php +++ b/engine/app/model/MusicModel.php @@ -3,14 +3,17 @@ namespace App\Model; use Shalex\Engine\DataBase; +use Shalex\Engine\Db; class MusicModel { static function getData() { - $db = new DataBase(); - $result = $db->getDataBase('SELECT * FROM top_chart'); - var_dump($result); + $link = Db::connection(CONFIG_DB); + $query = "SELECT * FROM track WHERE device_id = 3"; + $result = $link->executeQuery($query); + $composer = $result->fetchAssociative(); + var_dump($composer['composer_name']); $box = []; if ($handle = opendir('./uploads')) { while (false !== ($entry = readdir($handle))) { diff --git a/engine/composer.json b/engine/composer.json index f895b21..aa988f8 100644 --- a/engine/composer.json +++ b/engine/composer.json @@ -16,5 +16,7 @@ } ], "minimum-stability": "beta", - "require": {} + "require": { + "doctrine/dbal": "*" + } } diff --git a/engine/composer.lock b/engine/composer.lock new file mode 100644 index 0000000..56ffdf1 --- /dev/null +++ b/engine/composer.lock @@ -0,0 +1,269 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "e206ee293306241f55efdddf0c974774", + "packages": [ + { + "name": "doctrine/dbal", + "version": "4.2.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "19a2b7deb5fe8c2df0ff817ecea305e50acb62ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/19a2b7deb5fe8c2df0ff817ecea305e50acb62ec", + "reference": "19a2b7deb5fe8c2df0ff817ecea305e50acb62ec", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^0.5.3|^1", + "php": "^8.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": "2.1.1", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "10.5.39", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.10.2", + "symfony/cache": "^6.3.8|^7.0", + "symfony/console": "^5.4|^6.3|^7.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/4.2.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2025-01-16T08:40:56+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "1.4.10 || 2.0.3", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.4" + }, + "time": "2024-12-07T21:18:45+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "beta", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/engine/config/main.php b/engine/config/main.php new file mode 100644 index 0000000..0ae8f86 --- /dev/null +++ b/engine/config/main.php @@ -0,0 +1,3 @@ + 'music_store', + 'user' => 'postgres', + 'password' => '123', + 'host' => 'PostgreSQL-16', + 'driver' => 'pdo_pgsql', +]; diff --git a/engine/core/DataBase.php b/engine/core/DataBase.php index 2d89cf2..555b927 100644 --- a/engine/core/DataBase.php +++ b/engine/core/DataBase.php @@ -4,13 +4,13 @@ namespace Shalex\Engine; define('DB_PERSISTENCY', 'true'); define('DB_SERVER', '127.127.126.49'); -define('DB_USERNAME', 'music'); +define('DB_USERNAME', 'music_store'); define('DB_PASSWORD', '123'); define('DB_DATABASE', 'music'); define('PDO_DSN', 'pgsql:host=' . DB_SERVER . ';dbname=' . DB_DATABASE); -class DataBase +class DataBase_ { private static $db; diff --git a/engine/core/Db.php b/engine/core/Db.php new file mode 100644 index 0000000..1e9dd29 --- /dev/null +++ b/engine/core/Db.php @@ -0,0 +1,23 @@ +createQueryBuilder(); + } + +} diff --git a/engine/index.php b/engine/index.php index 4e40a9c..e0d7e9c 100644 --- a/engine/index.php +++ b/engine/index.php @@ -1,5 +1,7 @@ + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool + */ + private static $installedIsLocalDir; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + + // when using reload, we disable the duplicate protection to ensure that self::$installed data is + // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, + // so we have to assume it does not, and that may result in duplicate data being returned when listing + // all installed packages for example + self::$installedIsLocalDir = false; + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + $copiedLocalDir = false; + + if (self::$canGetVendors) { + $selfDir = strtr(__DIR__, '\\', '/'); + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + $vendorDir = strtr($vendorDir, '\\', '/'); + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + self::$installedByVendor[$vendorDir] = $required; + $installed[] = $required; + if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { + self::$installed = $required; + self::$installedIsLocalDir = true; + } + } + if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { + $copiedLocalDir = true; + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array() && !$copiedLocalDir) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/engine/vendor/composer/autoload_psr4.php b/engine/vendor/composer/autoload_psr4.php index 700f1de..ab64258 100644 --- a/engine/vendor/composer/autoload_psr4.php +++ b/engine/vendor/composer/autoload_psr4.php @@ -7,6 +7,10 @@ $baseDir = dirname($vendorDir); return array( 'Shalex\\Engine\\' => array($baseDir . '/core'), + 'Psr\\Log\\' => array($vendorDir . '/psr/log/src'), + 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), + 'Doctrine\\Deprecations\\' => array($vendorDir . '/doctrine/deprecations/src'), + 'Doctrine\\DBAL\\' => array($vendorDir . '/doctrine/dbal/src'), 'App\\Veiw\\' => array($baseDir . '/app/view'), 'App\\Model\\' => array($baseDir . '/app/model'), 'App\\Controller\\' => array($baseDir . '/app/controller'), diff --git a/engine/vendor/composer/autoload_real.php b/engine/vendor/composer/autoload_real.php index 653eef5..ffad0bc 100644 --- a/engine/vendor/composer/autoload_real.php +++ b/engine/vendor/composer/autoload_real.php @@ -22,6 +22,8 @@ class ComposerAutoloaderInit921c1eb0caad68e963758a25e4bcf636 return self::$loader; } + require __DIR__ . '/platform_check.php'; + spl_autoload_register(array('ComposerAutoloaderInit921c1eb0caad68e963758a25e4bcf636', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); spl_autoload_unregister(array('ComposerAutoloaderInit921c1eb0caad68e963758a25e4bcf636', 'loadClassLoader')); diff --git a/engine/vendor/composer/autoload_static.php b/engine/vendor/composer/autoload_static.php index ce93eb1..d51a4ed 100644 --- a/engine/vendor/composer/autoload_static.php +++ b/engine/vendor/composer/autoload_static.php @@ -11,6 +11,16 @@ class ComposerStaticInit921c1eb0caad68e963758a25e4bcf636 array ( 'Shalex\\Engine\\' => 14, ), + 'P' => + array ( + 'Psr\\Log\\' => 8, + 'Psr\\Cache\\' => 10, + ), + 'D' => + array ( + 'Doctrine\\Deprecations\\' => 22, + 'Doctrine\\DBAL\\' => 14, + ), 'A' => array ( 'App\\Veiw\\' => 9, @@ -24,6 +34,22 @@ class ComposerStaticInit921c1eb0caad68e963758a25e4bcf636 array ( 0 => __DIR__ . '/../..' . '/core', ), + 'Psr\\Log\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/log/src', + ), + 'Psr\\Cache\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/cache/src', + ), + 'Doctrine\\Deprecations\\' => + array ( + 0 => __DIR__ . '/..' . '/doctrine/deprecations/src', + ), + 'Doctrine\\DBAL\\' => + array ( + 0 => __DIR__ . '/..' . '/doctrine/dbal/src', + ), 'App\\Veiw\\' => array ( 0 => __DIR__ . '/../..' . '/app/view', diff --git a/engine/vendor/composer/installed.json b/engine/vendor/composer/installed.json new file mode 100644 index 0000000..534fa94 --- /dev/null +++ b/engine/vendor/composer/installed.json @@ -0,0 +1,268 @@ +{ + "packages": [ + { + "name": "doctrine/dbal", + "version": "4.2.2", + "version_normalized": "4.2.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "19a2b7deb5fe8c2df0ff817ecea305e50acb62ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/19a2b7deb5fe8c2df0ff817ecea305e50acb62ec", + "reference": "19a2b7deb5fe8c2df0ff817ecea305e50acb62ec", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^0.5.3|^1", + "php": "^8.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": "2.1.1", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "10.5.39", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.10.2", + "symfony/cache": "^6.3.8|^7.0", + "symfony/console": "^5.4|^6.3|^7.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "time": "2025-01-16T08:40:56+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/4.2.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "install-path": "../doctrine/dbal" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.4", + "version_normalized": "1.1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "1.4.10 || 2.0.3", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "time": "2024-12-07T21:18:45+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.4" + }, + "install-path": "../doctrine/deprecations" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "version_normalized": "3.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "time": "2021-02-03T23:26:27+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "install-path": "../psr/cache" + }, + { + "name": "psr/log", + "version": "3.0.2", + "version_normalized": "3.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "time": "2024-09-11T13:17:53+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "install-path": "../psr/log" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/engine/vendor/composer/installed.php b/engine/vendor/composer/installed.php new file mode 100644 index 0000000..ae814f5 --- /dev/null +++ b/engine/vendor/composer/installed.php @@ -0,0 +1,59 @@ + array( + 'name' => 'shalex/engine', + 'pretty_version' => 'dev-main', + 'version' => 'dev-main', + 'reference' => '7b6cf424673e92807c05fee5f5bddf065086a4c3', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + 'doctrine/dbal' => array( + 'pretty_version' => '4.2.2', + 'version' => '4.2.2.0', + 'reference' => '19a2b7deb5fe8c2df0ff817ecea305e50acb62ec', + 'type' => 'library', + 'install_path' => __DIR__ . '/../doctrine/dbal', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'doctrine/deprecations' => array( + 'pretty_version' => '1.1.4', + 'version' => '1.1.4.0', + 'reference' => '31610dbb31faa98e6b5447b62340826f54fbc4e9', + 'type' => 'library', + 'install_path' => __DIR__ . '/../doctrine/deprecations', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/cache' => array( + 'pretty_version' => '3.0.0', + 'version' => '3.0.0.0', + 'reference' => 'aa5030cfa5405eccfdcb1083ce040c2cb8d253bf', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/cache', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/log' => array( + 'pretty_version' => '3.0.2', + 'version' => '3.0.2.0', + 'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/log', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'shalex/engine' => array( + 'pretty_version' => 'dev-main', + 'version' => 'dev-main', + 'reference' => '7b6cf424673e92807c05fee5f5bddf065086a4c3', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/engine/vendor/composer/platform_check.php b/engine/vendor/composer/platform_check.php new file mode 100644 index 0000000..4c3a5d6 --- /dev/null +++ b/engine/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 80100)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/engine/vendor/doctrine/dbal/CONTRIBUTING.md b/engine/vendor/doctrine/dbal/CONTRIBUTING.md new file mode 100644 index 0000000..31b6eff --- /dev/null +++ b/engine/vendor/doctrine/dbal/CONTRIBUTING.md @@ -0,0 +1,6 @@ +This 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 diff --git a/engine/vendor/doctrine/dbal/LICENSE b/engine/vendor/doctrine/dbal/LICENSE new file mode 100644 index 0000000..e8fdec4 --- /dev/null +++ b/engine/vendor/doctrine/dbal/LICENSE @@ -0,0 +1,19 @@ +Copyright (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. diff --git a/engine/vendor/doctrine/dbal/README.md b/engine/vendor/doctrine/dbal/README.md new file mode 100644 index 0000000..a2b8595 --- /dev/null +++ b/engine/vendor/doctrine/dbal/README.md @@ -0,0 +1,55 @@ +# Doctrine DBAL + +| [5.0-dev][5.0] | [4.3-dev][4.3] | [4.2][4.2] | [3.10][3.10] | [3.9][3.9] | +|:---------------------------------------------------:|:---------------------------------------------------:|:---------------------------------------------------:|:-----------------------------------------------------:|:---------------------------------------------------:| +| [![GitHub Actions][GA 5.0 image]][GA 5.0] | [![GitHub Actions][GA 4.3 image]][GA 4.3] | [![GitHub Actions][GA 4.2 image]][GA 4.2] | [![GitHub Actions][GA 3.10 image]][GA 3.10] | [![GitHub Actions][GA 3.9 image]][GA 3.9] | +| [![AppVeyor][AppVeyor 5.0 image]][AppVeyor 5.0] | [![AppVeyor][AppVeyor 4.3 image]][AppVeyor 4.3] | [![AppVeyor][AppVeyor 4.2 image]][AppVeyor 4.2] | [![AppVeyor][AppVeyor 3.10 image]][AppVeyor 3.10] | [![AppVeyor][AppVeyor 3.9 image]][AppVeyor 3.9] | +| [![Code Coverage][Coverage 5.0 image]][CodeCov 5.0] | [![Code Coverage][Coverage 4.3 image]][CodeCov 4.3] | [![Code Coverage][Coverage 4.2 image]][CodeCov 4.2] | [![Code Coverage][Coverage 3.10 image]][CodeCov 3.10] | [![Code Coverage][Coverage 3.9 image]][CodeCov 3.9] | + +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/actions/workflows/continuous-integration.yml/badge.svg?branch=5.0.x + + [Coverage 4.3 image]: https://codecov.io/gh/doctrine/dbal/branch/4.3.x/graph/badge.svg + [4.3]: https://github.com/doctrine/dbal/tree/4.3.x + [CodeCov 4.3]: https://codecov.io/gh/doctrine/dbal/branch/4.3.x + [AppVeyor 4.3]: https://ci.appveyor.com/project/doctrine/dbal/branch/4.3.x + [AppVeyor 4.3 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/4.3.x?svg=true + [GA 4.3]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.3.x + [GA 4.3 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=4.3.x + + [Coverage 4.2 image]: https://codecov.io/gh/doctrine/dbal/branch/4.2.x/graph/badge.svg + [4.2]: https://github.com/doctrine/dbal/tree/4.2.x + [CodeCov 4.2]: https://codecov.io/gh/doctrine/dbal/branch/4.2.x + [AppVeyor 4.2]: https://ci.appveyor.com/project/doctrine/dbal/branch/4.2.x + [AppVeyor 4.2 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/4.2.x?svg=true + [GA 4.2]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.2.x + [GA 4.2 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=4.2.x + + [Coverage 3.10 image]: https://codecov.io/gh/doctrine/dbal/branch/3.10.x/graph/badge.svg + [3.10]: https://github.com/doctrine/dbal/tree/3.10.x + [CodeCov 3.10]: https://codecov.io/gh/doctrine/dbal/branch/3.10.x + [AppVeyor 3.10]: https://ci.appveyor.com/project/doctrine/dbal/branch/3.10.x + [AppVeyor 3.10 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/3.10.x?svg=true + [GA 3.10]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A3.10.x + [GA 3.10 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=3.10.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/actions/workflows/continuous-integration.yml/badge.svg?branch=3.9.x diff --git a/engine/vendor/doctrine/dbal/composer.json b/engine/vendor/doctrine/dbal/composer.json new file mode 100644 index 0000000..0e95d83 --- /dev/null +++ b/engine/vendor/doctrine/dbal/composer.json @@ -0,0 +1,68 @@ +{ + "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": "2.1.1", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "10.5.39", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.10.2", + "symfony/cache": "^6.3.8|^7.0", + "symfony/console": "^5.4|^6.3|^7.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" } + } +} diff --git a/engine/vendor/doctrine/dbal/phpstan-baseline.neon b/engine/vendor/doctrine/dbal/phpstan-baseline.neon new file mode 100644 index 0000000..98f0a68 --- /dev/null +++ b/engine/vendor/doctrine/dbal/phpstan-baseline.neon @@ -0,0 +1,91 @@ +parameters: + ignoreErrors: + - + message: '#^Method Doctrine\\DBAL\\Driver\\IBMDB2\\Connection\:\:exec\(\) never returns numeric\-string so it can be removed from the return type\.$#' + identifier: return.unusedType + count: 1 + path: src/Driver/IBMDB2/Connection.php + + - + message: '#^Method Doctrine\\DBAL\\Driver\\OCI8\\Connection\:\:exec\(\) never returns numeric\-string so it can be removed from the return type\.$#' + identifier: return.unusedType + count: 1 + path: src/Driver/OCI8/Connection.php + + - + message: '#^Method Doctrine\\DBAL\\Driver\\OCI8\\Result\:\:fetchAllAssociative\(\) should return list\\> but returns array\\.$#' + identifier: return.type + count: 1 + path: src/Driver/OCI8/Result.php + + - + message: '#^Method Doctrine\\DBAL\\Driver\\OCI8\\Result\:\:fetchAllNumeric\(\) should return list\\> but returns array\\.$#' + identifier: return.type + count: 1 + path: src/Driver/OCI8/Result.php + + - + message: '#^Method Doctrine\\DBAL\\Driver\\PDO\\Result\:\:fetchAll\(\) should return list\ but returns array\.$#' + identifier: return.type + count: 1 + path: src/Driver/PDO/Result.php + + - + message: '#^Method Doctrine\\DBAL\\Driver\\PgSQL\\Result\:\:fetchAllAssociative\(\) should return list\\> but returns array\\>\.$#' + identifier: return.type + count: 1 + path: src/Driver/PgSQL/Result.php + + - + message: '#^Method Doctrine\\DBAL\\Driver\\PgSQL\\Result\:\:fetchAllNumeric\(\) should return list\\> but returns array\\>\.$#' + identifier: return.type + count: 1 + path: src/Driver/PgSQL/Result.php + + - + message: '#^Method Doctrine\\DBAL\\Driver\\PgSQL\\Result\:\:fetchFirstColumn\(\) should return list\ but returns array\\.$#' + identifier: return.type + count: 1 + path: src/Driver/PgSQL/Result.php + + - + message: '#^Method Doctrine\\DBAL\\Driver\\SQLite3\\Result\:\:fetchNumeric\(\) should return list\\|false but returns array\|false\.$#' + identifier: return.type + count: 1 + path: src/Driver/SQLite3/Result.php + + - + message: '#^Template type T is declared as covariant, but occurs in invariant position in property Doctrine\\DBAL\\Schema\\AbstractSchemaManager\:\:\$platform\.$#' + identifier: generics.variance + count: 1 + path: src/Schema/AbstractSchemaManager.php + + - + message: '#^Loose comparison via "\!\=" is not allowed\.$#' + identifier: notEqual.notAllowed + count: 1 + path: src/Schema/ColumnDiff.php + + - + message: '#^Method Doctrine\\DBAL\\Schema\\SQLiteSchemaManager\:\:addDetailsToTableForeignKeyColumns\(\) should return list\\> but returns array\, array\\>\.$#' + identifier: return.type + count: 1 + path: src/Schema/SQLiteSchemaManager.php + + - + message: '#^Offset string might not exist on array\{application_name\?\: string, charset\?\: string, dbname\?\: string, defaultTableOptions\?\: array\, driver\?\: ''ibm_db2''\|''mysqli''\|''oci8''\|''pdo_mysql''\|''pdo_oci''\|''pdo_pgsql''\|''pdo_sqlite''\|''pdo_sqlsrv''\|''pgsql''\|''sqlite3''\|''sqlsrv'', driverClass\?\: class\-string\, driverOptions\?\: array\, host\?\: string, \.\.\.\}\.$#' + identifier: offsetAccess.notFound + count: 1 + path: tests/DriverManagerTest.php + + - + message: '#^Call to new Doctrine\\DBAL\\Driver\\PgSQL\\Result\(\) on a separate line has no effect\.$#' + identifier: new.resultUnused + count: 1 + path: tests/Functional/Driver/PgSQL/ResultTest.php + + - + message: '#^Call to function array_filter\(\) requires parameter \#2 to be passed to avoid loose comparison semantics\.$#' + identifier: arrayFilter.strict + count: 1 + path: tests/Functional/Schema/MySQL/JsonCollationTest.php diff --git a/engine/vendor/doctrine/dbal/src/ArrayParameterType.php b/engine/vendor/doctrine/dbal/src/ArrayParameterType.php new file mode 100644 index 0000000..851d47d --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/ArrayParameterType.php @@ -0,0 +1,39 @@ + ParameterType::INTEGER, + self::STRING => ParameterType::STRING, + self::ASCII => ParameterType::ASCII, + self::BINARY => ParameterType::BINARY, + }; + } +} diff --git a/engine/vendor/doctrine/dbal/src/ArrayParameters/Exception.php b/engine/vendor/doctrine/dbal/src/ArrayParameters/Exception.php new file mode 100644 index 0000000..e5a580b --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/ArrayParameters/Exception.php @@ -0,0 +1,12 @@ + $columnNames The names of the result columns. Must be non-empty. + * @param list> $rows The rows of the result. Each row must have the same number of columns + * as the number of column names. + */ + public function __construct( + private readonly array $columnNames, + private array $rows, + ) { + } + + public function fetchNumeric(): array|false + { + return $this->fetch(); + } + + public function fetchAssociative(): array|false + { + $row = $this->fetch(); + + if ($row === false) { + return false; + } + + return array_combine($this->columnNames, $row); + } + + public function fetchOne(): mixed + { + $row = $this->fetch(); + + if ($row === false) { + return false; + } + + return $row[0]; + } + + /** + * {@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->rows); + } + + public function columnCount(): int + { + return count($this->columnNames); + } + + public function getColumnName(int $index): string + { + return $this->columnNames[$index] ?? throw InvalidColumnIndex::new($index); + } + + public function free(): void + { + $this->rows = []; + } + + /** @return array{list, list>} */ + public function __serialize(): array + { + return [$this->columnNames, $this->rows]; + } + + /** @param mixed[] $data */ + public function __unserialize(array $data): void + { + // Handle objects serialized with DBAL 4.1 and earlier. + if (isset($data["\0" . self::class . "\0data"])) { + /** @var list> $legacyData */ + $legacyData = $data["\0" . self::class . "\0data"]; + + $this->columnNames = array_keys($legacyData[0] ?? []); + $this->rows = array_map(array_values(...), $legacyData); + + return; + } + + [$this->columnNames, $this->rows] = $data; + } + + /** @return list|false */ + private function fetch(): array|false + { + if (! isset($this->rows[$this->num])) { + return false; + } + + return $this->rows[$this->num++]; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Cache/CacheException.php b/engine/vendor/doctrine/dbal/src/Cache/CacheException.php new file mode 100644 index 0000000..780a833 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Cache/CacheException.php @@ -0,0 +1,11 @@ +resultCache; + } + + 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 + * @phpstan-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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/ColumnCase.php b/engine/vendor/doctrine/dbal/src/ColumnCase.php new file mode 100644 index 0000000..687a04f --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/ColumnCase.php @@ -0,0 +1,21 @@ +schemaAssetsFilter = 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Connection.php b/engine/vendor/doctrine/dbal/src/Connection.php new file mode 100644 index 0000000..faaeed8 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Connection.php @@ -0,0 +1,1422 @@ +, + * WrapperParameterType>|array + * @phpstan-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 + * @phpstan-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. + * @phpstan-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 + * @phpstan-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 + * @phpstan-param WrapperParameterTypeArray $types + * + * @return array|false False is returned if no rows are found. + * + * @throws Exception + */ + public function fetchAssociative(string $query, array $params = [], array $types = []): 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 + * @phpstan-param WrapperParameterTypeArray $types + * + * @return list|false False is returned if no rows are found. + * + * @throws Exception + */ + public function fetchNumeric(string $query, array $params = [], array $types = []): 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 + * @phpstan-param WrapperParameterTypeArray $types + * + * @return mixed|false False is returned if no rows are found. + * + * @throws Exception + */ + public function fetchOne(string $query, array $params = [], array $types = []): mixed + { + return $this->executeQuery($query, $params, $types)->fetchOne(); + } + + /** + * Whether an actual connection to the database is established. + * + * @phpstan-assert-if-true !null $this->_conn + */ + 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 + * @phpstan-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 + * @phpstan-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 + * @phpstan-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 + * @phpstan-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 + * @phpstan-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 + * @phpstan-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 + * @phpstan-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 + * @phpstan-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 + * @phpstan-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 + * @phpstan-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 + * @phpstan-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 + * @phpstan-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]) && $value[$realKey] instanceof ArrayResult) { + return new Result(clone $value[$realKey], $this); + } + } else { + $value = []; + } + + $result = $this->executeQuery($sql, $params, $types); + + $columnNames = []; + for ($i = 0; $i < $result->columnCount(); $i++) { + $columnNames[] = $result->getColumnName($i); + } + + $rows = $result->fetchAllNumeric(); + + $value[$realKey] = new ArrayResult($columnNames, $rows); + + $item->set($value); + + $lifetime = $qcp->getLifetime(); + if ($lifetime > 0) { + $item->expiresAfter($lifetime); + } + + $resultCache->save($item); + + return new Result(clone $value[$realKey], $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 + * @phpstan-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(); + + $successful = false; + + try { + $res = $func($this); + + $successful = true; + } finally { + if (! $successful) { + $this->rollBack(); + } + } + + $shouldRollback = true; + try { + $this->commit(); + + $shouldRollback = false; + } catch (TheDriverException $t) { + $shouldRollback = ! ( + $t instanceof TransactionRolledBack + || $t instanceof UniqueConstraintViolationException + || $t instanceof ForeignKeyConstraintViolationException + || $t instanceof DeadlockException + ); + + throw $t; + } finally { + if ($shouldRollback) { + $this->rollBack(); + } + } + + return $res; + } + + /** + * 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(); + + try { + if ($this->transactionNestingLevel === 1) { + try { + $connection->commit(); + } catch (Driver\Exception $e) { + throw $this->convertException($e); + } + } else { + $this->releaseSavepoint($this->_getNestedTransactionSavePointName()); + } + } finally { + $this->updateTransactionStateAfterCommit(); + } + } + + private function updateTransactionStateAfterCommit(): void + { + if ($this->transactionNestingLevel !== 0) { + --$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 + * @phpstan-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 + * @phpstan-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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Connection/StaticServerVersionProvider.php b/engine/vendor/doctrine/dbal/src/Connection/StaticServerVersionProvider.php new file mode 100644 index 0000000..aebe086 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Connection/StaticServerVersionProvider.php @@ -0,0 +1,19 @@ +version; + } +} diff --git a/engine/vendor/doctrine/dbal/src/ConnectionException.php b/engine/vendor/doctrine/dbal/src/ConnectionException.php new file mode 100644 index 0000000..bb11a23 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/ConnectionException.php @@ -0,0 +1,9 @@ +executeQuery("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: + * + * @phpstan-import-type Params from DriverManager + * @phpstan-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 + * @phpstan-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 + * @phpstan-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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver.php b/engine/vendor/doctrine/dbal/src/Driver.php new file mode 100644 index 0000000..5f305fb --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver.php @@ -0,0 +1,48 @@ + $params All connection parameters. + * @phpstan-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; +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/API/ExceptionConverter.php b/engine/vendor/doctrine/dbal/src/Driver/API/ExceptionConverter.php new file mode 100644 index 0000000..a7bf271 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/API/ExceptionConverter.php @@ -0,0 +1,25 @@ +getCode()) { + -104 => new SyntaxErrorException($exception, $query), + -203 => new NonUniqueFieldNameException($exception, $query), + -204 => new TableNotFoundException($exception, $query), + -206 => new InvalidFieldNameException($exception, $query), + -407 => new NotNullConstraintViolationException($exception, $query), + -530, + -531, + -532, + -20356 => new ForeignKeyConstraintViolationException($exception, $query), + -601 => new TableExistsException($exception, $query), + -803 => new UniqueConstraintViolationException($exception, $query), + -1336, + -30082 => new ConnectionException($exception, $query), + default => new DriverException($exception, $query), + }; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php b/engine/vendor/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php new file mode 100644 index 0000000..ad0f0e1 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php @@ -0,0 +1,94 @@ +getCode()) { + 1008 => new DatabaseDoesNotExist($exception, $query), + 1213 => new DeadlockException($exception, $query), + 1205 => new LockWaitTimeoutException($exception, $query), + 1050 => new TableExistsException($exception, $query), + 1051, + 1146 => new TableNotFoundException($exception, $query), + 1216, + 1217, + 1451, + 1452, + 1701 => new ForeignKeyConstraintViolationException($exception, $query), + 1062, + 1557, + 1569, + 1586 => new UniqueConstraintViolationException($exception, $query), + 1054, + 1166, + 1611 => new InvalidFieldNameException($exception, $query), + 1052, + 1060, + 1110 => new NonUniqueFieldNameException($exception, $query), + 1064, + 1149, + 1287, + 1341, + 1342, + 1343, + 1344, + 1382, + 1479, + 1541, + 1554, + 1626 => new SyntaxErrorException($exception, $query), + 1044, + 1045, + 1046, + 1049, + 1095, + 1142, + 1143, + 1227, + 1370, + 1429, + 2002, + 2005, + 2054 => new ConnectionException($exception, $query), + 2006, + 4031 => new ConnectionLost($exception, $query), + 1048, + 1121, + 1138, + 1171, + 1252, + 1263, + 1364, + 1566 => new NotNullConstraintViolationException($exception, $query), + default => new DriverException($exception, $query), + }; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/API/OCI/ExceptionConverter.php b/engine/vendor/doctrine/dbal/src/Driver/API/OCI/ExceptionConverter.php new file mode 100644 index 0000000..9bde91e --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/API/OCI/ExceptionConverter.php @@ -0,0 +1,80 @@ +getCode()) { + 1, + 2299, + 38911 => new UniqueConstraintViolationException($exception, $query), + 904 => new InvalidFieldNameException($exception, $query), + 918, + 960 => new NonUniqueFieldNameException($exception, $query), + 923 => new SyntaxErrorException($exception, $query), + 942 => new TableNotFoundException($exception, $query), + 955 => new TableExistsException($exception, $query), + 1017, + 12545 => new ConnectionException($exception, $query), + 1400 => new NotNullConstraintViolationException($exception, $query), + 1918 => new DatabaseDoesNotExist($exception, $query), + 2091 => (function () use ($exception, $query) { + //SQLSTATE[HY000]: General error: 2091 OCITransCommit: ORA-02091: transaction rolled back + //ORA-00001: unique constraint (DOCTRINE.GH3423_UNIQUE) violated + $lines = explode("\n", $exception->getMessage(), 2); + assert(count($lines) >= 2); + + [, $causeError] = $lines; + + [$causeCode] = explode(': ', $causeError, 2); + $code = (int) str_replace('ORA-', '', $causeCode); + + $sqlState = $exception->getSQLState(); + if ($exception instanceof DriverPDOException) { + $why = $this->convert(new DriverPDOException($causeError, $sqlState, $code, $exception), $query); + } else { + $why = $this->convert(new Error($causeError, $sqlState, $code, $exception), $query); + } + + return new TransactionRolledBack($why, $query); + })(), + 2289, + 2443, + 4080 => new DatabaseObjectNotFoundException($exception, $query), + 2266, + 2291, + 2292 => new ForeignKeyConstraintViolationException($exception, $query), + default => new DriverException($exception, $query), + }; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php b/engine/vendor/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php new file mode 100644 index 0000000..54e4966 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php @@ -0,0 +1,82 @@ +getSQLState()) { + 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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php b/engine/vendor/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php new file mode 100644 index 0000000..561e58b --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php @@ -0,0 +1,49 @@ +getCode()) { + 102 => new SyntaxErrorException($exception, $query), + 207 => new InvalidFieldNameException($exception, $query), + 208 => new TableNotFoundException($exception, $query), + 209 => new NonUniqueFieldNameException($exception, $query), + 515 => new NotNullConstraintViolationException($exception, $query), + 547, + 4712 => new ForeignKeyConstraintViolationException($exception, $query), + 2601, + 2627 => new UniqueConstraintViolationException($exception, $query), + 2714 => new TableExistsException($exception, $query), + 3701, + 15151 => new DatabaseObjectNotFoundException($exception, $query), + 11001, + 18456 => new ConnectionException($exception, $query), + default => new DriverException($exception, $query), + }; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php b/engine/vendor/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php new file mode 100644 index 0000000..5885195 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php @@ -0,0 +1,85 @@ +getMessage(), '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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/AbstractDB2Driver.php b/engine/vendor/doctrine/dbal/src/Driver/AbstractDB2Driver.php new file mode 100644 index 0000000..9955a38 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/AbstractDB2Driver.php @@ -0,0 +1,27 @@ +sqlState; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/AbstractMySQLDriver.php b/engine/vendor/doctrine/dbal/src/Driver/AbstractMySQLDriver.php new file mode 100644 index 0000000..79be2f3 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/AbstractMySQLDriver.php @@ -0,0 +1,109 @@ +getServerVersion(); + if (stripos($version, 'mariadb') !== false) { + $mariaDbVersion = $this->getMariaDbMysqlVersionNumber($version); + if (version_compare($mariaDbVersion, '10.10.0', '>=')) { + return new MariaDB1010Platform(); + } + + if (version_compare($mariaDbVersion, '10.6.0', '>=')) { + return new MariaDB1060Platform(); + } + + if (version_compare($mariaDbVersion, '10.5.2', '>=')) { + return new MariaDB1052Platform(); + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6343', + 'Support for MariaDB < 10.5.2 is deprecated and will be removed in DBAL 5', + ); + + return new MariaDBPlatform(); + } + + if (version_compare($version, '8.4.0', '>=')) { + return new MySQL84Platform(); + } + + if (version_compare($version, '8.0.0', '>=')) { + return new MySQL80Platform(); + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6343', + 'Support for MySQL < 8 is deprecated and will be removed in DBAL 5', + ); + + 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, + ) !== 1 + ) { + throw InvalidPlatformVersion::new( + $versionString, + '^(?:5\.5\.5-)?(mariadb-)?..', + ); + } + + return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch']; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/AbstractOracleDriver.php b/engine/vendor/doctrine/dbal/src/Driver/AbstractOracleDriver.php new file mode 100644 index 0000000..cf56cfa --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/AbstractOracleDriver.php @@ -0,0 +1,38 @@ + $params The connection parameters to return the Easy Connect String for. + */ + protected function getEasyConnectString(array $params): string + { + return (string) EasyConnectString::fromConnectionParameters($params); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php b/engine/vendor/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php new file mode 100644 index 0000000..be85d5f --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php @@ -0,0 +1,112 @@ +string; + } + + /** + * 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' => $params['driverOptions']['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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/AbstractPostgreSQLDriver.php b/engine/vendor/doctrine/dbal/src/Driver/AbstractPostgreSQLDriver.php new file mode 100644 index 0000000..7b679c6 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/AbstractPostgreSQLDriver.php @@ -0,0 +1,57 @@ +getServerVersion(); + + if (preg_match('/^(?P\d+)(?:\.(?P\d+)(?:\.(?P\d+))?)?/', $version, $versionParts) !== 1) { + throw InvalidPlatformVersion::new( + $version, + '..', + ); + } + + $majorVersion = $versionParts['major']; + $minorVersion = $versionParts['minor'] ?? 0; + $patchVersion = $versionParts['patch'] ?? 0; + $version = $majorVersion . '.' . $minorVersion . '.' . $patchVersion; + + if (version_compare($version, '12.0', '>=')) { + return new PostgreSQL120Platform(); + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6495', + 'Support for Postgres < 12 is deprecated and will be removed in DBAL 5', + ); + + return new PostgreSQLPlatform(); + } + + public function getExceptionConverter(): ExceptionConverterInterface + { + return new ExceptionConverter(); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/AbstractSQLServerDriver.php b/engine/vendor/doctrine/dbal/src/Driver/AbstractSQLServerDriver.php new file mode 100644 index 0000000..8c2d012 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/AbstractSQLServerDriver.php @@ -0,0 +1,27 @@ +exec('PRAGMA foreign_keys=ON'); + + return $connection; + } + }; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Connection.php b/engine/vendor/doctrine/dbal/src/Driver/Connection.php new file mode 100644 index 0000000..68852e9 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Connection.php @@ -0,0 +1,93 @@ +fetchNumeric(); + + 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/Connection.php b/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/Connection.php new file mode 100644 index 0000000..2c8783b --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/Connection.php @@ -0,0 +1,131 @@ +connection); + 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php b/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php new file mode 100644 index 0000000..a1e5948 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php @@ -0,0 +1,80 @@ +string; + } + + /** + * 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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/Driver.php b/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/Driver.php new file mode 100644 index 0000000..f2f4ed7 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/Driver.php @@ -0,0 +1,41 @@ +toString(); + + $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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php b/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php new file mode 100644 index 0000000..ee0aaf1 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php @@ -0,0 +1,23 @@ +statement); + + 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 getColumnName(int $index): string + { + $name = db2_field_name($this->statement, $index); + + if ($name === false) { + throw InvalidColumnIndex::new($index); + } + + return $name; + } + + public function free(): void + { + db2_free_result($this->statement); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/Statement.php b/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/Statement.php new file mode 100644 index 0000000..96852da --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/IBMDB2/Statement.php @@ -0,0 +1,156 @@ + + */ + 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()); + } + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Middleware.php b/engine/vendor/doctrine/dbal/src/Driver/Middleware.php new file mode 100644 index 0000000..4629d9a --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Middleware.php @@ -0,0 +1,12 @@ +wrappedConnection->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(); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php b/engine/vendor/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php new file mode 100644 index 0000000..482f134 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php @@ -0,0 +1,39 @@ +wrappedDriver->connect($params); + } + + public function getDatabasePlatform(ServerVersionProvider $versionProvider): AbstractPlatform + { + return $this->wrappedDriver->getDatabasePlatform($versionProvider); + } + + public function getExceptionConverter(): ExceptionConverter + { + return $this->wrappedDriver->getExceptionConverter(); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php b/engine/vendor/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php new file mode 100644 index 0000000..f335c21 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php @@ -0,0 +1,85 @@ +wrappedResult->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 getColumnName(int $index): string + { + if (! method_exists($this->wrappedResult, 'getColumnName')) { + throw new LogicException(sprintf( + 'The driver result %s does not support accessing the column name.', + get_debug_type($this->wrappedResult), + )); + } + + return $this->wrappedResult->getColumnName($index); + } + + public function free(): void + { + $this->wrappedResult->free(); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php b/engine/vendor/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php new file mode 100644 index 0000000..6eaad50 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php @@ -0,0 +1,26 @@ +wrappedStatement->bindValue($param, $value, $type); + } + + public function execute(): Result + { + return $this->wrappedStatement->execute(); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Connection.php b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Connection.php new file mode 100644 index 0000000..cc00fb6 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Connection.php @@ -0,0 +1,112 @@ +connection->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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Driver.php b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Driver.php new file mode 100644 index 0000000..9855e56 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Driver.php @@ -0,0 +1,117 @@ +compilePreInitializers($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']); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionError.php b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionError.php new file mode 100644 index 0000000..ccbc6cb --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionError.php @@ -0,0 +1,26 @@ +error, $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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionFailed.php b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionFailed.php new file mode 100644 index 0000000..d34aafb --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionFailed.php @@ -0,0 +1,31 @@ +connect_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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Exception/FailedReadingStreamOffset.php b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Exception/FailedReadingStreamOffset.php new file mode 100644 index 0000000..f20d8bc --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Exception/FailedReadingStreamOffset.php @@ -0,0 +1,18 @@ +error), + $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, + ); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidOption.php b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidOption.php new file mode 100644 index 0000000..1f1f7aa --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidOption.php @@ -0,0 +1,20 @@ +error, $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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Initializer.php b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Initializer.php new file mode 100644 index 0000000..efab67e --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Initializer.php @@ -0,0 +1,14 @@ +set_charset($this->charset); + } catch (mysqli_sql_exception $e) { + throw InvalidCharset::upcast($e, $this->charset); + } + + if ($success) { + return; + } + + throw InvalidCharset::fromCharset($connection, $this->charset); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Initializer/Options.php b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Initializer/Options.php new file mode 100644 index 0000000..3223951 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Initializer/Options.php @@ -0,0 +1,28 @@ + $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); + } + } + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Initializer/Secure.php b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Initializer/Secure.php new file mode 100644 index 0000000..fa819b5 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Initializer/Secure.php @@ -0,0 +1,27 @@ +ssl_set($this->key, $this->cert, $this->ca, $this->capath, $this->cipher); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Result.php b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Result.php new file mode 100644 index 0000000..89f94f3 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Result.php @@ -0,0 +1,177 @@ + + */ + private readonly array $columnNames; + + /** @var mixed[] */ + private array $boundValues = []; + + /** + * @internal The result can be only instantiated by its driver connection or statement. + * + * @param Statement|null $statementReference Maintains a reference to the Statement that generated this result. This + * ensures that the lifetime of the Statement is managed in conjunction + * with its associated results, so they are destroyed together at the + * appropriate time, see {@see Statement::__destruct()}. + * + * @throws Exception + */ + public function __construct( + private readonly mysqli_stmt $statement, + private ?Statement $statementReference = null, // @phpstan-ignore property.onlyWritten + ) { + $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 getColumnName(int $index): string + { + return $this->columnNames[$index] ?? throw InvalidColumnIndex::new($index); + } + + public function free(): void + { + $this->statement->free_result(); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Statement.php b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Statement.php new file mode 100644 index 0000000..9a21517 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Mysqli/Statement.php @@ -0,0 +1,159 @@ +stmt->param_count; + $this->types = str_repeat(self::PARAMETER_TYPE_STRING, $paramCount); + $this->boundValues = array_fill(1, $paramCount, null); + } + + public function __destruct() + { + @$this->stmt->close(); + } + + 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, $this); + } + + /** + * 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, + }; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/OCI8/Connection.php b/engine/vendor/doctrine/dbal/src/Driver/OCI8/Connection.php new file mode 100644 index 0000000..64c210c --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/OCI8/Connection.php @@ -0,0 +1,125 @@ +parser = 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 + * @throws Error + */ + public function prepare(string $sql): Statement + { + $visitor = new ConvertPositionalToNamedPlaceholders(); + + $this->parser->parse($sql, $visitor); + + $statement = @oci_parse($this->connection, $visitor->getSQL()); + + if (! is_resource($statement)) { + throw Error::new($this->connection); + } + + return new Statement($this->connection, $statement, $visitor->getParameterMap(), $this->executionMode); + } + + /** + * @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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php b/engine/vendor/doctrine/dbal/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php new file mode 100644 index 0000000..5898a2c --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php @@ -0,0 +1,58 @@ +). + * + * 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/OCI8/Driver.php b/engine/vendor/doctrine/dbal/src/Driver/OCI8/Driver.php new file mode 100644 index 0000000..d519cc9 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/OCI8/Driver.php @@ -0,0 +1,58 @@ +getEasyConnectString($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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/OCI8/Exception/ConnectionFailed.php b/engine/vendor/doctrine/dbal/src/Driver/OCI8/Exception/ConnectionFailed.php new file mode 100644 index 0000000..691d1e3 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/OCI8/Exception/ConnectionFailed.php @@ -0,0 +1,22 @@ +isAutoCommitEnabled = true; + } + + public function disableAutoCommit(): void + { + $this->isAutoCommitEnabled = false; + } + + public function isAutoCommitEnabled(): bool + { + return $this->isAutoCommitEnabled; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/OCI8/Middleware/InitializeSession.php b/engine/vendor/doctrine/dbal/src/Driver/OCI8/Middleware/InitializeSession.php new file mode 100644 index 0000000..b825a1a --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/OCI8/Middleware/InitializeSession.php @@ -0,0 +1,40 @@ +exec( + 'ALTER SESSION SET' + . " NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'" + . " NLS_TIME_FORMAT = '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; + } + }; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/OCI8/Result.php b/engine/vendor/doctrine/dbal/src/Driver/OCI8/Result.php new file mode 100644 index 0000000..fd19e41 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/OCI8/Result.php @@ -0,0 +1,142 @@ +fetch(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 getColumnName(int $index): string + { + // OCI expects a 1-based index while DBAL works with a O-based index. + $name = @oci_field_name($this->statement, $index + 1); + + if ($name === false) { + throw InvalidColumnIndex::new($index); + } + + return $name; + } + + 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/OCI8/Statement.php b/engine/vendor/doctrine/dbal/src/Driver/OCI8/Statement.php new file mode 100644 index 0000000..408f0dd --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/OCI8/Statement.php @@ -0,0 +1,103 @@ + $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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PDO/Connection.php b/engine/vendor/doctrine/dbal/src/Driver/PDO/Connection.php new file mode 100644 index 0000000..b1faca1 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PDO/Connection.php @@ -0,0 +1,133 @@ +setAttribute(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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PDO/Exception.php b/engine/vendor/doctrine/dbal/src/Driver/PDO/Exception.php new file mode 100644 index 0000000..0c0d155 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PDO/Exception.php @@ -0,0 +1,26 @@ +errorInfo !== null) { + [$sqlState, $code] = $exception->errorInfo; + + $code ??= 0; + } else { + $code = $exception->getCode(); + $sqlState = null; + } + + return new self($exception->getMessage(), $sqlState, $code, $exception); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PDO/Exception/InvalidConfiguration.php b/engine/vendor/doctrine/dbal/src/Driver/PDO/Exception/InvalidConfiguration.php new file mode 100644 index 0000000..e9f0452 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PDO/Exception/InvalidConfiguration.php @@ -0,0 +1,22 @@ +doConnect( + $this->constructPdoDsn($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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PDO/OCI/Driver.php b/engine/vendor/doctrine/dbal/src/Driver/PDO/OCI/Driver.php new file mode 100644 index 0000000..49882b0 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PDO/OCI/Driver.php @@ -0,0 +1,73 @@ +doConnect( + $this->constructPdoDsn($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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PDO/PDOConnect.php b/engine/vendor/doctrine/dbal/src/Driver/PDO/PDOConnect.php new file mode 100644 index 0000000..742298d --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PDO/PDOConnect.php @@ -0,0 +1,27 @@ + $options */ + private function doConnect( + string $dsn, + string $username, + string $password, + array $options, + ): PDO { + if (PHP_VERSION_ID < 80400) { + return new PDO($dsn, $username, $password, $options); + } + + return PDO::connect($dsn, $username, $password, $options); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php b/engine/vendor/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php new file mode 100644 index 0000000..cdf411a --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php @@ -0,0 +1,125 @@ +doConnect( + $this->constructPdoDsn($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'] . ';'; + } + + if (isset($params['gssencmode'])) { + $dsn .= 'gssencmode=' . $params['gssencmode'] . ';'; + } + + return $dsn; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PDO/Result.php b/engine/vendor/doctrine/dbal/src/Driver/PDO/Result.php new file mode 100644 index 0000000..a191894 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PDO/Result.php @@ -0,0 +1,129 @@ +fetch(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 getColumnName(int $index): string + { + try { + $meta = $this->statement->getColumnMeta($index); + } catch (ValueError $exception) { + throw InvalidColumnIndex::new($index, $exception); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + + if ($meta === false) { + throw InvalidColumnIndex::new($index); + } + + return $meta['name']; + } + + public function free(): void + { + $this->statement->closeCursor(); + } + + /** + * @phpstan-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); + } + } + + /** + * @phpstan-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); + } + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php b/engine/vendor/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php new file mode 100644 index 0000000..78ba7f8 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php @@ -0,0 +1,29 @@ +connection->prepare($sql), + ); + } + + public function getNativeConnection(): PDO + { + return $this->connection->getNativeConnection(); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PDO/SQLSrv/Driver.php b/engine/vendor/doctrine/dbal/src/Driver/PDO/SQLSrv/Driver.php new file mode 100644 index 0000000..2d7490b --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PDO/SQLSrv/Driver.php @@ -0,0 +1,119 @@ + $value) { + if (is_int($option)) { + $driverOptions[$option] = $value; + } else { + $dsnOptions[$option] = $value; + } + } + } + + if (! empty($params['persistent'])) { + $driverOptions[PDO::ATTR_PERSISTENT] = true; + } + + foreach (['user', 'password'] as $key) { + if (isset($params[$key]) && ! is_string($params[$key])) { + throw InvalidConfiguration::notAStringOrNull($key, $params[$key]); + } + } + + $safeParams = $params; + unset($safeParams['password']); + + try { + $pdo = $this->doConnect( + $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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PDO/SQLSrv/Statement.php b/engine/vendor/doctrine/dbal/src/Driver/PDO/SQLSrv/Statement.php new file mode 100644 index 0000000..44cecc9 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PDO/SQLSrv/Statement.php @@ -0,0 +1,46 @@ +statement->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); + } + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PDO/SQLite/Driver.php b/engine/vendor/doctrine/dbal/src/Driver/PDO/SQLite/Driver.php new file mode 100644 index 0000000..fbd4187 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PDO/SQLite/Driver.php @@ -0,0 +1,65 @@ +doConnect( + $this->constructPdoDsn(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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PDO/Statement.php b/engine/vendor/doctrine/dbal/src/Driver/PDO/Statement.php new file mode 100644 index 0000000..a2694fc --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PDO/Statement.php @@ -0,0 +1,80 @@ +convertParamType($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 + * + * @phpstan-return PDO::PARAM_* + */ + private function convertParamType(ParameterType $type): int + { + return match ($type) { + ParameterType::NULL => PDO::PARAM_NULL, + ParameterType::INTEGER => PDO::PARAM_INT, + ParameterType::STRING, + ParameterType::ASCII => PDO::PARAM_STR, + ParameterType::BINARY, + ParameterType::LARGE_OBJECT => PDO::PARAM_LOB, + ParameterType::BOOLEAN => PDO::PARAM_BOOL, + }; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PgSQL/Connection.php b/engine/vendor/doctrine/dbal/src/Driver/PgSQL/Connection.php new file mode 100644 index 0000000..4f280b0 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PgSQL/Connection.php @@ -0,0 +1,129 @@ +parser = 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PgSQL/ConvertParameters.php b/engine/vendor/doctrine/dbal/src/Driver/PgSQL/ConvertParameters.php new file mode 100644 index 0000000..795f12d --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PgSQL/ConvertParameters.php @@ -0,0 +1,49 @@ + */ + 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PgSQL/Driver.php b/engine/vendor/doctrine/dbal/src/Driver/PgSQL/Driver.php new file mode 100644 index 0000000..0d5d605 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PgSQL/Driver.php @@ -0,0 +1,88 @@ +constructConnectionString($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, + 'gssencmode' => $params['gssencmode'] ?? 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), + )); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PgSQL/Exception.php b/engine/vendor/doctrine/dbal/src/Driver/PgSQL/Exception.php new file mode 100644 index 0000000..91f4e3d --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PgSQL/Exception.php @@ -0,0 +1,27 @@ +result = $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 getColumnName(int $index): string + { + if ($this->result === null) { + throw InvalidColumnIndex::new($index); + } + + try { + return pg_field_name($this->result, $index); + } catch (ValueError) { + throw InvalidColumnIndex::new($index); + } + } + + public function free(): void + { + if ($this->result === null) { + 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, + }; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/PgSQL/Statement.php b/engine/vendor/doctrine/dbal/src/Driver/PgSQL/Statement.php new file mode 100644 index 0000000..d01505d --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/PgSQL/Statement.php @@ -0,0 +1,91 @@ + */ + private array $parameters = []; + + /** @phpstan-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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Result.php b/engine/vendor/doctrine/dbal/src/Driver/Result.php new file mode 100644 index 0000000..c26e38c --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Result.php @@ -0,0 +1,95 @@ +|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; +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/SQLSrv/Connection.php b/engine/vendor/doctrine/dbal/src/Driver/SQLSrv/Connection.php new file mode 100644 index 0000000..71050f1 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/SQLSrv/Connection.php @@ -0,0 +1,108 @@ +connection); + + 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/SQLSrv/Driver.php b/engine/vendor/doctrine/dbal/src/Driver/SQLSrv/Driver.php new file mode 100644 index 0000000..c9c2c34 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/SQLSrv/Driver.php @@ -0,0 +1,73 @@ +fetch(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 getColumnName(int $index): string + { + $meta = sqlsrv_field_metadata($this->statement); + + if ($meta === false || ! isset($meta[$index])) { + throw InvalidColumnIndex::new($index); + } + + return $meta[$index]['Name']; + } + + public function free(): void + { + // emulate it by fetching and discarding rows, similarly to what PDO does in this case + // @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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/SQLSrv/Statement.php b/engine/vendor/doctrine/dbal/src/Driver/SQLSrv/Statement.php new file mode 100644 index 0000000..dc7827a --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/SQLSrv/Statement.php @@ -0,0 +1,140 @@ + + */ + 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Connection.php b/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Connection.php new file mode 100644 index 0000000..1e9af93 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Connection.php @@ -0,0 +1,109 @@ +connection->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']; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Driver.php b/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Driver.php new file mode 100644 index 0000000..e6996d3 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Driver.php @@ -0,0 +1,48 @@ +enableExceptions(true); + + return new Connection($connection); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Exception.php b/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Exception.php new file mode 100644 index 0000000..be1a225 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Exception.php @@ -0,0 +1,16 @@ +getMessage(), null, (int) $exception->getCode(), $exception); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Result.php b/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Result.php new file mode 100644 index 0000000..601c026 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Result.php @@ -0,0 +1,104 @@ +result = $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 getColumnName(int $index): string + { + if ($this->result === null) { + throw InvalidColumnIndex::new($index); + } + + $name = $this->result->columnName($index); + + if ($name === false) { + throw InvalidColumnIndex::new($index); + } + + return $name; + } + + public function free(): void + { + if ($this->result === null) { + return; + } + + $this->result->finalize(); + $this->result = null; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Statement.php b/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Statement.php new file mode 100644 index 0000000..eb55667 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/SQLite3/Statement.php @@ -0,0 +1,61 @@ +statement->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()); + } + + /** @phpstan-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, + }; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Driver/Statement.php b/engine/vendor/doctrine/dbal/src/Driver/Statement.php new file mode 100644 index 0000000..5f91b49 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Driver/Statement.php @@ -0,0 +1,39 @@ +, + * 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, + * } + * @phpstan-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. + * @phpstan-param Params $params + * + * @phpstan-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[] + * @phpstan-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(); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Exception.php b/engine/vendor/doctrine/dbal/src/Exception.php new file mode 100644 index 0000000..304fb6b --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Exception.php @@ -0,0 +1,11 @@ +getMessage(); + } 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Exception/DriverRequired.php b/engine/vendor/doctrine/dbal/src/Exception/DriverRequired.php new file mode 100644 index 0000000..883f233 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Exception/DriverRequired.php @@ -0,0 +1,29 @@ + */ + private array $convertedSQL = []; + + /** @var list */ + private array $convertedParameters = []; + + /** @var array,string|ParameterType|Type> */ + private array $convertedTypes = []; + + /** + * @param array|array $parameters + * @phpstan-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++; + } + } +} diff --git a/engine/vendor/doctrine/dbal/src/LockMode.php b/engine/vendor/doctrine/dbal/src/LockMode.php new file mode 100644 index 0000000..035353e --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/LockMode.php @@ -0,0 +1,16 @@ +logger->info('Disconnecting'); + } + + public function prepare(string $sql): DriverStatement + { + return new Statement( + parent::prepare($sql), + $this->logger, + $sql, + ); + } + + public function query(string $sql): Result + { + $this->logger->debug('Executing query: {sql}', ['sql' => $sql]); + + return parent::query($sql); + } + + public function exec(string $sql): int|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(); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Logging/Driver.php b/engine/vendor/doctrine/dbal/src/Logging/Driver.php new file mode 100644 index 0000000..35acd39 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Logging/Driver.php @@ -0,0 +1,50 @@ +logger->info('Connecting with parameters {params}', ['params' => $this->maskPassword($params)]); + + return new Connection( + parent::connect($params), + $this->logger, + ); + } + + /** + * @param array $params Connection parameters + * + * @return array + */ + private function maskPassword( + #[SensitiveParameter] + array $params, + ): array { + if (isset($params['password'])) { + $params['password'] = ''; + } + + return $params; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Logging/Middleware.php b/engine/vendor/doctrine/dbal/src/Logging/Middleware.php new file mode 100644 index 0000000..989e0ca --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Logging/Middleware.php @@ -0,0 +1,21 @@ +logger); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Logging/Statement.php b/engine/vendor/doctrine/dbal/src/Logging/Statement.php new file mode 100644 index 0000000..ed1ca7f --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Logging/Statement.php @@ -0,0 +1,48 @@ +|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(); + } +} diff --git a/engine/vendor/doctrine/dbal/src/ParameterType.php b/engine/vendor/doctrine/dbal/src/ParameterType.php new file mode 100644 index 0000000..19f577e --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/ParameterType.php @@ -0,0 +1,46 @@ + 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 + { + $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->getChangedColumns() 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); + } + + $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(), + addedColumns: $diff->getAddedColumns(), + changedColumns: $diff->getChangedColumns(), + droppedColumns: $diff->getDroppedColumns(), + addedIndexes: array_values($addedIndexes), + modifiedIndexes: array_values($modifiedIndexes), + droppedIndexes: $diff->getDroppedIndexes(), + renamedIndexes: $diff->getRenamedIndexes(), + addedForeignKeys: $diff->getAddedForeignKeys(), + modifiedForeignKeys: $diff->getModifiedForeignKeys(), + droppedForeignKeys: $diff->getDroppedForeignKeys(), + ); + } + + $tableSql = []; + + if (count($queryParts) > 0) { + $tableSql[] = 'ALTER TABLE ' . $diff->getOldTable()->getQuotedName($this) . ' ' + . implode(', ', $queryParts); + } + + return array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $tableSql, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); + } + + /** + * {@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 getSmallFloatDeclarationSQL(array $column): string + { + return 'FLOAT' . $this->getUnsignedDeclaration($column); + } + + /** + * {@inheritDoc} + */ + public function getDecimalTypeDeclarationSQL(array $column): string + { + return parent::getDecimalTypeDeclarationSQL($column) . $this->getUnsignedDeclaration($column); + } + + /** + * {@inheritDoc} + */ + public function getEnumDeclarationSQL(array $column): string + { + if (! isset($column['values']) || ! is_array($column['values']) || $column['values'] === []) { + throw ColumnValuesRequired::new($this, 'ENUM'); + } + + return sprintf('ENUM(%s)', implode(', ', array_map( + $this->quoteStringLiteral(...), + $column['values'], + ))); + } + + /** + * Get unsigned declaration for a column. + * + * @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, + 'enum' => Types::ENUM, + 'float' => Types::SMALLFLOAT, + '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; + } + + public function fetchTableOptionsByTable(bool $includeTableName): string + { + $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 = ?']; + + if ($includeTableName) { + $conditions[] = 't.TABLE_NAME = ?'; + } + + $conditions[] = "t.TABLE_TYPE = 'BASE TABLE'"; + + return $sql . ' WHERE ' . implode(' AND ', $conditions); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php b/engine/vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php new file mode 100644 index 0000000..ea32cd4 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/AbstractPlatform.php @@ -0,0 +1,2302 @@ +initializeDoctrineTypeMappings(); + + foreach (Type::getTypesMap() as $typeName => $className) { + foreach (Type::getType($typeName)->getMappedDatabaseTypes($this) as $dbType) { + $dbType = strtolower($dbType); + $this->doctrineTypeMapping[$dbType] = $typeName; + } + } + } + + /** + * Returns the SQL snippet used to declare a column that can + * store characters in the ASCII character set + * + * @param array $column The column definition. + */ + public function getAsciiStringTypeDeclarationSQL(array $column): string + { + return $this->getStringTypeDeclarationSQL($column); + } + + /** + * Returns the SQL snippet used to declare a string column type. + * + * @param array $column The column definition. + */ + public function getStringTypeDeclarationSQL(array $column): string + { + $length = $column['length'] ?? null; + + if (empty($column['fixed'])) { + try { + return $this->getVarcharTypeDeclarationSQLSnippet($length); + } catch (InvalidColumnType $e) { + throw InvalidColumnDeclaration::fromInvalidColumnType($column['name'], $e); + } + } + + return $this->getCharTypeDeclarationSQLSnippet($length); + } + + /** + * Returns the SQL snippet used to declare a binary string column type. + * + * @param array $column The column definition. + */ + public function getBinaryTypeDeclarationSQL(array $column): string + { + $length = $column['length'] ?? null; + + try { + if (empty($column['fixed'])) { + return $this->getVarbinaryTypeDeclarationSQLSnippet($length); + } + + return $this->getBinaryTypeDeclarationSQLSnippet($length); + } catch (InvalidColumnType $e) { + throw InvalidColumnDeclaration::fromInvalidColumnType($column['name'], $e); + } + } + + /** + * Returns the SQL snippet to declare an ENUM column. + * + * Enum is a non-standard type that is especially popular in MySQL and MariaDB. By default, this method map to + * a simple VARCHAR field which allows us to deploy it on any platform, e.g. SQLite. + * + * @param array $column + * + * @throws ColumnValuesRequired If the column definition does not contain any values. + */ + public function getEnumDeclarationSQL(array $column): string + { + if (! isset($column['values']) || ! is_array($column['values']) || $column['values'] === []) { + throw ColumnValuesRequired::new($this, 'ENUM'); + } + + $length = count($column['values']) > 1 + ? max(...array_map(mb_strlen(...), $column['values'])) + : mb_strlen($column['values'][key($column['values'])]); + + return $this->getStringTypeDeclarationSQL(['length' => $length]); + } + + /** + * 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'); + } + + public function createUnionSQLBuilder(): UnionSQLBuilder + { + return new DefaultUnionSQLBuilder($this); + } + + /** + * @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; + + continue; + } + + $options['primary'] = $index->getQuotedColumns($this); + $options['primary_index'] = $index; + } + + foreach ($table->getUniqueConstraints() as $uniqueConstraint) { + $options['uniqueConstraints'][] = $uniqueConstraint; + } + + if ($createForeignKeys) { + $options['foreignKeys'] = []; + + foreach ($table->getForeignKeys() as $fkConstraint) { + $options['foreignKeys'][] = $fkConstraint; + } + } + + $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 $sql; + } + + /** + * @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 list + */ + 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 $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 + { + $sql = 'ALTER TABLE ' . $tableName . ' ADD'; + + if ($constraint->getName() !== '') { + $sql .= ' CONSTRAINT ' . $constraint->getQuotedName($this); + } + + return $sql . ' 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), + ]; + } + + /** + * Returns the SQL for renaming a column + * + * @param string $tableName The table to rename the column on. + * @param string $oldColumnName The name of the column we want to rename. + * @param string $newColumnName The name we should rename it to. + * + * @return list The sequence of SQL statements for renaming the given column. + */ + protected function getRenameColumnSQL(string $tableName, string $oldColumnName, string $newColumnName): array + { + return [sprintf('ALTER TABLE %s RENAME COLUMN %s TO %s', $tableName, $oldColumnName, $newColumnName)]; + } + + /** + * 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 = []; + + if ($constraint->getName() !== '') { + $chunks[] = 'CONSTRAINT'; + $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'; + } + + /** @param mixed[] $column */ + public function getSmallFloatDeclarationSQL(array $column): string + { + return 'REAL'; + } + + /** + * 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(); + } + + /** + * Returns the union select query part surrounded by parenthesis if possible for platform. + */ + public function getUnionSelectPartSQL(string $subQuery): string + { + return sprintf('(%s)', $subQuery); + } + + /** + * Returns the `UNION ALL` keyword. + */ + public function getUnionAllSQL(): string + { + return 'UNION ALL'; + } + + /** + * Returns the compatible `UNION DISTINCT` keyword. + */ + public function getUnionDistinctSQL(): string + { + return 'UNION'; + } + + /** + * Creates the schema manager that can be used to inspect and change the underlying + * database schema according to the dialect of the platform. + */ + abstract public function createSchemaManager(Connection $connection): AbstractSchemaManager; +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/DB2Platform.php b/engine/vendor/doctrine/dbal/src/Platforms/DB2Platform.php new file mode 100644 index 0000000..b66bf1c --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/DB2Platform.php @@ -0,0 +1,603 @@ +doctrineTypeMapping = [ + 'bigint' => Types::BIGINT, + 'binary' => Types::BINARY, + 'blob' => Types::BLOB, + 'character' => Types::STRING, + 'clob' => Types::TEXT, + 'date' => Types::DATE_MUTABLE, + 'decimal' => Types::DECIMAL, + 'double' => Types::FLOAT, + 'integer' => Types::INTEGER, + 'real' => Types::SMALLFLOAT, + '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 = []; + $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, + ); + } + + $needsReorg = false; + foreach ($diff->getDroppedColumns() as $column) { + $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); + $needsReorg = true; + } + + foreach ($diff->getChangedColumns() as $columnDiff) { + if ($columnDiff->hasCommentChanged()) { + $newColumn = $columnDiff->getNewColumn(); + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $newColumn->getQuotedName($this), + $newColumn->getComment(), + ); + } + + $this->gatherAlterColumnSQL( + $tableNameSQL, + $columnDiff, + $sql, + $queryParts, + $needsReorg, + ); + } + + if (count($queryParts) > 0) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . implode(' ', $queryParts); + } + + // Some table alteration operations require a table reorganization. + if ($needsReorg) { + $sql[] = "CALL SYSPROC.ADMIN_CMD ('REORG TABLE " . $tableNameSQL . "')"; + } + + return array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $commentsSQL, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); + } + + 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, + bool &$needsReorg, + ): void { + $alterColumnClauses = $this->getAlterColumnClausesSQL($columnDiff, $needsReorg); + + if (count($alterColumnClauses) < 1) { + 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, bool &$needsReorg): array + { + $newColumn = $columnDiff->getNewColumn(); + $columnArray = $newColumn->toArray(); + + $newName = $columnDiff->getNewColumn()->getQuotedName($this); + $oldName = $columnDiff->getOldColumn()->getQuotedName($this); + + $alterClause = 'ALTER COLUMN ' . $newName; + + if ($newColumn->getColumnDefinition() !== null) { + $needsReorg = true; + + return [$alterClause . ' ' . $newColumn->getColumnDefinition()]; + } + + $clauses = []; + + if ($columnDiff->hasNameChanged()) { + $clauses[] = 'RENAME COLUMN ' . $oldName . ' TO ' . $newName; + } + + if ( + $columnDiff->hasTypeChanged() || + $columnDiff->hasLengthChanged() || + $columnDiff->hasPrecisionChanged() || + $columnDiff->hasScaleChanged() || + $columnDiff->hasFixedChanged() + ) { + $needsReorg = true; + $clauses[] = $alterClause . ' SET DATA TYPE ' . $newColumn->getType() + ->getSQLDeclaration($columnArray, $this); + } + + if ($columnDiff->hasNotNullChanged()) { + $needsReorg = true; + $clauses[] = $newColumn->getNotnull() ? $alterClause . ' SET NOT NULL' : $alterClause . ' DROP NOT NULL'; + } + + if ($columnDiff->hasDefaultChanged()) { + if ($newColumn->getDefault() !== null) { + $defaultClause = $this->getDefaultValueDeclarationSQL($columnArray); + + if ($defaultClause !== '') { + $needsReorg = true; + $clauses[] = $alterClause . ' SET' . $defaultClause; + } + } else { + $needsReorg = true; + $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 (isset($column['autoincrement']) && $column['autoincrement'] === true) { + return ''; + } + + if (isset($column['version']) && $column['version'] === true) { + if ($column['type'] instanceof DateTimeType) { + $column['default'] = '1'; + } + } + + return parent::getDefaultValueDeclarationSQL($column); + } + + public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string + { + return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)'; + } + + public function getCreateTemporaryTableSnippetSQL(): string + { + return 'DECLARE GLOBAL TEMPORARY TABLE'; + } + + public function getTemporaryTableName(string $tableName): string + { + return 'SESSION.' . $tableName; + } + + protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string + { + if ($offset > 0) { + $query .= sprintf(' OFFSET %d ROWS', $offset); + } + + if ($limit !== null) { + $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit); + } + + return $query; + } + + public function getLocateExpression(string $string, string $substring, ?string $start = null): string + { + if ($start === null) { + return sprintf('LOCATE(%s, %s)', $substring, $string); + } + + return sprintf('LOCATE(%s, %s, %s)', $substring, $string, $start); + } + + public function getSubstringExpression(string $string, string $start, ?string $length = null): string + { + if ($length === null) { + return sprintf('SUBSTR(%s, %s)', $string, $start); + } + + return sprintf('SUBSTR(%s, %s, %s)', $string, $start, $length); + } + + public function getLengthExpression(string $string): string + { + return 'LENGTH(' . $string . ', CODEUNITS32)'; + } + + public function getCurrentDatabaseExpression(): string + { + return 'CURRENT_USER'; + } + + public function supportsIdentityColumns(): bool + { + return true; + } + + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, 'WITH RR USE AND KEEP UPDATE LOCKS', null); + } + + public function getDummySelectSQL(string $expression = '1'): string + { + return sprintf('SELECT %s FROM sysibm.sysdummy1', $expression); + } + + /** + * {@inheritDoc} + * + * DB2 supports savepoints, but they work semantically different than on other vendor platforms. + * + * TODO: We have to investigate how to get DB2 up and running with savepoints. + */ + public function supportsSavepoints(): bool + { + return false; + } + + protected function createReservedKeywordsList(): KeywordList + { + return new DB2Keywords(); + } + + public function createSchemaManager(Connection $connection): DB2SchemaManager + { + return new DB2SchemaManager($connection, $this); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/DateIntervalUnit.php b/engine/vendor/doctrine/dbal/src/Platforms/DateIntervalUnit.php new file mode 100644 index 0000000..ba783f3 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/DateIntervalUnit.php @@ -0,0 +1,17 @@ +keywords === null) { + $this->initializeKeywords(); + } + + return isset($this->keywords[strtoupper($word)]); + } + + protected function initializeKeywords(): void + { + $this->keywords = array_flip(array_map('strtoupper', $this->getKeywords())); + } + + /** + * Returns the list of keywords. + * + * @return string[] + */ + abstract protected function getKeywords(): array; +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php b/engine/vendor/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php new file mode 100644 index 0000000..6857cd3 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php @@ -0,0 +1,264 @@ +getQuotedName($this)]; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/MariaDB1060Platform.php b/engine/vendor/doctrine/dbal/src/Platforms/MariaDB1060Platform.php new file mode 100644 index 0000000..028473d --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/MariaDB1060Platform.php @@ -0,0 +1,18 @@ +quoteStringLiteral($databaseName); + + // The check for `CONSTRAINT_SCHEMA = $databaseName` is mandatory here to prevent performance issues + return <<getOldTable()->getQuotedName($this); + + $modifiedForeignKeys = $diff->getModifiedForeignKeys(); + + foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) { + if (in_array($foreignKey, $modifiedForeignKeys, true)) { + continue; + } + + $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableName); + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff): array + { + return array_merge( + parent::getPostAlterTableIndexForeignKeySQL($diff), + $this->getPostAlterTableRenameIndexForeignKeySQL($diff), + ); + } + + /** @return list */ + private function getPostAlterTableRenameIndexForeignKeySQL(TableDiff $diff): array + { + $sql = []; + + $tableName = $diff->getOldTable()->getQuotedName($this); + + $modifiedForeignKeys = $diff->getModifiedForeignKeys(); + + foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) { + if (in_array($foreignKey, $modifiedForeignKeys, true)) { + continue; + } + + $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName); + } + + return $sql; + } + + /** + * Returns the remaining foreign key constraints that require one of the renamed indexes. + * + * "Remaining" here refers to the diff between the foreign keys currently defined in the associated + * table and the foreign keys to be removed. + * + * @param TableDiff $diff The table diff to evaluate. + * + * @return ForeignKeyConstraint[] + */ + private function getRemainingForeignKeyConstraintsRequiringRenamedIndexes(TableDiff $diff): array + { + $renamedIndexes = $diff->getRenamedIndexes(); + + if (count($renamedIndexes) === 0) { + return []; + } + + $foreignKeys = []; + + $remainingForeignKeys = array_diff_key( + $diff->getOldTable()->getForeignKeys(), + $diff->getDroppedForeignKeys(), + ); + + foreach ($remainingForeignKeys as $foreignKey) { + foreach ($renamedIndexes as $index) { + if ($foreignKey->intersectsIndexColumns($index)) { + $foreignKeys[] = $foreignKey; + + break; + } + } + } + + return $foreignKeys; + } + + /** {@inheritDoc} */ + public function getColumnDeclarationSQL(string $name, array $column): string + { + // MariaDb forces column collation to utf8mb4_bin where the column was declared as JSON so ignore + // collation and character set for json columns as attempting to set them can cause an error. + if ($this->getJsonTypeDeclarationSQL([]) === 'JSON' && ($column['type'] ?? null) instanceof JsonType) { + unset($column['collation']); + unset($column['charset']); + } + + return parent::getColumnDeclarationSQL($name, $column); + } + + protected function createReservedKeywordsList(): KeywordList + { + return new MariaDBKeywords(); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider.php b/engine/vendor/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider.php new file mode 100644 index 0000000..665543e --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider.php @@ -0,0 +1,11 @@ + */ + private array $cache = []; + + public function __construct(private readonly CharsetMetadataProvider $charsetMetadataProvider) + { + } + + public function getDefaultCharsetCollation(string $charset): ?string + { + if (array_key_exists($charset, $this->cache)) { + return $this->cache[$charset]; + } + + return $this->cache[$charset] = $this->charsetMetadataProvider->getDefaultCharsetCollation($charset); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php b/engine/vendor/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php new file mode 100644 index 0000000..65b63df --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/MySQL/CharsetMetadataProvider/ConnectionCharsetMetadataProvider.php @@ -0,0 +1,37 @@ +connection->fetchOne( + <<<'SQL' + SELECT DEFAULT_COLLATE_NAME + FROM information_schema.CHARACTER_SETS + WHERE CHARACTER_SET_NAME = ?; + SQL + , + [$charset], + ); + + if ($collation !== false) { + return $collation; + } + + return null; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php b/engine/vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php new file mode 100644 index 0000000..d52ca74 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php @@ -0,0 +1,11 @@ + */ + private array $cache = []; + + public function __construct(private readonly CollationMetadataProvider $collationMetadataProvider) + { + } + + public function getCollationCharset(string $collation): ?string + { + if (array_key_exists($collation, $this->cache)) { + return $this->cache[$collation]; + } + + return $this->cache[$collation] = $this->collationMetadataProvider->getCollationCharset($collation); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php b/engine/vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php new file mode 100644 index 0000000..fcd9995 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php @@ -0,0 +1,37 @@ +connection->fetchOne( + <<<'SQL' +SELECT CHARACTER_SET_NAME +FROM information_schema.COLLATIONS +WHERE COLLATION_NAME = ?; +SQL + , + [$collation], + ); + + if ($charset !== false) { + return $charset; + } + + return null; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/MySQL/Comparator.php b/engine/vendor/doctrine/dbal/src/Platforms/MySQL/Comparator.php new file mode 100644 index 0000000..ebe025d --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/MySQL/Comparator.php @@ -0,0 +1,93 @@ +normalizeTable($oldTable), + $this->normalizeTable($newTable), + ); + } + + private function normalizeTable(Table $table): Table + { + $charset = $table->getOption('charset'); + $collation = $table->getOption('collation'); + + if ($charset === null && $collation !== null) { + $charset = $this->collationMetadataProvider->getCollationCharset($collation); + } elseif ($charset !== null && $collation === null) { + $collation = $this->charsetMetadataProvider->getDefaultCharsetCollation($charset); + } elseif ($charset === null && $collation === null) { + $charset = $this->defaultTableOptions->getCharset(); + $collation = $this->defaultTableOptions->getCollation(); + } + + $tableOptions = [ + 'charset' => $charset, + 'collation' => $collation, + ]; + + $table = clone $table; + + foreach ($table->getColumns() as $column) { + $originalOptions = $column->getPlatformOptions(); + $normalizedOptions = $this->normalizeOptions($originalOptions); + + $overrideOptions = array_diff_assoc($normalizedOptions, $tableOptions); + + if ($overrideOptions === $originalOptions) { + continue; + } + + $column->setPlatformOptions($overrideOptions); + } + + return $table; + } + + /** + * @param array $options + * + * @return array + */ + private function normalizeOptions(array $options): array + { + if (isset($options['charset']) && ! isset($options['collation'])) { + $options['collation'] = $this->charsetMetadataProvider->getDefaultCharsetCollation($options['charset']); + } elseif (isset($options['collation']) && ! isset($options['charset'])) { + $options['charset'] = $this->collationMetadataProvider->getCollationCharset($options['collation']); + } + + return $options; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/MySQL/DefaultTableOptions.php b/engine/vendor/doctrine/dbal/src/Platforms/MySQL/DefaultTableOptions.php new file mode 100644 index 0000000..ede3ba2 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/MySQL/DefaultTableOptions.php @@ -0,0 +1,23 @@ +charset; + } + + public function getCollation(): string + { + return $this->collation; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/MySQL80Platform.php b/engine/vendor/doctrine/dbal/src/Platforms/MySQL80Platform.php new file mode 100644 index 0000000..f5517e9 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/MySQL80Platform.php @@ -0,0 +1,27 @@ +getQuotedName($this)]; + } + + protected function createReservedKeywordsList(): KeywordList + { + return new MySQLKeywords(); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/OraclePlatform.php b/engine/vendor/doctrine/dbal/src/Platforms/OraclePlatform.php new file mode 100644 index 0000000..692516e --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/OraclePlatform.php @@ -0,0 +1,805 @@ +multiplyInterval($interval, 3); + break; + + case DateIntervalUnit::YEAR: + $interval = $this->multiplyInterval($interval, 12); + break; + } + + return 'ADD_MONTHS(' . $date . ', ' . $operator . $interval . ')'; + + default: + $calculationClause = ''; + + switch ($unit) { + case DateIntervalUnit::SECOND: + $calculationClause = '/24/60/60'; + break; + + case DateIntervalUnit::MINUTE: + $calculationClause = '/24/60'; + break; + + case DateIntervalUnit::HOUR: + $calculationClause = '/24'; + break; + + case DateIntervalUnit::WEEK: + $calculationClause = '*7'; + break; + } + + return '(' . $date . $operator . $interval . $calculationClause . ')'; + } + } + + public function getDateDiffExpression(string $date1, string $date2): string + { + return sprintf('TRUNC(%s) - TRUNC(%s)', $date1, $date2); + } + + public function getBitAndComparisonExpression(string $value1, string $value2): string + { + return 'BITAND(' . $value1 . ', ' . $value2 . ')'; + } + + public function getCurrentDatabaseExpression(): string + { + return "SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')"; + } + + public function getBitOrComparisonExpression(string $value1, string $value2): string + { + return '(' . $value1 . '-' . + $this->getBitAndComparisonExpression($value1, $value2) + . '+' . $value2 . ')'; + } + + public function getCreatePrimaryKeySQL(Index $index, string $table): string + { + return 'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $index->getQuotedName($this) + . ' PRIMARY KEY (' . implode(', ', $index->getQuotedColumns($this)) . ')'; + } + + /** + * {@inheritDoc} + * + * Need to specifiy minvalue, since start with is hidden in the system and MINVALUE <= START WITH. + * Therefore we can use MINVALUE to be able to get a hint what START WITH was for later introspection + * in {@see listSequences()} + */ + public function getCreateSequenceSQL(Sequence $sequence): string + { + return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . + ' START WITH ' . $sequence->getInitialValue() . + ' MINVALUE ' . $sequence->getInitialValue() . + ' INCREMENT BY ' . $sequence->getAllocationSize() . + $this->getSequenceCacheSQL($sequence); + } + + public function getAlterSequenceSQL(Sequence $sequence): string + { + return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . + ' INCREMENT BY ' . $sequence->getAllocationSize() + . $this->getSequenceCacheSQL($sequence); + } + + /** + * Cache definition for sequences + */ + private function getSequenceCacheSQL(Sequence $sequence): string + { + if ($sequence->getCache() === 0) { + return ' NOCACHE'; + } + + if ($sequence->getCache() === 1) { + return ' NOCACHE'; + } + + if ($sequence->getCache() > 1) { + return ' CACHE ' . $sequence->getCache(); + } + + return ''; + } + + public function getSequenceNextValSQL(string $sequence): string + { + return 'SELECT ' . $sequence . '.nextval FROM DUAL'; + } + + public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string + { + return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + protected function _getTransactionIsolationLevelSQL(TransactionIsolationLevel $level): string + { + return match ($level) { + TransactionIsolationLevel::READ_UNCOMMITTED => 'READ UNCOMMITTED', + TransactionIsolationLevel::READ_COMMITTED => 'READ COMMITTED', + TransactionIsolationLevel::REPEATABLE_READ, + TransactionIsolationLevel::SERIALIZABLE => 'SERIALIZABLE', + }; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column): string + { + return 'NUMBER(1)'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column): string + { + return 'NUMBER(10)'; + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column): string + { + return 'NUMBER(20)'; + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column): string + { + return 'NUMBER(5)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column): string + { + return 'TIMESTAMP(0)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzTypeDeclarationSQL(array $column): string + { + return 'TIMESTAMP(0) WITH TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column): string + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column): string + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column): string + { + return ''; + } + + protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string + { + if ($length === null) { + throw ColumnLengthRequired::new($this, 'VARCHAR2'); + } + + return sprintf('VARCHAR2(%d)', $length); + } + + protected function getBinaryTypeDeclarationSQLSnippet(?int $length): string + { + if ($length === null) { + throw ColumnLengthRequired::new($this, 'RAW'); + } + + return sprintf('RAW(%d)', $length); + } + + protected function getVarbinaryTypeDeclarationSQLSnippet(?int $length): string + { + return $this->getBinaryTypeDeclarationSQLSnippet($length); + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column): string + { + return 'CLOB'; + } + + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListDatabasesSQL(): string + { + return 'SELECT username FROM all_users'; + } + + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListSequencesSQL(string $database): string + { + return 'SELECT SEQUENCE_NAME, MIN_VALUE, INCREMENT_BY FROM SYS.ALL_SEQUENCES WHERE SEQUENCE_OWNER = ' + . $this->quoteStringLiteral( + $this->normalizeIdentifier($database)->getName(), + ); + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array + { + $indexes = $options['indexes'] ?? []; + $options['indexes'] = []; + $sql = parent::_getCreateTableSQL($name, $columns, $options); + + foreach ($columns as $column) { + if (isset($column['sequence'])) { + $sql[] = $this->getCreateSequenceSQL($column['sequence']); + } + + if ( + ! isset($column['autoincrement']) || $column['autoincrement'] === false + ) { + 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 = []; + $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->getChangedColumns() as $columnDiff) { + $newColumn = $columnDiff->getNewColumn(); + $oldColumn = $columnDiff->getOldColumn(); + + // Column names in Oracle are case insensitive and automatically uppercased on the server. + if ($columnDiff->hasNameChanged()) { + $newColumnName = $newColumn->getQuotedName($this); + $oldColumnName = $oldColumn->getQuotedName($this); + + $sql = array_merge( + $sql, + $this->getRenameColumnSQL($tableNameSQL, $oldColumnName, $newColumnName), + ); + } + + $countChangedProperties = $columnDiff->countChangedProperties(); + // Do not generate column alteration clause if type is binary and only fixed property has changed. + // Oracle only supports binary type columns with variable length. + // Avoids unnecessary table alteration statements. + if ( + $newColumn->getType() instanceof BinaryType && + $columnDiff->hasFixedChanged() && + $countChangedProperties === 1 + ) { + continue; + } + + $columnHasChangedComment = $columnDiff->hasCommentChanged(); + + /** + * Do not add query part if only comment has changed + */ + if ($countChangedProperties > ($columnHasChangedComment ? 1 : 0)) { + $newColumnProperties = $newColumn->toArray(); + + $oldSQL = $this->getColumnDeclarationSQL('', $oldColumn->toArray()); + $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) . ')'; + } + + $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), + ); + } + + /** + * {@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, + 'real' => Types::SMALLFLOAT, + 'rowid' => Types::STRING, + 'timestamp' => Types::DATETIME_MUTABLE, + 'timestamptz' => Types::DATETIMETZ_MUTABLE, + 'urowid' => Types::STRING, + 'varchar' => Types::STRING, + 'varchar2' => Types::STRING, + ]; + } + + public function releaseSavePoint(string $savepoint): string + { + return ''; + } + + protected function createReservedKeywordsList(): KeywordList + { + return new OracleKeywords(); + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column): string + { + return 'BLOB'; + } + + public function createSchemaManager(Connection $connection): OracleSchemaManager + { + return new OracleSchemaManager($connection, $this); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/PostgreSQL120Platform.php b/engine/vendor/doctrine/dbal/src/Platforms/PostgreSQL120Platform.php new file mode 100644 index 0000000..3fff90e --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/PostgreSQL120Platform.php @@ -0,0 +1,32 @@ + [ + '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 = []; + + $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->getChangedColumns() as $columnDiff) { + $oldColumn = $columnDiff->getOldColumn(); + $newColumn = $columnDiff->getNewColumn(); + + $oldColumnName = $oldColumn->getQuotedName($this); + $newColumnName = $newColumn->getQuotedName($this); + + if ($columnDiff->hasNameChanged()) { + $sql = array_merge( + $sql, + $this->getRenameColumnSQL($tableNameSQL, $oldColumnName, $newColumnName), + ); + } + + if ( + $columnDiff->hasTypeChanged() + || $columnDiff->hasPrecisionChanged() + || $columnDiff->hasScaleChanged() + || $columnDiff->hasFixedChanged() + || $columnDiff->hasLengthChanged() + || $columnDiff->hasPlatformOptionsChanged() + ) { + $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 ' . $newColumnName . ' 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 ' . $newColumnName . $defaultClause; + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + if ($columnDiff->hasNotNullChanged()) { + $query = 'ALTER ' . $newColumnName . ' ' . ($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 ' . $newColumnName . ' ' . $query; + } + + if (! $columnDiff->hasCommentChanged()) { + continue; + } + + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $newColumn->getQuotedName($this), + $newColumn->getComment(), + ); + } + + return array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $commentsSQL, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); + } + + /** + * {@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"') { + if (str_ends_with($table, '"')) { + $constraintName = substr($table, 0, -1) . '_pkey"'; + } else { + $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; + } + + /** + * Get the snippet used to retrieve the default value for a given column + */ + public function getDefaultColumnValueSQLSnippet(): string + { + return <<<'SQL' + SELECT pg_get_expr(adbin, adrelid) + FROM pg_attrdef + WHERE c.oid = pg_attrdef.adrelid + AND pg_attrdef.adnum=a.attnum + 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::SMALLFLOAT, + '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::SMALLFLOAT, + 'smallint' => Types::SMALLINT, + 'text' => Types::TEXT, + 'time' => Types::TIME_MUTABLE, + 'timestamp' => Types::DATETIME_MUTABLE, + 'timestamptz' => Types::DATETIMETZ_MUTABLE, + 'timetz' => Types::TIME_MUTABLE, + 'tsvector' => Types::TEXT, + 'uuid' => Types::GUID, + 'varchar' => Types::STRING, + 'year' => Types::DATE_MUTABLE, + '_varchar' => Types::STRING, + ]; + } + + protected function createReservedKeywordsList(): KeywordList + { + return new PostgreSQLKeywords(); + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column): string + { + return 'BYTEA'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getDefaultValueDeclarationSQL(array $column): string + { + if (isset($column['autoincrement']) && $column['autoincrement'] === true) { + return ''; + } + + return parent::getDefaultValueDeclarationSQL($column); + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsColumnCollation(): bool + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getJsonTypeDeclarationSQL(array $column): string + { + if (! empty($column['jsonb'])) { + return 'JSONB'; + } + + return 'JSON'; + } + + public function createSchemaManager(Connection $connection): PostgreSQLSchemaManager + { + return new PostgreSQLSchemaManager($connection, $this); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/SQLServer/Comparator.php b/engine/vendor/doctrine/dbal/src/Platforms/SQLServer/Comparator.php new file mode 100644 index 0000000..aa8d9fb --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/SQLServer/Comparator.php @@ -0,0 +1,50 @@ +normalizeColumns($oldTable), + $this->normalizeColumns($newTable), + ); + } + + private function normalizeColumns(Table $table): Table + { + $table = clone $table; + + foreach ($table->getColumns() as $column) { + $options = $column->getPlatformOptions(); + + if (! isset($options['collation']) || $options['collation'] !== $this->databaseCollation) { + continue; + } + + unset($options['collation']); + $column->setPlatformOptions($options); + } + + return $table; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php b/engine/vendor/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php new file mode 100644 index 0000000..ea6bb60 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php @@ -0,0 +1,84 @@ +isDistinct()) { + $parts[] = 'DISTINCT'; + } + + $parts[] = implode(', ', $query->getColumns()); + + $from = $query->getFrom(); + + if (count($from) > 0) { + $parts[] = 'FROM ' . implode(', ', $from); + } + + $forUpdate = $query->getForUpdate(); + + if ($forUpdate !== null) { + $with = ['UPDLOCK', 'ROWLOCK']; + + if ($forUpdate->getConflictResolutionMode() === ConflictResolutionMode::SKIP_LOCKED) { + $with[] = 'READPAST'; + } + + $parts[] = 'WITH (' . implode(', ', $with) . ')'; + } + + $where = $query->getWhere(); + + if ($where !== null) { + $parts[] = 'WHERE ' . $where; + } + + $groupBy = $query->getGroupBy(); + + if (count($groupBy) > 0) { + $parts[] = 'GROUP BY ' . implode(', ', $groupBy); + } + + $having = $query->getHaving(); + + if ($having !== null) { + $parts[] = 'HAVING ' . $having; + } + + $orderBy = $query->getOrderBy(); + + if (count($orderBy) > 0) { + $parts[] = 'ORDER BY ' . implode(', ', $orderBy); + } + + $sql = implode(' ', $parts); + $limit = $query->getLimit(); + + if ($limit->isDefined()) { + $sql = $this->platform->modifyLimitQuery($sql, $limit->getMaxResults(), $limit->getFirstResult()); + } + + return $sql; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php b/engine/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php new file mode 100644 index 0000000..18324eb --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/SQLServerPlatform.php @@ -0,0 +1,1243 @@ +getConvertExpression('date', 'GETDATE()'); + } + + public function getCurrentTimeSQL(): string + { + return $this->getConvertExpression('time', 'GETDATE()'); + } + + /** + * Returns an expression that converts an expression of one data type to another. + * + * @param string $dataType The target native data type. Alias data types cannot be used. + * @param string $expression The SQL expression to convert. + */ + private function getConvertExpression(string $dataType, string $expression): string + { + return sprintf('CONVERT(%s, %s)', $dataType, $expression); + } + + protected function getDateArithmeticIntervalExpression( + string $date, + string $operator, + string $interval, + DateIntervalUnit $unit, + ): string { + $factorClause = ''; + + if ($operator === '-') { + $factorClause = '-1 * '; + } + + return 'DATEADD(' . $unit->value . ', ' . $factorClause . $interval . ', ' . $date . ')'; + } + + public function getDateDiffExpression(string $date1, string $date2): string + { + return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')'; + } + + /** + * {@inheritDoc} + * + * Microsoft SQL Server supports this through AUTO_INCREMENT columns. + */ + public function supportsIdentityColumns(): bool + { + return true; + } + + public function supportsReleaseSavepoints(): bool + { + return false; + } + + public function supportsSchemas(): bool + { + return true; + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function supportsColumnCollation(): bool + { + return true; + } + + public function supportsSequences(): bool + { + return true; + } + + public function getAlterSequenceSQL(Sequence $sequence): string + { + return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . + ' INCREMENT BY ' . $sequence->getAllocationSize(); + } + + public function getCreateSequenceSQL(Sequence $sequence): string + { + return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . + ' START WITH ' . $sequence->getInitialValue() . + ' INCREMENT BY ' . $sequence->getAllocationSize() . + ' MINVALUE ' . $sequence->getInitialValue(); + } + + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListSequencesSQL(string $database): string + { + return 'SELECT seq.name, + CAST( + seq.increment AS VARCHAR(MAX) + ) AS increment, -- CAST avoids driver error for sql_variant type + CAST( + seq.start_value AS VARCHAR(MAX) + ) AS start_value -- CAST avoids driver error for sql_variant type + FROM sys.sequences AS seq'; + } + + public function getSequenceNextValSQL(string $sequence): string + { + return 'SELECT NEXT VALUE FOR ' . $sequence; + } + + public function getDropForeignKeySQL(string $foreignKey, string $table): string + { + return $this->getDropConstraintSQL($foreignKey, $table); + } + + public function getDropIndexSQL(string $name, string $table): string + { + return 'DROP INDEX ' . $name . ' ON ' . $table; + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL(string $name, array $columns, array $options = []): array + { + $defaultConstraintsSql = []; + $commentsSql = []; + + $tableComment = $options['comment'] ?? null; + if ($tableComment !== null) { + $commentsSql[] = $this->getCommentOnTableSQL($name, $tableComment); + } + + // @todo does other code breaks because of this? + // force primary keys to be not null + foreach ($columns as &$column) { + if (! empty($column['primary'])) { + $column['notnull'] = true; + } + + // Build default constraints SQL statements. + if (isset($column['default'])) { + $defaultConstraintsSql[] = 'ALTER TABLE ' . $name . + ' ADD' . $this->getDefaultConstraintDeclarationSQL($column); + } + + if (empty($column['comment']) && ! is_numeric($column['comment'])) { + continue; + } + + $commentsSql[] = $this->getCreateColumnCommentSQL($name, $column['name'], $column['comment']); + } + + $columnListSql = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $definition) { + $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($definition); + } + } + + if (isset($options['primary']) && ! empty($options['primary'])) { + $flags = ''; + if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) { + $flags = ' NONCLUSTERED'; + } + + $columnListSql .= ', PRIMARY KEY' . $flags + . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')'; + } + + $query = 'CREATE TABLE ' . $name . ' (' . $columnListSql; + + $check = $this->getCheckDeclarationSQL($columns); + if (! empty($check)) { + $query .= ', ' . $check; + } + + $query .= ')'; + + $sql = [$query]; + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach ($options['indexes'] as $index) { + $sql[] = $this->getCreateIndexSQL($index, $name); + } + } + + if (isset($options['foreignKeys'])) { + foreach ($options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $name); + } + } + + return array_merge($sql, $commentsSql, $defaultConstraintsSql); + } + + public function getCreatePrimaryKeySQL(Index $index, string $table): string + { + $sql = 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY'; + + if ($index->hasFlag('nonclustered')) { + $sql .= ' NONCLUSTERED'; + } + + return $sql . ' (' . implode(', ', $index->getQuotedColumns($this)) . ')'; + } + + private function unquoteSingleIdentifier(string $possiblyQuotedName): string + { + return str_starts_with($possiblyQuotedName, '[') && str_ends_with($possiblyQuotedName, ']') + ? substr($possiblyQuotedName, 1, -1) + : $possiblyQuotedName; + } + + /** + * Returns the SQL statement for creating a column comment. + * + * SQL Server does not support native column comments, + * therefore the extended properties functionality is used + * as a workaround to store them. + * The property name used to store column comments is "MS_Description" + * which provides compatibility with SQL Server Management Studio, + * as column comments are stored in the same property there when + * specifying a column's "Description" attribute. + * + * @param string $tableName The quoted table name to which the column belongs. + * @param string $columnName The quoted column name to create the comment for. + * @param string $comment The column's comment. + */ + protected function getCreateColumnCommentSQL(string $tableName, string $columnName, string $comment): string + { + if (str_contains($tableName, '.')) { + [$schemaName, $tableName] = explode('.', $tableName); + } else { + $schemaName = 'dbo'; + } + + return $this->getAddExtendedPropertySQL( + 'MS_Description', + $comment, + 'SCHEMA', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($schemaName)), + 'TABLE', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), + 'COLUMN', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($columnName)), + ); + } + + /** + * Returns the SQL snippet for declaring a default constraint. + * + * @param mixed[] $column Column definition. + */ + protected function getDefaultConstraintDeclarationSQL(array $column): string + { + if (! isset($column['default'])) { + throw new InvalidArgumentException('Incomplete column definition. "default" required.'); + } + + $columnName = new Identifier($column['name']); + + return $this->getDefaultValueDeclarationSQL($column) . ' FOR ' . $columnName->getQuotedName($this); + } + + public function getCreateIndexSQL(Index $index, string $table): string + { + $constraint = parent::getCreateIndexSQL($index, $table); + + if ($index->isUnique() && ! $index->isPrimary()) { + $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index); + } + + return $constraint; + } + + protected function getCreateIndexSQLFlags(Index $index): string + { + $type = ''; + if ($index->isUnique()) { + $type .= 'UNIQUE '; + } + + if ($index->hasFlag('clustered')) { + $type .= 'CLUSTERED '; + } elseif ($index->hasFlag('nonclustered')) { + $type .= 'NONCLUSTERED '; + } + + return $type; + } + + /** + * Extend unique key constraint with required filters + */ + private function _appendUniqueConstraintDefinition(string $sql, Index $index): string + { + $fields = []; + + foreach ($index->getQuotedColumns($this) as $field) { + $fields[] = $field . ' IS NOT NULL'; + } + + return $sql . ' WHERE ' . implode(' AND ', $fields); + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff): array + { + $queryParts = []; + $sql = []; + $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); + } + + $tableNameSQL = $table->getQuotedName($this); + + foreach ($diff->getChangedColumns() as $columnDiff) { + $newColumn = $columnDiff->getNewColumn(); + $oldColumn = $columnDiff->getOldColumn(); + $nameChanged = $columnDiff->hasNameChanged(); + + if ($nameChanged) { + // sp_rename accepts the old name as a qualified name, so it should be quoted. + $oldColumnNameSQL = $oldColumn->getQuotedName($this); + + // sp_rename accepts the new name as a literal value, so it cannot be quoted. + $newColumnName = $newColumn->getName(); + + $sql = array_merge( + $sql, + $this->getRenameColumnSQL($tableNameSQL, $oldColumnNameSQL, $newColumnName), + ); + } + + $newComment = $newColumn->getComment(); + $hasNewComment = $newComment !== ''; + + $oldComment = $oldColumn->getComment(); + $hasOldComment = $oldComment !== ''; + + if ($hasOldComment && $hasNewComment && $oldComment !== $newComment) { + $commentsSql[] = $this->getAlterColumnCommentSQL( + $tableName, + $newColumn->getQuotedName($this), + $newComment, + ); + } elseif ($hasOldComment && ! $hasNewComment) { + $commentsSql[] = $this->getDropColumnCommentSQL( + $tableName, + $newColumn->getQuotedName($this), + ); + } elseif (! $hasOldComment && $hasNewComment) { + $commentsSql[] = $this->getCreateColumnCommentSQL( + $tableName, + $newColumn->getQuotedName($this), + $newComment, + ); + } + + $columnNameSQL = $newColumn->getQuotedName($this); + + $newDeclarationSQL = $this->getColumnDeclarationSQL($columnNameSQL, $newColumn->toArray()); + $oldDeclarationSQL = $this->getColumnDeclarationSQL($columnNameSQL, $oldColumn->toArray()); + $declarationSQLChanged = $newDeclarationSQL !== $oldDeclarationSQL; + + $defaultChanged = $columnDiff->hasDefaultChanged(); + + if (! $declarationSQLChanged && ! $defaultChanged && ! $nameChanged) { + 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); + } + + foreach ($queryParts as $query) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + return array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $commentsSql, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); + } + + public function getRenameTableSQL(string $oldName, string $newName): string + { + return $this->getRenameSQL($oldName, $newName); + } + + /** + * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement. + * + * @param string $tableName The name of the table to generate the clause for. + * @param Column $column The column to generate the clause for. + */ + private function getAlterTableAddDefaultConstraintClause(string $tableName, Column $column): string + { + $columnDef = $column->toArray(); + $columnDef['name'] = $column->getQuotedName($this); + + return 'ADD' . $this->getDefaultConstraintDeclarationSQL($columnDef); + } + + /** + * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement. + */ + private function getAlterTableDropDefaultConstraintClause(Column $column): string + { + if (! $column->hasPlatformOption(self::OPTION_DEFAULT_CONSTRAINT_NAME)) { + throw new InvalidArgumentException( + 'Column ' . $column->getName() . ' was not properly introspected as it has a default value' + . ' but does not have the default constraint name.', + ); + } + + return 'DROP CONSTRAINT ' . $this->quoteIdentifier( + $column->getPlatformOption(self::OPTION_DEFAULT_CONSTRAINT_NAME), + ); + } + + /** + * Checks whether a column alteration requires dropping its default constraint first. + * + * Different to other database vendors SQL Server implements column default values + * as constraints and therefore changes in a column's default value as well as changes + * in a column's type require dropping the default constraint first before being to + * alter the particular column to the new definition. + */ + private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff): bool + { + // We only need to drop an existing default constraint if we know the + // column was defined with a default value before. + if ($columnDiff->getOldColumn()->getDefault() === null) { + return false; + } + + // We need to drop an existing default constraint if the column was + // defined with a default value before and it has changed. + if ($columnDiff->hasDefaultChanged()) { + return true; + } + + // We need to drop an existing default constraint if the column was + // defined with a default value before and the native column type has changed. + return $columnDiff->hasTypeChanged() || $columnDiff->hasFixedChanged(); + } + + /** + * Returns the SQL statement for altering a column comment. + * + * SQL Server does not support native column comments, + * therefore the extended properties functionality is used + * as a workaround to store them. + * The property name used to store column comments is "MS_Description" + * which provides compatibility with SQL Server Management Studio, + * as column comments are stored in the same property there when + * specifying a column's "Description" attribute. + * + * @param string $tableName The quoted table name to which the column belongs. + * @param string $columnName The quoted column name to alter the comment for. + * @param string $comment The column's comment. + */ + protected function getAlterColumnCommentSQL(string $tableName, string $columnName, string $comment): string + { + if (str_contains($tableName, '.')) { + [$schemaName, $tableName] = explode('.', $tableName); + } else { + $schemaName = 'dbo'; + } + + return $this->getUpdateExtendedPropertySQL( + 'MS_Description', + $comment, + 'SCHEMA', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($schemaName)), + 'TABLE', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), + 'COLUMN', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($columnName)), + ); + } + + /** + * Returns the SQL statement for dropping a column comment. + * + * SQL Server does not support native column comments, + * therefore the extended properties functionality is used + * as a workaround to store them. + * The property name used to store column comments is "MS_Description" + * which provides compatibility with SQL Server Management Studio, + * as column comments are stored in the same property there when + * specifying a column's "Description" attribute. + * + * @param string $tableName The quoted table name to which the column belongs. + * @param string $columnName The quoted column name to drop the comment for. + */ + protected function getDropColumnCommentSQL(string $tableName, string $columnName): string + { + if (str_contains($tableName, '.')) { + [$schemaName, $tableName] = explode('.', $tableName); + } else { + $schemaName = 'dbo'; + } + + return $this->getDropExtendedPropertySQL( + 'MS_Description', + 'SCHEMA', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($schemaName)), + 'TABLE', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), + 'COLUMN', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($columnName)), + ); + } + + /** + * {@inheritDoc} + */ + protected function getRenameIndexSQL(string $oldIndexName, Index $index, string $tableName): array + { + return [$this->getRenameSQL($tableName . '.' . $oldIndexName, $index->getName(), 'INDEX')]; + } + + /** + * Returns the SQL for renaming a column + * + * @param string $tableName The table to rename the column on. + * @param string $oldColumnName The name of the column we want to rename. + * @param string $newColumnName The name we should rename it to. + * + * @return list The sequence of SQL statements for renaming the given column. + */ + protected function getRenameColumnSQL(string $tableName, string $oldColumnName, string $newColumnName): array + { + return [$this->getRenameSQL($tableName . '.' . $oldColumnName, $newColumnName)]; + } + + /** + * Returns the SQL statement that will execute sp_rename with the given arguments. + */ + private function getRenameSQL(string ...$arguments): string + { + return 'EXEC sp_rename ' + . implode(', ', array_map(function (string $argument): string { + return 'N' . $this->quoteStringLiteral($argument); + }, $arguments)); + } + + /** + * Returns the SQL statement for adding an extended property to a database object. + * + * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx + * + * @param string $name The name of the property to add. + * @param string|null $value The value of the property to add. + * @param string|null $level0Type The type of the object at level 0 the property belongs to. + * @param string|null $level0Name The name of the object at level 0 the property belongs to. + * @param string|null $level1Type The type of the object at level 1 the property belongs to. + * @param string|null $level1Name The name of the object at level 1 the property belongs to. + * @param string|null $level2Type The type of the object at level 2 the property belongs to. + * @param string|null $level2Name The name of the object at level 2 the property belongs to. + */ + protected function getAddExtendedPropertySQL( + string $name, + ?string $value = null, + ?string $level0Type = null, + ?string $level0Name = null, + ?string $level1Type = null, + ?string $level1Name = null, + ?string $level2Type = null, + ?string $level2Name = null, + ): string { + return 'EXEC sp_addextendedproperty ' . + 'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral($value ?? '') . ', ' . + 'N' . $this->quoteStringLiteral($level0Type ?? '') . ', ' . $level0Name . ', ' . + 'N' . $this->quoteStringLiteral($level1Type ?? '') . ', ' . $level1Name . + ($level2Type !== null || $level2Name !== null + ? ', N' . $this->quoteStringLiteral($level2Type ?? '') . ', ' . $level2Name + : '' + ); + } + + /** + * Returns the SQL statement for dropping an extended property from a database object. + * + * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx + * + * @param string $name The name of the property to drop. + * @param string|null $level0Type The type of the object at level 0 the property belongs to. + * @param string|null $level0Name The name of the object at level 0 the property belongs to. + * @param string|null $level1Type The type of the object at level 1 the property belongs to. + * @param string|null $level1Name The name of the object at level 1 the property belongs to. + * @param string|null $level2Type The type of the object at level 2 the property belongs to. + * @param string|null $level2Name The name of the object at level 2 the property belongs to. + */ + protected function getDropExtendedPropertySQL( + string $name, + ?string $level0Type = null, + ?string $level0Name = null, + ?string $level1Type = null, + ?string $level1Name = null, + ?string $level2Type = null, + ?string $level2Name = null, + ): string { + return 'EXEC sp_dropextendedproperty ' . + 'N' . $this->quoteStringLiteral($name) . ', ' . + 'N' . $this->quoteStringLiteral($level0Type ?? '') . ', ' . $level0Name . ', ' . + 'N' . $this->quoteStringLiteral($level1Type ?? '') . ', ' . $level1Name . + ($level2Type !== null || $level2Name !== null + ? ', N' . $this->quoteStringLiteral($level2Type ?? '') . ', ' . $level2Name + : '' + ); + } + + /** + * Returns the SQL statement for updating an extended property of a database object. + * + * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx + * + * @param string $name The name of the property to update. + * @param string|null $value The value of the property to update. + * @param string|null $level0Type The type of the object at level 0 the property belongs to. + * @param string|null $level0Name The name of the object at level 0 the property belongs to. + * @param string|null $level1Type The type of the object at level 1 the property belongs to. + * @param string|null $level1Name The name of the object at level 1 the property belongs to. + * @param string|null $level2Type The type of the object at level 2 the property belongs to. + * @param string|null $level2Name The name of the object at level 2 the property belongs to. + */ + protected function getUpdateExtendedPropertySQL( + string $name, + ?string $value = null, + ?string $level0Type = null, + ?string $level0Name = null, + ?string $level1Type = null, + ?string $level1Name = null, + ?string $level2Type = null, + ?string $level2Name = null, + ): string { + return 'EXEC sp_updateextendedproperty ' . + 'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral($value ?? '') . ', ' . + 'N' . $this->quoteStringLiteral($level0Type ?? '') . ', ' . $level0Name . ', ' . + 'N' . $this->quoteStringLiteral($level1Type ?? '') . ', ' . $level1Name . + ($level2Type !== null || $level2Name !== null + ? ', N' . $this->quoteStringLiteral($level2Type ?? '') . ', ' . $level2Name + : '' + ); + } + + public function getEmptyIdentityInsertSQL(string $quotedTableName, string $quotedIdentifierColumnName): string + { + return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES'; + } + + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListViewsSQL(string $database): string + { + return "SELECT name, definition FROM sysobjects + INNER JOIN sys.sql_modules ON sysobjects.id = sys.sql_modules.object_id + WHERE type = 'V' ORDER BY name"; + } + + public function getLocateExpression(string $string, string $substring, ?string $start = null): string + { + if ($start === null) { + return sprintf('CHARINDEX(%s, %s)', $substring, $string); + } + + return sprintf('CHARINDEX(%s, %s, %s)', $substring, $string, $start); + } + + public function getModExpression(string $dividend, string $divisor): string + { + return $dividend . ' % ' . $divisor; + } + + public function getTrimExpression( + string $str, + TrimMode $mode = TrimMode::UNSPECIFIED, + ?string $char = null, + ): string { + if ($char === null) { + return match ($mode) { + TrimMode::LEADING => 'LTRIM(' . $str . ')', + TrimMode::TRAILING => 'RTRIM(' . $str . ')', + default => 'LTRIM(RTRIM(' . $str . '))', + }; + } + + $pattern = "'%[^' + " . $char . " + ']%'"; + + if ($mode === TrimMode::LEADING) { + return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)'; + } + + if ($mode === TrimMode::TRAILING) { + return 'reverse(stuff(reverse(' . $str . '), 1, ' + . 'patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))'; + } + + return 'reverse(stuff(reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)), 1, ' + . 'patindex(' . $pattern . ', reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str + . ') - 1, null))) - 1, null))'; + } + + public function getConcatExpression(string ...$string): string + { + return sprintf('CONCAT(%s)', implode(', ', $string)); + } + + /** @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. */ + public function getListDatabasesSQL(): string + { + return 'SELECT * FROM sys.databases'; + } + + public function getSubstringExpression(string $string, string $start, ?string $length = null): string + { + if ($length === null) { + return sprintf('SUBSTRING(%s, %s, LEN(%s) - %s + 1)', $string, $start, $string, $start); + } + + return sprintf('SUBSTRING(%s, %s, %s)', $string, $start, $length); + } + + public function getLengthExpression(string $string): string + { + return 'LEN(' . $string . ')'; + } + + public function getCurrentDatabaseExpression(): string + { + return 'DB_NAME()'; + } + + public function getSetTransactionIsolationSQL(TransactionIsolationLevel $level): string + { + return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column): string + { + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column): string + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column): string + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getGuidTypeDeclarationSQL(array $column): string + { + return 'UNIQUEIDENTIFIER'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzTypeDeclarationSQL(array $column): string + { + return 'DATETIMEOFFSET(6)'; + } + + protected function getCharTypeDeclarationSQLSnippet(?int $length): string + { + $sql = 'NCHAR'; + + if ($length !== null) { + $sql .= sprintf('(%d)', $length); + } + + return $sql; + } + + protected function getVarcharTypeDeclarationSQLSnippet(?int $length): string + { + if ($length === null) { + throw ColumnLengthRequired::new($this, 'NVARCHAR'); + } + + return sprintf('NVARCHAR(%d)', $length); + } + + /** + * {@inheritDoc} + */ + public function getAsciiStringTypeDeclarationSQL(array $column): string + { + $length = $column['length'] ?? null; + + if (empty($column['fixed'])) { + return parent::getVarcharTypeDeclarationSQLSnippet($length); + } + + return parent::getCharTypeDeclarationSQLSnippet($length); + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column): string + { + return 'VARCHAR(MAX)'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column): string + { + return ! empty($column['autoincrement']) ? ' IDENTITY' : ''; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column): string + { + // 3 - microseconds precision length + // http://msdn.microsoft.com/en-us/library/ms187819.aspx + return 'DATETIME2(6)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column): string + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column): string + { + return 'TIME(0)'; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column): string + { + return 'BIT'; + } + + protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string + { + if ($limit === null && $offset <= 0) { + return $query; + } + + if ($this->shouldAddOrderBy($query)) { + if (preg_match('/^SELECT\s+DISTINCT/im', $query) > 0) { + // SQL Server won't let us order by a non-selected column in a DISTINCT query, + // so we have to do this madness. This says, order by the first column in the + // result. SQL Server's docs say that a nonordered query's result order is non- + // deterministic anyway, so this won't do anything that a bunch of update and + // deletes to the table wouldn't do anyway. + $query .= ' ORDER BY 1'; + } else { + // In another DBMS, we could do ORDER BY 0, but SQL Server gets angry if you + // use constant expressions in the order by list. + $query .= ' ORDER BY (SELECT 0)'; + } + } + + // This looks somewhat like MYSQL, but limit/offset are in inverse positions + // Supposedly SQL:2008 core standard. + // Per TSQL spec, FETCH NEXT n ROWS ONLY is not valid without OFFSET n ROWS. + $query .= sprintf(' OFFSET %d ROWS', $offset); + + if ($limit !== null) { + $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit); + } + + return $query; + } + + public function convertBooleans(mixed $item): mixed + { + if (is_array($item)) { + foreach ($item as $key => $value) { + if (! is_bool($value) && ! is_numeric($value)) { + continue; + } + + $item[$key] = (int) (bool) $value; + } + } elseif (is_bool($item) || is_numeric($item)) { + $item = (int) (bool) $item; + } + + return $item; + } + + public function getCreateTemporaryTableSnippetSQL(): string + { + return 'CREATE TABLE'; + } + + public function getTemporaryTableName(string $tableName): string + { + return '#' . $tableName; + } + + public function getDateTimeFormatString(): string + { + return 'Y-m-d H:i:s.u'; + } + + public function getDateFormatString(): string + { + return 'Y-m-d'; + } + + public function getTimeFormatString(): string + { + return 'H:i:s'; + } + + public function getDateTimeTzFormatString(): string + { + return 'Y-m-d H:i:s.u P'; + } + + protected function initializeDoctrineTypeMappings(): void + { + $this->doctrineTypeMapping = [ + 'bigint' => Types::BIGINT, + 'binary' => Types::BINARY, + 'bit' => Types::BOOLEAN, + 'blob' => Types::BLOB, + 'char' => Types::STRING, + 'date' => Types::DATE_MUTABLE, + 'datetime' => Types::DATETIME_MUTABLE, + 'datetime2' => Types::DATETIME_MUTABLE, + 'datetimeoffset' => Types::DATETIMETZ_MUTABLE, + 'decimal' => Types::DECIMAL, + 'double' => Types::FLOAT, + 'double precision' => Types::FLOAT, + 'float' => Types::FLOAT, + 'image' => Types::BLOB, + 'int' => Types::INTEGER, + 'money' => Types::INTEGER, + 'nchar' => Types::STRING, + 'ntext' => Types::TEXT, + 'numeric' => Types::DECIMAL, + 'nvarchar' => Types::STRING, + 'real' => Types::SMALLFLOAT, + 'smalldatetime' => Types::DATETIME_MUTABLE, + 'smallint' => Types::SMALLINT, + 'smallmoney' => Types::INTEGER, + 'sysname' => Types::STRING, + 'text' => Types::TEXT, + 'time' => Types::TIME_MUTABLE, + 'tinyint' => Types::SMALLINT, + 'uniqueidentifier' => Types::GUID, + 'varbinary' => Types::BINARY, + 'varchar' => Types::STRING, + 'xml' => Types::TEXT, + ]; + } + + public function createSavePoint(string $savepoint): string + { + return 'SAVE TRANSACTION ' . $savepoint; + } + + public function releaseSavePoint(string $savepoint): string + { + return ''; + } + + public function rollbackSavePoint(string $savepoint): string + { + return 'ROLLBACK TRANSACTION ' . $savepoint; + } + + /** @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. */ + public function getForeignKeyReferentialActionSQL(string $action): string + { + // RESTRICT is not supported, therefore falling back to NO ACTION. + if (strtoupper($action) === 'RESTRICT') { + return 'NO ACTION'; + } + + return parent::getForeignKeyReferentialActionSQL($action); + } + + public function appendLockHint(string $fromClause, LockMode $lockMode): string + { + return match ($lockMode) { + LockMode::NONE, + LockMode::OPTIMISTIC => $fromClause, + LockMode::PESSIMISTIC_READ => $fromClause . ' WITH (HOLDLOCK, ROWLOCK)', + LockMode::PESSIMISTIC_WRITE => $fromClause . ' WITH (UPDLOCK, ROWLOCK)', + }; + } + + protected function createReservedKeywordsList(): KeywordList + { + return new SQLServerKeywords(); + } + + public function quoteSingleIdentifier(string $str): string + { + return '[' . str_replace(']', ']]', $str) . ']'; + } + + public function getTruncateTableSQL(string $tableName, bool $cascade = false): string + { + $tableIdentifier = new Identifier($tableName); + + return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this); + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column): string + { + return 'VARBINARY(MAX)'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getColumnDeclarationSQL(string $name, array $column): string + { + if (isset($column['columnDefinition'])) { + $declaration = $column['columnDefinition']; + } else { + $collation = ! empty($column['collation']) ? + ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : ''; + + $notnull = ! empty($column['notnull']) ? ' NOT NULL' : ''; + + $typeDecl = $column['type']->getSQLDeclaration($column, $this); + $declaration = $typeDecl . $collation . $notnull; + } + + return $name . ' ' . $declaration; + } + + /** + * SQL Server does not support quoting collation identifiers. + */ + public function getColumnCollationDeclarationSQL(string $collation): string + { + return 'COLLATE ' . $collation; + } + + public function columnsEqual(Column $column1, Column $column2): bool + { + if (! parent::columnsEqual($column1, $column2)) { + return false; + } + + return $this->getDefaultValueDeclarationSQL($column1->toArray()) + === $this->getDefaultValueDeclarationSQL($column2->toArray()); + } + + protected function getLikeWildcardCharacters(): string + { + return parent::getLikeWildcardCharacters() . '[]^'; + } + + protected function getCommentOnTableSQL(string $tableName, string $comment): string + { + return $this->getAddExtendedPropertySQL( + 'MS_Description', + $comment, + 'SCHEMA', + $this->quoteStringLiteral('dbo'), + 'TABLE', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), + ); + } + + private function shouldAddOrderBy(string $query): bool + { + // Find the position of the last instance of ORDER BY and ensure it is not within a parenthetical statement + // but can be in a newline + $matches = []; + $matchesCount = preg_match_all('/[\\s]+order\\s+by\\s/im', $query, $matches, PREG_OFFSET_CAPTURE); + if ($matchesCount === 0) { + return true; + } + + // ORDER BY instance may be in a subquery after ORDER BY + // e.g. SELECT col1 FROM test ORDER BY (SELECT col2 from test ORDER BY col2) + // if in the searched query ORDER BY clause was found where + // number of open parentheses after the occurrence of the clause is equal to + // number of closed brackets after the occurrence of the clause, + // it means that ORDER BY is included in the query being checked + while ($matchesCount > 0) { + $orderByPos = $matches[0][--$matchesCount][1]; + $openBracketsCount = substr_count($query, '(', $orderByPos); + $closedBracketsCount = substr_count($query, ')', $orderByPos); + if ($openBracketsCount === $closedBracketsCount) { + return false; + } + } + + return true; + } + + public function createSchemaManager(Connection $connection): SQLServerSchemaManager + { + return new SQLServerSchemaManager($connection, $this); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/SQLite/Comparator.php b/engine/vendor/doctrine/dbal/src/Platforms/SQLite/Comparator.php new file mode 100644 index 0000000..f27e1b4 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/SQLite/Comparator.php @@ -0,0 +1,52 @@ +normalizeColumns($oldTable), + $this->normalizeColumns($newTable), + ); + } + + private function normalizeColumns(Table $table): Table + { + $table = clone $table; + + foreach ($table->getColumns() as $column) { + $options = $column->getPlatformOptions(); + + if (! isset($options['collation']) || strcasecmp($options['collation'], 'binary') !== 0) { + continue; + } + + unset($options['collation']); + $column->setPlatformOptions($options); + } + + return $table; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php b/engine/vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php new file mode 100644 index 0000000..7cc4d4b --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/SQLitePlatform.php @@ -0,0 +1,975 @@ + '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' => 'smallfloat', + 'serial' => 'integer', + 'smallint' => 'smallint', + 'string' => 'string', + 'text' => 'text', + 'time' => 'time', + 'timestamp' => 'datetime', + 'tinyint' => 'boolean', + 'tinytext' => 'text', + 'varchar' => 'string', + 'varchar2' => 'string', + ]; + } + + protected function createReservedKeywordsList(): KeywordList + { + return new SQLiteKeywords(); + } + + /** + * {@inheritDoc} + */ + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff): array + { + return []; + } + + /** + * {@inheritDoc} + */ + protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff): array + { + $table = $diff->getOldTable(); + + $sql = []; + + foreach ($this->getIndexesInAlteredTable($diff, $table) as $index) { + if ($index->isPrimary()) { + continue; + } + + $sql[] = $this->getCreateIndexSQL($index, $table->getQuotedName($this)); + } + + return $sql; + } + + protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string + { + if ($limit === null && $offset > 0) { + $limit = -1; + } + + return parent::doModifyLimitQuery($query, $limit, $offset); + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column): string + { + return 'BLOB'; + } + + public function getTemporaryTableName(string $tableName): string + { + return $tableName; + } + + /** + * {@inheritDoc} + */ + public function getCreateTablesSQL(array $tables): array + { + $sql = []; + + foreach ($tables as $table) { + $sql = array_merge($sql, $this->getCreateTableSQL($table)); + } + + return $sql; + } + + /** {@inheritDoc} */ + public function getCreateIndexSQL(Index $index, string $table): string + { + $name = $index->getQuotedName($this); + $columns = $index->getColumns(); + + if (count($columns) === 0) { + throw new InvalidArgumentException(sprintf( + 'Incomplete or invalid index definition %s on table %s', + $name, + $table, + )); + } + + if ($index->isPrimary()) { + return $this->getCreatePrimaryKeySQL($index, $table); + } + + if (strpos($table, '.') !== false) { + [$schema, $table] = explode('.', $table); + $name = $schema . '.' . $name; + } + + $query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table; + $query .= ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index); + + return $query; + } + + /** + * {@inheritDoc} + */ + public function getDropTablesSQL(array $tables): array + { + $sql = []; + + foreach ($tables as $table) { + $sql[] = $this->getDropTableSQL($table->getQuotedName($this)); + } + + return $sql; + } + + public function getCreatePrimaryKeySQL(Index $index, string $table): string + { + throw NotSupported::new(__METHOD__); + } + + public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, string $table): string + { + throw NotSupported::new(__METHOD__); + } + + public function getDropForeignKeySQL(string $foreignKey, string $table): string + { + throw NotSupported::new(__METHOD__); + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff): array + { + $sql = $this->getSimpleAlterTableSQL($diff); + if ($sql !== false) { + return $sql; + } + + $table = $diff->getOldTable(); + + $columns = []; + $oldColumnNames = []; + $newColumnNames = []; + + foreach ($table->getColumns() as $column) { + $columnName = strtolower($column->getName()); + $columns[$columnName] = $column; + $oldColumnNames[$columnName] = $newColumnNames[$columnName] = $column->getQuotedName($this); + } + + foreach ($diff->getDroppedColumns() as $column) { + $columnName = strtolower($column->getName()); + if (! isset($columns[$columnName])) { + continue; + } + + unset( + $columns[$columnName], + $oldColumnNames[$columnName], + $newColumnNames[$columnName], + ); + } + + foreach ($diff->getChangedColumns() as $columnDiff) { + $oldColumnName = strtolower($columnDiff->getOldColumn()->getName()); + $newColumn = $columnDiff->getNewColumn(); + + $columns = $this->replaceColumn( + $table->getName(), + $columns, + $oldColumnName, + $newColumn, + ); + + if (! isset($newColumnNames[$oldColumnName])) { + continue; + } + + $newColumnNames[$oldColumnName] = $newColumn->getQuotedName($this); + } + + foreach ($diff->getAddedColumns() as $column) { + $columns[strtolower($column->getName())] = $column; + } + + $tableName = $table->getName(); + $pos = strpos($tableName, '.'); + if ($pos !== false) { + $tableName = substr($tableName, $pos + 1); + } + + $dataTable = new Table('__temp__' . $tableName); + + $newTable = new Table( + $table->getQuotedName($this), + $columns, + $this->getPrimaryIndexInAlteredTable($diff, $table), + [], + $this->getForeignKeysInAlteredTable($diff, $table), + $table->getOptions(), + ); + + $newTable->addOption('alter', true); + + $sql = $this->getPreAlterTableIndexForeignKeySQL($diff); + + $sql[] = sprintf( + 'CREATE TEMPORARY TABLE %s AS SELECT %s FROM %s', + $dataTable->getQuotedName($this), + implode(', ', $oldColumnNames), + $table->getQuotedName($this), + ); + $sql[] = $this->getDropTableSQL($table->getQuotedName($this)); + + $sql = array_merge($sql, $this->getCreateTableSQL($newTable)); + $sql[] = sprintf( + 'INSERT INTO %s (%s) SELECT %s FROM %s', + $newTable->getQuotedName($this), + implode(', ', $newColumnNames), + implode(', ', $oldColumnNames), + $dataTable->getQuotedName($this), + ); + $sql[] = $this->getDropTableSQL($dataTable->getQuotedName($this)); + + return array_merge($sql, $this->getPostAlterTableIndexForeignKeySQL($diff)); + } + + /** + * 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->getChangedColumns()) > 0 + || count($diff->getDroppedColumns()) > 0 + || count($diff->getAddedIndexes()) > 0 + || count($diff->getModifiedIndexes()) > 0 + || count($diff->getDroppedIndexes()) > 0 + || count($diff->getRenamedIndexes()) > 0 + || count($diff->getAddedForeignKeys()) > 0 + || count($diff->getModifiedForeignKeys()) > 0 + || count($diff->getDroppedForeignKeys()) > 0 + ) { + return false; + } + + $table = $diff->getOldTable(); + + $sql = []; + + foreach ($diff->getAddedColumns() as $column) { + $definition = 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 $sql; + } + + /** @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->getChangedColumns() 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); + } + + /** + * Returns the union select query part surrounded by parenthesis if possible for platform. + */ + public function getUnionSelectPartSQL(string $subQuery): string + { + return $subQuery; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Platforms/TrimMode.php b/engine/vendor/doctrine/dbal/src/Platforms/TrimMode.php new file mode 100644 index 0000000..31c2375 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Platforms/TrimMode.php @@ -0,0 +1,13 @@ +converter, + ); + } + + public function query(string $sql): Result + { + return new Result( + parent::query($sql), + $this->converter, + ); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Portability/Converter.php b/engine/vendor/doctrine/dbal/src/Portability/Converter.php new file mode 100644 index 0000000..d26e85f --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Portability/Converter.php @@ -0,0 +1,294 @@ +createConvertValue($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); + + $this->convertColumnName = match ($case) { + null => static fn (string $name) => $name, + self::CASE_LOWER => strtolower(...), + self::CASE_UPPER => strtoupper(...), + }; + } + + /** + * @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); + } + + public function convertColumnName(string $name): string + { + return ($this->convertColumnName)($name); + } + + /** + * @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 + * @phpstan-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 + * + * @phpstan-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)); + }); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Portability/Driver.php b/engine/vendor/doctrine/dbal/src/Portability/Driver.php new file mode 100644 index 0000000..bb67c25 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Portability/Driver.php @@ -0,0 +1,69 @@ +getDatabasePlatform($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), + ); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Portability/Middleware.php b/engine/vendor/doctrine/dbal/src/Portability/Middleware.php new file mode 100644 index 0000000..b97897c --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Portability/Middleware.php @@ -0,0 +1,25 @@ +mode !== 0) { + return new Driver($driver, $this->mode, $this->case); + } + + return $driver; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Portability/OptimizeFlags.php b/engine/vendor/doctrine/dbal/src/Portability/OptimizeFlags.php new file mode 100644 index 0000000..c985d4b --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Portability/OptimizeFlags.php @@ -0,0 +1,42 @@ + + */ + 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Portability/Result.php b/engine/vendor/doctrine/dbal/src/Portability/Result.php new file mode 100644 index 0000000..bb1208f --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Portability/Result.php @@ -0,0 +1,75 @@ +converter->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(), + ); + } + + public function getColumnName(int $index): string + { + return $this->converter->convertColumnName( + parent::getColumnName($index), + ); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Portability/Statement.php b/engine/vendor/doctrine/dbal/src/Portability/Statement.php new file mode 100644 index 0000000..de0c76f --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Portability/Statement.php @@ -0,0 +1,31 @@ +Statement 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, + ); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Query.php b/engine/vendor/doctrine/dbal/src/Query.php new file mode 100644 index 0000000..5ea162b --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Query.php @@ -0,0 +1,41 @@ + $params + * @phpstan-param array $types + */ + 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; + } + + /** @phpstan-return array */ + public function getTypes(): array + { + return $this->types; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Query/Exception/NonUniqueAlias.php b/engine/vendor/doctrine/dbal/src/Query/Exception/NonUniqueAlias.php new file mode 100644 index 0000000..dbd7051 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Query/Exception/NonUniqueAlias.php @@ -0,0 +1,26 @@ + + */ + 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. + * + * @phpstan-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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php b/engine/vendor/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php new file mode 100644 index 0000000..14942b2 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php @@ -0,0 +1,244 @@ +'; + 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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Query/ForUpdate.php b/engine/vendor/doctrine/dbal/src/Query/ForUpdate.php new file mode 100644 index 0000000..24a853e --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Query/ForUpdate.php @@ -0,0 +1,21 @@ +conflictResolutionMode; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php b/engine/vendor/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php new file mode 100644 index 0000000..f45f774 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php @@ -0,0 +1,18 @@ +maxResults !== null || $this->firstResult !== 0; + } + + public function getMaxResults(): ?int + { + return $this->maxResults; + } + + public function getFirstResult(): int + { + return $this->firstResult; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Query/QueryBuilder.php b/engine/vendor/doctrine/dbal/src/Query/QueryBuilder.php new file mode 100644 index 0000000..d0700be --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Query/QueryBuilder.php @@ -0,0 +1,1559 @@ +|array + */ + private array $params = []; + + /** + * The parameter type map of this query. + * + * @phpstan-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 QueryBuilder for the union parts. + * + * @var Union[] + */ + private array $unionParts = []; + + /** + * 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(), + QueryType::UNION => $this->getSQLForUnion(), + }; + } + + /** + * 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 + * @phpstan-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. + * + * @phpstan-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 union parts to be used to build a UNION query. + * Replaces any previously specified parts. + * + * + * $qb = $conn->createQueryBuilder() + * ->union('SELECT 1 AS field1', 'SELECT 2 AS field1'); + * + * + * @return $this + */ + public function union(string|QueryBuilder $part): self + { + $this->type = QueryType::UNION; + + $this->unionParts = [new Union($part)]; + + $this->sql = null; + + return $this; + } + + /** + * Add parts to be used to build a UNION query. + * + * + * $qb = $conn->createQueryBuilder() + * ->union('SELECT 1 AS field1') + * ->addUnion('SELECT 2 AS field1', 'SELECT 3 AS field1') + * + * + * @return $this + */ + public function addUnion(string|QueryBuilder $part, UnionType $type = UnionType::DISTINCT): self + { + $this->type = QueryType::UNION; + + if (count($this->unionParts) === 0) { + throw new QueryException('No initial UNION part set, use union() to set one first.'); + } + + $this->unionParts[] = new Union($part, $type); + + $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; + } + + /** + * Converts this instance into a UNION string in SQL. + */ + private function getSQLForUnion(): string + { + $countUnions = count($this->unionParts); + if ($countUnions < 2) { + throw new QueryException( + 'Insufficient UNION parts give, need at least 2.' + . ' Please use union() and addUnion() to set enough UNION parts.', + ); + } + + return $this->connection->getDatabasePlatform() + ->createUnionSQLBuilder() + ->buildSQL( + new UnionQuery( + $this->unionParts, + $this->orderBy, + new Limit($this->maxResults, $this->firstResult), + ), + ); + } + + /** + * 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 which 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Query/QueryException.php b/engine/vendor/doctrine/dbal/src/Query/QueryException.php new file mode 100644 index 0000000..34b9527 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Query/QueryException.php @@ -0,0 +1,11 @@ +distinct; + } + + /** @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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Query/Union.php b/engine/vendor/doctrine/dbal/src/Query/Union.php new file mode 100644 index 0000000..4441924 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Query/Union.php @@ -0,0 +1,15 @@ +unionParts; + } + + /** @return string[] */ + public function getOrderBy(): array + { + return $this->orderBy; + } + + public function getLimit(): Limit + { + return $this->limit; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Query/UnionType.php b/engine/vendor/doctrine/dbal/src/Query/UnionType.php new file mode 100644 index 0000000..e7c0df6 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Query/UnionType.php @@ -0,0 +1,11 @@ +|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); + } + } + + /** + * Returns the name of the column in the result set for the given 0-based index. + * + * If the index is not a valid column index ({@see columnCount}), an exception will be thrown. + * + * @throws Exception + */ + public function getColumnName(int $index): string + { + if (! method_exists($this->result, 'getColumnName')) { + throw new LogicException(sprintf( + 'The driver result %s does not support accessing the column name.', + get_debug_type($this->result), + )); + } + + try { + return $this->result->getColumnName($index); + } 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); + } + } +} diff --git a/engine/vendor/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php b/engine/vendor/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php new file mode 100644 index 0000000..598a6ad --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php @@ -0,0 +1,73 @@ + */ + public function buildSQL(Schema $schema): array + { + return array_merge( + $this->buildNamespaceStatements($schema->getNamespaces()), + $this->buildSequenceStatements($schema->getSequences()), + $this->buildTableStatements($schema->getTables()), + ); + } + + /** + * @param string[] $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 Table[] $tables + * + * @return list + */ + private function buildTableStatements(array $tables): array + { + return $this->platform->getCreateTablesSQL($tables); + } + + /** + * @param Sequence[] $sequences + * + * @return list + */ + private function buildSequenceStatements(array $sequences): array + { + $statements = []; + + foreach ($sequences as $sequence) { + $statements[] = $this->platform->getCreateSequenceSQL($sequence); + } + + return $statements; + } +} diff --git a/engine/vendor/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php b/engine/vendor/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php new file mode 100644 index 0000000..a30120e --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php @@ -0,0 +1,94 @@ +isDistinct()) { + $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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/SQL/Builder/DefaultUnionSQLBuilder.php b/engine/vendor/doctrine/dbal/src/SQL/Builder/DefaultUnionSQLBuilder.php new file mode 100644 index 0000000..289382d --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/SQL/Builder/DefaultUnionSQLBuilder.php @@ -0,0 +1,48 @@ +getUnionParts() as $union) { + if ($union->type !== null) { + $parts[] = $union->type === UnionType::ALL + ? $this->platform->getUnionAllSQL() + : $this->platform->getUnionDistinctSQL(); + } + + $parts[] = $this->platform->getUnionSelectPartSQL((string) $union->query); + } + + $orderBy = $query->getOrderBy(); + if (count($orderBy) > 0) { + $parts[] = 'ORDER BY ' . implode(', ', $orderBy); + } + + $sql = implode(' ', $parts); + $limit = $query->getLimit(); + + if ($limit->isDefined()) { + $sql = $this->platform->modifyLimitQuery($sql, $limit->getMaxResults(), $limit->getFirstResult()); + } + + return $sql; + } +} diff --git a/engine/vendor/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php b/engine/vendor/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php new file mode 100644 index 0000000..c038489 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php @@ -0,0 +1,54 @@ + */ + 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php b/engine/vendor/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php new file mode 100644 index 0000000..c013f96 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php @@ -0,0 +1,14 @@ +getMySQLStringLiteralPattern("'"), + $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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/SQL/Parser/Exception.php b/engine/vendor/doctrine/dbal/src/SQL/Parser/Exception.php new file mode 100644 index 0000000..0c14b35 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/SQL/Parser/Exception.php @@ -0,0 +1,11 @@ + Table($tableName)); if you want to rename the table, you have to make sure this does not get + * recreated during schema migration. + */ +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)); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/AbstractSchemaManager.php b/engine/vendor/doctrine/dbal/src/Schema/AbstractSchemaManager.php new file mode 100644 index 0000000..9730797 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/AbstractSchemaManager.php @@ -0,0 +1,864 @@ + + * + * @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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/Column.php b/engine/vendor/doctrine/dbal/src/Schema/Column.php new file mode 100644 index 0000000..2d02403 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/Column.php @@ -0,0 +1,274 @@ + */ + protected array $_values = []; + + /** @var array */ + 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; + } + + /** + * @param list $values + * + * @return $this + */ + public function setValues(array $values): static + { + $this->_values = $values; + + return $this; + } + + /** @return list */ + public function getValues(): array + { + return $this->_values; + } + + /** @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, + 'values' => $this->_values, + ], $this->_platformOptions); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/ColumnDiff.php b/engine/vendor/doctrine/dbal/src/Schema/ColumnDiff.php new file mode 100644 index 0000000..230bba3 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/ColumnDiff.php @@ -0,0 +1,139 @@ +hasUnsignedChanged() + + (int) $this->hasAutoIncrementChanged() + + (int) $this->hasDefaultChanged() + + (int) $this->hasFixedChanged() + + (int) $this->hasPrecisionChanged() + + (int) $this->hasScaleChanged() + + (int) $this->hasLengthChanged() + + (int) $this->hasNotNullChanged() + + (int) $this->hasNameChanged() + + (int) $this->hasTypeChanged() + + (int) $this->hasPlatformOptionsChanged() + + (int) $this->hasCommentChanged(); + } + + public function getOldColumn(): Column + { + return $this->oldColumn; + } + + public function getNewColumn(): Column + { + return $this->newColumn; + } + + public function hasNameChanged(): bool + { + $oldColumn = $this->getOldColumn(); + + // Column names are case insensitive + return strcasecmp($oldColumn->getName(), $this->getNewColumn()->getName()) !== 0; + } + + public function hasTypeChanged(): bool + { + return $this->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(); + }); + } + + public function hasPlatformOptionsChanged(): bool + { + return $this->hasPropertyChanged(static function (Column $column): array { + return $column->getPlatformOptions(); + }); + } + + private function hasPropertyChanged(callable $property): bool + { + return $property($this->newColumn) !== $property($this->oldColumn); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/Comparator.php b/engine/vendor/doctrine/dbal/src/Schema/Comparator.php new file mode 100644 index 0000000..240df89 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/Comparator.php @@ -0,0 +1,435 @@ +getNamespaces() 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[$oldColumnName] = new ColumnDiff($oldColumn, $newColumn); + } + + $renamedColumnNames = $newTable->getRenamedColumns(); + + foreach ($addedColumns as $addedColumnName => $addedColumn) { + if (! isset($renamedColumnNames[$addedColumn->getName()])) { + continue; + } + + $removedColumnName = strtolower($renamedColumnNames[$addedColumn->getName()]); + // Explicitly renamed columns need to be diffed, because their types can also have changed + $modifiedColumns[$removedColumnName] = new ColumnDiff( + $droppedColumns[$removedColumnName], + $addedColumn, + ); + + unset( + $addedColumns[$addedColumnName], + $droppedColumns[$removedColumnName], + ); + } + + $this->detectRenamedColumns($modifiedColumns, $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: $addedColumns, + changedColumns: $modifiedColumns, + droppedColumns: $droppedColumns, + addedIndexes: $addedIndexes, + modifiedIndexes: $modifiedIndexes, + droppedIndexes: $droppedIndexes, + renamedIndexes: $renamedIndexes, + addedForeignKeys: $addedForeignKeys, + modifiedForeignKeys: $modifiedForeignKeys, + droppedForeignKeys: $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 $modifiedColumns + * @param array $addedColumns + * @param array $removedColumns + */ + private function detectRenamedColumns(array &$modifiedColumns, array &$addedColumns, array &$removedColumns): void + { + /** @var array>> $candidatesByName */ + $candidatesByName = []; + + foreach ($addedColumns as $addedColumnName => $addedColumn) { + foreach ($removedColumns as $removedColumn) { + if (! $this->columnsEqual($addedColumn, $removedColumn)) { + continue; + } + + $candidatesByName[$addedColumnName][] = [$removedColumn, $addedColumn]; + } + } + + foreach ($candidatesByName as $addedColumnName => $candidates) { + if (count($candidates) !== 1) { + continue; + } + + [$oldColumn, $newColumn] = $candidates[0]; + $oldColumnName = strtolower($oldColumn->getName()); + + if (isset($modifiedColumns[$oldColumnName])) { + continue; + } + + $modifiedColumns[$oldColumnName] = new ColumnDiff( + $oldColumn, + $newColumn, + ); + + unset( + $addedColumns[$addedColumnName], + $removedColumns[$oldColumnName], + ); + } + } + + /** + * 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)); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/DB2SchemaManager.php b/engine/vendor/doctrine/dbal/src/Schema/DB2SchemaManager.php new file mode 100644 index 0000000..f2ed089 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/DB2SchemaManager.php @@ -0,0 +1,371 @@ + + */ +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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php b/engine/vendor/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php new file mode 100644 index 0000000..efba87f --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php @@ -0,0 +1,20 @@ +getDatabasePlatform()->createSchemaManager($connection); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php b/engine/vendor/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php new file mode 100644 index 0000000..c9bc82b --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php @@ -0,0 +1,18 @@ + + */ + 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/Identifier.php b/engine/vendor/doctrine/dbal/src/Schema/Identifier.php new file mode 100644 index 0000000..c3c84a7 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/Identifier.php @@ -0,0 +1,29 @@ +_setName($identifier); + + if (! $quote || $this->_quoted) { + return; + } + + $this->_setName('"' . $this->getName() . '"'); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/Index.php b/engine/vendor/doctrine/dbal/src/Schema/Index.php new file mode 100644 index 0000000..1755654 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/Index.php @@ -0,0 +1,314 @@ + + */ + 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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php b/engine/vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php new file mode 100644 index 0000000..becb681 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php @@ -0,0 +1,545 @@ + + */ +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); + + $values = []; + + 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; + + case 'enum': + $values = $this->parseEnumExpression($tableColumn['type']); + 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'), + 'values' => $values, + ]; + + 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 list */ + private function parseEnumExpression(string $expression): array + { + $result = preg_match_all("/'([^']*(?:''[^']*)*)'/", $expression, $matches); + assert($result !== false); + + return array_map( + static fn (string $match): string => strtr($match, ["''" => "'"]), + $matches[1], + ); + } + + /** + * Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers. + * + * - Since MariaDb 10.2.7 column defaults stored in information_schema are 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 = $this->platform->fetchTableOptionsByTable($tableName !== null); + + $params = [$databaseName]; + if ($tableName !== null) { + $params[] = $tableName; + } + + /** @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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/OracleSchemaManager.php b/engine/vendor/doctrine/dbal/src/Schema/OracleSchemaManager.php new file mode 100644 index 0000000..25123e5 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/OracleSchemaManager.php @@ -0,0 +1,480 @@ + + */ +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} + */ + 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 'float': + if ($precision === 63) { + $type = 'smallfloat'; + } + + 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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php b/engine/vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php new file mode 100644 index 0000000..ae09979 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php @@ -0,0 +1,579 @@ + + */ +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} + */ + 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 'json': + 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 .= sprintf(<<<'SQL' + a.attnum, + quote_ident(a.attname) AS field, + t.typname AS type, + format_type(a.atttypid, a.atttypmod) AS complete_type, + (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation, + (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type, + (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM + pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type, + a.attnotnull AS isnotnull, + 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, + (%s) 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, $this->platform->getDefaultColumnValueSQLSnippet()); + + $conditions = array_merge([ + 'a.attnum > 0', + 'd.refobjid IS NULL', + + // 'r' for regular tables - 'p' for partitioned tables + "c.relkind IN('r', 'p')", + + // exclude partitions (tables that inherit from partitioned tables) + <<<'SQL' + NOT EXISTS ( + SELECT 1 + FROM pg_inherits + INNER JOIN pg_class parent on pg_inherits.inhparent = parent.oid + AND parent.relkind = 'p' + WHERE inhrelid = c.oid + ) + SQL, + ], $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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php b/engine/vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php new file mode 100644 index 0000000..e0a74ce --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php @@ -0,0 +1,498 @@ + + */ +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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php b/engine/vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php new file mode 100644 index 0000000..47eabd6 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php @@ -0,0 +1,635 @@ + + */ +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, modifiedForeignKeys: [$foreignKey])); + } + + public function dropForeignKey(string $name, string $table): void + { + $table = $this->introspectTable($table); + + $foreignKey = $table->getForeignKey($name); + + $this->alterTable(new TableDiff($table, droppedForeignKeys: [$foreignKey])); + } + + /** + * {@inheritDoc} + */ + public function listTableForeignKeys(string $table): array + { + $table = $this->normalizeName($table); + + $columns = $this->selectForeignKeyColumns('main', $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} + */ + 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 + { + $matchResult = preg_match('/^([^()]*)\\s*(\\(((\\d+)(,\\s*(\\d+))?)\\))?/', $tableColumn['type'], $matches); + assert($matchResult === 1); + + $dbType = trim(strtolower($matches[1])); + + $length = $precision = null; + $fixed = $unsigned = false; + $scale = 0; + + if (isset($matches[4])) { + if (isset($matches[6])) { + $precision = (int) $matches[4]; + $scale = (int) $matches[6]; + } else { + $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 = '{' . $this->buildIdentifierPattern($column) + . '[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is'; + + if (preg_match($pattern, $sql, $match) !== 1) { + return null; + } + + return $match[1]; + } + + private function parseTableCommentFromSQL(string $table, string $sql): ?string + { + $pattern = '/\s* # Allow whitespace characters at start of line +CREATE\sTABLE' . $this->buildIdentifierPattern($table) . ' +( # Start capture + (?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s) +)/ix'; + + if (preg_match($pattern, $sql, $match) !== 1) { + return null; + } + + $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n")); + + return $comment === '' ? null : $comment; + } + + private function parseColumnCommentFromSQL(string $column, string $sql): string + { + $pattern = '{[\s(,]' . $this->buildIdentifierPattern($column) + . '(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i'; + + if (preg_match($pattern, $sql, $match) !== 1) { + return ''; + } + + $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n")); + assert(is_string($comment)); + + return $comment; + } + + /** + * Returns a regular expression pattern that matches the given unquoted or quoted identifier. + */ + private function buildIdentifierPattern(string $identifier): string + { + return '(?:' . implode('|', array_map( + static function (string $sql): string { + return '\W' . preg_quote($sql, '/') . '\W'; + }, + [ + $identifier, + $this->platform->quoteSingleIdentifier($identifier), + ], + )) . ')'; + } + + /** @throws Exception */ + private function getCreateTableSQL(string $table): string + { + $sql = $this->connection->fetchOne( + <<<'SQL' +SELECT sql + FROM ( + SELECT * + FROM sqlite_master + UNION ALL + SELECT * + FROM sqlite_temp_master + ) +WHERE type = 'table' +AND name = ? +SQL + , + [$table], + ); + + if ($sql !== false) { + return $sql; + } + + return ''; + } + + /** + * @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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/Schema.php b/engine/vendor/doctrine/dbal/src/Schema/Schema.php new file mode 100644 index 0000000..25fe4a3 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/Schema.php @@ -0,0 +1,374 @@ + + */ + 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; + } + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/SchemaConfig.php b/engine/vendor/doctrine/dbal/src/Schema/SchemaConfig.php new file mode 100644 index 0000000..86cc84f --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/SchemaConfig.php @@ -0,0 +1,61 @@ + */ + 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/SchemaDiff.php b/engine/vendor/doctrine/dbal/src/Schema/SchemaDiff.php new file mode 100644 index 0000000..28c014d --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/SchemaDiff.php @@ -0,0 +1,109 @@ + */ + 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/SchemaException.php b/engine/vendor/doctrine/dbal/src/Schema/SchemaException.php new file mode 100644 index 0000000..4277139 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/SchemaException.php @@ -0,0 +1,11 @@ +_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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/Table.php b/engine/vendor/doctrine/dbal/src/Schema/Table.php new file mode 100644 index 0000000..c7f5930 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/Table.php @@ -0,0 +1,797 @@ + keys are new names, values are old names */ + protected array $renamedColumns = []; + + /** @var Index[] */ + protected array $_indexes = []; + + protected ?string $_primaryKeyName = null; + + /** @var UniqueConstraint[] */ + protected array $uniqueConstraints = []; + + /** @var ForeignKeyConstraint[] */ + protected array $_fkConstraints = []; + + /** @var mixed[] */ + protected array $_options = [ + 'create_options' => [], + ]; + + 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; + } + + /** @return array */ + final public function getRenamedColumns(): array + { + return $this->renamedColumns; + } + + /** @throws LogicException */ + final public function renameColumn(string $oldName, string $newName): Column + { + $oldName = $this->normalizeIdentifier($oldName); + $newName = $this->normalizeIdentifier($newName); + + if ($oldName === $newName) { + throw new LogicException(sprintf( + 'Attempt to rename column "%s.%s" to the same name.', + $this->getName(), + $oldName, + )); + } + + $column = $this->getColumn($oldName); + $column->_setName($newName); + unset($this->_columns[$oldName]); + $this->_addColumn($column); + + // If a column is renamed multiple times, we only want to know the original and last new name + if (isset($this->renamedColumns[$oldName])) { + $toRemove = $oldName; + $oldName = $this->renamedColumns[$oldName]; + unset($this->renamedColumns[$toRemove]); + } + + if ($newName !== $oldName) { + $this->renamedColumns[$newName] = $oldName; + } + + return $column; + } + + /** @param 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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/TableDiff.php b/engine/vendor/doctrine/dbal/src/Schema/TableDiff.php new file mode 100644 index 0000000..2bb514b --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/TableDiff.php @@ -0,0 +1,204 @@ + $droppedForeignKeys + * @param array $addedColumns + * @param array $changedColumns + * @param array $droppedColumns + * @param array $addedIndexes + * @param array $modifiedIndexes + * @param array $droppedIndexes + * @param array $renamedIndexes + * @param array $addedForeignKeys + * @param array $modifiedForeignKeys + */ + public function __construct( + private readonly Table $oldTable, + private readonly array $addedColumns = [], + private readonly array $changedColumns = [], + private readonly array $droppedColumns = [], + private array $addedIndexes = [], + private readonly array $modifiedIndexes = [], + private array $droppedIndexes = [], + private readonly array $renamedIndexes = [], + private readonly array $addedForeignKeys = [], + private readonly array $modifiedForeignKeys = [], + private readonly array $droppedForeignKeys = [], + ) { + } + + public function getOldTable(): Table + { + return $this->oldTable; + } + + /** @return array */ + public function getAddedColumns(): array + { + return $this->addedColumns; + } + + /** @return array */ + public function getChangedColumns(): array + { + return $this->changedColumns; + } + + /** + * @deprecated Use {@see getChangedColumns()} instead. + * + * @return list + */ + public function getModifiedColumns(): array + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6280', + '%s is deprecated, use `getChangedColumns()` instead.', + __METHOD__, + ); + + return array_values(array_filter( + $this->getChangedColumns(), + static fn (ColumnDiff $diff): bool => $diff->countChangedProperties() > ($diff->hasNameChanged() ? 1 : 0), + )); + } + + /** + * @deprecated Use {@see getChangedColumns()} instead. + * + * @return array + */ + public function getRenamedColumns(): array + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6280', + '%s is deprecated, you should use `getChangedColumns()` instead.', + __METHOD__, + ); + $renamed = []; + foreach ($this->getChangedColumns() as $diff) { + if (! $diff->hasNameChanged()) { + continue; + } + + $oldColumnName = $diff->getOldColumn()->getName(); + $renamed[$oldColumnName] = $diff->getNewColumn(); + } + + return $renamed; + } + + /** @return array */ + public function getDroppedColumns(): array + { + return $this->droppedColumns; + } + + /** @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->changedColumns) === 0 + && count($this->droppedColumns) === 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/UniqueConstraint.php b/engine/vendor/doctrine/dbal/src/Schema/UniqueConstraint.php new file mode 100644 index 0000000..a33d446 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/UniqueConstraint.php @@ -0,0 +1,152 @@ + + */ + 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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Schema/View.php b/engine/vendor/doctrine/dbal/src/Schema/View.php new file mode 100644 index 0000000..81f5f8a --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Schema/View.php @@ -0,0 +1,21 @@ +_setName($name); + } + + public function getSql(): string + { + return $this->sql; + } +} diff --git a/engine/vendor/doctrine/dbal/src/ServerVersionProvider.php b/engine/vendor/doctrine/dbal/src/ServerVersionProvider.php new file mode 100644 index 0000000..91dd9ab --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/ServerVersionProvider.php @@ -0,0 +1,13 @@ +Statement for the given SQL and Connection. + * + * @internal The statement can be only instantiated by {@see Connection}. + * + * @param Connection $conn The connection for handling statement errors. + * @param Driver\Statement $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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php b/engine/vendor/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php new file mode 100644 index 0000000..8ec6f23 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php @@ -0,0 +1,119 @@ +setName('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))); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php b/engine/vendor/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php new file mode 100644 index 0000000..049d658 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php @@ -0,0 +1,11 @@ +connection; + } + + 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Tools/DsnParser.php b/engine/vendor/doctrine/dbal/src/Tools/DsnParser.php new file mode 100644 index 0000000..52f5c26 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Tools/DsnParser.php @@ -0,0 +1,217 @@ +> $schemeMapping An array used to map DSN schemes to DBAL drivers */ + public function __construct( + private readonly array $schemeMapping = [], + ) { + } + + /** + * @phpstan-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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/TransactionIsolationLevel.php b/engine/vendor/doctrine/dbal/src/TransactionIsolationLevel.php new file mode 100644 index 0000000..2b094db --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/TransactionIsolationLevel.php @@ -0,0 +1,13 @@ +getAsciiStringTypeDeclarationSQL($column); + } + + public function getBindingType(): ParameterType + { + return ParameterType::ASCII; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/BigIntType.php b/engine/vendor/doctrine/dbal/src/Types/BigIntType.php new file mode 100644 index 0000000..a9b0967 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/BigIntType.php @@ -0,0 +1,64 @@ +getBigIntTypeDeclarationSQL($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; + } + + assert( + is_string($value), + 'DBAL assumes values outside of the integer range to be returned as string by the database driver.', + ); + + if ( + ($value > PHP_INT_MIN && $value < PHP_INT_MAX) + || $value === (string) (int) $value + ) { + return (int) $value; + } + + return $value; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/BinaryType.php b/engine/vendor/doctrine/dbal/src/Types/BinaryType.php new file mode 100644 index 0000000..d400dd5 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/BinaryType.php @@ -0,0 +1,49 @@ +getBinaryTypeDeclarationSQL($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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/BlobType.php b/engine/vendor/doctrine/dbal/src/Types/BlobType.php new file mode 100644 index 0000000..c5415bc --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/BlobType.php @@ -0,0 +1,56 @@ +getBlobTypeDeclarationSQL($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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/BooleanType.php b/engine/vendor/doctrine/dbal/src/Types/BooleanType.php new file mode 100644 index 0000000..e837e58 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/BooleanType.php @@ -0,0 +1,44 @@ +getBooleanTypeDeclarationSQL($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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/ConversionException.php b/engine/vendor/doctrine/dbal/src/Types/ConversionException.php new file mode 100644 index 0000000..7cc375d --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/ConversionException.php @@ -0,0 +1,14 @@ +getDateTypeDeclarationSQL($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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/DateIntervalType.php b/engine/vendor/doctrine/dbal/src/Types/DateIntervalType.php new file mode 100644 index 0000000..29842ae --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/DateIntervalType.php @@ -0,0 +1,84 @@ +getStringTypeDeclarationSQL($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); + } + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/DateTimeImmutableType.php b/engine/vendor/doctrine/dbal/src/Types/DateTimeImmutableType.php new file mode 100644 index 0000000..2d49d1d --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/DateTimeImmutableType.php @@ -0,0 +1,80 @@ +getDateTimeTypeDeclarationSQL($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, + ); + } + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/DateTimeType.php b/engine/vendor/doctrine/dbal/src/Types/DateTimeType.php new file mode 100644 index 0000000..9fd0ba0 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/DateTimeType.php @@ -0,0 +1,80 @@ +getDateTimeTypeDeclarationSQL($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, + ); + } + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/DateTimeTzImmutableType.php b/engine/vendor/doctrine/dbal/src/Types/DateTimeTzImmutableType.php new file mode 100644 index 0000000..350964d --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/DateTimeTzImmutableType.php @@ -0,0 +1,74 @@ +getDateTimeTzTypeDeclarationSQL($column); + } + + /** + * @phpstan-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(), + ); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/DateTimeTzType.php b/engine/vendor/doctrine/dbal/src/Types/DateTimeTzType.php new file mode 100644 index 0000000..98e6569 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/DateTimeTzType.php @@ -0,0 +1,87 @@ +getDateTimeTzTypeDeclarationSQL($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(), + ); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/DateType.php b/engine/vendor/doctrine/dbal/src/Types/DateType.php new file mode 100644 index 0000000..7dcbe4f --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/DateType.php @@ -0,0 +1,69 @@ +getDateTypeDeclarationSQL($column); + } + + /** + * @phpstan-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(), + ); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/DecimalType.php b/engine/vendor/doctrine/dbal/src/Types/DecimalType.php new file mode 100644 index 0000000..7301a33 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/DecimalType.php @@ -0,0 +1,35 @@ +getDecimalTypeDeclarationSQL($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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/EnumType.php b/engine/vendor/doctrine/dbal/src/Types/EnumType.php new file mode 100644 index 0000000..489dc4b --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/EnumType.php @@ -0,0 +1,18 @@ +getEnumDeclarationSQL($column); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/Exception/InvalidFormat.php b/engine/vendor/doctrine/dbal/src/Types/Exception/InvalidFormat.php new file mode 100644 index 0000000..221da2f --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/Exception/InvalidFormat.php @@ -0,0 +1,37 @@ + 32 ? substr($value, 0, 20) . '...' : $value, + $toType, + $expectedFormat ?? '', + ), + 0, + $previous, + ); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/Exception/InvalidType.php b/engine/vendor/doctrine/dbal/src/Types/Exception/InvalidType.php new file mode 100644 index 0000000..743e41f --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/Exception/InvalidType.php @@ -0,0 +1,51 @@ + 32 ? substr($value, 0, 20) . '...' : $value, + $toType, + ); + } + + return new self($message, 0, $previous); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/FloatType.php b/engine/vendor/doctrine/dbal/src/Types/FloatType.php new file mode 100644 index 0000000..40e11f9 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/FloatType.php @@ -0,0 +1,30 @@ +getFloatDeclarationSQL($column); + } + + /** + * @param T $value + * + * @return (T is null ? null : float) + * + * @template T + */ + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?float + { + return $value === null ? null : (float) $value; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/GuidType.php b/engine/vendor/doctrine/dbal/src/Types/GuidType.php new file mode 100644 index 0000000..cc7cc5f --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/GuidType.php @@ -0,0 +1,21 @@ +getGuidTypeDeclarationSQL($column); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/IntegerType.php b/engine/vendor/doctrine/dbal/src/Types/IntegerType.php new file mode 100644 index 0000000..8a711c0 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/IntegerType.php @@ -0,0 +1,39 @@ +getIntegerTypeDeclarationSQL($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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/JsonType.php b/engine/vendor/doctrine/dbal/src/Types/JsonType.php new file mode 100644 index 0000000..04c3dce --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/JsonType.php @@ -0,0 +1,69 @@ +getJsonTypeDeclarationSQL($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); + } + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/PhpDateMappingType.php b/engine/vendor/doctrine/dbal/src/Types/PhpDateMappingType.php new file mode 100644 index 0000000..200f277 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/PhpDateMappingType.php @@ -0,0 +1,14 @@ +getClobTypeDeclarationSQL($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); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/SmallFloatType.php b/engine/vendor/doctrine/dbal/src/Types/SmallFloatType.php new file mode 100644 index 0000000..431ddb2 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/SmallFloatType.php @@ -0,0 +1,30 @@ +getSmallFloatDeclarationSQL($column); + } + + /** + * @param T $value + * + * @return (T is null ? null : float) + * + * @template T + */ + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?float + { + return $value === null ? null : (float) $value; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/SmallIntType.php b/engine/vendor/doctrine/dbal/src/Types/SmallIntType.php new file mode 100644 index 0000000..ba0899c --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/SmallIntType.php @@ -0,0 +1,39 @@ +getSmallIntTypeDeclarationSQL($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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/StringType.php b/engine/vendor/doctrine/dbal/src/Types/StringType.php new file mode 100644 index 0000000..d3f92aa --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/StringType.php @@ -0,0 +1,21 @@ +getStringTypeDeclarationSQL($column); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/TextType.php b/engine/vendor/doctrine/dbal/src/Types/TextType.php new file mode 100644 index 0000000..a682be5 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/TextType.php @@ -0,0 +1,29 @@ +getClobTypeDeclarationSQL($column); + } + + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed + { + return is_resource($value) ? stream_get_contents($value) : $value; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/TimeImmutableType.php b/engine/vendor/doctrine/dbal/src/Types/TimeImmutableType.php new file mode 100644 index 0000000..c1c24d8 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/TimeImmutableType.php @@ -0,0 +1,74 @@ +getTimeTypeDeclarationSQL($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(), + ); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/TimeType.php b/engine/vendor/doctrine/dbal/src/Types/TimeType.php new file mode 100644 index 0000000..0f96fd5 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/TimeType.php @@ -0,0 +1,69 @@ +getTimeTypeDeclarationSQL($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(), + ); + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/Type.php b/engine/vendor/doctrine/dbal/src/Types/Type.php new file mode 100644 index 0000000..ee7797f --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/Type.php @@ -0,0 +1,223 @@ + 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::ENUM => EnumType::class, + Types::FLOAT => FloatType::class, + Types::GUID => GuidType::class, + Types::INTEGER => IntegerType::class, + Types::JSON => JsonType::class, + Types::SIMPLE_ARRAY => SimpleArrayType::class, + Types::SMALLFLOAT => SmallFloatType::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 []; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/TypeRegistry.php b/engine/vendor/doctrine/dbal/src/Types/TypeRegistry.php new file mode 100644 index 0000000..5ef36c3 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/TypeRegistry.php @@ -0,0 +1,131 @@ + 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; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/Types.php b/engine/vendor/doctrine/dbal/src/Types/Types.php new file mode 100644 index 0000000..319218b --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/Types.php @@ -0,0 +1,42 @@ +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; + } + + try { + $dateTime = new DateTimeImmutable($value); + } catch (Exception $e) { + throw ValueNotConvertible::new($value, DateTimeImmutable::class, $e->getMessage(), $e); + } + + return $dateTime; + } +} diff --git a/engine/vendor/doctrine/dbal/src/Types/VarDateTimeType.php b/engine/vendor/doctrine/dbal/src/Types/VarDateTimeType.php new file mode 100644 index 0000000..55dec49 --- /dev/null +++ b/engine/vendor/doctrine/dbal/src/Types/VarDateTimeType.php @@ -0,0 +1,42 @@ + 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; + } +} diff --git a/engine/vendor/doctrine/deprecations/LICENSE b/engine/vendor/doctrine/deprecations/LICENSE new file mode 100644 index 0000000..156905c --- /dev/null +++ b/engine/vendor/doctrine/deprecations/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020-2021 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. diff --git a/engine/vendor/doctrine/deprecations/README.md b/engine/vendor/doctrine/deprecations/README.md new file mode 100644 index 0000000..8b806d1 --- /dev/null +++ b/engine/vendor/doctrine/deprecations/README.md @@ -0,0 +1,218 @@ +# Doctrine Deprecations + +A small (side-effect free by default) layer on top of +`trigger_error(E_USER_DEPRECATED)` or PSR-3 logging. + +- no side-effects by default, making it a perfect fit for libraries that don't know how the error handler works they operate under +- options to avoid having to rely on error handlers global state by using PSR-3 logging +- deduplicate deprecation messages to avoid excessive triggering and reduce overhead + +We recommend to collect Deprecations using a PSR logger instead of relying on +the global error handler. + +## Usage from consumer perspective: + +Enable Doctrine deprecations to be sent to a PSR3 logger: + +```php +\Doctrine\Deprecations\Deprecation::enableWithPsrLogger($logger); +``` + +Enable Doctrine deprecations to be sent as `@trigger_error($message, E_USER_DEPRECATED)` +messages by setting the `DOCTRINE_DEPRECATIONS` environment variable to `trigger`. +Alternatively, call: + +```php +\Doctrine\Deprecations\Deprecation::enableWithTriggerError(); +``` + +If you only want to enable deprecation tracking, without logging or calling `trigger_error` +then set the `DOCTRINE_DEPRECATIONS` environment variable to `track`. +Alternatively, call: + +```php +\Doctrine\Deprecations\Deprecation::enableTrackingDeprecations(); +``` + +Tracking is enabled with all three modes and provides access to all triggered +deprecations and their individual count: + +```php +$deprecations = \Doctrine\Deprecations\Deprecation::getTriggeredDeprecations(); + +foreach ($deprecations as $identifier => $count) { + echo $identifier . " was triggered " . $count . " times\n"; +} +``` + +### Suppressing Specific Deprecations + +Disable triggering about specific deprecations: + +```php +\Doctrine\Deprecations\Deprecation::ignoreDeprecations("https://link/to/deprecations-description-identifier"); +``` + +Disable all deprecations from a package + +```php +\Doctrine\Deprecations\Deprecation::ignorePackage("doctrine/orm"); +``` + +### Other Operations + +When used within PHPUnit or other tools that could collect multiple instances of the same deprecations +the deduplication can be disabled: + +```php +\Doctrine\Deprecations\Deprecation::withoutDeduplication(); +``` + +Disable deprecation tracking again: + +```php +\Doctrine\Deprecations\Deprecation::disable(); +``` + +## Usage from a library/producer perspective: + +When you want to unconditionally trigger a deprecation even when called +from the library itself then the `trigger` method is the way to go: + +```php +\Doctrine\Deprecations\Deprecation::trigger( + "doctrine/orm", + "https://link/to/deprecations-description", + "message" +); +``` + +If variable arguments are provided at the end, they are used with `sprintf` on +the message. + +```php +\Doctrine\Deprecations\Deprecation::trigger( + "doctrine/orm", + "https://github.com/doctrine/orm/issue/1234", + "message %s %d", + "foo", + 1234 +); +``` + +When you want to trigger a deprecation only when it is called by a function +outside of the current package, but not trigger when the package itself is the cause, +then use: + +```php +\Doctrine\Deprecations\Deprecation::triggerIfCalledFromOutside( + "doctrine/orm", + "https://link/to/deprecations-description", + "message" +); +``` + +Based on the issue link each deprecation message is only triggered once per +request. + +A limited stacktrace is included in the deprecation message to find the +offending location. + +Note: A producer/library should never call `Deprecation::enableWith` methods +and leave the decision how to handle deprecations to application and +frameworks. + +## Usage in PHPUnit tests + +There is a `VerifyDeprecations` trait that you can use to make assertions on +the occurrence of deprecations within a test. + +```php +use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; + +class MyTest extends TestCase +{ + use VerifyDeprecations; + + public function testSomethingDeprecation() + { + $this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issue/1234'); + + triggerTheCodeWithDeprecation(); + } + + public function testSomethingDeprecationFixed() + { + $this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/issue/1234'); + + triggerTheCodeWithoutDeprecation(); + } +} +``` + +## Displaying deprecations after running a PHPUnit test suite + +It is possible to integrate this library with PHPUnit to display all +deprecations triggered during the test suite execution. + +```xml + + + + + + + + + + + + src + + + +``` + +Note that you can still trigger Deprecations in your code, provided you use the +`#[WithoutErrorHandler]` attribute to disable PHPUnit's error handler for tests +that call it. Be wary that this will disable all error handling, meaning it +will mask any warnings or errors that would otherwise be caught by PHPUnit. + +At the moment, it is not possible to disable deduplication with an environment +variable, but you can use a bootstrap file to achieve that: + +```php +// tests/bootstrap.php + + … + +``` + +## What is a deprecation identifier? + +An identifier for deprecations is just a link to any resource, most often a +Github Issue or Pull Request explaining the deprecation and potentially its +alternative. diff --git a/engine/vendor/doctrine/deprecations/composer.json b/engine/vendor/doctrine/deprecations/composer.json new file mode 100644 index 0000000..a7a51e3 --- /dev/null +++ b/engine/vendor/doctrine/deprecations/composer.json @@ -0,0 +1,36 @@ +{ + "name": "doctrine/deprecations", + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "license": "MIT", + "type": "library", + "homepage": "https://www.doctrine-project.org/", + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "1.4.10 || 2.0.3", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "DeprecationTests\\": "test_fixtures/src", + "Doctrine\\Foo\\": "test_fixtures/vendor/doctrine/foo" + } + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} diff --git a/engine/vendor/doctrine/deprecations/src/Deprecation.php b/engine/vendor/doctrine/deprecations/src/Deprecation.php new file mode 100644 index 0000000..1801e6c --- /dev/null +++ b/engine/vendor/doctrine/deprecations/src/Deprecation.php @@ -0,0 +1,309 @@ +|null */ + private static $type; + + /** @var LoggerInterface|null */ + private static $logger; + + /** @var array */ + private static $ignoredPackages = []; + + /** @var array */ + private static $triggeredDeprecations = []; + + /** @var array */ + private static $ignoredLinks = []; + + /** @var bool */ + private static $deduplication = true; + + /** + * Trigger a deprecation for the given package and identfier. + * + * The link should point to a Github issue or Wiki entry detailing the + * deprecation. It is additionally used to de-duplicate the trigger of the + * same deprecation during a request. + * + * @param float|int|string $args + */ + public static function trigger(string $package, string $link, string $message, ...$args): void + { + $type = self::$type ?? self::getTypeFromEnv(); + + if ($type === self::TYPE_NONE) { + return; + } + + if (isset(self::$ignoredLinks[$link])) { + return; + } + + if (array_key_exists($link, self::$triggeredDeprecations)) { + self::$triggeredDeprecations[$link]++; + } else { + self::$triggeredDeprecations[$link] = 1; + } + + if (self::$deduplication === true && self::$triggeredDeprecations[$link] > 1) { + return; + } + + if (isset(self::$ignoredPackages[$package])) { + return; + } + + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); + + $message = sprintf($message, ...$args); + + self::delegateTriggerToBackend($message, $backtrace, $link, $package); + } + + /** + * Trigger a deprecation for the given package and identifier when called from outside. + * + * "Outside" means we assume that $package is currently installed as a + * dependency and the caller is not a file in that package. When $package + * is installed as a root package then deprecations triggered from the + * tests folder are also considered "outside". + * + * This deprecation method assumes that you are using Composer to install + * the dependency and are using the default /vendor/ folder and not a + * Composer plugin to change the install location. The assumption is also + * that $package is the exact composer packge name. + * + * Compared to {@link trigger()} this method causes some overhead when + * deprecation tracking is enabled even during deduplication, because it + * needs to call {@link debug_backtrace()} + * + * @param float|int|string $args + */ + public static function triggerIfCalledFromOutside(string $package, string $link, string $message, ...$args): void + { + $type = self::$type ?? self::getTypeFromEnv(); + + if ($type === self::TYPE_NONE) { + return; + } + + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); + + // first check that the caller is not from a tests folder, in which case we always let deprecations pass + if (isset($backtrace[1]['file'], $backtrace[0]['file']) && strpos($backtrace[1]['file'], DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR) === false) { + $path = DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $package) . DIRECTORY_SEPARATOR; + + if (strpos($backtrace[0]['file'], $path) === false) { + return; + } + + if (strpos($backtrace[1]['file'], $path) !== false) { + return; + } + } + + if (isset(self::$ignoredLinks[$link])) { + return; + } + + if (array_key_exists($link, self::$triggeredDeprecations)) { + self::$triggeredDeprecations[$link]++; + } else { + self::$triggeredDeprecations[$link] = 1; + } + + if (self::$deduplication === true && self::$triggeredDeprecations[$link] > 1) { + return; + } + + if (isset(self::$ignoredPackages[$package])) { + return; + } + + $message = sprintf($message, ...$args); + + self::delegateTriggerToBackend($message, $backtrace, $link, $package); + } + + /** @param list $backtrace */ + private static function delegateTriggerToBackend(string $message, array $backtrace, string $link, string $package): void + { + $type = self::$type ?? self::getTypeFromEnv(); + + if (($type & self::TYPE_PSR_LOGGER) > 0) { + $context = [ + 'file' => $backtrace[0]['file'] ?? null, + 'line' => $backtrace[0]['line'] ?? null, + 'package' => $package, + 'link' => $link, + ]; + + assert(self::$logger !== null); + + self::$logger->notice($message, $context); + } + + if (! (($type & self::TYPE_TRIGGER_ERROR) > 0)) { + return; + } + + $message .= sprintf( + ' (%s:%d called by %s:%d, %s, package %s)', + self::basename($backtrace[0]['file'] ?? 'native code'), + $backtrace[0]['line'] ?? 0, + self::basename($backtrace[1]['file'] ?? 'native code'), + $backtrace[1]['line'] ?? 0, + $link, + $package + ); + + @trigger_error($message, E_USER_DEPRECATED); + } + + /** + * A non-local-aware version of PHPs basename function. + */ + private static function basename(string $filename): string + { + $pos = strrpos($filename, DIRECTORY_SEPARATOR); + + if ($pos === false) { + return $filename; + } + + return substr($filename, $pos + 1); + } + + public static function enableTrackingDeprecations(): void + { + self::$type = self::$type ?? self::getTypeFromEnv(); + self::$type |= self::TYPE_TRACK_DEPRECATIONS; + } + + public static function enableWithTriggerError(): void + { + self::$type = self::$type ?? self::getTypeFromEnv(); + self::$type |= self::TYPE_TRIGGER_ERROR; + } + + public static function enableWithPsrLogger(LoggerInterface $logger): void + { + self::$type = self::$type ?? self::getTypeFromEnv(); + self::$type |= self::TYPE_PSR_LOGGER; + self::$logger = $logger; + } + + public static function withoutDeduplication(): void + { + self::$deduplication = false; + } + + public static function disable(): void + { + self::$type = self::TYPE_NONE; + self::$logger = null; + self::$deduplication = true; + self::$ignoredLinks = []; + + foreach (self::$triggeredDeprecations as $link => $count) { + self::$triggeredDeprecations[$link] = 0; + } + } + + public static function ignorePackage(string $packageName): void + { + self::$ignoredPackages[$packageName] = true; + } + + public static function ignoreDeprecations(string ...$links): void + { + foreach ($links as $link) { + self::$ignoredLinks[$link] = true; + } + } + + public static function getUniqueTriggeredDeprecationsCount(): int + { + return array_reduce(self::$triggeredDeprecations, static function (int $carry, int $count) { + return $carry + $count; + }, 0); + } + + /** + * Returns each triggered deprecation link identifier and the amount of occurrences. + * + * @return array + */ + public static function getTriggeredDeprecations(): array + { + return self::$triggeredDeprecations; + } + + /** @return int-mask-of */ + private static function getTypeFromEnv(): int + { + switch ($_SERVER['DOCTRINE_DEPRECATIONS'] ?? $_ENV['DOCTRINE_DEPRECATIONS'] ?? null) { + case 'trigger': + self::$type = self::TYPE_TRIGGER_ERROR; + break; + + case 'track': + self::$type = self::TYPE_TRACK_DEPRECATIONS; + break; + + default: + self::$type = self::TYPE_NONE; + break; + } + + return self::$type; + } +} diff --git a/engine/vendor/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php b/engine/vendor/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php new file mode 100644 index 0000000..8b322b7 --- /dev/null +++ b/engine/vendor/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php @@ -0,0 +1,62 @@ + */ + private $doctrineDeprecationsExpectations = []; + + /** @var array */ + private $doctrineNoDeprecationsExpectations = []; + + public function expectDeprecationWithIdentifier(string $identifier): void + { + $this->doctrineDeprecationsExpectations[$identifier] = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0; + } + + public function expectNoDeprecationWithIdentifier(string $identifier): void + { + $this->doctrineNoDeprecationsExpectations[$identifier] = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0; + } + + /** @before */ + public function enableDeprecationTracking(): void + { + Deprecation::enableTrackingDeprecations(); + } + + /** @after */ + public function verifyDeprecationsAreTriggered(): void + { + foreach ($this->doctrineDeprecationsExpectations as $identifier => $expectation) { + $actualCount = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0; + + $this->assertTrue( + $actualCount > $expectation, + sprintf( + "Expected deprecation with identifier '%s' was not triggered by code executed in test.", + $identifier + ) + ); + } + + foreach ($this->doctrineNoDeprecationsExpectations as $identifier => $expectation) { + $actualCount = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0; + + $this->assertTrue( + $actualCount === $expectation, + sprintf( + "Expected deprecation with identifier '%s' was triggered by code executed in test, but expected not to.", + $identifier + ) + ); + } + } +} diff --git a/engine/vendor/psr/cache/CHANGELOG.md b/engine/vendor/psr/cache/CHANGELOG.md new file mode 100644 index 0000000..58ddab0 --- /dev/null +++ b/engine/vendor/psr/cache/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented in this file, in reverse chronological order by release. + +## 1.0.1 - 2016-08-06 + +### Fixed + +- Make spacing consistent in phpdoc annotations php-fig/cache#9 - chalasr +- Fix grammar in phpdoc annotations php-fig/cache#10 - chalasr +- Be more specific in docblocks that `getItems()` and `deleteItems()` take an array of strings (`string[]`) compared to just `array` php-fig/cache#8 - GrahamCampbell +- For `expiresAt()` and `expiresAfter()` in CacheItemInterface fix docblock to specify null as a valid parameters as well as an implementation of DateTimeInterface php-fig/cache#7 - GrahamCampbell + +## 1.0.0 - 2015-12-11 + +Initial stable release; reflects accepted PSR-6 specification diff --git a/engine/vendor/psr/cache/LICENSE.txt b/engine/vendor/psr/cache/LICENSE.txt new file mode 100644 index 0000000..b1c2c97 --- /dev/null +++ b/engine/vendor/psr/cache/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2015 PHP Framework Interoperability Group + +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. diff --git a/engine/vendor/psr/cache/README.md b/engine/vendor/psr/cache/README.md new file mode 100644 index 0000000..9855a31 --- /dev/null +++ b/engine/vendor/psr/cache/README.md @@ -0,0 +1,12 @@ +Caching Interface +============== + +This repository holds all interfaces related to [PSR-6 (Caching Interface)][psr-url]. + +Note that this is not a Caching implementation of its own. It is merely interfaces that describe the components of a Caching mechanism. + +The installable [package][package-url] and [implementations][implementation-url] are listed on Packagist. + +[psr-url]: https://www.php-fig.org/psr/psr-6/ +[package-url]: https://packagist.org/packages/psr/cache +[implementation-url]: https://packagist.org/providers/psr/cache-implementation diff --git a/engine/vendor/psr/cache/composer.json b/engine/vendor/psr/cache/composer.json new file mode 100644 index 0000000..4b68797 --- /dev/null +++ b/engine/vendor/psr/cache/composer.json @@ -0,0 +1,25 @@ +{ + "name": "psr/cache", + "description": "Common interface for caching libraries", + "keywords": ["psr", "psr-6", "cache"], + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "require": { + "php": ">=8.0.0" + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/engine/vendor/psr/cache/src/CacheException.php b/engine/vendor/psr/cache/src/CacheException.php new file mode 100644 index 0000000..bb785f4 --- /dev/null +++ b/engine/vendor/psr/cache/src/CacheException.php @@ -0,0 +1,10 @@ +logger = $logger; + } + + public function doSomething() + { + if ($this->logger) { + $this->logger->info('Doing work'); + } + + try { + $this->doSomethingElse(); + } catch (Exception $exception) { + $this->logger->error('Oh no!', array('exception' => $exception)); + } + + // do something useful + } +} +``` + +You can then pick one of the implementations of the interface to get a logger. + +If you want to implement the interface, you can require this package and +implement `Psr\Log\LoggerInterface` in your code. Please read the +[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) +for details. diff --git a/engine/vendor/psr/log/composer.json b/engine/vendor/psr/log/composer.json new file mode 100644 index 0000000..879fc6f --- /dev/null +++ b/engine/vendor/psr/log/composer.json @@ -0,0 +1,26 @@ +{ + "name": "psr/log", + "description": "Common interface for logging libraries", + "keywords": ["psr", "psr-3", "log"], + "homepage": "https://github.com/php-fig/log", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "require": { + "php": ">=8.0.0" + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + } +} diff --git a/engine/vendor/psr/log/src/AbstractLogger.php b/engine/vendor/psr/log/src/AbstractLogger.php new file mode 100644 index 0000000..d60a091 --- /dev/null +++ b/engine/vendor/psr/log/src/AbstractLogger.php @@ -0,0 +1,15 @@ +logger = $logger; + } +} diff --git a/engine/vendor/psr/log/src/LoggerInterface.php b/engine/vendor/psr/log/src/LoggerInterface.php new file mode 100644 index 0000000..cb4cf64 --- /dev/null +++ b/engine/vendor/psr/log/src/LoggerInterface.php @@ -0,0 +1,98 @@ +log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + */ + public function alert(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + */ + public function critical(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + */ + public function error(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + */ + public function warning(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + */ + public function notice(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + */ + public function info(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + */ + public function debug(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::DEBUG, $message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * + * @throws \Psr\Log\InvalidArgumentException + */ + abstract public function log($level, string|\Stringable $message, array $context = []): void; +} diff --git a/engine/vendor/psr/log/src/NullLogger.php b/engine/vendor/psr/log/src/NullLogger.php new file mode 100644 index 0000000..de0561e --- /dev/null +++ b/engine/vendor/psr/log/src/NullLogger.php @@ -0,0 +1,26 @@ +logger) { }` + * blocks. + */ +class NullLogger extends AbstractLogger +{ + /** + * Logs with an arbitrary level. + * + * @param mixed[] $context + * + * @throws \Psr\Log\InvalidArgumentException + */ + public function log($level, string|\Stringable $message, array $context = []): void + { + // noop + } +}