From a6bfaca79a08e9edd74cd1f0f6a4c300232423b7 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sat, 27 Jul 2024 13:31:31 +0700 Subject: [PATCH 1/4] feat: native query adapter now supports row values method --- CHANGELOG.md | 1 + .../src/NativeQueryAdapter.php | 111 +++++++++++++----- ...tPageableNativeQueryAdapterNativeQuery.php | 16 +-- ...NativeQueryAdapterNativeQueryRowValues.php | 106 +++++++++++++++++ .../PageableGeneratorProvider.php | 3 + 5 files changed, 194 insertions(+), 43 deletions(-) create mode 100644 tests/src/App/PageableGenerator/KeySetPageableNativeQueryAdapterNativeQueryRowValues.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b602c2..0ea9b5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * refactor: simplify `KeysetExpressionCalculator` * chore: cleanup +* feat: native query adapter now supports row values method # 0.15.1 diff --git a/packages/rekapager-doctrine-orm-adapter/src/NativeQueryAdapter.php b/packages/rekapager-doctrine-orm-adapter/src/NativeQueryAdapter.php index 2045761..d9d98be 100644 --- a/packages/rekapager-doctrine-orm-adapter/src/NativeQueryAdapter.php +++ b/packages/rekapager-doctrine-orm-adapter/src/NativeQueryAdapter.php @@ -13,7 +13,6 @@ namespace Rekalogika\Rekapager\Doctrine\ORM; -use Doctrine\Common\Collections\Expr\Expression; use Doctrine\Common\Collections\Order; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query\ResultSetMapping; @@ -57,6 +56,7 @@ public function __construct( private ?string $countAllSql = null, private array $parameters = [], private string|null $indexBy = null, + private bool $useRowValues = false, ) { // clone the ResultSetMapping to avoid modifying the original $resultSetMapping = clone $resultSetMapping; @@ -106,33 +106,87 @@ private function verifySQL( } /** - * @param null|array $boundaryValues Key is the property name, value is the bound value. Null if unbounded. + * @param array $boundaryValues Key is the property name, value is the bound value. Null if unbounded. * @param non-empty-array $orderings + * @return array{string,array} */ private function generateWhereExpression( - null|array $boundaryValues, + array $boundaryValues, array $orderings, - ): ?Expression { - // wrap boundary values using QueryParameter + ): array { + $fields = $this->createCalculatorFields($boundaryValues, $orderings); - $newBoundaryValues = []; + if ($fields === []) { + return ['', []]; + } - /** @var mixed $value */ - foreach ($boundaryValues ?? [] as $property => $value) { - $newBoundaryValues[$property] = new QueryParameter($value, null); + if ($this->useRowValues) { + return $this->generateRowValuesWhereExpression($fields); } - $boundaryValues = $newBoundaryValues; + return $this->generateApproximatedWhereExpression($fields); + } - // construct the metadata for the next step + /** + * @param non-empty-list $fields + * @return array{string,array} + */ + private function generateApproximatedWhereExpression(array $fields): array + { + $expression = KeysetExpressionCalculator::calculate($fields); - $fields = $this->createCalculatorFields($boundaryValues, $orderings); + $visitor = new KeysetExpressionSQLVisitor(); + $result = $visitor->dispatch($expression); + \assert(\is_string($result)); - if ($fields === []) { - return null; + $where = 'AND ' . $result; + + /** @var array */ + $parameters = $visitor->getParameters(); + + return [$where, $parameters]; + } + + /** + * @param non-empty-list $fields + * @return array{string,array} + */ + private function generateRowValuesWhereExpression(array $fields): array + { + $order = null; + $whereFields = []; + $whereValues = []; + $queryParameters = []; + $i = 1; + + foreach ($fields as $field) { + if ($order === null) { + $order = $field->getOrder(); + } elseif ($order !== $field->getOrder()) { + throw new LogicException('Row values require all fields to have the same order.'); + } + + $template = 'rekapager_where_' . $i; + + $whereFields[] = $field->getName(); + $whereValues[] = ':' . $template; + + $value = $field->getValue(); + assert($value instanceof QueryParameter); + + $queryParameters[$template] = $value; + + $i++; } - return KeysetExpressionCalculator::calculate($fields); + $where = sprintf( + 'AND (%s) %s (%s)', + implode(', ', $whereFields), + $order === Order::Ascending ? '>' : '<', + implode(', ', $whereValues) + ); + + return [$where, $queryParameters]; } /** @@ -206,29 +260,30 @@ private function getSQL( BoundaryType $boundaryType, bool $count = false, ): SQLStatement { + // wrap boundary values using QueryParameter + + $newBoundaryValues = []; + + /** @var mixed $value */ + foreach ($boundaryValues ?? [] as $property => $value) { + $newBoundaryValues[$property] = new QueryParameter($value, null); + } + + $boundaryValues = $newBoundaryValues; + + // orderings + $orderings = $this->getSortOrder($boundaryType === BoundaryType::Upper); if ($orderings === []) { throw new LogicException('No ordering is set.'); } - $expression = $this->generateWhereExpression( + [$where, $parameters] = $this->generateWhereExpression( boundaryValues: $boundaryValues, orderings: $orderings ); - if ($expression !== null) { - $visitor = new KeysetExpressionSQLVisitor(); - $result = $visitor->dispatch($expression); - \assert(\is_string($result)); - $where = 'AND ' . $result; - - $parameters = $visitor->getParameters(); - } else { - $where = ''; - $parameters = []; - } - $orderBy = $this->generateOrderBy($orderings); $sql = str_replace( diff --git a/tests/src/App/PageableGenerator/KeySetPageableNativeQueryAdapterNativeQuery.php b/tests/src/App/PageableGenerator/KeySetPageableNativeQueryAdapterNativeQuery.php index cce4753..8b16b2e 100644 --- a/tests/src/App/PageableGenerator/KeySetPageableNativeQueryAdapterNativeQuery.php +++ b/tests/src/App/PageableGenerator/KeySetPageableNativeQueryAdapterNativeQuery.php @@ -42,7 +42,7 @@ public static function getKey(): string #[\Override] public function getTitle(): string { - return 'KeysetPageable - NativeQueryAdapter - NativeQuery'; + return 'KeysetPageable - NativeQueryAdapter (approximated) - NativeQuery '; } #[\Override] @@ -56,8 +56,6 @@ public function generatePageable( $resultSetMapping = new ResultSetMappingBuilder($this->entityManager); $resultSetMapping->addRootEntityFromClassMetadata(Post::class, 'p'); - - $sql = " SELECT {$resultSetMapping}, {{SELECT}} FROM post p @@ -66,17 +64,6 @@ public function generatePageable( LIMIT {{LIMIT}} OFFSET {{OFFSET}} "; - // $countSql = " - // SELECT COUNT(*) AS count - // FROM ( - // SELECT * - // FROM post p - // WHERE p.set_name = :setName {{WHERE}} - // ORDER BY {{ORDER}} - // LIMIT {{LIMIT}} OFFSET {{OFFSET}} - // ) - // "; - $countAllSql = " SELECT COUNT(*) AS count FROM post p @@ -87,7 +74,6 @@ public function generatePageable( entityManager: $this->entityManager, resultSetMapping: $resultSetMapping, sql: $sql, - // countSql: $countSql, // optional, will encase $sql in a subquery countAllSql: $countAllSql, // optional, if null, total will not be available orderBy: [ 'p.date' => Order::Descending, diff --git a/tests/src/App/PageableGenerator/KeySetPageableNativeQueryAdapterNativeQueryRowValues.php b/tests/src/App/PageableGenerator/KeySetPageableNativeQueryAdapterNativeQueryRowValues.php new file mode 100644 index 0000000..3292ae2 --- /dev/null +++ b/tests/src/App/PageableGenerator/KeySetPageableNativeQueryAdapterNativeQueryRowValues.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Rekapager\Tests\App\PageableGenerator; + +use Doctrine\Common\Collections\Order; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Query\ResultSetMappingBuilder; +use Rekalogika\Contracts\Rekapager\PageableInterface; +use Rekalogika\Rekapager\Doctrine\ORM\NativeQueryAdapter; +use Rekalogika\Rekapager\Doctrine\ORM\Parameter; +use Rekalogika\Rekapager\Keyset\KeysetPageable; +use Rekalogika\Rekapager\Tests\App\Contracts\PageableGeneratorInterface; +use Rekalogika\Rekapager\Tests\App\Entity\Post; + +/** + * @implements PageableGeneratorInterface + */ +class KeySetPageableNativeQueryAdapterNativeQueryRowValues implements PageableGeneratorInterface +{ + public function __construct( + private readonly EntityManagerInterface $entityManager, + ) { + } + + #[\Override] + public static function getKey(): string + { + return 'keysetpageable-nativequeryadapter-nativequery-row-values'; + } + + #[\Override] + public function getTitle(): string + { + return 'KeysetPageable - NativeQueryAdapter (row values) - NativeQuery'; + } + + #[\Override] + public function generatePageable( + int $itemsPerPage, + bool|int|\Closure $count, + string $setName, + ?int $pageLimit = null, + ): PageableInterface { + // @highlight-start + $resultSetMapping = new ResultSetMappingBuilder($this->entityManager); + $resultSetMapping->addRootEntityFromClassMetadata(Post::class, 'p'); + + $sql = " + SELECT {$resultSetMapping}, {{SELECT}} + FROM post p + WHERE p.set_name = :setName {{WHERE}} + ORDER BY {{ORDER}} + LIMIT {{LIMIT}} OFFSET {{OFFSET}} + "; + + $countAllSql = " + SELECT COUNT(*) AS count + FROM post p + WHERE p.set_name = :setName + "; + + $adapter = new NativeQueryAdapter( + entityManager: $this->entityManager, + resultSetMapping: $resultSetMapping, + sql: $sql, + countAllSql: $countAllSql, // optional, if null, total will not be available + orderBy: [ + 'p.date' => Order::Ascending, + 'p.title' => Order::Ascending, + 'p.id' => Order::Ascending, + ], + parameters: [ + new Parameter('setName', $setName), + ], + indexBy: 'id', + useRowValues: true, + ); + + $pageable = new KeysetPageable( + adapter: $adapter, + itemsPerPage: $itemsPerPage, + count: $count, + ); + // @highlight-end + + // @phpstan-ignore-next-line + return $pageable; + } + + #[\Override] + public function count(): int + { + return 0; + } +} diff --git a/tests/src/IntegrationTests/DataProvider/PageableGeneratorProvider.php b/tests/src/IntegrationTests/DataProvider/PageableGeneratorProvider.php index 8546b42..cb71147 100644 --- a/tests/src/IntegrationTests/DataProvider/PageableGeneratorProvider.php +++ b/tests/src/IntegrationTests/DataProvider/PageableGeneratorProvider.php @@ -14,6 +14,7 @@ namespace Rekalogika\Rekapager\Tests\IntegrationTests\DataProvider; use Rekalogika\Rekapager\Tests\App\PageableGenerator\KeySetPageableNativeQueryAdapterNativeQuery; +use Rekalogika\Rekapager\Tests\App\PageableGenerator\KeySetPageableNativeQueryAdapterNativeQueryRowValues; use Rekalogika\Rekapager\Tests\App\PageableGenerator\KeysetPageableQueryBuilderAdapterQueryBuilder; use Rekalogika\Rekapager\Tests\App\PageableGenerator\KeysetPageableSelectableAdapterCollection; use Rekalogika\Rekapager\Tests\App\PageableGenerator\KeysetPageableSelectableAdapterEntityRepository; @@ -33,6 +34,7 @@ public static function all(): iterable yield [KeysetPageableSelectableAdapterCollection::class]; yield [KeysetPageableSelectableAdapterEntityRepository::class]; yield [KeySetPageableNativeQueryAdapterNativeQuery::class]; + yield [KeySetPageableNativeQueryAdapterNativeQueryRowValues::class]; yield [OffsetPageableQueryBuilderAdapterQueryBuilder::class]; yield [OffsetPageableCollectionAdapterCollection::class]; yield [OffsetPageableSelectableAdapterCollection::class]; @@ -48,6 +50,7 @@ public static function keyset(): iterable yield [KeysetPageableSelectableAdapterCollection::class]; yield [KeysetPageableSelectableAdapterEntityRepository::class]; yield [KeySetPageableNativeQueryAdapterNativeQuery::class]; + yield [KeySetPageableNativeQueryAdapterNativeQueryRowValues::class]; } /** From 110ecb13d1fa977ad0cb5221693f8048bf3306d5 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sat, 27 Jul 2024 13:34:48 +0700 Subject: [PATCH 2/4] cs --- .../rekapager-doctrine-orm-adapter/src/NativeQueryAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rekapager-doctrine-orm-adapter/src/NativeQueryAdapter.php b/packages/rekapager-doctrine-orm-adapter/src/NativeQueryAdapter.php index d9d98be..a8833b2 100644 --- a/packages/rekapager-doctrine-orm-adapter/src/NativeQueryAdapter.php +++ b/packages/rekapager-doctrine-orm-adapter/src/NativeQueryAdapter.php @@ -172,7 +172,7 @@ private function generateRowValuesWhereExpression(array $fields): array $whereValues[] = ':' . $template; $value = $field->getValue(); - assert($value instanceof QueryParameter); + \assert($value instanceof QueryParameter); $queryParameters[$template] = $value; From 287c58fe0965513c0f2122333dde33a9b91d90fb Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sat, 27 Jul 2024 13:56:48 +0700 Subject: [PATCH 3/4] seek method --- .../src/SeekMethod.php | 21 ++++++++++++++ .../src/NativeQueryAdapter.php | 28 ++++++++++++++++--- 2 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 packages/rekapager-adapter-common/src/SeekMethod.php diff --git a/packages/rekapager-adapter-common/src/SeekMethod.php b/packages/rekapager-adapter-common/src/SeekMethod.php new file mode 100644 index 0000000..ed58331 --- /dev/null +++ b/packages/rekapager-adapter-common/src/SeekMethod.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Rekapager\Adapter\Common; + +enum SeekMethod +{ + case Approximated; + case RowValues; + case Auto; +} diff --git a/packages/rekapager-doctrine-orm-adapter/src/NativeQueryAdapter.php b/packages/rekapager-doctrine-orm-adapter/src/NativeQueryAdapter.php index a8833b2..f3ca969 100644 --- a/packages/rekapager-doctrine-orm-adapter/src/NativeQueryAdapter.php +++ b/packages/rekapager-doctrine-orm-adapter/src/NativeQueryAdapter.php @@ -22,6 +22,7 @@ use Rekalogika\Rekapager\Adapter\Common\IndexResolver; use Rekalogika\Rekapager\Adapter\Common\KeysetExpressionCalculator; use Rekalogika\Rekapager\Adapter\Common\KeysetExpressionSQLVisitor; +use Rekalogika\Rekapager\Adapter\Common\SeekMethod; use Rekalogika\Rekapager\Doctrine\ORM\Exception\MissingPlaceholderInSQLException; use Rekalogika\Rekapager\Doctrine\ORM\Exception\NoCountResultFoundException; use Rekalogika\Rekapager\Doctrine\ORM\Internal\QueryBuilderKeysetItem; @@ -56,7 +57,7 @@ public function __construct( private ?string $countAllSql = null, private array $parameters = [], private string|null $indexBy = null, - private bool $useRowValues = false, + private SeekMethod $seekMethod = SeekMethod::Approximated, ) { // clone the ResultSetMapping to avoid modifying the original $resultSetMapping = clone $resultSetMapping; @@ -120,11 +121,30 @@ private function generateWhereExpression( return ['', []]; } - if ($this->useRowValues) { - return $this->generateRowValuesWhereExpression($fields); + return match ($this->seekMethod) { + SeekMethod::Approximated => $this->generateApproximatedWhereExpression($fields), + SeekMethod::RowValues => $this->generateRowValuesWhereExpression($fields), + SeekMethod::Auto => $this->generateAutoWhereExpression($fields), + }; + } + + /** + * @param non-empty-list $fields + * @return array{string,array} + */ + private function generateAutoWhereExpression(array $fields): array + { + $order = null; + + foreach ($fields as $field) { + if ($order === null) { + $order = $field->getOrder(); + } elseif ($order !== $field->getOrder()) { + return $this->generateApproximatedWhereExpression($fields); + } } - return $this->generateApproximatedWhereExpression($fields); + return $this->generateRowValuesWhereExpression($fields); } /** From e72b729732c0071561c65bd1d53b0bd850500a88 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sat, 27 Jul 2024 14:02:36 +0700 Subject: [PATCH 4/4] fix --- .../KeySetPageableNativeQueryAdapterNativeQueryRowValues.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/src/App/PageableGenerator/KeySetPageableNativeQueryAdapterNativeQueryRowValues.php b/tests/src/App/PageableGenerator/KeySetPageableNativeQueryAdapterNativeQueryRowValues.php index 3292ae2..2374dbf 100644 --- a/tests/src/App/PageableGenerator/KeySetPageableNativeQueryAdapterNativeQueryRowValues.php +++ b/tests/src/App/PageableGenerator/KeySetPageableNativeQueryAdapterNativeQueryRowValues.php @@ -17,6 +17,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query\ResultSetMappingBuilder; use Rekalogika\Contracts\Rekapager\PageableInterface; +use Rekalogika\Rekapager\Adapter\Common\SeekMethod; use Rekalogika\Rekapager\Doctrine\ORM\NativeQueryAdapter; use Rekalogika\Rekapager\Doctrine\ORM\Parameter; use Rekalogika\Rekapager\Keyset\KeysetPageable; @@ -84,7 +85,7 @@ public function generatePageable( new Parameter('setName', $setName), ], indexBy: 'id', - useRowValues: true, + seekMethod: SeekMethod::RowValues, ); $pageable = new KeysetPageable(