Skip to content

Commit 47abe36

Browse files
committed
Do not break proper LIMIT/OFFSET caching; mention limitations
1 parent 6aab32f commit 47abe36

File tree

4 files changed

+46
-20
lines changed

4 files changed

+46
-20
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ class MaxExecutionTimeSqlWalker extends HintHandler
5151

5252
SqlNode is an enum of all `walkXxx` methods in Doctrine's SqlWalker, so you are able to intercept any part of AST processing the SqlWalker does.
5353

54+
### Limitations
55+
- Please note that since [doctrine/orm 3.3.0](https://github.com/doctrine/orm/pull/11188), the produced SQL gets finalized with `LIMIT` / `OFFSET` / `FOR UPDATE` after `SqlWalker` processing is done.
56+
- Thus, implementors should be aware that those SQL parts can be appended to the SQL after `HintHandler` processing.
57+
- This means that e.g. placing a comment at the end of the SQL breaks LIMIT functionality completely
58+
5459
### Implementors
5560
- [shipmonk/doctrine-mysql-optimizer-hints](https://github.com/shipmonk-rnd/doctrine-mysql-optimizer-hints) (since v2)
5661
- [shipmonk/doctrine-mysql-index-hints](https://github.com/shipmonk-rnd/doctrine-mysql-index-hints) (since v3)

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"phpunit/phpunit": "10.5.36",
2020
"shipmonk/composer-dependency-analyser": "1.7.0",
2121
"shipmonk/phpstan-rules": "3.2.1",
22-
"slevomat/coding-standard": "8.15.0"
22+
"slevomat/coding-standard": "8.15.0",
23+
"symfony/cache": "^6.4"
2324
},
2425
"autoload": {
2526
"psr-4": {

src/HintDrivenSqlWalker.php

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,15 @@
5353
use Doctrine\ORM\Query\AST\UpdateItem;
5454
use Doctrine\ORM\Query\AST\UpdateStatement;
5555
use Doctrine\ORM\Query\AST\WhereClause;
56-
use Doctrine\ORM\Query\Exec\PreparedExecutorFinalizer;
57-
use Doctrine\ORM\Query\Exec\SqlFinalizer;
58-
use Doctrine\ORM\Query\OutputWalker;
5956
use Doctrine\ORM\Query\Parser;
60-
use Doctrine\ORM\Query\SqlWalker;
57+
use Doctrine\ORM\Query\SqlOutputWalker;
6158
use LogicException;
6259
use function is_a;
6360

6461
/**
6562
* @psalm-import-type QueryComponent from Parser
6663
*/
67-
class HintDrivenSqlWalker extends SqlWalker implements OutputWalker
64+
class HintDrivenSqlWalker extends SqlOutputWalker
6865
{
6966

7067
/**
@@ -94,20 +91,10 @@ public function __construct(
9491
}
9592
}
9693

97-
public function getFinalizer(DeleteStatement|UpdateStatement|SelectStatement $AST): SqlFinalizer
94+
protected function createSqlForFinalizer(SelectStatement $selectStatement): string
9895
{
99-
switch (true) {
100-
case $AST instanceof SelectStatement:
101-
return new NoopSqlFinalizer($this->walkSelectStatement($AST));
102-
103-
case $AST instanceof UpdateStatement:
104-
return new PreparedExecutorFinalizer($this->createUpdateStatementExecutor($AST));
105-
106-
case $AST instanceof DeleteStatement: // @phpstan-ignore instanceof.alwaysTrue (keep it readable)
107-
return new PreparedExecutorFinalizer($this->createDeleteStatementExecutor($AST));
108-
}
109-
110-
throw new LogicException('Unexpected AST node type');
96+
$selectStatementSql = parent::createSqlForFinalizer($selectStatement);
97+
return $this->callWalkers(SqlNode::SelectStatement, $selectStatementSql);
11198
}
11299

113100
public function walkSelectStatement(SelectStatement $AST): string

tests/HintDrivenSqlWalkerTest.php

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPUnit\Framework\TestCase;
1515
use ShipMonk\Doctrine\Walker\Handlers\CommentWholeSqlHintHandler;
1616
use ShipMonk\Doctrine\Walker\Handlers\LowercaseSelectHintHandler;
17+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
1718
use function sprintf;
1819

1920
class HintDrivenSqlWalkerTest extends TestCase
@@ -40,6 +41,37 @@ public function testWalker(
4041
self::assertSame($expectedSql, $producedSql);
4142
}
4243

44+
public function testPagination(): void
45+
{
46+
$pageSize = 10;
47+
$entityManagerMock = $this->createEntityManagerMock();
48+
49+
self::assertNotNull(
50+
$entityManagerMock->getConfiguration()->getQueryCache(),
51+
'QueryCache needed. The purpose of this test is to ensure that we do not break the pagination by using a cache',
52+
);
53+
54+
$expectedSqls = [
55+
'select d0_.id AS id_0 FROM dummy_entity d0_ LIMIT 10',
56+
'select d0_.id AS id_0 FROM dummy_entity d0_ LIMIT 10 OFFSET 10',
57+
'select d0_.id AS id_0 FROM dummy_entity d0_ LIMIT 10 OFFSET 20',
58+
];
59+
60+
foreach ([0, 1, 2] as $page) {
61+
$query = $entityManagerMock->createQueryBuilder()
62+
->select('w')
63+
->from(DummyEntity::class, 'w')
64+
->getQuery()
65+
->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, HintDrivenSqlWalker::class)
66+
->setHint(LowercaseSelectHintHandler::class, null)
67+
->setFirstResult($page * $pageSize)
68+
->setMaxResults($pageSize);
69+
$producedSql = $query->getSQL();
70+
71+
self::assertSame($expectedSqls[$page], $producedSql, 'Page ' . $page . ' failed:');
72+
}
73+
}
74+
4375
/**
4476
* @return Generator<string, array{callable(EntityManager):Query, class-string<HintHandler>, mixed, string}>
4577
*/
@@ -85,7 +117,7 @@ static function (EntityManager $entityManager): Query {
85117
},
86118
CommentWholeSqlHintHandler::class,
87119
'custom comment',
88-
'SELECT d0_.id AS id_0 FROM dummy_entity d0_ LIMIT 1 -- custom comment',
120+
'SELECT d0_.id AS id_0 FROM dummy_entity d0_ -- custom comment LIMIT 1', // see readme limitations
89121
];
90122
}
91123

@@ -94,6 +126,7 @@ private function createEntityManagerMock(): EntityManager
94126
$config = new Configuration();
95127
$config->setProxyNamespace('Tmp\Doctrine\Tests\Proxies');
96128
$config->setProxyDir('/tmp/doctrine');
129+
$config->setQueryCache(new ArrayAdapter());
97130
$config->setAutoGenerateProxyClasses(false);
98131
$config->setSecondLevelCacheEnabled(false);
99132
$config->setMetadataDriverImpl(new AttributeDriver([__DIR__]));

0 commit comments

Comments
 (0)