From 3f19742dc689a72c659ed32b6f62fda22f2e772b Mon Sep 17 00:00:00 2001 From: Vincent Quatrevieux Date: Wed, 13 Mar 2024 22:44:00 +0100 Subject: [PATCH] [FRAM-150] Migrate deprecated doctrine APIs (#88) * chore: Migrate deprecate doctrine APIs (#FRAM-150) * test: improve test coverage (#FRAM-150) --- CHANGELOG | 8 +- composer.json | 2 +- src/Configuration.php | 1 + src/Connection/ConnectionInterface.php | 24 +++ .../ConnectionClosedListenerInterface.php | 5 + src/Connection/Result/ArrayResultSet.php | 2 + src/Connection/Result/DoctrineResultSet.php | 2 + src/Connection/SimpleConnection.php | 43 +++++ src/Console/GraphCommand.php | 2 +- src/Console/MapperCommand.php | 2 +- src/IdGenerators/TableGenerator.php | 68 +++++-- src/Logger/PsrDecorator.php | 1 + src/Migration/Migration.php | 2 +- src/Platform/PlatformInterface.php | 17 ++ .../PlatformSpecificOperationInterface.php | 27 +++ src/Platform/Sql/SqlPlatform.php | 25 +++ .../Sql/SqlPlatformOperationInterface.php | 49 +++++ .../Sql/SqlPlatformOperationTrait.php | 52 ++++++ src/Prime.php | 2 +- src/Query/Compiler/SqlCompiler.php | 42 +++-- .../BulkInsert/BulkInsertSqlCompiler.php | 3 +- .../Custom/KeyValue/KeyValueSqlCompiler.php | 23 +-- .../Custom/KeyValue/OffsetExpression.php | 55 ++++++ .../AbstractPlatformSpecificExpression.php | 147 +++++++++++++++ src/Query/Expression/Aggregate.php | 10 +- src/Query/Expression/Json/JsonContains.php | 50 +++-- .../Expression/Json/JsonContainsPath.php | 53 ++++-- src/Query/Expression/Json/JsonExtract.php | 36 +++- src/Query/Expression/Json/ToJson.php | 65 +++---- src/Query/QueryRepositoryExtension.php | 2 + src/Relations/AbstractRelation.php | 2 + src/Repository/EntityRepository.php | 37 +++- .../Adapter/Doctrine/DoctrineColumn.php | 7 +- src/Schema/Comparator.php | 9 +- src/Schema/SchemaManager.php | 40 ++-- .../Doctrine/ColumnTransformer.php | 2 +- src/Schema/Visitor/Graphviz.php | 35 +++- src/Schema/Visitor/MapperVisitor.php | 41 ++++- src/Sharding/ShardingConnection.php | 2 + tests/Connection/SimpleConnectionTest.php | 27 ++- tests/Console/GraphCommandTest.php | 101 ++++++++++ .../TableGeneratorWithMysqlTest.php | 25 +++ .../Sql/SqlPlatformOperationTraitTest.php | 60 ++++++ tests/Platform/Sql/SqlPlatformTest.php | 51 ++++++ .../KeyValue/KeyValueSqlCompilerTest.php | 21 +++ ...AbstractPlatformSpecificExpressionTest.php | 120 ++++++++++++ tests/Query/QueryOrmTest.php | 3 +- tests/Query/QueryTest.php | 12 ++ tests/Schema/AbstractSchemaManagerTest.php | 2 +- .../Adapter/Doctrine/DoctrineColumnTest.php | 2 +- tests/Schema/Visitor/GraphvizTest.php | 173 ++++++++++++++++++ tests/Schema/Visitor/MapperVisitorTest.php | 20 +- tests/_files/DummyPlatform.php | 9 + 53 files changed, 1434 insertions(+), 187 deletions(-) create mode 100644 src/Platform/PlatformSpecificOperationInterface.php create mode 100644 src/Platform/Sql/SqlPlatformOperationInterface.php create mode 100644 src/Platform/Sql/SqlPlatformOperationTrait.php create mode 100644 src/Query/Custom/KeyValue/OffsetExpression.php create mode 100644 src/Query/Expression/AbstractPlatformSpecificExpression.php create mode 100644 tests/Console/GraphCommandTest.php create mode 100644 tests/IdGenerators/TableGeneratorWithMysqlTest.php create mode 100644 tests/Platform/Sql/SqlPlatformOperationTraitTest.php create mode 100644 tests/Query/Expression/AbstractPlatformSpecificExpressionTest.php create mode 100644 tests/Schema/Visitor/GraphvizTest.php diff --git a/CHANGELOG b/CHANGELOG index e15a43a..bf0ddc9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,10 +12,16 @@ v2.2.0 * `Whereable::orWhere()` on first parameter (column) * `Whereable::whereNull()` * `Whereable::whereNotNull()` - +* Feat: Add `Bdf\Prime\Platform\PlatformSpecificOperationInterface` and `PlatformInterface::apply()` to allow changing the behavior on the platform +* Change: Rework visitors to remove the use of deprecated doctrine visitor API + BC Breaks * Change: `Bdf\Prime\Configuration` instances are not shared anymore between connections. So change of the default configuration during runtime will not affect already created connections. * Change: `Bdf\Prime\Query/Expression/Raw::__toString()` is now deprecated, and its constructor value will be cast to string. +* Minimum version of doctrine/dbal is now 3.7 +* Deprecate: `Bdf\Prime\Platform\PlatformInterface::name()`. Use `PlatformInterface::apply()` instead. +* Deprecate: `Bdf\Prime\Connection\ConnectionInterface::getEventManager()` and `Bdf\Prime\Connection\Event\ConnectionClosedListenerInterface`. Use `ConnectionInterface::addConnectionClosedListener()` and `ConnectionInterface::removeConnectionClosedListener()` instead. +* Change: `Doctrine\DBAL\Schema\Column::getPlatformOptions()`/`setPlatformOptions()` is used instead of `getCustomSchemaOptions()`/`setCustomSchemaOptions()`, which can cause change on migration queries. v2.1.0 ------ diff --git a/composer.json b/composer.json index 6d060d4..cf21592 100755 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "b2pweb/bdf-event-notifier": "~1.0", "b2pweb/bdf-serializer": "~1.0", "b2pweb/bdf-util": "~1.0", - "doctrine/dbal": "~3.0", + "doctrine/dbal": "~3.7", "doctrine/inflector": "~1.0|~2.0", "doctrine/instantiator": "^1.0.3|~2.0", "psr/container": "~1.0|~2.0", diff --git a/src/Configuration.php b/src/Configuration.php index 7273e37..f3aad05 100755 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -34,6 +34,7 @@ class Configuration extends BaseConfiguration * Set configuration * * @param array $options + * @psalm-suppress DeprecatedMethod */ public function __construct(array $options = []) { diff --git a/src/Connection/ConnectionInterface.php b/src/Connection/ConnectionInterface.php index dbbe6ff..f74321b 100755 --- a/src/Connection/ConnectionInterface.php +++ b/src/Connection/ConnectionInterface.php @@ -16,12 +16,16 @@ use Bdf\Prime\Schema\Manager\DatabaseManagerInterface; use Bdf\Prime\Schema\SchemaManagerInterface; use Bdf\Prime\Types\TypeInterface; +use Closure; use Doctrine\Common\EventManager; /** * Base connection type * * Allows creating and executing queries, and handle a platform + * + * @method void addConnectionClosedListener(Closure $listener) + * @method void removeConnectionClosedListener(Closure $listener) */ interface ConnectionInterface { @@ -163,9 +167,29 @@ public function platform(): PlatformInterface; * C'est actuellement le plus simple et léger, mais ajoute une dépendence forte à Doctrine * * @internal + * @deprecated Since 2.2. Will be removed in 3.0 without replacement */ public function getEventManager(); + /** + * Add a new listener to be notified when the connection is closed or reset + * + * @param Closure(ConnectionInterface):void $listener The listener to add. The connection instance is passed as argument. + * + * @return void + */ + //public function addConnectionClosedListener(Closure $listener): void; + + /** + * Remove the connection closed listener + * If the listener is not registered, do nothing + * + * @param Closure(ConnectionInterface):void $listener The listener to remove. Should be the same instance as the added listener. + * + * @return void + */ + //public function removeConnectionClosedListener(Closure $listener): void; + /** * Closes the connection and trigger "onConnectionClosed" event * diff --git a/src/Connection/Event/ConnectionClosedListenerInterface.php b/src/Connection/Event/ConnectionClosedListenerInterface.php index ac009f3..f7677ce 100644 --- a/src/Connection/Event/ConnectionClosedListenerInterface.php +++ b/src/Connection/Event/ConnectionClosedListenerInterface.php @@ -2,8 +2,12 @@ namespace Bdf\Prime\Connection\Event; +use Bdf\Prime\Connection\ConnectionInterface; + /** * Listener for closed connection + * + * @deprecated Since 2.2. Use {@see ConnectionInterface::addConnectionClosedListener()} instead. */ interface ConnectionClosedListenerInterface { @@ -13,6 +17,7 @@ interface ConnectionClosedListenerInterface * The connection is closed * * @return void + * @deprecated Since 2.2. Use {@see ConnectionInterface::addConnectionClosedListener()} instead. */ public function onConnectionClosed(); } diff --git a/src/Connection/Result/ArrayResultSet.php b/src/Connection/Result/ArrayResultSet.php index 51abc8e..c1a63e7 100644 --- a/src/Connection/Result/ArrayResultSet.php +++ b/src/Connection/Result/ArrayResultSet.php @@ -40,6 +40,8 @@ public function __construct($array = [], $flags = 0) /** * {@inheritdoc} + * + * @psalm-suppress DeprecatedConstant */ public function fetchMode($mode, $options = null) { diff --git a/src/Connection/Result/DoctrineResultSet.php b/src/Connection/Result/DoctrineResultSet.php index 1c31c25..cb11aed 100644 --- a/src/Connection/Result/DoctrineResultSet.php +++ b/src/Connection/Result/DoctrineResultSet.php @@ -93,6 +93,8 @@ public function valid(): bool /** * {@inheritdoc} + * + * @psalm-suppress DeprecatedConstant */ public function fetchMode($mode, $options = null) { diff --git a/src/Connection/SimpleConnection.php b/src/Connection/SimpleConnection.php index d276f1e..cf49bf9 100755 --- a/src/Connection/SimpleConnection.php +++ b/src/Connection/SimpleConnection.php @@ -38,6 +38,8 @@ use Doctrine\DBAL\Result; use Doctrine\DBAL\Statement; +use function spl_object_id; + /** * Connection * @@ -72,6 +74,14 @@ class SimpleConnection extends BaseConnection implements ConnectionInterface, Tr */ private $factory; + /** + * List of listeners to call when the connection is closed, + * indexed by the listener object id + * + * @var array + */ + private array $onConnectionClosedListeners = []; + /** * SimpleConnection constructor. * @@ -170,6 +180,28 @@ public function platform(): PlatformInterface return $this->platform; } + /** + * {@inheritdoc} + * + * @param Closure(ConnectionInterface):void $listener + */ + public function addConnectionClosedListener(Closure $listener): void + { + $id = spl_object_id($listener); + + $this->onConnectionClosedListeners[$id] = $listener; + } + + /** + * {@inheritdoc} + */ + public function removeConnectionClosedListener(Closure $listener): void + { + $id = spl_object_id($listener); + + unset($this->onConnectionClosedListeners[$id]); + } + /** * {@inheritdoc} */ @@ -399,18 +431,29 @@ public function rollBack(): bool /** * {@inheritdoc} + * + * @psalm-suppress DeprecatedProperty + * @psalm-suppress DeprecatedClass */ public function close(): void { parent::close(); + // To remove in 3.0 $this->_eventManager->dispatchEvent(ConnectionClosedListenerInterface::EVENT_NAME); + + foreach ($this->onConnectionClosedListeners as $listener) { + $listener($this); + } } /** * Setup the logger by setting the connection * * @return void + * @psalm-suppress DeprecatedMethod + * @psalm-suppress DeprecatedClass + * @todo remove on prime 3.0 */ protected function prepareLogger(): void { diff --git a/src/Console/GraphCommand.php b/src/Console/GraphCommand.php index f8edc11..0e4a9d9 100755 --- a/src/Console/GraphCommand.php +++ b/src/Console/GraphCommand.php @@ -68,7 +68,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int : $this->getSchemaFromModel($io, $io->argument('path')); $graph = new Graphviz(); - $schema->visit($graph); + $graph->onSchema($schema); if (!$io->option('output')) { $io->line($graph->getOutput()); diff --git a/src/Console/MapperCommand.php b/src/Console/MapperCommand.php index 1760c3b..6329a94 100755 --- a/src/Console/MapperCommand.php +++ b/src/Console/MapperCommand.php @@ -74,7 +74,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $visitor = new MapperVisitor($connection->getName(), $this->locator->mappers()->getNameResolver()); - $schema->visit($visitor); + $visitor->onSchema($schema); if (!$io->option('output')) { $io->line($visitor->getOutput()); diff --git a/src/IdGenerators/TableGenerator.php b/src/IdGenerators/TableGenerator.php index 7daaf38..944989b 100644 --- a/src/IdGenerators/TableGenerator.php +++ b/src/IdGenerators/TableGenerator.php @@ -5,7 +5,18 @@ use Bdf\Prime\Connection\ConnectionInterface; use Bdf\Prime\Exception\PrimeException; use Bdf\Prime\Mapper\Metadata; +use Bdf\Prime\Platform\Sql\SqlPlatform; +use Bdf\Prime\Platform\Sql\SqlPlatformOperationInterface; +use Bdf\Prime\Platform\Sql\SqlPlatformOperationTrait; use Bdf\Prime\ServiceLocator; +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\SqlitePlatform; + +use LogicException; + +use function get_class; +use function method_exists; /** * Sequence table @@ -45,24 +56,49 @@ protected function doGenerate($property, array &$data, ServiceLocator $serviceLo */ protected function incrementSequence(ConnectionInterface $connection, Metadata $metadata) { + $table = $metadata->sequence['table']; + $column = $metadata->sequence['column']; + $platform = $connection->platform(); - switch ($platform->name()) { - case 'mysql': - $connection->executeUpdate('UPDATE '.$metadata->sequence['table'] - .' SET '. $metadata->sequence['column'].' = LAST_INSERT_ID('.$metadata->sequence['column'].'+1)'); - return (string) $connection->lastInsertId(); - - case 'sqlite': - $connection->executeUpdate('UPDATE '.$metadata->sequence['table'] - .' SET '.$metadata->sequence['column'].' = '.$metadata->sequence['column'].'+1'); - return (string) $connection->executeQuery('SELECT '.$metadata->sequence['column'] - .' FROM '.$metadata->sequence['table'])->fetchOne(); - - default: - return (string) $connection->executeQuery( - $platform->grammar()->getSequenceNextValSQL($metadata->sequence['table']) - )->fetchOne(); + if (!method_exists($platform, 'apply')) { + throw new LogicException('The platform ' . get_class($platform) . ' does not support the method apply().'); } + + return $platform->apply(new class ($connection, $table, $column) implements SqlPlatformOperationInterface { + use SqlPlatformOperationTrait; + + /** @var \Bdf\Prime\Connection\ConnectionInterface&\Doctrine\DBAL\Connection */ + private ConnectionInterface $connection; + private string $table; + private string $column; + + /** + * @param \Bdf\Prime\Connection\ConnectionInterface&\Doctrine\DBAL\Connection $connection + */ + public function __construct(ConnectionInterface $connection, string $table, string $column) + { + $this->connection = $connection; + $this->table = $table; + $this->column = $column; + } + + public function onMysqlPlatform(SqlPlatform $platform, AbstractMySQLPlatform $grammar): string + { + $this->connection->executeStatement('UPDATE '.$this->table.' SET '. $this->column.' = LAST_INSERT_ID('.$this->column.'+1)'); + return (string) $this->connection->lastInsertId(); + } + + public function onSqlitePlatform(SqlPlatform $platform, SqlitePlatform $grammar): string + { + $this->connection->executeStatement('UPDATE '.$this->table.' SET '.$this->column.' = '.$this->column.'+1'); + return (string) $this->connection->executeQuery('SELECT '.$this->column.' FROM '.$this->table)->fetchOne(); + } + + public function onGenericSqlPlatform(SqlPlatform $platform, AbstractPlatform $grammar): string + { + return (string) $this->connection->executeQuery($grammar->getSequenceNextValSQL($this->table))->fetchOne(); + } + }); } } diff --git a/src/Logger/PsrDecorator.php b/src/Logger/PsrDecorator.php index 030fdc8..98afdb9 100644 --- a/src/Logger/PsrDecorator.php +++ b/src/Logger/PsrDecorator.php @@ -13,6 +13,7 @@ * * @deprecated Since Prime 2.2. Use middleware instead. * @see \Bdf\Prime\Connection\Middleware\LoggerMiddleware for the replacement + * @psalm-suppress DeprecatedInterface */ class PsrDecorator implements SQLLogger, ConnectionAwareInterface { diff --git a/src/Migration/Migration.php b/src/Migration/Migration.php index dbd5421..f4dd4d9 100755 --- a/src/Migration/Migration.php +++ b/src/Migration/Migration.php @@ -211,7 +211,7 @@ public function query($sql, array $params = [], $connectionName = null): Result */ public function update($sql, array $params = [], $connectionName = null) { - return $this->connection($connectionName)->executeUpdate($sql, $params); + return (int) $this->connection($connectionName)->executeStatement($sql, $params); } /** diff --git a/src/Platform/PlatformInterface.php b/src/Platform/PlatformInterface.php index 7564dc4..b50e243 100644 --- a/src/Platform/PlatformInterface.php +++ b/src/Platform/PlatformInterface.php @@ -6,6 +6,8 @@ /** * Interface for Prime connection platforms + * + * @method mixed apply(PlatformSpecificOperationInterface $operation) */ interface PlatformInterface { @@ -13,6 +15,7 @@ interface PlatformInterface * Get the platform name * * @return string + * @deprecated Since 2.2. Use {@see PlatformInterface::apply()} to discriminate platform. */ public function name(): string; @@ -30,6 +33,20 @@ public function types(): PlatformTypesInterface; * Get the platform grammar instance * * @return AbstractPlatform + * @internal This method should not be used by end user. Use {@see PlatformInterface::apply()} instead. */ public function grammar(); + + /** + * Apply the operation on the platform + * + * The platform will try to find the correct operation to apply. + * If the operation do not support the platform, {@see PlatformSpecificOperationInterface::onUnknownPlatform()} will be called. + * + * @param PlatformSpecificOperationInterface $operation + * @return R The value returned by the operation + * + * @template R + */ + //public function apply(PlatformSpecificOperationInterface $operation); } diff --git a/src/Platform/PlatformSpecificOperationInterface.php b/src/Platform/PlatformSpecificOperationInterface.php new file mode 100644 index 0000000..40ce64a --- /dev/null +++ b/src/Platform/PlatformSpecificOperationInterface.php @@ -0,0 +1,27 @@ +grammar->getName(); } @@ -94,4 +98,25 @@ public function grammar() { return $this->grammar; } + + /** + * {@inheritdoc} + */ + public function apply(PlatformSpecificOperationInterface $operation) + { + $grammar = $this->grammar; + + if (!$operation instanceof SqlPlatformOperationInterface) { + return $operation->onUnknownPlatform($this, $this->grammar); + } + + switch (true) { + case $grammar instanceof AbstractMySQLPlatform: + return $operation->onMysqlPlatform($this, $grammar); + case $grammar instanceof SqlitePlatform: + return $operation->onSqlitePlatform($this, $grammar); + default: + return $operation->onGenericSqlPlatform($this, $grammar); + } + } } diff --git a/src/Platform/Sql/SqlPlatformOperationInterface.php b/src/Platform/Sql/SqlPlatformOperationInterface.php new file mode 100644 index 0000000..cdcd346 --- /dev/null +++ b/src/Platform/Sql/SqlPlatformOperationInterface.php @@ -0,0 +1,49 @@ + + */ +interface SqlPlatformOperationInterface extends PlatformSpecificOperationInterface +{ + /** + * Fallback method when the specific platform is not found + * + * @param SqlPlatform $platform + * @param AbstractPlatform $grammar + * + * @return R + */ + public function onGenericSqlPlatform(SqlPlatform $platform, AbstractPlatform $grammar); + + /** + * Apply operation on a MySQL platform (or compatible like MariaDB) + * + * @param SqlPlatform $platform + * @param AbstractMySQLPlatform $grammar + * + * @return R + */ + public function onMysqlPlatform(SqlPlatform $platform, AbstractMySQLPlatform $grammar); + + /** + * Apply operation on a SQLite platform + * + * @param SqlPlatform $platform + * @param SqlitePlatform $grammar + * + * @return R + */ + public function onSqlitePlatform(SqlPlatform $platform, SqlitePlatform $grammar); +} diff --git a/src/Platform/Sql/SqlPlatformOperationTrait.php b/src/Platform/Sql/SqlPlatformOperationTrait.php new file mode 100644 index 0000000..0a7bbbc --- /dev/null +++ b/src/Platform/Sql/SqlPlatformOperationTrait.php @@ -0,0 +1,52 @@ +onUnknownPlatform($platform, $grammar); + } + + /** + * {@inheritdoc} + */ + public function onMysqlPlatform(SqlPlatform $platform, AbstractMySQLPlatform $grammar) + { + return $this->onGenericSqlPlatform($platform, $grammar); + } + + /** + * {@inheritdoc} + */ + public function onSqlitePlatform(SqlPlatform $platform, SqlitePlatform $grammar) + { + return $this->onGenericSqlPlatform($platform, $grammar); + } + + /** + * {@inheritdoc} + */ + public function onUnknownPlatform(PlatformInterface $platform, object $grammar) + { + throw new BadMethodCallException('The platform ' . get_class($platform) . ' is not supported by ' . static::class); + } +} diff --git a/src/Prime.php b/src/Prime.php index 30e68b0..9d38383 100755 --- a/src/Prime.php +++ b/src/Prime.php @@ -460,7 +460,7 @@ protected static function initialize() if ($logger = static::$config['logger'] ?? null) { if ($logger instanceof SQLLogger) { - /** @psalm-suppress InternalMethod */ + /** @psalm-suppress DeprecatedMethod */ $configuration->setSQLLogger($logger); } elseif ($logger instanceof LoggerInterface) { $configuration->setMiddlewares([new LoggerMiddleware($logger)]); diff --git a/src/Query/Compiler/SqlCompiler.php b/src/Query/Compiler/SqlCompiler.php index 5d33eec..9929026 100755 --- a/src/Query/Compiler/SqlCompiler.php +++ b/src/Query/Compiler/SqlCompiler.php @@ -14,10 +14,12 @@ use Bdf\Prime\Query\SqlQueryInterface; use Bdf\Prime\Types\TypeInterface; use Doctrine\DBAL\LockMode; +use Doctrine\DBAL\Platforms\SqlitePlatform; use Doctrine\DBAL\Query\Expression\CompositeExpression; use UnexpectedValueException; use function is_string; +use function sprintf; /** * Base compiler for SQL queries @@ -74,7 +76,7 @@ protected function doCompileInsert(CompilableClause $query) $query->state()->currentPart = 0; if ($query->statements['ignore'] && $this->platform()->grammar()->getReservedKeywordsList()->isKeyword('IGNORE')) { - if ($this->platform()->grammar()->getName() === 'sqlite') { + if ($this->platform()->grammar() instanceof SqlitePlatform) { $insert = 'INSERT OR IGNORE INTO '; } else { $insert = 'INSERT IGNORE INTO '; @@ -400,12 +402,12 @@ protected function compileAggregate(CompilableClause $query, $function, $column, } switch ($function) { - case 'avg' : return $this->platform()->grammar()->getAvgExpression($column).' AS aggregate'; - case 'count': return $this->platform()->grammar()->getCountExpression($column).' AS aggregate'; - case 'max' : return $this->platform()->grammar()->getMaxExpression($column).' AS aggregate'; - case 'min' : return $this->platform()->grammar()->getMinExpression($column).' AS aggregate'; - case 'pagination': return $this->platform()->grammar()->getCountExpression($column).' AS aggregate'; - case 'sum' : return $this->platform()->grammar()->getSumExpression($column).' AS aggregate'; + case 'avg' : return "AVG($column) AS aggregate"; + case 'count': return "COUNT($column) AS aggregate"; + case 'max' : return "MAX($column) AS aggregate"; + case 'min' : return "MIN($column) AS aggregate"; + case 'pagination': return "COUNT($column) AS aggregate"; + case 'sum' : return "SUM($column) AS aggregate"; default: $method = 'get'.ucfirst($function).'Expression'; @@ -721,7 +723,7 @@ protected function compileExpression(CompilableClause $query, $column, string $o case 'in': case ':in': if (empty($value)) { - return $this->platform()->grammar()->getIsNullExpression($this->compileLeftExpression($query, $column)); + return $this->compileLeftExpression($query, $column) . ' IS NULL'; } return $this->compileInExpression($query, $value, $column, 'IN', $converted); @@ -729,27 +731,39 @@ protected function compileExpression(CompilableClause $query, $column, string $o case '!in': case ':notin': if (empty($value)) { - return $this->platform()->grammar()->getIsNotNullExpression($this->compileLeftExpression($query, $column)); + return $this->compileLeftExpression($query, $column) . ' IS NOT NULL'; } return $this->compileInExpression($query, $value, $column, 'NOT IN', $converted); case 'between': case ':between': if (is_array($value)) { - return $this->platform()->grammar()->getBetweenExpression($this->compileLeftExpression($query, $column), $this->compileExpressionValue($query, $value[0], $converted), $this->compileExpressionValue($query, $value[1], $converted)); + return sprintf( + '%s BETWEEN %s AND %s', + $this->compileLeftExpression($query, $column), + $this->compileExpressionValue($query, $value[0], $converted), + $this->compileExpressionValue($query, $value[1], $converted) + ); } - return $this->platform()->grammar()->getBetweenExpression($this->compileLeftExpression($query, $column), '0', $this->compileExpressionValue($query, $value, $converted)); + + // @todo deprecate ? + return sprintf( + '%s BETWEEN %s AND %s', + $this->compileLeftExpression($query, $column), + '0', + $this->compileExpressionValue($query, $value, $converted) + ); case '!between': case ':notbetween': - return $this->platform()->grammar()->getNotExpression($this->compileExpression($query, $column, ':between', $value, $converted)); + return 'NOT(' . $this->compileExpression($query, $column, ':between', $value, $converted). ')'; case '<>': case '!=': case ':ne': case ':not': if (is_null($value)) { - return $this->platform()->grammar()->getIsNotNullExpression($this->compileLeftExpression($query, $column)); + return $this->compileLeftExpression($query, $column) . ' IS NOT NULL'; } if (is_array($value)) { return $this->compileExpression($query, $column, ':notin', $value, $converted); @@ -759,7 +773,7 @@ protected function compileExpression(CompilableClause $query, $column, string $o case '=': case ':eq': if (is_null($value)) { - return $this->platform()->grammar()->getIsNullExpression($this->compileLeftExpression($query, $column)); + return $this->compileLeftExpression($query, $column) . ' IS NULL'; } if (is_array($value)) { return $this->compileExpression($query, $column, ':in', $value, $converted); diff --git a/src/Query/Custom/BulkInsert/BulkInsertSqlCompiler.php b/src/Query/Custom/BulkInsert/BulkInsertSqlCompiler.php index 4a8f088..5454f71 100644 --- a/src/Query/Custom/BulkInsert/BulkInsertSqlCompiler.php +++ b/src/Query/Custom/BulkInsert/BulkInsertSqlCompiler.php @@ -7,6 +7,7 @@ use Bdf\Prime\Query\Compiler\AbstractCompiler; use Bdf\Prime\Query\Compiler\QuoteCompilerInterface; use Bdf\Prime\Types\TypeInterface; +use Doctrine\DBAL\Platforms\SqlitePlatform; /** * Compiler for @see BulkInsertQuery @@ -120,7 +121,7 @@ private function compileMode(CompilableClause $query) return 'REPLACE'; case BulkInsertQuery::MODE_IGNORE: - if ($this->platform()->grammar()->getName() === 'sqlite') { + if ($this->platform()->grammar() instanceof SqlitePlatform) { return 'INSERT OR IGNORE'; } else { return 'INSERT IGNORE'; diff --git a/src/Query/Custom/KeyValue/KeyValueSqlCompiler.php b/src/Query/Custom/KeyValue/KeyValueSqlCompiler.php index 1658e12..1f01c6b 100644 --- a/src/Query/Custom/KeyValue/KeyValueSqlCompiler.php +++ b/src/Query/Custom/KeyValue/KeyValueSqlCompiler.php @@ -182,12 +182,12 @@ private function compileAggregate(CompilableClause $query, $function, $column) } switch ($function) { - case 'avg' : return $this->platform()->grammar()->getAvgExpression($column).' AS aggregate'; - case 'count': return $this->platform()->grammar()->getCountExpression($column).' AS aggregate'; - case 'max' : return $this->platform()->grammar()->getMaxExpression($column).' AS aggregate'; - case 'min' : return $this->platform()->grammar()->getMinExpression($column).' AS aggregate'; - case 'pagination': return $this->platform()->grammar()->getCountExpression($column).' AS aggregate'; - case 'sum' : return $this->platform()->grammar()->getSumExpression($column).' AS aggregate'; + case 'avg' : return "AVG($column) AS aggregate"; + case 'count': return "COUNT($column) AS aggregate"; + case 'max' : return "MAX($column) AS aggregate"; + case 'min' : return "MIN($column) AS aggregate"; + case 'pagination': return "COUNT($column) AS aggregate"; + case 'sum' : return "SUM($column) AS aggregate"; default: $method = 'get'.ucfirst($function).'Expression'; @@ -246,16 +246,7 @@ private function compileLimit(CompilableClause $query) } if (!isset($query->statements['limit'])) { - switch ($this->platform()->name()) { - case 'sqlite': - return ' LIMIT -1 OFFSET '.$query->statements['offset']; - - case 'mysql': - return ' LIMIT 18446744073709551615 OFFSET '.$query->statements['offset']; - - default: - return ' OFFSET '.$query->statements['offset']; - } + return $this->platform()->apply(new OffsetExpression($query->statements['offset'])); } // Use prepared only for pagination diff --git a/src/Query/Custom/KeyValue/OffsetExpression.php b/src/Query/Custom/KeyValue/OffsetExpression.php new file mode 100644 index 0000000..4c77908 --- /dev/null +++ b/src/Query/Custom/KeyValue/OffsetExpression.php @@ -0,0 +1,55 @@ + + */ +final class OffsetExpression implements SqlPlatformOperationInterface +{ + use SqlPlatformOperationTrait; + + private int $offset; + + /** + * @param int $offset + */ + public function __construct(int $offset) + { + $this->offset = $offset; + } + + /** + * {@inheritdoc} + */ + public function onSqlitePlatform(SqlPlatform $platform, SqlitePlatform $grammar): string + { + return ' LIMIT -1 OFFSET '.$this->offset; + } + + /** + * {@inheritdoc} + */ + public function onMysqlPlatform(SqlPlatform $platform, AbstractMySQLPlatform $grammar): string + { + return ' LIMIT 18446744073709551615 OFFSET '.$this->offset; + } + + /** + * {@inheritdoc} + */ + public function onGenericSqlPlatform(SqlPlatform $platform, AbstractPlatform $grammar): string + { + return ' OFFSET '.$this->offset; + } +} diff --git a/src/Query/Expression/AbstractPlatformSpecificExpression.php b/src/Query/Expression/AbstractPlatformSpecificExpression.php new file mode 100644 index 0000000..ff79204 --- /dev/null +++ b/src/Query/Expression/AbstractPlatformSpecificExpression.php @@ -0,0 +1,147 @@ + + */ +abstract class AbstractPlatformSpecificExpression implements ExpressionInterface, SqlPlatformOperationInterface +{ + use SqlPlatformOperationTrait; + + private Q $query; + private CompilerInterface $compiler; + + /** + * {@inheritdoc} + */ + final public function build(Q $query, object $compiler) + { + // @todo create a dedicated interface for platform() getter ? + if (!$compiler instanceof CompilerInterface) { + throw new BadMethodCallException('The expression ' . static::class . ' is not supported by the current compiler'); + } + + $configured = clone $this; + $configured->query = $query; + $configured->compiler = $compiler; + + $platform = $compiler->platform(); + + if (!method_exists($platform, 'apply')) { + throw new LogicException('The platform ' . get_class($platform) . ' does not support the method apply().'); + } + + return $platform->apply($configured); + } + + /** + * Compile the expression for MySQL platform + * + * @param Q $query + * @param CompilerInterface $compiler + * @param SqlPlatform $platform + * @param AbstractMySQLPlatform $grammar + * + * @return string + */ + protected function buildForMySql(Q $query, CompilerInterface $compiler, SqlPlatform $platform, AbstractMySQLPlatform $grammar): string + { + return $this->buildForGenericSql($query, $compiler, $platform, $grammar); + } + + /** + * Compile the expression for SQLite platform + * + * @param Q $query + * @param CompilerInterface $compiler + * @param SqlPlatform $platform + * @param SqlitePlatform $grammar + * + * @return string + */ + protected function buildForSqlite(Q $query, CompilerInterface $compiler, SqlPlatform $platform, SqlitePlatform $grammar): string + { + return $this->buildForGenericSql($query, $compiler, $platform, $grammar); + } + + /** + * Compile the expression for generic SQL platform + * + * @param Q $query + * @param CompilerInterface $compiler + * @param SqlPlatform $platform + * @param AbstractPlatform $grammar + * + * @return string + */ + protected function buildForGenericSql(Q $query, CompilerInterface $compiler, SqlPlatform $platform, AbstractPlatform $grammar): string + { + return $this->buildForUnknownPlatform($query, $compiler, $platform, $grammar); + } + + /** + * Compile the expression for unknown platform + * + * @param Q $query + * @param CompilerInterface $compiler + * @param PlatformInterface $platform + * @param object $grammar + * + * @return string + */ + protected function buildForUnknownPlatform(Q $query, CompilerInterface $compiler, PlatformInterface $platform, object $grammar): string + { + throw new BadMethodCallException('The expression ' . static::class . ' is not supported by the platform ' . get_class($platform)); + } + + /** + * {@inheritdoc} + */ + final public function onGenericSqlPlatform(SqlPlatform $platform, AbstractPlatform $grammar) + { + return $this->buildForGenericSql($this->query, $this->compiler, $platform, $grammar); + } + + /** + * {@inheritdoc} + */ + final public function onMysqlPlatform(SqlPlatform $platform, AbstractMySQLPlatform $grammar) + { + return $this->buildForMySql($this->query, $this->compiler, $platform, $grammar); + } + + /** + * {@inheritdoc} + */ + final public function onSqlitePlatform(SqlPlatform $platform, SqlitePlatform $grammar) + { + return $this->buildForSqlite($this->query, $this->compiler, $platform, $grammar); + } + + /** + * {@inheritdoc} + */ + final public function onUnknownPlatform(PlatformInterface $platform, object $grammar) + { + return $this->buildForUnknownPlatform($this->query, $this->compiler, $platform, $grammar); + } +} diff --git a/src/Query/Expression/Aggregate.php b/src/Query/Expression/Aggregate.php index 1fbf562..7180bb1 100644 --- a/src/Query/Expression/Aggregate.php +++ b/src/Query/Expression/Aggregate.php @@ -77,7 +77,7 @@ public static function min(string $attribute): self return new class ($attribute) extends Aggregate { protected function expression(AbstractPlatform $platform, string $attribute): string { - return $platform->getMinExpression($attribute); + return 'MIN('.$attribute.')'; } }; } @@ -94,7 +94,7 @@ public static function max(string $attribute): self return new class ($attribute) extends Aggregate { protected function expression(AbstractPlatform $platform, string $attribute): string { - return $platform->getMaxExpression($attribute); + return 'MAX('.$attribute.')'; } }; } @@ -111,7 +111,7 @@ public static function avg(string $attribute): self return new class ($attribute) extends Aggregate { protected function expression(AbstractPlatform $platform, string $attribute): string { - return $platform->getAvgExpression($attribute); + return 'AVG('.$attribute.')'; } }; } @@ -128,7 +128,7 @@ public static function count(string $attribute = '*'): self return new class ($attribute) extends Aggregate { protected function expression(AbstractPlatform $platform, string $attribute): string { - return $platform->getCountExpression($attribute); + return 'COUNT('.$attribute.')'; } }; } @@ -145,7 +145,7 @@ public static function sum(string $attribute): self return new class ($attribute) extends Aggregate { protected function expression(AbstractPlatform $platform, string $attribute): string { - return $platform->getSumExpression($attribute); + return 'SUM('.$attribute.')'; } }; } diff --git a/src/Query/Expression/Json/JsonContains.php b/src/Query/Expression/Json/JsonContains.php index 134ae47..6e07a08 100644 --- a/src/Query/Expression/Json/JsonContains.php +++ b/src/Query/Expression/Json/JsonContains.php @@ -2,13 +2,16 @@ namespace Bdf\Prime\Query\Expression\Json; +use Bdf\Prime\Platform\Sql\SqlPlatform; use Bdf\Prime\Query\CompilableClause as Q; use Bdf\Prime\Query\Compiler\CompilerInterface; use Bdf\Prime\Query\Compiler\QuoteCompilerInterface; +use Bdf\Prime\Query\Expression\AbstractPlatformSpecificExpression; use Bdf\Prime\Query\Expression\ExpressionInterface; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\SqlitePlatform; use InvalidArgumentException; use LogicException; - use phpDocumentor\Reflection\Types\Scalar; use function get_debug_type; @@ -26,7 +29,7 @@ * $query->whereRaw(new JsonContains(new JsonExtract('json_field', '$.tags', false), 'value')); // Search from a nested field "tags". Note that the value is not unquoted * */ -final class JsonContains implements ExpressionInterface +final class JsonContains extends AbstractPlatformSpecificExpression { /** * @var string|ExpressionInterface @@ -55,26 +58,41 @@ public function __construct($target, $candidate) /** * {@inheritdoc} */ - public function build(Q $query, object $compiler): string + protected function buildForSqlite(Q $query, CompilerInterface $compiler, SqlPlatform $platform, SqlitePlatform $grammar): string { - if (!$compiler instanceof QuoteCompilerInterface || !$compiler instanceof CompilerInterface) { + if (!$compiler instanceof QuoteCompilerInterface) { throw new LogicException('JsonContains expression is not supported by the current compiler'); } - $target = $this->target instanceof ExpressionInterface - ? $this->target->build($query, $compiler) - : $compiler->quoteIdentifier($query, $query->preprocessor()->field($this->target)) - ; + return self::getSqliteExpression( + $compiler, + $this->target($query, $compiler), + $this->candidate + ); + } - $dbms = $compiler->platform()->name(); + /** + * {@inheritdoc} + */ + protected function buildForGenericSql(Q $query, CompilerInterface $compiler, SqlPlatform $platform, AbstractPlatform $grammar): string + { + if (!$compiler instanceof QuoteCompilerInterface) { + throw new LogicException('JsonContains expression is not supported by the current compiler'); + } - switch ($dbms) { - case 'sqlite': - return self::getSqliteExpression($compiler, $target, $this->candidate); + return self::getDefaultExpression( + $compiler, + $this->target($query, $compiler), + $this->candidate + ); + } - default: - return self::getDefaultExpression($compiler, $target, $this->candidate); - } + private function target(Q $query, QuoteCompilerInterface $compiler): string + { + return $this->target instanceof ExpressionInterface + ? $this->target->build($query, $compiler) + : $compiler->quoteIdentifier($query, $query->preprocessor()->field($this->target)) + ; } /** @@ -98,7 +116,7 @@ private static function getSqliteExpression(QuoteCompilerInterface $compiler, st * * @return string */ - private static function getDefaultExpression(CompilerInterface $compiler, string $target, $candidate): string + private static function getDefaultExpression(QuoteCompilerInterface $compiler, string $target, $candidate): string { $candidate = $compiler->quote(json_encode($candidate)); diff --git a/src/Query/Expression/Json/JsonContainsPath.php b/src/Query/Expression/Json/JsonContainsPath.php index 4face22..7ed3e6e 100644 --- a/src/Query/Expression/Json/JsonContainsPath.php +++ b/src/Query/Expression/Json/JsonContainsPath.php @@ -2,10 +2,14 @@ namespace Bdf\Prime\Query\Expression\Json; +use Bdf\Prime\Platform\Sql\SqlPlatform; use Bdf\Prime\Query\CompilableClause as Q; use Bdf\Prime\Query\Compiler\CompilerInterface; use Bdf\Prime\Query\Compiler\QuoteCompilerInterface; +use Bdf\Prime\Query\Expression\AbstractPlatformSpecificExpression; use Bdf\Prime\Query\Expression\ExpressionInterface; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\SqlitePlatform; use LogicException; /** @@ -17,7 +21,7 @@ * $query->whereRaw(new JsonContainsPath('json_field', '$.bar')); // Search rows that contains has the "bar" field the json_field * */ -final class JsonContainsPath implements ExpressionInterface +final class JsonContainsPath extends AbstractPlatformSpecificExpression { /** * @var string|ExpressionInterface @@ -35,29 +39,44 @@ public function __construct($target, string $path) $this->path = $path; } - /** - * {@inheritdoc} - */ - public function build(Q $query, object $compiler): string + protected function buildForSqlite(Q $query, CompilerInterface $compiler, SqlPlatform $platform, SqlitePlatform $grammar): string { - if (!$compiler instanceof QuoteCompilerInterface || !$compiler instanceof CompilerInterface) { + if (!$compiler instanceof QuoteCompilerInterface) { throw new LogicException('JsonContainsPath expression is not supported by the current compiler'); } - $target = $this->target instanceof ExpressionInterface - ? $this->target->build($query, $compiler) - : $compiler->quoteIdentifier($query, $query->preprocessor()->field($this->target)) - ; + return self::getSqliteExpression( + $compiler, + $this->target($query, $compiler), + $this->path + ); + } - $dbms = $compiler->platform()->name(); + protected function buildForGenericSql(Q $query, CompilerInterface $compiler, SqlPlatform $platform, AbstractPlatform $grammar): string + { + if (!$compiler instanceof QuoteCompilerInterface) { + throw new LogicException('JsonContainsPath expression is not supported by the current compiler'); + } - switch ($dbms) { - case 'sqlite': - return self::getSqliteExpression($compiler, $target, $this->path); + return self::getDefaultExpression( + $compiler, + $this->target($query, $compiler), + $this->path + ); + } - default: - return self::getDefaultExpression($compiler, $target, $this->path); - } + /** + * @param Q $query + * @param CompilerInterface&QuoteCompilerInterface $compiler + * @return string + * @throws \Bdf\Prime\Exception\PrimeException + */ + private function target(Q $query, CompilerInterface $compiler) + { + return $this->target instanceof ExpressionInterface + ? $this->target->build($query, $compiler) + : $compiler->quoteIdentifier($query, $query->preprocessor()->field($this->target)) + ; } /** diff --git a/src/Query/Expression/Json/JsonExtract.php b/src/Query/Expression/Json/JsonExtract.php index 9abcd34..7bea9fb 100644 --- a/src/Query/Expression/Json/JsonExtract.php +++ b/src/Query/Expression/Json/JsonExtract.php @@ -2,12 +2,17 @@ namespace Bdf\Prime\Query\Expression\Json; +use Bdf\Prime\Platform\Sql\SqlPlatform; use Bdf\Prime\Query\CompilableClause as Q; use Bdf\Prime\Query\Compiler\CompilerInterface; use Bdf\Prime\Query\Compiler\QuoteCompilerInterface; -use Bdf\Prime\Query\Expression\ExpressionInterface; +use Bdf\Prime\Query\Expression\AbstractPlatformSpecificExpression; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\SqlitePlatform; use LogicException; +use function sprintf; + /** * Extract a value from a JSON field * Use ->> operator, or JSON_EXTRACT() function @@ -16,7 +21,7 @@ * * @see https://mariadb.com/kb/en/jsonpath-expressions/ For the path syntax */ -final class JsonExtract implements ExpressionInterface +final class JsonExtract extends AbstractPlatformSpecificExpression { private string $field; private string $path; @@ -39,28 +44,41 @@ public function __construct(string $field, string $path, bool $unquote = true) /** * {@inheritdoc} */ - public function build(Q $query, object $compiler): string + protected function buildForSqlite(Q $query, CompilerInterface $compiler, SqlPlatform $platform, SqlitePlatform $grammar): string { - if (!$compiler instanceof QuoteCompilerInterface || !$compiler instanceof CompilerInterface) { + if (!$compiler instanceof QuoteCompilerInterface) { throw new LogicException('JsonExtract expression is not supported by the current compiler'); } $field = $compiler->quoteIdentifier($query, $query->preprocessor()->field($this->field)); - $dbms = $compiler->platform()->name(); return sprintf( - self::getExpression($dbms, $this->unquote), + $this->unquote ? '%s->>%s' : '%s->%s', $field, (string) $compiler->quote($this->path) ); } - private static function getExpression(string $dbms, bool $unquote): string + /** + * {@inheritdoc} + */ + protected function buildForGenericSql(Q $query, CompilerInterface $compiler, SqlPlatform $platform, AbstractPlatform $grammar): string { - if ($dbms === 'sqlite') { - return $unquote ? '%s->>%s' : '%s->%s'; + if (!$compiler instanceof QuoteCompilerInterface) { + throw new LogicException('JsonExtract expression is not supported by the current compiler'); } + $field = $compiler->quoteIdentifier($query, $query->preprocessor()->field($this->field)); + + return sprintf( + self::getExpression($this->unquote), + $field, + (string) $compiler->quote($this->path) + ); + } + + private static function getExpression(bool $unquote): string + { $expression = 'JSON_EXTRACT(%s, %s)'; if ($unquote) { diff --git a/src/Query/Expression/Json/ToJson.php b/src/Query/Expression/Json/ToJson.php index 6c7d6cc..1059de2 100644 --- a/src/Query/Expression/Json/ToJson.php +++ b/src/Query/Expression/Json/ToJson.php @@ -2,11 +2,16 @@ namespace Bdf\Prime\Query\Expression\Json; +use Bdf\Prime\Platform\Sql\SqlPlatform; use Bdf\Prime\Query\CompilableClause as Q; use Bdf\Prime\Query\Compiler\CompilerInterface; use Bdf\Prime\Query\Compiler\QuoteCompilerInterface; +use Bdf\Prime\Query\Expression\AbstractPlatformSpecificExpression; use Bdf\Prime\Query\Expression\ExpressionInterface; +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\MariaDBPlatform; +use Doctrine\DBAL\Platforms\SqlitePlatform; use LogicException; use function array_is_list; @@ -24,7 +29,7 @@ /** * Expression for convert a value to json */ -final class ToJson implements ExpressionInterface +final class ToJson extends AbstractPlatformSpecificExpression { /** * @var mixed|ExpressionInterface @@ -42,34 +47,14 @@ public function __construct($value) /** * {@inheritdoc} */ - public function build(Q $query, object $compiler): string + protected function buildForSqlite(Q $query, CompilerInterface $compiler, SqlPlatform $platform, SqlitePlatform $grammar): string { - if (!$compiler instanceof QuoteCompilerInterface || !$compiler instanceof CompilerInterface) { + if (!$compiler instanceof QuoteCompilerInterface) { throw new LogicException('ToJson expression is not supported by the current compiler'); } - $dbms = $compiler->platform()->name(); + $value = $this->value; - switch ($dbms) { - case 'sqlite': - return $this->buildSqliteExpression($query, $compiler, $this->value); - - case 'mysql': - return $this->buildMysqlExpression($query, $compiler, $this->value); - - default: - return $this->buildDefaultExpression($query, $compiler, $this->value); - } - } - - /** - * @param QuoteCompilerInterface&CompilerInterface $compiler - * @param mixed|ExpressionInterface $value - * - * @return string - */ - private function buildSqliteExpression(Q $query, $compiler, $value): string - { if ($value instanceof ExpressionInterface) { $value = $value->build($query, $compiler); } else { @@ -80,32 +65,38 @@ private function buildSqliteExpression(Q $query, $compiler, $value): string } /** - * @param QuoteCompilerInterface&CompilerInterface $compiler - * @param mixed|ExpressionInterface $value - * - * @return string + * {@inheritdoc} */ - private function buildMysqlExpression(Q $query, CompilerInterface $compiler, $value) + protected function buildForMySql(Q $query, CompilerInterface $compiler, SqlPlatform $platform, AbstractMySQLPlatform $grammar): string { + if (!$compiler instanceof QuoteCompilerInterface) { + throw new LogicException('ToJson expression is not supported by the current compiler'); + } + + $value = $this->value; + if ($value instanceof ExpressionInterface) { $value = $value->build($query, $compiler); } else { $value = $compiler->quote(json_encode($value)); } - $function = $compiler->platform()->grammar() instanceof MariaDBPlatform ? 'JSON_COMPACT(%s)' : 'CAST(%s AS JSON)'; + $function = $grammar instanceof MariaDBPlatform ? 'JSON_COMPACT(%s)' : 'CAST(%s AS JSON)'; return sprintf($function, (string) $value); } /** - * @param QuoteCompilerInterface&CompilerInterface $compiler - * @param mixed|ExpressionInterface $value - * - * @return string + * {@inheritdoc} */ - private function buildDefaultExpression(Q $query, $compiler, $value) + protected function buildForGenericSql(Q $query, CompilerInterface $compiler, SqlPlatform $platform, AbstractPlatform $grammar): string { + if (!$compiler instanceof QuoteCompilerInterface) { + throw new LogicException('ToJson expression is not supported by the current compiler'); + } + + $value = $this->value; + if (!$value instanceof ExpressionInterface) { return $this->convertValue($compiler, $value); } @@ -120,12 +111,12 @@ private function buildDefaultExpression(Q $query, $compiler, $value) * - array list will be converted to JSON_ARRAY(...) * - array and object will be converted to JSON_OBJECT(...) * - * @param QuoteCompilerInterface&CompilerInterface $compiler + * @param QuoteCompilerInterface $compiler * @param mixed $value * * @return string */ - private function convertValue($compiler, $value): string + private function convertValue(QuoteCompilerInterface $compiler, $value): string { if (is_string($value)) { return (string) $compiler->quote($value); diff --git a/src/Query/QueryRepositoryExtension.php b/src/Query/QueryRepositoryExtension.php index 4038128..e4716d8 100644 --- a/src/Query/QueryRepositoryExtension.php +++ b/src/Query/QueryRepositoryExtension.php @@ -154,6 +154,7 @@ public function get(ReadCommandInterface $query, $id, $attributes = null) */ public function getOrFail(ReadCommandInterface $query, $id, $attributes = null) { + /** @psalm-suppress DeprecatedMethod */ $entity = $this->get($query, $id, $attributes); if ($entity !== null) { @@ -175,6 +176,7 @@ public function getOrFail(ReadCommandInterface $query, $id, $attributes = null) */ public function getOrNew(ReadCommandInterface $query, $id, $attributes = null) { + /** @psalm-suppress DeprecatedMethod */ $entity = $this->get($query, $id, $attributes); if ($entity !== null) { diff --git a/src/Relations/AbstractRelation.php b/src/Relations/AbstractRelation.php index 6d588bc..c04489d 100644 --- a/src/Relations/AbstractRelation.php +++ b/src/Relations/AbstractRelation.php @@ -246,6 +246,7 @@ public function isLoaded($entity): bool */ public function clearInfo($entity): void { + /** @psalm-suppress DeprecatedMethod */ $this->relationInfo->clear($entity); } @@ -357,6 +358,7 @@ protected function setRelation($entity, $relation): void if ($relation !== null) { $this->relationInfo->markAsLoaded($entity); } else { + /** @psalm-suppress DeprecatedMethod */ $this->relationInfo->clear($entity); } } diff --git a/src/Repository/EntityRepository.php b/src/Repository/EntityRepository.php index 376373b..c7acb2f 100755 --- a/src/Repository/EntityRepository.php +++ b/src/Repository/EntityRepository.php @@ -37,6 +37,8 @@ use Doctrine\Common\EventSubscriber; use Exception; +use function method_exists; + /** * Db repository * @@ -57,6 +59,8 @@ * @method E getOrFail(mixed $key) * @method E getOrNew(mixed $key) * @method QueryInterface filter(Closure $filter) + * + * @psalm-suppress DeprecatedInterface */ class EntityRepository implements RepositoryInterface, EventSubscriber, ConnectionClosedListenerInterface, RepositoryEventsSubscriberInterface { @@ -115,6 +119,11 @@ class EntityRepository implements RepositoryInterface, EventSubscriber, Connecti */ protected $connection; + /** + * @var Closure(ConnectionInterface):void + */ + private Closure $onCloseListener; + /** * Constructor @@ -128,6 +137,7 @@ public function __construct(Mapper $mapper, ServiceLocator $serviceLocator, ?Cac $this->resultCache = $cache; $this->mapper = $mapper; $this->serviceLocator = $serviceLocator; + $this->onCloseListener = fn (ConnectionInterface $connection) => $this->reset(); $this->collectionFactory = CollectionFactory::forRepository($this); $this->queries = new RepositoryQueryFactory($this, $cache, $serviceLocator->mappers()->getMetadataCache()); @@ -255,7 +265,13 @@ public function connection(): ConnectionInterface if ($this->connection === null) { //Repository query factory load the connection on its constructor. Use lazy to let the connection being loaded as late as possible. $this->connection = $this->serviceLocator->connection($this->mapper->metadata()->connection); - $this->connection->getEventManager()->addEventSubscriber($this); + + if (method_exists($this->connection, 'addConnectionClosedListener')) { + $this->connection->addConnectionClosedListener($this->onCloseListener); + } else { + /** @psalm-suppress DeprecatedMethod */ + $this->connection->getEventManager()->addEventSubscriber($this); + } } return $this->connection; @@ -988,9 +1004,12 @@ public function onConnectionClosed() /** * {@inheritdoc} + * + * @deprecated Since 2.2, will be removed in 3.0. */ public function getSubscribedEvents() { + /** @psalm-suppress DeprecatedClass */ return [ConnectionClosedListenerInterface::EVENT_NAME]; } @@ -1019,7 +1038,13 @@ public function free($entity) public function destroy(): void { if ($this->connection !== null) { - $this->connection->getEventManager()->removeEventSubscriber($this); + if (method_exists($this->connection, 'removeConnectionClosedListener')) { + $this->connection->removeConnectionClosedListener($this->onCloseListener); + } else { + /** @psalm-suppress DeprecatedMethod */ + $this->connection->getEventManager()->removeEventSubscriber($this); + } + $this->connection = null; } @@ -1068,7 +1093,13 @@ private function changeActiveConnection($connectionName) private function reset(): void { if ($this->connection !== null) { - $this->connection->getEventManager()->removeEventSubscriber($this); + if (method_exists($this->connection, 'removeConnectionClosedListener')) { + $this->connection->removeConnectionClosedListener($this->onCloseListener); + } else { + /** @psalm-suppress DeprecatedMethod */ + $this->connection->getEventManager()->removeEventSubscriber($this); + } + $this->connection = null; } diff --git a/src/Schema/Adapter/Doctrine/DoctrineColumn.php b/src/Schema/Adapter/Doctrine/DoctrineColumn.php index 623ecea..177c826 100644 --- a/src/Schema/Adapter/Doctrine/DoctrineColumn.php +++ b/src/Schema/Adapter/Doctrine/DoctrineColumn.php @@ -6,6 +6,7 @@ use Bdf\Prime\Schema\ColumnInterface; use Bdf\Prime\Types\TypesRegistryInterface; use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Types\Type; /** * Adapt doctrine column to prime column @@ -50,7 +51,7 @@ public function name(): string */ public function type(): PlatformTypeInterface { - return $this->types->get($this->column->getType()->getName()); + return $this->types->get(Type::lookupName($this->column->getType())); } /** @@ -130,7 +131,7 @@ public function scale(): ?int */ public function options(): array { - return $this->column->getCustomSchemaOptions(); + return $this->column->getPlatformOptions(); } /** @@ -138,6 +139,6 @@ public function options(): array */ public function option(string $name) { - return $this->column->getCustomSchemaOption($name); + return $this->column->getPlatformOption($name); } } diff --git a/src/Schema/Comparator.php b/src/Schema/Comparator.php index f30e537..93b9c82 100644 --- a/src/Schema/Comparator.php +++ b/src/Schema/Comparator.php @@ -4,13 +4,14 @@ use Doctrine\DBAL\Schema\Comparator as BaseComparator; use Doctrine\DBAL\Schema\Table as BaseTable; +use Doctrine\DBAL\Schema\TableDiff; /** * Schema comparator * * @package Bdf\Prime\Schema * - * @deprecated since 1.3 Use Prime comparators instead + * @internal Use {@see SchemaManager::diff()} instead */ class Comparator extends BaseComparator { @@ -36,11 +37,11 @@ public function setListDropColumn($flag): void /** * {@inheritdoc} */ - public function diffTable(BaseTable $fromTable, BaseTable $toTable) + public function compareTables(BaseTable $fromTable, BaseTable $toTable): TableDiff { - $diff = parent::diffTable($fromTable, $toTable); + $diff = parent::compareTables($fromTable, $toTable); - if ($diff && !$this->listDropColumn) { + if (!$this->listDropColumn) { /** @psalm-suppress InternalProperty */ $diff->removedColumns = []; } diff --git a/src/Schema/SchemaManager.php b/src/Schema/SchemaManager.php index efbb753..a2efb49 100644 --- a/src/Schema/SchemaManager.php +++ b/src/Schema/SchemaManager.php @@ -6,10 +6,10 @@ use Bdf\Prime\Schema\Adapter\Doctrine\DoctrineTable as PrimeTableAdapter; use Bdf\Prime\Schema\Transformer\Doctrine\TableTransformer; use Doctrine\DBAL\Exception as DoctrineDBALException; +use Doctrine\DBAL\Schema\AbstractSchemaManager as DoctrineSchemaManager; use Doctrine\DBAL\Schema\Schema as DoctrineSchema; use Doctrine\DBAL\Schema\SchemaConfig; use Doctrine\DBAL\Schema\SchemaDiff as DoctrineSchemaDiff; -use Doctrine\DBAL\Schema\TableDiff as DoctrineTableDiff; use Doctrine\DBAL\Schema\Table as DoctrineTable; /** @@ -25,17 +25,15 @@ class SchemaManager extends AbstractSchemaManager * * @var array */ - private $queries = []; - + private array $queries = []; + private ?DoctrineSchemaManager $doctrineManager = null; /** * Get the doctrine schema manager - * - * @return \Doctrine\DBAL\Schema\AbstractSchemaManager */ - public function getDoctrineManager() + public function getDoctrineManager(): DoctrineSchemaManager { - return $this->connection->getSchemaManager(); + return $this->doctrineManager ??= $this->connection->createSchemaManager(); } /** @@ -111,7 +109,7 @@ public function schema($tables = []) */ public function loadSchema() { - return $this->getDoctrineManager()->createSchema(); + return $this->getDoctrineManager()->introspectSchema(); } /** @@ -193,19 +191,13 @@ public function load(string $name): TableInterface try { $manager = $this->getDoctrineManager(); - $foreignKeys = []; - - if ($this->platform->grammar()->supportsForeignKeyConstraints()) { - $foreignKeys = $manager->listTableForeignKeys($name); - } - return new PrimeTableAdapter( new DoctrineTable( $name, $manager->listTableColumns($name), $manager->listTableIndexes($name), [], - $foreignKeys + $manager->listTableForeignKeys($name) ), $this->connection->platform()->types() ); @@ -258,7 +250,7 @@ public function diff($new, $old) $comparator = new Comparator(); $comparator->setListDropColumn($this->useDrop); - return $comparator->compare( + return $comparator->compareSchemas( $this->schema($old), $this->schema($new) ); @@ -269,21 +261,13 @@ public function diff($new, $old) */ public function rename(string $from, string $to) { - /** @psalm-suppress InternalMethod */ - $diff = new DoctrineTableDiff($from); - $diff->newName = $to; - try { if ($this->generateRollback) { - /** @psalm-suppress InternalMethod */ - $rollbackDiff = new DoctrineTableDiff($to); - $rollbackDiff->newName = $from; - - $this->pushRollback($this->platform->grammar()->getAlterTableSQL($rollbackDiff)); + $this->pushRollback($this->platform->grammar()->getRenameTableSQL($to, $from)); } return $this->push( - $this->platform->grammar()->getAlterTableSQL($diff) + $this->platform->grammar()->getRenameTableSQL($from, $to) ); } catch (DoctrineDBALException $e) { /** @psalm-suppress InvalidScalarArgument */ @@ -296,7 +280,9 @@ public function rename(string $from, string $to) */ public function push($queries) { - if ($queries instanceof DoctrineSchema || $queries instanceof DoctrineSchemaDiff) { + if ($queries instanceof DoctrineSchemaDiff) { + $queries = $this->platform->grammar()->getAlterSchemaSQL($queries); + } elseif ($queries instanceof DoctrineSchema) { $queries = $queries->toSql($this->platform->grammar()); } diff --git a/src/Schema/Transformer/Doctrine/ColumnTransformer.php b/src/Schema/Transformer/Doctrine/ColumnTransformer.php index fe3cd95..f347caa 100644 --- a/src/Schema/Transformer/Doctrine/ColumnTransformer.php +++ b/src/Schema/Transformer/Doctrine/ColumnTransformer.php @@ -51,7 +51,7 @@ public function toDoctrine() $this->columnOptions() ); - $column->setCustomSchemaOptions($this->column->options()); + $column->setPlatformOptions($this->column->options()); return $column; } diff --git a/src/Schema/Visitor/Graphviz.php b/src/Schema/Visitor/Graphviz.php index 33b7186..8b10b60 100644 --- a/src/Schema/Visitor/Graphviz.php +++ b/src/Schema/Visitor/Graphviz.php @@ -6,11 +6,14 @@ use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Schema\Visitor\AbstractVisitor; +use Doctrine\DBAL\Types\Type; /** * Create a Graphviz output of a Schema. * * @package Bdf\Prime\Schema\Visitor + * @psalm-suppress DeprecatedClass + * @psalm-suppress DeprecatedInterface */ class Graphviz extends AbstractVisitor { @@ -19,13 +22,39 @@ class Graphviz extends AbstractVisitor */ protected $output = ''; + /** + * Parse the schema and create to build the graphviz output + * + * @param Schema $schema + * @return void + * + * @throws \Doctrine\DBAL\Schema\SchemaException + */ + public function onSchema(Schema $schema): void + { + $this->acceptSchema($schema); + + foreach ($schema->getTables() as $table) { + $this->onTable($table); + } + } + + private function onTable(Table $table): void + { + $this->acceptTable($table); + + foreach ($table->getForeignKeys() as $foreignKey) { + $this->acceptForeignKey($table, $foreignKey); + } + } + /** * {@inheritdoc} */ public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) { $this->output .= $this->createNodeRelation( - $fkConstraint->getLocalTableName().':col'.current($fkConstraint->getLocalColumns()).':se', + $localTable->getName().':col'.current($fkConstraint->getLocalColumns()).':se', $fkConstraint->getForeignTableName().':col'.current($fkConstraint->getForeignColumns()).':se', [ 'dir' => 'back', @@ -81,7 +110,7 @@ protected function createTableLabel(Table $table) foreach ($table->getColumns() as $column) { $columnName = $column->getName(); - if ($table->hasPrimaryKey() && in_array($column->getName(), $table->getPrimaryKey()->getColumns())) { + if (($pk = $table->getPrimaryKey()) && in_array($column->getName(), $pk->getColumns())) { $columnName = ''.$columnName.''; } @@ -91,7 +120,7 @@ protected function createTableLabel(Table $table) . $columnName . '' . '' - . ''.lcfirst($column->getType()->getName()).'' + . ''.lcfirst(Type::lookupName($column->getType())).'' . '' . ''; } diff --git a/src/Schema/Visitor/MapperVisitor.php b/src/Schema/Visitor/MapperVisitor.php index 8b9d540..db63fb9 100644 --- a/src/Schema/Visitor/MapperVisitor.php +++ b/src/Schema/Visitor/MapperVisitor.php @@ -13,9 +13,12 @@ use Doctrine\DBAL\Schema\Sequence; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Schema\Visitor\AbstractVisitor; +use Doctrine\DBAL\Types\Type; /** * Create a mapper output from a Schema. + * @psalm-suppress DeprecatedClass + * @psalm-suppress DeprecatedInterface */ class MapperVisitor extends AbstractVisitor { @@ -112,6 +115,36 @@ public function __construct($connectionName = null, ResolverInterface $nameResol $this->inflector = $inflector ?: new SimpleInfector(); } + /** + * Parse the schema to create mapper definition + * + * @param Schema $schema + * @return void + * + * @throws \Doctrine\DBAL\Schema\SchemaException + */ + public function onSchema(Schema $schema): void + { + $this->acceptSchema($schema); + + foreach ($schema->getTables() as $table) { + $this->onTable($table); + } + } + + private function onTable(Table $table): void + { + $this->acceptTable($table); + + foreach ($table->getColumns() as $column) { + $this->acceptColumn($table, $column); + } + + foreach ($table->getIndexes() as $index) { + $this->acceptIndex($table, $index); + } + } + /** * {@inheritdoc} */ @@ -128,9 +161,10 @@ public function acceptTable(Table $table) $primaries = []; $sequence = null; $tableName = $table->getName(); + $pkIndex = $table->getPrimaryKey(); // Evaluate metadata for primary keys - if ($table->hasPrimaryKey()) { + if ($pkIndex) { // prepare sequence info for the method Mapper::sequence() and the metadata primary $sequence = $this->inflector->getSequenceName($tableName); if (!$this->schema->hasTable($sequence)) { @@ -138,8 +172,7 @@ public function acceptTable(Table $table) } // get the type of primary - foreach ($table->getPrimaryKeyColumns() as $primary) { - $primary = $primary->getName(); + foreach ($pkIndex->getColumns() as $primary) { $column = $table->getColumn($primary); if ($column->getAutoincrement()) { @@ -168,7 +201,7 @@ public function acceptTable(Table $table) public function acceptColumn(Table $table, Column $column) { $field = $column->getName(); - $type = $column->getType()->getName(); + $type = Type::lookupName($column->getType()); $default = $column->getDefault(); $length = $column->getLength(); $tableName = $table->getName(); diff --git a/src/Sharding/ShardingConnection.php b/src/Sharding/ShardingConnection.php index deee788..0494421 100644 --- a/src/Sharding/ShardingConnection.php +++ b/src/Sharding/ShardingConnection.php @@ -438,6 +438,8 @@ public function lastInsertId($name = null) /** * {@inheritdoc} + * + * @psalm-suppress DeprecatedMethod */ public function getWrappedConnection() { diff --git a/tests/Connection/SimpleConnectionTest.php b/tests/Connection/SimpleConnectionTest.php index 6987794..8ff54f9 100755 --- a/tests/Connection/SimpleConnectionTest.php +++ b/tests/Connection/SimpleConnectionTest.php @@ -482,7 +482,7 @@ public function type(): string /** * */ - public function test_close_should_call_listener() + public function test_close_should_call_listener_legacy() { $listener = $this->createMock(ConnectionClosedListenerInterface::class); $this->connection->getEventManager()->addEventListener(ConnectionClosedListenerInterface::EVENT_NAME, $listener); @@ -492,6 +492,31 @@ public function test_close_should_call_listener() $this->connection->close(); } + /** + * + */ + public function test_close_should_call_listener() + { + $called = false; + $parameter = false; + $listener = function ($connection) use(&$called, &$parameter) { + $called = true; + $parameter = $connection; + }; + + $this->connection->addConnectionClosedListener($listener); + $this->connection->close(); + + $this->assertTrue($called); + $this->assertSame($this->connection, $parameter); + + $called = false; + $this->connection->removeConnectionClosedListener($listener); + + $this->connection->close(); + $this->assertFalse($called); + } + /** * @group prime-reconnection */ diff --git a/tests/Console/GraphCommandTest.php b/tests/Console/GraphCommandTest.php new file mode 100644 index 0000000..036adeb --- /dev/null +++ b/tests/Console/GraphCommandTest.php @@ -0,0 +1,101 @@ +primeStart(); + + $this->command = new GraphCommand($this->prime()); + } + + /** + * + */ + protected function tearDown(): void + { + $this->primeReset(); + } + + /** + * + */ + public function test_execute() + { + $tester = new CommandTester($this->command); + $tester->execute(['path' => __DIR__.'/UpgradeModels']); + + $lines = explode(PHP_EOL, $tester->getDisplay(true)); + $id = substr($lines[0], 9, 40); + + $this->assertEquals(<<addressidintegerstreetstringnumberintegercitystringzipCodestringcountrystring> shape=plaintext ] +person [label=<
person
idinteger
firstNamestring
lastNamestring
address_idinteger
> shape=plaintext ] +} + +OUT + , implode(PHP_EOL, $lines) +); + } + + /** + * + */ + public function test_execute_output_file() + { + $out = tempnam(sys_get_temp_dir(), 'graph'); + + $tester = new CommandTester($this->command); + $tester->execute([ + 'path' => __DIR__.'/UpgradeModels', + '--output' => $out + ]); + + $lines = file($out); + $id = substr($lines[0], 9, 40); + + $this->assertEquals(<<addressidintegerstreetstringnumberintegercitystringzipCodestringcountrystring> shape=plaintext ] +person [label=<
person
idinteger
firstNamestring
lastNamestring
address_idinteger
> shape=plaintext ] +} +OUT + , implode($lines) +); + } +} diff --git a/tests/IdGenerators/TableGeneratorWithMysqlTest.php b/tests/IdGenerators/TableGeneratorWithMysqlTest.php new file mode 100644 index 0000000..0c1eda2 --- /dev/null +++ b/tests/IdGenerators/TableGeneratorWithMysqlTest.php @@ -0,0 +1,25 @@ +prime()->connections()->removeConnection('test'); + $this->prime()->connections()->declareConnection('test', MYSQL_CONNECTION_DSN); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->prime()->repository(TableUser::class)->schema()->drop(); + $this->unsetPrime(); + } +} diff --git a/tests/Platform/Sql/SqlPlatformOperationTraitTest.php b/tests/Platform/Sql/SqlPlatformOperationTraitTest.php new file mode 100644 index 0000000..0bb24f5 --- /dev/null +++ b/tests/Platform/Sql/SqlPlatformOperationTraitTest.php @@ -0,0 +1,60 @@ +assertSame('generic', $op->onMysqlPlatform(new SqlPlatform(new MySQLPlatform(), new TypesRegistry()), new MySQLPlatform())); + $this->assertSame('generic', $op->onSqlitePlatform(new SqlPlatform(new SqlitePlatform(), new TypesRegistry()), new SqlitePlatform())); + } + + public function test_forward_to_onUnknownPlatform() + { + $op = new class implements SqlPlatformOperationInterface { + use SqlPlatformOperationTrait; + + public function onUnknownPlatform(PlatformInterface $platform, object $grammar) + { + return 'unknown'; + } + }; + + $this->assertSame('unknown', $op->onMysqlPlatform(new SqlPlatform(new MySQLPlatform(), new TypesRegistry()), new MySQLPlatform())); + $this->assertSame('unknown', $op->onSqlitePlatform(new SqlPlatform(new SqlitePlatform(), new TypesRegistry()), new SqlitePlatform())); + $this->assertSame('unknown', $op->onGenericSqlPlatform(new SqlPlatform(new SqlitePlatform(), new TypesRegistry()), new SqlitePlatform())); + } + + public function test_default_should_raise_exception() + { + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('The platform Bdf\Prime\Platform\Sql\SqlPlatform is not supported by '); + + $op = new class implements SqlPlatformOperationInterface { + use SqlPlatformOperationTrait; + }; + + $op->onMysqlPlatform(new SqlPlatform(new MySQLPlatform(), new TypesRegistry()), new MySQLPlatform()); + } +} diff --git a/tests/Platform/Sql/SqlPlatformTest.php b/tests/Platform/Sql/SqlPlatformTest.php index 36e2889..8460bcf 100644 --- a/tests/Platform/Sql/SqlPlatformTest.php +++ b/tests/Platform/Sql/SqlPlatformTest.php @@ -2,6 +2,7 @@ namespace Bdf\Prime\Platform\Sql; +use Bdf\Prime\Platform\PlatformSpecificOperationInterface; use Bdf\Prime\Platform\PlatformTypes; use Bdf\Prime\Platform\Sql\Types\SqlBooleanType; use Bdf\Prime\Platform\Sql\Types\SqlDateTimeType; @@ -12,6 +13,8 @@ use Bdf\Prime\Types\TypesRegistry; use DateTime; use Doctrine\DBAL\Platforms\MySQLPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\DBAL\Platforms\SqlitePlatform; use PHPUnit\Framework\TestCase; /** @@ -41,6 +44,54 @@ public function test_name() $this->assertEquals('mysql', $this->platform->name()); } + public function test_apply_not_sql_operation() + { + $operation = $this->createMock(PlatformSpecificOperationInterface::class); + $operation->expects($this->once())->method('onUnknownPlatform') + ->with($this->platform, $this->platform->grammar()) + ->willReturn('foo') + ; + + $this->assertSame('foo', $this->platform->apply($operation)); + } + + public function test_apply_sql_operation_mysql() + { + $operation = $this->createMock(SqlPlatformOperationInterface::class); + $operation->expects($this->once())->method('onMysqlPlatform') + ->with($this->platform, $this->platform->grammar()) + ->willReturn('foo') + ; + + $this->assertSame('foo', $this->platform->apply($operation)); + } + + public function test_apply_sql_operation_sqlite() + { + $this->platform = new SqlPlatform(new SqlitePlatform(), new TypesRegistry()); + + $operation = $this->createMock(SqlPlatformOperationInterface::class); + $operation->expects($this->once())->method('onSqlitePlatform') + ->with($this->platform, $this->platform->grammar()) + ->willReturn('foo') + ; + + $this->assertSame('foo', $this->platform->apply($operation)); + } + + public function test_apply_sql_operation_other() + { + $this->platform = new SqlPlatform(new PostgreSQLPlatform(), new TypesRegistry()); + + $operation = $this->createMock(SqlPlatformOperationInterface::class); + $operation->expects($this->once())->method('onGenericSqlPlatform') + ->with($this->platform, $this->platform->grammar()) + ->willReturn('foo') + ; + + $this->assertSame('foo', $this->platform->apply($operation)); + } + /** * */ diff --git a/tests/Query/Custom/KeyValue/KeyValueSqlCompilerTest.php b/tests/Query/Custom/KeyValue/KeyValueSqlCompilerTest.php index ac1db86..ce91e47 100644 --- a/tests/Query/Custom/KeyValue/KeyValueSqlCompilerTest.php +++ b/tests/Query/Custom/KeyValue/KeyValueSqlCompilerTest.php @@ -8,6 +8,7 @@ use Bdf\Prime\Query\Compiler\Preprocessor\OrmPreprocessor; use Bdf\Prime\Query\Contract\Compilable; use Bdf\Prime\Query\Expression\Raw; +use Bdf\Prime\Repository\EntityRepository; use Bdf\Prime\TestEntity; use Bdf\Prime\User; use PHPUnit\Framework\TestCase; @@ -183,6 +184,26 @@ public function test_compileSelect_offset_simple() $this->assertSame([5], $this->compiler->getBindings($query)); } + /** + * + */ + public function test_compileSelect_offset_simple_mysql() + { + $this->prime()->connections()->declareConnection('mysql', MYSQL_CONNECTION_DSN); + $this->connection = Prime::connection('mysql'); + + TestEntity::repository()->on('mysql', function (EntityRepository $repository) { + $repository->schema()->migrate(); + }); + + $this->compiler = new KeyValueSqlCompiler($this->connection); + + $query = $this->query()->from('test_')->where('id', 5)->offset(1); + + $this->assertEquals($this->connection->prepare('SELECT * FROM test_ WHERE id = ? LIMIT 18446744073709551615 OFFSET 1'), $this->compiler->compileSelect($query)); + $this->assertSame([5], $this->compiler->getBindings($query)); + } + /** * */ diff --git a/tests/Query/Expression/AbstractPlatformSpecificExpressionTest.php b/tests/Query/Expression/AbstractPlatformSpecificExpressionTest.php new file mode 100644 index 0000000..74a9a63 --- /dev/null +++ b/tests/Query/Expression/AbstractPlatformSpecificExpressionTest.php @@ -0,0 +1,120 @@ +unsetPrime(); + } + + public function test_default_implementation() + { + $expr = new class extends AbstractPlatformSpecificExpression { }; + + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('The expression ' . get_class($expr) . ' is not supported by the platform Bdf\Prime\Platform\Sql\SqlPlatform'); + + $this->setupConnection('sqlite::memory:'); + + TestEntity::where('name', $expr)->toSql(); + } + + public function test_should_forward_to_buildForUnknownPlatform() + { + $expr = new class extends AbstractPlatformSpecificExpression { + protected function buildForUnknownPlatform(Q $query, CompilerInterface $compiler, PlatformInterface $platform, object $grammar): string + { + return 'foo'; + } + }; + + $this->setupConnection('sqlite::memory:'); + + $this->assertSame('SELECT t0.* FROM test_ t0 WHERE t0.name = foo', TestEntity::where('name', $expr)->toSql()); + } + + public function test_should_discriminate_platform() + { + $expr = new class extends AbstractPlatformSpecificExpression { + protected function buildForMySql(Q $query, CompilerInterface $compiler, SqlPlatform $platform, AbstractMySQLPlatform $grammar): string + { + return 'mysql'; + } + + protected function buildForSqlite(Q $query, CompilerInterface $compiler, SqlPlatform $platform, SqlitePlatform $grammar): string + { + return 'sqlite'; + } + + protected function buildForGenericSql(Q $query, CompilerInterface $compiler, SqlPlatform $platform, AbstractPlatform $grammar): string + { + return 'generic'; + } + }; + + $this->setupConnection('sqlite::memory:'); + $this->assertSame('SELECT t0.* FROM test_ t0 WHERE t0.name = sqlite', TestEntity::where('name', $expr)->toSql()); + + $this->setupConnection(MYSQL_CONNECTION_DSN); + $this->assertSame('SELECT t0.* FROM test_ t0 WHERE t0.name = mysql', TestEntity::where('name', $expr)->toSql()); + + $this->setupConnection([ + 'adapter' => 'sqlite', + 'memory' => true, + 'platform' => new PostgreSQLPlatform(), + ]); + $this->assertSame('SELECT t0.* FROM test_ t0 WHERE t0.name = generic', TestEntity::where('name', $expr)->toSql()); + } + + public function test_invalid_compiler() + { + $expr = new class extends AbstractPlatformSpecificExpression { }; + + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('The expression ' . get_class($expr) . ' is not supported by the current compiler'); + + $expr->build($this->createMock(CompilableClause::class), new \stdClass()); + } + + public function test_invalid_platform() + { + $expr = new class extends AbstractPlatformSpecificExpression { }; + $compiler = $this->createMock(CompilerInterface::class); + $platform = $this->createMock(PlatformInterface::class); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The platform ' . get_class($platform) . ' does not support the method apply().'); + + $compiler->expects($this->once())->method('platform')->willReturn($platform); + + $expr->build($this->createMock(CompilableClause::class), $compiler); + } + + private function setupConnection($dsn): void + { + $this->configurePrime(); + + $this->prime()->connections()->removeConnection('test'); + $this->prime()->connections()->declareConnection('test', $dsn); + } +} diff --git a/tests/Query/QueryOrmTest.php b/tests/Query/QueryOrmTest.php index 6339882..8b648c7 100644 --- a/tests/Query/QueryOrmTest.php +++ b/tests/Query/QueryOrmTest.php @@ -23,6 +23,7 @@ use Bdf\Prime\TestFiltersEntityMapper; use Bdf\Prime\User; use Doctrine\DBAL\Cache\ArrayResult; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Result; use InvalidArgumentException; use PHPUnit\Framework\TestCase; @@ -113,7 +114,7 @@ public function test_insert_ignore() $this->assertStringContainsString('IGNORE', $this->query->toSql()); - if ($this->repository->connection()->platform()->name() === 'mysql') { + if ($this->repository->connection()->platform()->grammar() instanceof MySQLPlatform) { $this->assertEquals("INSERT IGNORE INTO $this->table (id, name, foreign_key) VALUES(?, ?, ?)", $this->query->toSql()); } else { $this->assertEquals("INSERT OR IGNORE INTO $this->table (id, name, foreign_key) VALUES(?, ?, ?)", $this->query->toSql()); diff --git a/tests/Query/QueryTest.php b/tests/Query/QueryTest.php index 4d27225..552326d 100755 --- a/tests/Query/QueryTest.php +++ b/tests/Query/QueryTest.php @@ -817,6 +817,18 @@ public function test_between() $this->assertEquals([1, 3], $query->getBindings()); } + /** + * + */ + public function test_between_one_operand() + { + $query = $this->query() + ->where(['id :between' => 3]); + + $this->assertEquals("SELECT * FROM test_ WHERE id BETWEEN 0 AND ?", $query->toSql()); + $this->assertEquals([3], $query->getBindings()); + } + /** * */ diff --git a/tests/Schema/AbstractSchemaManagerTest.php b/tests/Schema/AbstractSchemaManagerTest.php index 64dcad6..add1d24 100644 --- a/tests/Schema/AbstractSchemaManagerTest.php +++ b/tests/Schema/AbstractSchemaManagerTest.php @@ -175,7 +175,7 @@ public function test_change_table() $this->assertEquals([ 'CREATE TEMPORARY TABLE __temp__test_ AS SELECT id, name, foreign_key, date_insert FROM test_', 'DROP TABLE test_', - 'CREATE TABLE test_ (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, foreign_key INTEGER DEFAULT NULL, date_insert DATETIME DEFAULT NULL)', + 'CREATE TABLE test_ (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL COLLATE "BINARY", foreign_key INTEGER DEFAULT NULL, date_insert DATETIME DEFAULT NULL)', 'INSERT INTO test_ (id, name, foreign_key, date_insert) SELECT id, name, foreign_key, date_insert FROM __temp__test_', 'DROP TABLE __temp__test_', ], $schema->rollbackQueries()); diff --git a/tests/Schema/Adapter/Doctrine/DoctrineColumnTest.php b/tests/Schema/Adapter/Doctrine/DoctrineColumnTest.php index c347caf..acab2c1 100644 --- a/tests/Schema/Adapter/Doctrine/DoctrineColumnTest.php +++ b/tests/Schema/Adapter/Doctrine/DoctrineColumnTest.php @@ -101,7 +101,7 @@ public function test_options() $this->types ); - $doctrine->setCustomSchemaOptions(['foo' => 'bar']); + $doctrine->setPlatformOptions(['foo' => 'bar']); $this->assertEquals(['foo' => 'bar'], $column->options()); $this->assertEquals('bar', $column->option('foo')); diff --git a/tests/Schema/Visitor/GraphvizTest.php b/tests/Schema/Visitor/GraphvizTest.php new file mode 100644 index 0000000..ec38e04 --- /dev/null +++ b/tests/Schema/Visitor/GraphvizTest.php @@ -0,0 +1,173 @@ +configurePrime(); + } + + protected function tearDown(): void + { + $this->unsetPrime(); + } + + public function test_legacy_visitor() + { + $visitor = new Graphviz(); + $this->schema()->visit($visitor); + + $output = $visitor->getOutput(); + $id = substr($output, 9, 40); + + $this->assertEquals(<<test_idintegerforeign_keyintegernamestringdate_insertdatetime> shape=plaintext ] +test_:colforeign_key:se -> foreign_:colpk_id:se [dir=back arrowtail=dot arrowhead=normal ] +foreign_ [label=<
foreign_
pk_idinteger
name_string
citystring
> shape=plaintext ] +foreign__seq [label=<
foreign__seq
idbigint
> shape=plaintext ] +document_ [label=<
document_
id_bigint
customer_idbigint
uploader_typestring
uploader_idbigint
contact_namestring
contact_addressstring
contact_citystring
> shape=plaintext ] +document_:colcustomer_id:se -> customer_:colid_:se [dir=back arrowtail=dot arrowhead=normal ] +user_ [label=<
user_
id_bigint
customer_idbigint
faction_idbigint
name_string
roles_text
> shape=plaintext ] +user_:colcustomer_id:se -> customer_:colid_:se [dir=back arrowtail=dot arrowhead=normal ] +user_:colfaction_id:se -> faction_:colid_:se [dir=back arrowtail=dot arrowhead=normal ] +adminuser_ [label=<
adminuser_
id_bigint
faction_idbigint
name_string
roles_text
> shape=plaintext ] +adminuser_:colfaction_id:se -> faction_:colid_:se [dir=back arrowtail=dot arrowhead=normal ] +faction_ [label=<
faction_
id_bigint
name_string
enabled_boolean
domain_string
> shape=plaintext ] +customer_ [label=<
customer_
id_bigint
parent_idbigint
name_string
> shape=plaintext ] +customer_:colparent_id:se -> customer_:colid_:se [dir=back arrowtail=dot arrowhead=normal ] +customer_seq_ [label=<
customer_seq_
idbigint
> shape=plaintext ] +location_ [label=<
location_
id_bigint
address_string
city_string
> shape=plaintext ] +location_:colid_:se -> customer_:colid_:se [dir=back arrowtail=dot arrowhead=normal ] +pack_ [label=<
pack_
id_integer
name_string
> shape=plaintext ] +customer_pack_ [label=<
customer_pack_
customer_idbigint
pack_idinteger
> shape=plaintext ] +project_ [label=<
project_
idinteger
namestring
> shape=plaintext ] +commit_ [label=<
commit_
idinteger
project_idinteger
messagetext
author_idinteger
author_typestring
> shape=plaintext ] +commit_:colproject_id:se -> project_:colid:se [dir=back arrowtail=dot arrowhead=normal ] +developer_ [label=<
developer_
idinteger
project_idinteger
company_idinteger
namestring
leadboolean
> shape=plaintext ] +developer_:colproject_id:se -> project_:colid:se [dir=back arrowtail=dot arrowhead=normal ] +developer_:colcompany_id:se -> company_:colid:se [dir=back arrowtail=dot arrowhead=normal ] +integrator_ [label=<
integrator_
idinteger
company_idinteger
namestring
> shape=plaintext ] +integrator_:colcompany_id:se -> company_:colid:se [dir=back arrowtail=dot arrowhead=normal ] +project_integrator_ [label=<
project_integrator_
projectIdinteger
integratorIdinteger
> shape=plaintext ] +company_ [label=<
company_
idinteger
namestring
> shape=plaintext ] +} +DOT + , $output +); + } + + public function test_onSchema() + { + $visitor = new Graphviz(); + $visitor->onSchema($this->schema()); + + $output = $visitor->getOutput(); + $id = substr($output, 9, 40); + + $this->assertEquals(<<test_idintegerforeign_keyintegernamestringdate_insertdatetime> shape=plaintext ] +test_:colforeign_key:se -> foreign_:colpk_id:se [dir=back arrowtail=dot arrowhead=normal ] +foreign_ [label=<
foreign_
pk_idinteger
name_string
citystring
> shape=plaintext ] +foreign__seq [label=<
foreign__seq
idbigint
> shape=plaintext ] +document_ [label=<
document_
id_bigint
customer_idbigint
uploader_typestring
uploader_idbigint
contact_namestring
contact_addressstring
contact_citystring
> shape=plaintext ] +document_:colcustomer_id:se -> customer_:colid_:se [dir=back arrowtail=dot arrowhead=normal ] +user_ [label=<
user_
id_bigint
customer_idbigint
faction_idbigint
name_string
roles_text
> shape=plaintext ] +user_:colcustomer_id:se -> customer_:colid_:se [dir=back arrowtail=dot arrowhead=normal ] +user_:colfaction_id:se -> faction_:colid_:se [dir=back arrowtail=dot arrowhead=normal ] +adminuser_ [label=<
adminuser_
id_bigint
faction_idbigint
name_string
roles_text
> shape=plaintext ] +adminuser_:colfaction_id:se -> faction_:colid_:se [dir=back arrowtail=dot arrowhead=normal ] +faction_ [label=<
faction_
id_bigint
name_string
enabled_boolean
domain_string
> shape=plaintext ] +customer_ [label=<
customer_
id_bigint
parent_idbigint
name_string
> shape=plaintext ] +customer_:colparent_id:se -> customer_:colid_:se [dir=back arrowtail=dot arrowhead=normal ] +customer_seq_ [label=<
customer_seq_
idbigint
> shape=plaintext ] +location_ [label=<
location_
id_bigint
address_string
city_string
> shape=plaintext ] +location_:colid_:se -> customer_:colid_:se [dir=back arrowtail=dot arrowhead=normal ] +pack_ [label=<
pack_
id_integer
name_string
> shape=plaintext ] +customer_pack_ [label=<
customer_pack_
customer_idbigint
pack_idinteger
> shape=plaintext ] +project_ [label=<
project_
idinteger
namestring
> shape=plaintext ] +commit_ [label=<
commit_
idinteger
project_idinteger
messagetext
author_idinteger
author_typestring
> shape=plaintext ] +commit_:colproject_id:se -> project_:colid:se [dir=back arrowtail=dot arrowhead=normal ] +developer_ [label=<
developer_
idinteger
project_idinteger
company_idinteger
namestring
leadboolean
> shape=plaintext ] +developer_:colproject_id:se -> project_:colid:se [dir=back arrowtail=dot arrowhead=normal ] +developer_:colcompany_id:se -> company_:colid:se [dir=back arrowtail=dot arrowhead=normal ] +integrator_ [label=<
integrator_
idinteger
company_idinteger
namestring
> shape=plaintext ] +integrator_:colcompany_id:se -> company_:colid:se [dir=back arrowtail=dot arrowhead=normal ] +project_integrator_ [label=<
project_integrator_
projectIdinteger
integratorIdinteger
> shape=plaintext ] +company_ [label=<
company_
idinteger
namestring
> shape=plaintext ] +} +DOT + , $output +); + } + + private function schema(): Schema + { + $tables = []; + $entities = [ + TestEntity::class, + TestEmbeddedEntity::class, + Document::class, + User::class, + Admin::class, + Faction::class, + Customer::class, + Location::class, + Pack::class, + CustomerPack::class, + Project::class, + Commit::class, + Developer::class, + Integrator::class, + ProjectIntegrator::class, + Company::class, + ]; + + foreach ($entities as $entity) { + $schemaManager = $this->prime()->repository($entity)->schema(true); + $platform = $this->prime()->repository($entity)->connection()->platform(); + + $table = $schemaManager->table(true); + $tables[$table->name()] = (new TableTransformer($table, $platform))->toDoctrine(); + + if (($sequence = $schemaManager->sequence()) !== null) { + $tables[$sequence->name()] = (new TableTransformer($sequence, $platform))->toDoctrine(); + } + } + + return new Schema($tables); + } +} diff --git a/tests/Schema/Visitor/MapperVisitorTest.php b/tests/Schema/Visitor/MapperVisitorTest.php index 8710753..d77f2c4 100644 --- a/tests/Schema/Visitor/MapperVisitorTest.php +++ b/tests/Schema/Visitor/MapperVisitorTest.php @@ -42,7 +42,7 @@ protected function declareTestData($pack) /** * */ - public function test_functionnal() + public function test_functional_legacy() { $connection = $this->prime()->connection(); $schemaManager = $connection->schema(); @@ -57,6 +57,24 @@ public function test_functionnal() $this->assertEquals($this->getExpectedDocumentMapper(), $visitor->getOutput()); } + /** + * + */ + public function test_functional_new() + { + $connection = $this->prime()->connection(); + $schemaManager = $connection->schema(); + + $schema = $schemaManager->schema( + $schemaManager->load('document_') + ); + + $visitor = new MapperVisitor($connection->getName()); + $visitor->onSchema($schema); + + $this->assertEquals($this->getExpectedDocumentMapper(), $visitor->getOutput()); + } + /** * @return string */ diff --git a/tests/_files/DummyPlatform.php b/tests/_files/DummyPlatform.php index dc3e02c..ce00063 100644 --- a/tests/_files/DummyPlatform.php +++ b/tests/_files/DummyPlatform.php @@ -3,6 +3,7 @@ namespace Bdf\Prime\Bench; use Bdf\Prime\Platform\PlatformInterface; +use Bdf\Prime\Platform\PlatformSpecificOperationInterface; use Bdf\Prime\Platform\PlatformTypesInterface; use Bdf\Prime\Platform\Sql\SqlPlatform; use Bdf\Prime\Types\ArrayType; @@ -46,4 +47,12 @@ public function grammar() { return $this->platform->grammar(); } + + /** + * @inheritDoc + */ + public function apply(PlatformSpecificOperationInterface $operation) + { + return $this->platform->apply($operation); + } }