diff --git a/CHANGELOG.md b/CHANGELOG.md index 49fdf55..098fea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Next + +* [feature] ⭐️ Add `MakinaCorpus\QueryBuilder\BridgeFactory` for creating + standalone connections. +* [fix] Better version compare algorithm, with less erroneous edge cases. +* [deprecation] ⚠️ Renamed `MakinaCorpus\QueryBuilder\Bridge\Doctrine\DoctrineQueryBuilder` + to `MakinaCorpus\QueryBuilder\Bridge\Doctrine\DoctrineBridge`. +* [deprecation] ⚠️ Renamed `MakinaCorpus\QueryBuilder\Bridge\Pdo\PdoQueryBuilder` + to `MakinaCorpus\QueryBuilder\Bridge\Pdo\PdoBridge`. + ## 1.5.5 * [fix] Handle `mysqli` in `Dsn` class. diff --git a/docs/content/bridges/error.md b/docs/content/bridges/error.md index fe01921..9501214 100644 --- a/docs/content/bridges/error.md +++ b/docs/content/bridges/error.md @@ -35,10 +35,10 @@ call the `Bridge::disableErrorConverter()` method when initializing the bridge: ```php use Doctrine\DBAL\DriverManager; -use MakinaCorpus\QueryBuilder\Bridge\Doctrine\DoctrineQueryBuilder; +use MakinaCorpus\QueryBuilder\Bridge\Doctrine\DoctrineBridge; $connection = DriverManager::getConnection(['driver' => 'pdo_pgsql', /* ... */]); -$bridge = new DoctrineQueryBuilder($connection); +$bridge = new DoctrineBridge($connection); // Here it is: $bridge->disableErrorConverter(); diff --git a/docs/content/introduction/getting-started.md b/docs/content/introduction/getting-started.md index dd4cd23..86682b4 100644 --- a/docs/content/introduction/getting-started.md +++ b/docs/content/introduction/getting-started.md @@ -116,7 +116,7 @@ Setting it up is easier than standalone setup: ```php use Doctrine\DBAL\DriverManager; -use MakinaCorpus\QueryBuilder\Bridge\Doctrine\DoctrineQueryBuilder; +use MakinaCorpus\QueryBuilder\Bridge\Doctrine\DoctrineBridge; use MakinaCorpus\QueryBuilder\DatabaseSession; // Create or fetch your doctrine/dbal connection. @@ -126,7 +126,7 @@ $connection = DriverManager::getConnection([ ]); // Create the query builder. -$session = new DoctrineQueryBuilder($connection); +$session = new DoctrineBridge($connection); \assert($session instanceof DatabaseSession); ``` @@ -189,14 +189,14 @@ composer require makinacorpus/query-builder Setting it up is easier than standalone setup: ```php -use MakinaCorpus\QueryBuilder\Bridge\Pdo\PdoQueryBuilder; +use MakinaCorpus\QueryBuilder\Bridge\Pdo\PdoBridge; use MakinaCorpus\QueryBuilder\DatabaseSession; // Create or fetch your PDO connection. $connection = new \PDO('pgsql:...'); // User facade for you to build SQL queries. -$session = new PdoQueryBuilder($connection); +$session = new PdoBridge($connection); \assert($session instanceof DatabaseSession); ``` diff --git a/docs/content/query/result.md b/docs/content/query/result.md index 8d71910..21b5d7b 100644 --- a/docs/content/query/result.md +++ b/docs/content/query/result.md @@ -22,9 +22,9 @@ you can call `Query::executeQuery()` over your queries, for example: ```php use Doctrine\DBAL\DriverManager; -use MakinaCorpus\QueryBuilder\Bridge\Doctrine\DoctrineQueryBuilder; +use MakinaCorpus\QueryBuilder\Bridge\Doctrine\DoctrineBridge; -$session = new DoctrineQueryBuilder( +$session = new DoctrineBridge( DriverManager::getConnection([ 'driver' => 'pdo_pgsql', // ... driver options. @@ -46,9 +46,9 @@ You may also directly execute raw SQL code as such: ```php use Doctrine\DBAL\DriverManager; -use MakinaCorpus\QueryBuilder\Bridge\Doctrine\DoctrineQueryBuilder; +use MakinaCorpus\QueryBuilder\Bridge\Doctrine\DoctrineBridge; -$session = new DoctrineQueryBuilder( +$session = new DoctrineBridge( DriverManager::getConnection([ 'driver' => 'pdo_pgsql', // ... driver options. @@ -79,10 +79,10 @@ instances: ```php use Doctrine\DBAL\DriverManager; -use MakinaCorpus\QueryBuilder\Bridge\Doctrine\DoctrineQueryBuilder; +use MakinaCorpus\QueryBuilder\Bridge\Doctrine\DoctrineBridge; use MakinaCorpus\QueryBuilder\Result\ResultRow; -$session = new DoctrineQueryBuilder( +$session = new DoctrineBridge( DriverManager::getConnection([ 'driver' => 'pdo_pgsql', // ... driver options. diff --git a/src/Bridge/Doctrine/DoctrineBridge.php b/src/Bridge/Doctrine/DoctrineBridge.php new file mode 100644 index 0000000..836e72e --- /dev/null +++ b/src/Bridge/Doctrine/DoctrineBridge.php @@ -0,0 +1,9 @@ +connection = $connection; + + if (static::class === self::class) { + @\trigger_error(\sprintf("Class '%s' is deprecated and will be removed in 2.0, use '%s' instead.", DoctrineQueryBuilder::class, DoctrineBridge::class)); + } } /** diff --git a/src/Bridge/Pdo/PdoBridge.php b/src/Bridge/Pdo/PdoBridge.php new file mode 100644 index 0000000..5bceb89 --- /dev/null +++ b/src/Bridge/Pdo/PdoBridge.php @@ -0,0 +1,9 @@ +connection = $connection; + + if (static::class === self::class) { + @\trigger_error(\sprintf("Class '%s' is deprecated and will be removed in 2.0, use '%s' instead.", PdoQueryBuilder::class, PdoBridge::class)); + } } /** diff --git a/src/BridgeFactory.php b/src/BridgeFactory.php new file mode 100644 index 0000000..edc0c4a --- /dev/null +++ b/src/BridgeFactory.php @@ -0,0 +1,361 @@ +getDriver()) { + Dsn::DRIVER_ANY => self::createPdo($dsn), + Dsn::DRIVER_PDO => self::createPdo($dsn), + default => self::createDoctrine($dsn), + }; + } + + /** + * Creates connection using doctrine/dbal. + * + * Doctrine is the bridge that will give you the most vendor support and + * configuration options. + */ + public static function createDoctrine(#[\SensitiveParameter] array|string|Dsn $uri): DoctrineBridge + { + $dsn = self::normalizeDsn($uri); + + $driver = $dsn->getDriver(); + $vendor = $dsn->getVendor(); + + // These are opiniated choices. + // @see https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html + $doctrineDriver = match ($driver) { + Dsn::DRIVER_ANY, Dsn::DRIVER_DOCTRINE => match ($vendor) { + Vendor::MARIADB, Vendor::MYSQL => 'pdo_mysql', + Vendor::ORACLE => 'oci8', + Vendor::POSTGRESQL => 'pgsql', + Vendor::SQLITE => 'sqlite3', + Vendor::SQLSERVER => 'sqlsrv', + default => $driver . '_' . $vendor, + }, + Dsn::DRIVER_MYSQLI => 'mysqli', + Dsn::DRIVER_SQLITE3 => 'sqlite3', + default => match ($vendor) { + Vendor::MARIADB, Vendor::MYSQL => 'pdo_mysql', + Vendor::ORACLE => 'pdo_oci', + Vendor::POSTGRESQL => 'pdo_pgsql', + Vendor::SQLITE => 'pdo_sqlite', + Vendor::SQLSERVER => 'pdo_sqlsrv', + default => $driver . '_' . $vendor, + }, + }; + + $params = \array_filter([ + 'dbname' => $dsn->getDatabase(), + 'driver' => $doctrineDriver, + 'driverOptions' => $dsn->getOptions(), + 'host' => $dsn->getHost(), + 'memory' => $dsn->inMemory(), + 'password' => $dsn->getPassword(), + 'path' => $dsn->getFilename(), + 'persistent' => self::valueBool('persistent', $dsn->getOption('memory')), + 'port' => $dsn->getPort(), + 'user' => $dsn->getUser(), + ]); + + return new DoctrineBridge( + DriverManager::getConnection( + $params, + self::doctrineConfiguration($params['driver']), + ), + ); + } + + /** + * Creates connection using PDO. + * + * Whenever supported, you should choose PDO which will give you the best + * performances (ext-pgsql aside). + */ + public static function createPdo(#[\SensitiveParameter] array|string|Dsn $uri): PdoBridge + { + $dsn = self::normalizeDsn($uri); + + return new PdoBridge( + match ($dsn->getVendor()) { + Vendor::MYSQL => self::pdoConnectionMySQL($dsn), + Vendor::POSTGRESQL => self::pdoConnectionPostgreSQL($dsn), + Vendor::SQLITE => self::pdoConnectionSQLite($dsn), + Vendor::SQLSERVER => self::pdoConnectionSQLServer($dsn), + default => throw new UnsupportedFeatureError(\sprintf("Unsupported vendor '%s' in bridge factory.", $dsn->getVendor())), + }, + ); + } + + /** + * Normalize DSN. + * + * @internal + * Public for unit testing purpose. + */ + public static function normalizeDsn(#[\SensitiveParameter] array|string|Dsn $uri): Dsn + { + if (\is_array($uri)) { + if (empty($uri['driver'])) { + throw new ConfigurationError("Option 'driver' is missing from parameters array."); + } + + $options = \array_diff_key($uri, ['dbname' => 1, 'driver' => 1, 'host' => 1, 'memory' => 1, 'password' => 1, 'path' => 1, 'port' => 1, 'user' => 1]); + + return new Dsn( + database: self::valueString('dbname', $uri['dbname'] ?? null), + filename: self::valueString('path', $uri['path'] ?? null), + host: self::valueString('host', $uri['host'] ?? null), + memory: self::valueBool('memory', $uri['memory'] ?? false), + password: self::valueString('password', $uri['password'] ?? null), + port: self::valueInt('port', $uri['port'] ?? null), + query: $options, + user: self::valueString('user', $uri['user'] ?? null), + vendor: self::valueString('driver', $uri['driver']), + ); + } + + if (\is_string($uri)) { + return Dsn::fromString($uri); + } + + return $uri; + } + + /** + * Create MySQL PDO connection. + */ + private static function pdoConnectionMySQL(#[\SensitiveParameter] Dsn $dsn): \PDO + { + $options = []; + if ($value = $dsn->getHost()) { + $options['host'] = $value; + } + if ($value = $dsn->getPort()) { + $options['port'] = $value; + } + if ($value = $dsn->getDatabase()) { + $options['dbname'] = $value; + } + $options += \array_diff_key($dsn->getOptions(), ['persistent' => 1]); + + $flags = []; + $persistent = $dsn->getOption('peristent'); + if (null === $persistent || true === $persistent) { + $flags[\PDO::ATTR_PERSISTENT] = true; + } + + // @todo Make persistent flag user-configurable. + return new \PDO('mysql:' . self::pdoConnectionString($options), $dsn->getUser(), $dsn->getPassword(), $flags); + } + + /** + * Create PostgreSQL PDO connection. + */ + private static function pdoConnectionPostgreSQL(#[\SensitiveParameter] Dsn $dsn): \PDO + { + $options = []; + if ($value = $dsn->getHost()) { + $options['host'] = $value; + } + if ($value = $dsn->getPort()) { + $options['port'] = $value; + } + if ($value = $dsn->getDatabase()) { + $options['dbname'] = $value; + } + $options += \array_diff_key($dsn->getOptions(), ['persistent' => 1]); + + $flags = []; + $persistent = $dsn->getOption('peristent'); + if (null === $persistent || true === $persistent) { + $flags[\PDO::ATTR_PERSISTENT] = true; + } + + // @todo Make persistent flag user-configurable. + return new \PDO('pgsql:' . self::pdoConnectionString($options), $dsn->getUser(), $dsn->getPassword(), $flags); + } + + /** + * Create PostgreSQL PDO connection. + */ + private static function pdoConnectionSQLServer(#[\SensitiveParameter] Dsn $dsn): \PDO + { + $options = []; + if ($value = $dsn->getHost()) { + $options['server'] = $value; + if ($value = $dsn->getPort()) { + $options['server'] .= ',' . $value; + } + } + if ($value = $dsn->getDatabase()) { + $options['Database'] = $value; + } + $options += \array_diff_key($dsn->getOptions(), ['persistent' => 1]); + + if (isset($options['MultipleActiveResultSets'])) { + $options['MultipleActiveResultSets'] = $options['MultipleActiveResultSets'] ? 'true' : 'false'; + } + if (isset($options['TrustServerCertificate'])) { + $options['TrustServerCertificate'] = $options['TrustServerCertificate'] ? 'true' : 'false'; + } + + $flags = []; + $persistent = $dsn->getOption('peristent'); + if (null === $persistent || true === $persistent) { + // @todo This doesn't seem to be supported somehow. + // $flags[\PDO::ATTR_PERSISTENT] = true; + } + + // @todo Make persistent flag user-configurable. + return new \PDO('sqlsrv:' . self::pdoConnectionString($options), $dsn->getUser(), $dsn->getPassword(), $flags); + } + + /** + * Create SQLite PDO connection. + */ + private static function pdoConnectionSQLite(#[\SensitiveParameter] Dsn $dsn): \PDO + { + // Dsn::getFilename() may return ":memory:" which is supported by PDO. + return new \PDO('sqlite:' . $dsn->getFilename()); + } + + /** + * Compute own PDO connection string. + */ + private static function pdoConnectionString(array $options): string + { + $values = []; + foreach ($options as $name => $value) { + $values[] = \sprintf("%s=%s", $name, $value); + } + return \implode(';', $values); + } + + /** + * Code copied from doctrine/dbal package. + * + * See the \Doctrine\DBAL\Tests\FunctionalTestCase class. + */ + private static function doctrineConfiguration(string $driver): Configuration + { + $configuration = new Configuration(); + + switch ($driver) { + case 'pdo_oci': + case 'oci8': + $configuration->setMiddlewares([new InitializeSession()]); + break; + case 'pdo_sqlite': + case 'sqlite3': + $configuration->setMiddlewares([new EnableForeignKeys()]); + break; + } + + $configuration->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); + + return $configuration; + } + + /** + * Covnert user input as string. + */ + private static function valueString(string $name, #[\SensitiveParameter] mixed $value): ?string + { + if (null === $value || '' === $value) { + return null; + } + if (\is_string($value)) { + return $value; + } + if ($value instanceof \Stringable) { + return (string) $value; + } + + throw new ConfigurationError(\sprintf("Option '%s' must be a string, '%s' given.", $name, \get_debug_type($value))); + } + + /** + * Convert user input as integer. + */ + private static function valueInt(string $name, #[\SensitiveParameter] mixed $value): ?int + { + if (null === $value || '' === $value) { + return null; + } + if (\is_int($value)) { + return $value; + } + if (\is_string($value) && \ctype_digit($value)) { + return (int) $value; + } + + throw new ConfigurationError(\sprintf("Option '%s' must be a int, '%s' given.", $name, \get_debug_type($value))); + } + + /** + * Convert user input as integer. + */ + private static function valueBool(string $name, #[\SensitiveParameter] mixed $value): ?bool + { + if (null === $value || '' === $value) { + return null; + } + if (\is_bool($value)) { + return $value; + } + if (\is_int($value)) { + return (bool) $value; + } + if (\is_string($value)) { + return !\in_array($value, ['false', 'f', 'no', 'n']); + } + + throw new ConfigurationError(\sprintf("Option '%s' must be a bool, '%s' given.", $name, \get_debug_type($value))); + } +} diff --git a/src/Converter/ConverterPluginRegistry.php b/src/Converter/ConverterPluginRegistry.php index f662926..1a7be46 100644 --- a/src/Converter/ConverterPluginRegistry.php +++ b/src/Converter/ConverterPluginRegistry.php @@ -11,6 +11,7 @@ use MakinaCorpus\QueryBuilder\Converter\OutputConverter\DateOutputConverter; use MakinaCorpus\QueryBuilder\Converter\OutputConverter\RamseyUuidOutputConverter; use MakinaCorpus\QueryBuilder\Converter\OutputConverter\SymfonyUidOutputConverter; +use MakinaCorpus\QueryBuilder\Error\ConfigurationError; use MakinaCorpus\QueryBuilder\Type\Type; use Ramsey\Uuid\UuidInterface; use Symfony\Component\Uid\AbstractUid; @@ -85,7 +86,7 @@ public function register(ConverterPlugin $plugin): void } if (!$found) { - throw new \InvalidArgumentException(\sprintf("Unsupported plugin class %s", \get_class($plugin))); + throw new ConfigurationError(\sprintf("Unsupported plugin class %s", \get_class($plugin))); } } diff --git a/src/Dsn.php b/src/Dsn.php index bf77488..83d684b 100644 --- a/src/Dsn.php +++ b/src/Dsn.php @@ -4,10 +4,13 @@ namespace MakinaCorpus\QueryBuilder; +use MakinaCorpus\QueryBuilder\Error\ConfigurationError; + /** * Various use case this DSN implements: * - driver://user:pass@host:port/database?arg=value&arg=value... * - driver:///path/to/file?arg=value&... (socket connection, or database file name). + * - driver:///:memory:?arg=value&... (for example sqlite in memory) */ class Dsn { @@ -20,6 +23,7 @@ class Dsn private string $scheme; private readonly bool $isFile; + private readonly bool $memory; private readonly string $driver; private readonly string $vendor; private readonly ?string $host; @@ -29,10 +33,13 @@ class Dsn public function __construct( /** Database vendor, eg. "mysql", "pgsql", ... */ string $vendor, - /** Host or local filename (unix socket, database file) */ + /** Database hostname, mutually exclusive with filename and memory. */ ?string $host = null, + /** Database filename or unix socket, mutually exclusive with host and memory. */ ?string $filename = null, bool $isFile = false, + /** Database in memory, mutually exclusive with host and filename. */ + bool $memory = false, /** Driver, eg. "pdo", "ext", ... */ ?string $driver = null, ?string $database = null, @@ -74,11 +81,20 @@ public function __construct( break; } - if (!$host && !$filename) { - throw new \InvalidArgumentException("Either one of \$host or \$filename parameter must be provided."); + if (!$host && !$filename && !$memory) { + throw new ConfigurationError("Either one of \$host, \$filename or \$memory parameter must be provided."); + } + + if ($memory) { + if ($filename && $filename !== ':memory:') { + throw new ConfigurationError(\sprintf("When \$memory is true, filename must be null or equal to ':memory:', found '%s'.", $filename)); + } + $filename = ':memory:'; + } else if (':memory:' === $filename) { + $memory = true; } - $isFile = $isFile || $filename || '/' === $host[0] || Vendor::SQLITE === $this->vendor; + $isFile = !$memory && ($isFile || $filename || '/' === $host[0] || Vendor::SQLITE === $this->vendor); // Fix an edge case where sqlite is not detected. if ($isFile) { @@ -91,10 +107,11 @@ public function __construct( $database = $database ? \trim($database, '/') : null; } - $this->isFile = $isFile; $this->database = $database; $this->filename = $filename; $this->host = $host; + $this->isFile = $isFile; + $this->memory = $memory; } public static function fromString(#[\SensitiveParameter] string $dsn): self @@ -117,23 +134,23 @@ public static function fromString(#[\SensitiveParameter] string $dsn): self $dsn = 'mock://' . $matches[2]; } } else { - throw new \InvalidArgumentException('The database DSN must contain a scheme.'); + throw new ConfigurationError('The database DSN must contain a scheme.'); } if (false === ($params = \parse_url($dsn))) { - throw new \InvalidArgumentException('The database DSN is invalid.'); + throw new ConfigurationError('The database DSN is invalid.'); } $database = $host = $filename = null; if ($isFile) { if (empty($params['path'])) { - throw new \InvalidArgumentException('The database DSN must contain a path when a targetting a local filename.'); + throw new ConfigurationError('The database DSN must contain a path when a targetting a local filename.'); } $filename = $params['path']; } else { if (empty($params['host'])) { - throw new \InvalidArgumentException('The database DSN must contain a host (use "default" by default).'); + throw new ConfigurationError('The database DSN must contain a host (use "default" by default).'); } if (isset($params['path'])) { // If path was absolute and is a filename then it should take @@ -202,11 +219,19 @@ public function isFile(): bool } /** - * Get database, if none provided, "default" is returned. + * Is a database in memory. */ - public function getDatabase(): string + public function inMemory(): bool { - return $this->database ?? 'default'; + return $this->memory; + } + + /** + * Get database name, can be null. + */ + public function getDatabase(): ?string + { + return $this->database; } /** @@ -237,6 +262,11 @@ public function getOption(string $key, mixed $default = null): mixed return $this->query[$key] ?? $default; } + public function getOptions(): array + { + return $this->query; + } + public function toUrl(array $excludeParams = []): string { $database = $this->database ?? ''; diff --git a/src/Error/ConfigurationError.php b/src/Error/ConfigurationError.php new file mode 100644 index 0000000..d09f57a --- /dev/null +++ b/src/Error/ConfigurationError.php @@ -0,0 +1,9 @@ +fetchOne(); } - throw new \LogicException('Only fetch modes declared on Doctrine\DBAL\FetchMode are supported by legacy API.'); + throw new QueryBuilderError('Only fetch modes declared on Doctrine\DBAL\FetchMode are supported by legacy API.'); } #[\Override] diff --git a/src/Testing/FunctionalDoctrineTestCaseTrait.php b/src/Testing/FunctionalDoctrineTestCaseTrait.php index 52f2606..cfc8300 100644 --- a/src/Testing/FunctionalDoctrineTestCaseTrait.php +++ b/src/Testing/FunctionalDoctrineTestCaseTrait.php @@ -4,13 +4,8 @@ namespace MakinaCorpus\QueryBuilder\Testing; -use Doctrine\DBAL\Configuration; -use Doctrine\DBAL\DriverManager; -use Doctrine\DBAL\Driver\AbstractSQLiteDriver\Middleware\EnableForeignKeys; -use Doctrine\DBAL\Driver\OCI8\Middleware\InitializeSession; -use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; +use MakinaCorpus\QueryBuilder\BridgeFactory; use MakinaCorpus\QueryBuilder\Bridge\Bridge; -use MakinaCorpus\QueryBuilder\Bridge\Doctrine\DoctrineQueryBuilder; trait FunctionalDoctrineTestCaseTrait { @@ -19,40 +14,6 @@ trait FunctionalDoctrineTestCaseTrait #[\Override] protected function doCreateBridge(array $params): Bridge { - if (\str_contains($params['driver'], 'sqlite')) { - $params['memory'] = true; - } - - return new DoctrineQueryBuilder( - DriverManager::getConnection( - $params, - $this->createDoctrineConfiguration($params['driver']), - ), - ); - } - - /** - * Code copied from doctrine/dbal package. - * - * @see \Doctrine\DBAL\Tests\FunctionalTestCase - */ - private function createDoctrineConfiguration(string $driver): Configuration - { - $configuration = new Configuration(); - - switch ($driver) { - case 'pdo_oci': - case 'oci8': - $configuration->setMiddlewares([new InitializeSession()]); - break; - case 'pdo_sqlite': - case 'sqlite3': - $configuration->setMiddlewares([new EnableForeignKeys()]); - break; - } - - $configuration->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); - - return $configuration; + return BridgeFactory::createDoctrine($params); } } diff --git a/src/Testing/FunctionalPdoTestCaseTrait.php b/src/Testing/FunctionalPdoTestCaseTrait.php index 3a3c2c9..056b45c 100644 --- a/src/Testing/FunctionalPdoTestCaseTrait.php +++ b/src/Testing/FunctionalPdoTestCaseTrait.php @@ -4,8 +4,8 @@ namespace MakinaCorpus\QueryBuilder\Testing; +use MakinaCorpus\QueryBuilder\BridgeFactory; use MakinaCorpus\QueryBuilder\Bridge\Bridge; -use MakinaCorpus\QueryBuilder\Bridge\Pdo\PdoQueryBuilder; trait FunctionalPdoTestCaseTrait { @@ -14,59 +14,6 @@ trait FunctionalPdoTestCaseTrait #[\Override] protected function doCreateBridge(array $params): Bridge { - return new PdoQueryBuilder( - match ($params['driver']) { - 'pdo_mysql' => $this->createPdoConnectionMySQL($params), - 'pdo_pgsql' => $this->createPdoConnectionPostgreSQL($params), - 'pdo_sqlite' => $this->createPdoConnectionSQLite($params), - default => self::markTestSkipped(\sprintf("Unsupported 'DBAL_DRIVER' value '%s' for PDO bridge.", $params['driver'])), - }, - ); - } - - /** - * Create MySQL PDO connection. - */ - private function createPdoConnectionMySQL(array $params): \PDO - { - $dsn = []; - if ($value = ($params['host'] ?? null)) { - $dsn[] = 'host=' . $value; - } - if ($value = ($params['port'] ?? null)) { - $dsn[] = 'port=' . $value; - } - if ($value = ($params['dbname'] ?? null)) { - $dsn[] = 'dbname=' . $value; - } - - return new \PDO('mysql:' . \implode(';', $dsn), $params['user'] ?? null, $params['password'] ?? null, [\PDO::ATTR_PERSISTENT => true]); - } - - /** - * Create PostgreSQL PDO connection. - */ - private function createPdoConnectionPostgreSQL(array $params): \PDO - { - $dsn = []; - if ($value = ($params['host'] ?? null)) { - $dsn[] = 'host=' . $value; - } - if ($value = ($params['port'] ?? null)) { - $dsn[] = 'port=' . $value; - } - if ($value = ($params['dbname'] ?? null)) { - $dsn[] = 'dbname=' . $value; - } - - return new \PDO('pgsql:' . \implode(';', $dsn), $params['user'] ?? null, $params['password'] ?? null, [\PDO::ATTR_PERSISTENT => true]); - } - - /** - * Create SQLite PDO connection. - */ - private function createPdoConnectionSQLite(array $params): \PDO - { - return new \PDO('sqlite:' . $params['host']); + return BridgeFactory::createPdo($params); } } diff --git a/src/Testing/FunctionalTestCaseTrait.php b/src/Testing/FunctionalTestCaseTrait.php index 5bd495a..4e730a5 100644 --- a/src/Testing/FunctionalTestCaseTrait.php +++ b/src/Testing/FunctionalTestCaseTrait.php @@ -228,16 +228,16 @@ private function getPriviledgedConnectionParameters(): array if (\str_contains($driver, 'sqlsrv')) { // https://stackoverflow.com/questions/71688125/odbc-driver-18-for-sql-serverssl-provider-error1416f086 $driverOptions['TrustServerCertificate'] = "true"; + $driverOptions['MultipleActiveResultSets'] = "false"; } return \array_filter([ 'driver' => $driver, - 'driverOptions' => $driverOptions, 'host' => \getenv('DBAL_HOST'), 'password' => \getenv('DBAL_ROOT_PASSWORD'), 'port' => \getenv('DBAL_PORT'), 'user' => \getenv('DBAL_ROOT_USER'), - ]); + ]) + $driverOptions; } /** @@ -259,11 +259,10 @@ private function getConnectionParameters(): array return \array_filter([ 'dbname' => 'test_db', 'driver' => $driver, - 'driverOptions' => $driverOptions, 'host' => \getenv('DBAL_HOST'), 'password' => \getenv('DBAL_PASSWORD'), 'port' => \getenv('DBAL_PORT'), 'user' => \getenv('DBAL_USER'), - ]); + ] + $driverOptions); } } diff --git a/src/Vendor.php b/src/Vendor.php index df69086..01ca5b5 100644 --- a/src/Vendor.php +++ b/src/Vendor.php @@ -5,6 +5,7 @@ namespace MakinaCorpus\QueryBuilder; use MakinaCorpus\QueryBuilder\Error\QueryBuilderError; +use MakinaCorpus\QueryBuilder\Version\Version; /** * RDMBS identification. @@ -40,17 +41,7 @@ public static function versionNormalize(string $version): string */ public static function versionCompare(string $userGiven, string $serverVersion, string $operator): bool { - $userGiven = self::versionNormalize($userGiven); - $serverVersion = self::versionNormalize($serverVersion); - - return match ($operator) { - '<' => 0 > \version_compare($userGiven, $serverVersion), - '<=' => 0 >= \version_compare($userGiven, $serverVersion), - '=' => 0 === \version_compare($userGiven, $serverVersion), - '>=' => 0 <= \version_compare($userGiven, $serverVersion), - '>' => 0 < \version_compare($userGiven, $serverVersion), - default => throw new QueryBuilderError("Version comparison operator must be one of '<', '<=', '=', '>=', '>'"), - }; + return (new Version($userGiven))->compare($serverVersion, $operator); } /** diff --git a/src/Version/Version.php b/src/Version/Version.php new file mode 100644 index 0000000..8baa578 --- /dev/null +++ b/src/Version/Version.php @@ -0,0 +1,79 @@ +major = (int) $matches[1]; + + if (isset($matches[3]) && $matches[3] !== '') { + $this->minor = (int) $matches[3]; + if (isset($matches[5]) && $matches[5] !== '') { + $this->patch = (int) $matches[5]; + $this->precision = 3; + } else { + $this->precision = 2; + } + } else { + $this->precision = 1; + } + } + + /** + * Versions cannot be simply + */ + private function compareTo(string|Version $other): int + { + $other = \is_string($other) ? new Version($other) : $other; + + $precision = \min($this->precision, $other->precision); + + if (1 === $precision || $this->major !== $other->major) { + return $this->major - $other->major; + } + if (2 === $precision || $this->minor !== $other->minor) { + return $this->minor - $other->minor; + } + return $this->patch - $other->patch; + } + + /** + * Is the given version OP this version? + */ + public function compare(string|Version $other, string $operator = '='): bool + { + $compare = $this->compareTo($other); + + return match ($operator) { + '<' => 0 < $compare, + '<=' => 0 <= $compare, + '=' => 0 === $compare, + '>=' => 0 >= $compare, + '>' => 0 > $compare, + default => throw new QueryBuilderError("Version comparison operator must be one of '<', '<=', '=', '>=', '>'"), + }; + } +} diff --git a/tests/Bridge/Doctrine/DoctrineQueryBuilderTest.php b/tests/Bridge/Doctrine/DoctrineBridgeTest.php similarity index 97% rename from tests/Bridge/Doctrine/DoctrineQueryBuilderTest.php rename to tests/Bridge/Doctrine/DoctrineBridgeTest.php index 197a1a1..69ca825 100644 --- a/tests/Bridge/Doctrine/DoctrineQueryBuilderTest.php +++ b/tests/Bridge/Doctrine/DoctrineBridgeTest.php @@ -6,7 +6,7 @@ use MakinaCorpus\QueryBuilder\Expression\RandomInt; -class DoctrineQueryBuilderTest extends DoctrineTestCase +class DoctrineBridgeTest extends DoctrineTestCase { public function testSelectExecuteQuery(): void { diff --git a/tests/DsnTest.php b/tests/DsnTest.php index bcdf677..a68da2e 100644 --- a/tests/DsnTest.php +++ b/tests/DsnTest.php @@ -55,7 +55,7 @@ public function testWithFilename(): void self::assertSame('sqlite-3', $dsn->getOption('server')); self::assertSame('/some/path.db', $dsn->getHost()); self::assertSame('/some/path.db', $dsn->getFilename()); - self::assertSame('default', $dsn->getDatabase()); + self::assertNull($dsn->getDatabase()); } public function testSQLiteEdgeCaseRelative(): void @@ -67,7 +67,7 @@ public function testSQLiteEdgeCaseRelative(): void self::assertSame('pdo', $dsn->getDriver()); self::assertSame('somedb.sqlite', $dsn->getFilename()); self::assertSame('somedb.sqlite', $dsn->getHost()); - self::assertSame('default', $dsn->getDatabase()); + self::assertNull($dsn->getDatabase()); } public function testSQLiteEdgeCaseAbsolute(): void @@ -78,7 +78,7 @@ public function testSQLiteEdgeCaseAbsolute(): void self::assertSame('sqlite', $dsn->getVendor()); self::assertSame('pdo', $dsn->getDriver()); self::assertSame('/somedb.sqlite', $dsn->getFilename()); - self::assertSame('default', $dsn->getDatabase()); + self::assertNull($dsn->getDatabase()); self::assertSame('/somedb.sqlite', $dsn->getHost()); } @@ -90,8 +90,7 @@ public function testSQLiteEdgeCaseNoHostRelative(): void self::assertSame('sqlite', $dsn->getVendor()); self::assertSame('pdo', $dsn->getDriver()); self::assertSame('somedb.sqlite', $dsn->getFilename()); - self::assertSame('default', $dsn->getDatabase()); - + self::assertNull($dsn->getDatabase()); self::assertSame('somedb.sqlite', $dsn->getHost()); } @@ -116,24 +115,26 @@ public function testSQLiteEdgeCaseMemoryRelative(): void { $dsn = Dsn::fromString('pdo-sqlite://:memory:'); - self::assertTrue($dsn->isFile()); + self::assertFalse($dsn->isFile()); + self::assertTrue($dsn->inMemory()); self::assertSame('sqlite', $dsn->getVendor()); self::assertSame('pdo', $dsn->getDriver()); self::assertSame(':memory:', $dsn->getHost()); self::assertSame(':memory:', $dsn->getFilename()); - self::assertSame('default', $dsn->getDatabase()); + self::assertNull($dsn->getDatabase()); } public function testSQLiteEdgeCaseMemoryAbsolute(): void { $dsn = Dsn::fromString('pdo-sqlite:///:memory:'); - self::assertTrue($dsn->isFile()); + self::assertFalse($dsn->isFile()); + self::assertTrue($dsn->inMemory()); self::assertSame('sqlite', $dsn->getVendor()); self::assertSame('pdo', $dsn->getDriver()); self::assertSame(':memory:', $dsn->getHost()); self::assertSame(':memory:', $dsn->getFilename()); - self::assertSame('default', $dsn->getDatabase()); + self::assertNull($dsn->getDatabase()); } public function testToUrl(): void diff --git a/tests/VendorTest.php b/tests/VendorTest.php new file mode 100644 index 0000000..5cbbd6b --- /dev/null +++ b/tests/VendorTest.php @@ -0,0 +1,86 @@ +=')); + self::assertTrue(Vendor::versionCompare('5.7', '5.7.44', '>=')); + self::assertFalse(Vendor::versionCompare('8.0', '5.7.44', '>=')); + + self::assertTrue(Vendor::versionCompare('5.6.0', '5.7.44', '>=')); + self::assertTrue(Vendor::versionCompare('5.7.0', '5.7.44', '>=')); + self::assertFalse(Vendor::versionCompare('8.0.0', '5.7.44', '>=')); + + self::assertTrue(Vendor::versionCompare('5.7.43', '5.7.44', '>=')); + self::assertTrue(Vendor::versionCompare('5.7.44', '5.7.44', '>=')); + self::assertFalse(Vendor::versionCompare('5.7.45', '5.7.44', '>=')); + } + + public function testVersionCompareGreaterThan(): void + { + self::assertTrue(Vendor::versionCompare('5.6', '5.7.44', '>')); + self::assertFalse(Vendor::versionCompare('5.7', '5.7.44', '>')); + self::assertFalse(Vendor::versionCompare('8.0', '5.7.44', '>')); + + self::assertTrue(Vendor::versionCompare('5.6.0', '5.7.44', '>')); + self::assertTrue(Vendor::versionCompare('5.7.0', '5.7.44', '>')); + self::assertFalse(Vendor::versionCompare('8.0.0', '5.7.44', '>')); + + self::assertTrue(Vendor::versionCompare('5.7.43', '5.7.44', '>')); + self::assertFalse(Vendor::versionCompare('5.7.44', '5.7.44', '>')); + self::assertFalse(Vendor::versionCompare('5.7.45', '5.7.44', '>')); + } +}