Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix different first/max result values taking up query cache space #11188

Merged
merged 46 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
fb0b27e
Add a test covering the #11112 issue
mpdude Jan 27, 2024
65c5a72
Add new OutputWalker and SqlFinalizer interfaces
mpdude Jan 27, 2024
f2b0a37
Add a SingleSelectSqlFinalizer that can take care of adding offset/li…
mpdude Jan 27, 2024
04b89f5
In SqlWalker, split SQL query generation into the two parts that shal…
mpdude Jan 27, 2024
9f085f8
Fix CS violations
mpdude Jan 27, 2024
89e5084
Skip the GH11112 test while applying refactorings
mpdude Jan 27, 2024
67f3108
Avoid a Psalm complaint due to invalid (?) docblock syntax
mpdude Jan 28, 2024
47ff896
Establish alternate code path - queries can obtain the sql executor t…
mpdude Jan 29, 2024
9b6c9dd
Remove a possibly premature comment
mpdude Jan 29, 2024
77062c4
Re-enable the #11112 test
mpdude Jan 29, 2024
9695ec6
Fix CS
mpdude Jan 29, 2024
a477dfe
Make RootTypeWalker inherit from SqlOutputWalker so it becomes finali…
mpdude Jan 29, 2024
c098fbc
Update QueryCacheTest, since first/max results no longer need extra c…
mpdude Jan 29, 2024
5b7a340
Fix ParserResultSerializationTest by forcing the parser to produce a …
mpdude Jan 29, 2024
1573420
Fix WhereInWalkerTest
mpdude Jan 29, 2024
293771d
Update lib/Doctrine/ORM/Query/Exec/PreparedExecutorFinalizer.php
mpdude Jan 29, 2024
4f0c01f
Fix tests
mpdude Jan 29, 2024
302a9b7
Fix a Psalm complaint
mpdude Jan 29, 2024
02cad0b
Fix a test
mpdude Jan 29, 2024
c27c800
Fix CS
mpdude Jan 29, 2024
6b4c9a2
Make the NullSqlWalker an instance of SqlOutputWalker
mpdude Jan 29, 2024
5103ccc
Merge remote-tracking branch 'origin/2.17.x' into fix-11112
mpdude Jan 30, 2024
eb64795
Avoid multiple cache entries caused by LimitSubqueryOutputWalker
mpdude Jan 30, 2024
dee1818
Fix Psalm complaints
mpdude Jan 30, 2024
e270a84
Merge branch '2.17.x' into fix-11112
mpdude Jan 31, 2024
2fd79ea
Fix static analysis complaints
mpdude Jan 31, 2024
db76e84
Remove experimental code that I committed accidentally
mpdude Jan 31, 2024
5ff75bf
Remove unnecessary baseline entry
mpdude Jan 31, 2024
59eed73
Make AddUnknownQueryComponentWalker subclass SqlOutputWalker
mpdude Jan 31, 2024
a19245f
Merge remote-tracking branch 'origin/2.18.x' into fix-11112
mpdude Feb 4, 2024
259dfe7
Use more expressive exception classes
mpdude Oct 8, 2024
29e1e40
Merge remote-tracking branch 'origin/2.20.x' into fix-11112
mpdude Oct 8, 2024
50e1f98
Add a deprecation message
mpdude Oct 8, 2024
9c93af9
Move SqlExecutor creation to ParserResult, to minimize public methods…
mpdude Oct 9, 2024
8beab61
Avoid keeping the SqlExecutor in the Query, since it must be generate…
mpdude Oct 10, 2024
b2f14c6
Address PHPStan complaints
mpdude Oct 10, 2024
f4bda83
Fix tests
mpdude Oct 10, 2024
b674182
Small refactorings
mpdude Oct 10, 2024
860bce8
Add an upgrade notice
mpdude Oct 10, 2024
6092429
Small refactorings
mpdude Oct 10, 2024
f04927b
Update the Psalm baseline
mpdude Oct 10, 2024
7f576e5
Add a missing namespace import
mpdude Oct 10, 2024
efd7e15
Merge remote-tracking branch 'origin/2.20.x' into fix-11112
mpdude Oct 10, 2024
89a46d0
Update Psalm baseline
mpdude Oct 10, 2024
e180e2b
Fix CS
mpdude Oct 10, 2024
534640a
Fix Psalm baseline
mpdude Oct 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Upgrade to 2.20

## Add `Doctrine\ORM\Query\OutputWalker` interface, deprecate `Doctrine\ORM\Query\SqlWalker::getExecutor()`

Output walkers should implement the new `\Doctrine\ORM\Query\OutputWalker` interface and create
`Doctrine\ORM\Query\Exec\SqlFinalizer` instances instead of `Doctrine\ORM\Query\Exec\AbstractSqlExecutor`s.
The output walker must not base its workings on the query `firstResult`/`maxResult` values, so that the
`SqlFinalizer` can be kept in the query cache and used regardless of the actual `firstResult`/`maxResult` values.
Any operation dependent on `firstResult`/`maxResult` should take place within the `SqlFinalizer::createExecutor()`
method. Details can be found at https://github.com/doctrine/orm/pull/11188.

## Explictly forbid property hooks

Property hooks are not supported yet by Doctrine ORM. Until support is added,
Expand Down
14 changes: 9 additions & 5 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1868,6 +1868,12 @@
<code><![CDATA[$this->_sqlStatements = &$this->sqlStatements]]></code>
</UnsupportedPropertyReferenceUsage>
</file>
<file src="src/Query/Exec/FinalizedSelectExecutor.php">
<PropertyNotSetInConstructor>
<code><![CDATA[FinalizedSelectExecutor]]></code>
<code><![CDATA[FinalizedSelectExecutor]]></code>
</PropertyNotSetInConstructor>
</file>
<file src="src/Query/Exec/MultiTableDeleteExecutor.php">
<InvalidReturnStatement>
<code><![CDATA[$numDeleted]]></code>
Expand Down Expand Up @@ -1996,6 +2002,9 @@
<ArgumentTypeCoercion>
<code><![CDATA[$stringPattern]]></code>
</ArgumentTypeCoercion>
<DeprecatedMethod>
<code><![CDATA[setSqlExecutor]]></code>
</DeprecatedMethod>
<InvalidNullableReturnType>
<code><![CDATA[AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement]]></code>
</InvalidNullableReturnType>
Expand Down Expand Up @@ -2057,11 +2066,6 @@
<code><![CDATA[$token === TokenType::T_IDENTIFIER]]></code>
</RedundantConditionGivenDocblockType>
</file>
<file src="src/Query/ParserResult.php">
<PropertyNotSetInConstructor>
<code><![CDATA[$sqlExecutor]]></code>
</PropertyNotSetInConstructor>
</file>
<file src="src/Query/QueryExpressionVisitor.php">
<InvalidReturnStatement>
<code><![CDATA[new ArrayCollection($this->parameters)]]></code>
Expand Down
34 changes: 31 additions & 3 deletions src/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\AST\UpdateStatement;
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
use Doctrine\ORM\Query\Exec\SqlFinalizer;
use Doctrine\ORM\Query\OutputWalker;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\Query\ParameterTypeInferer;
use Doctrine\ORM\Query\Parser;
Expand All @@ -33,6 +35,7 @@
use function count;
use function get_debug_type;
use function in_array;
use function is_a;
use function is_int;
use function ksort;
use function md5;
Expand Down Expand Up @@ -196,7 +199,7 @@ class Query extends AbstractQuery
*/
public function getSQL()
{
return $this->parse()->getSqlExecutor()->getSqlStatements();
return $this->getSqlExecutor()->getSqlStatements();
}

/**
Expand Down Expand Up @@ -285,7 +288,7 @@ private function parse(): ParserResult
*/
protected function _doExecute()
{
$executor = $this->parse()->getSqlExecutor();
$executor = $this->getSqlExecutor();

if ($this->_queryCacheProfile) {
$executor->setQueryCacheProfile($this->_queryCacheProfile);
Expand Down Expand Up @@ -813,11 +816,31 @@ protected function getQueryCacheId(): string
{
ksort($this->_hints);

if (! $this->hasHint(self::HINT_CUSTOM_OUTPUT_WALKER)) {
// Assume Parser will create the SqlOutputWalker; save is_a call, which might trigger a class load
$firstAndMaxResult = '';
} else {
$outputWalkerClass = $this->getHint(self::HINT_CUSTOM_OUTPUT_WALKER);
if (is_a($outputWalkerClass, OutputWalker::class, true)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this immediately trigger a deprecation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It now does.

$firstAndMaxResult = '';
} else {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11188/',
'Your output walker class %s should implement %s in order to provide a %s. This also means the output walker should not use the query firstResult/maxResult values, which should be read from the query by the SqlFinalizer only.',
$outputWalkerClass,
OutputWalker::class,
SqlFinalizer::class
);
$firstAndMaxResult = '&firstResult=' . $this->firstResult . '&maxResult=' . $this->maxResults;
}
}

return md5(
$this->getDQL() . serialize($this->_hints) .
'&platform=' . get_debug_type($this->getEntityManager()->getConnection()->getDatabasePlatform()) .
($this->_em->hasFilters() ? $this->_em->getFilters()->getHash() : '') .
'&firstResult=' . $this->firstResult . '&maxResult=' . $this->maxResults .
$firstAndMaxResult .
'&hydrationMode=' . $this->_hydrationMode . '&types=' . serialize($this->parsedTypes) . 'DOCTRINE_QUERY_CACHE_SALT'
);
}
Expand All @@ -836,4 +859,9 @@ public function __clone()

$this->state = self::STATE_DIRTY;
}

private function getSqlExecutor(): AbstractSqlExecutor
{
return $this->parse()->prepareSqlExecutor($this);
}
}
33 changes: 33 additions & 0 deletions src/Query/Exec/FinalizedSelectExecutor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM\Query\Exec;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Types\Type;

/**
* SQL executor for a given, final, single SELECT SQL query
*
* @method string getSqlStatements()
*/
class FinalizedSelectExecutor extends AbstractSqlExecutor
{
public function __construct(string $sql)
{
parent::__construct();

$this->sqlStatements = $sql;
}

/**
* @param list<mixed>|array<string, mixed> $params
* @param array<int, int|string|Type|null>|array<string, int|string|Type|null> $types
*/
public function execute(Connection $conn, array $params, array $types): Result
{
return $conn->executeQuery($this->getSqlStatements(), $params, $types, $this->queryCacheProfile);
}
}
28 changes: 28 additions & 0 deletions src/Query/Exec/PreparedExecutorFinalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM\Query\Exec;

use Doctrine\ORM\Query;

/**
* PreparedExecutorFinalizer is a wrapper for the SQL finalization
* phase that does nothing - it is constructed with the sql executor
* already.
*/
final class PreparedExecutorFinalizer implements SqlFinalizer
{
/** @var AbstractSqlExecutor */
private $executor;

public function __construct(AbstractSqlExecutor $exeutor)
{
$this->executor = $exeutor;
}

public function createExecutor(Query $query): AbstractSqlExecutor
{
return $this->executor;
}
}
64 changes: 64 additions & 0 deletions src/Query/Exec/SingleSelectSqlFinalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM\Query\Exec;

use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Utility\LockSqlHelper;

/**
* SingleSelectSqlFinalizer finalizes a given SQL query by applying
* the query's firstResult/maxResult values as well as extra read lock/write lock
* statements, both through the platform-specific methods.
*
* The resulting, "finalized" SQL is passed to a FinalizedSelectExecutor.
*/
class SingleSelectSqlFinalizer implements SqlFinalizer
{
use LockSqlHelper;

/** @var string */
private $sql;

public function __construct(string $sql)
{
$this->sql = $sql;
}

/**
* This method exists temporarily to support old SqlWalker interfaces.
*
* @internal
*
* @psalm-internal Doctrine\ORM
*/
public function finalizeSql(Query $query): string
{
$platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();

$sql = $platform->modifyLimitQuery($this->sql, $query->getMaxResults(), $query->getFirstResult());

$lockMode = $query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE;

if ($lockMode !== LockMode::NONE && $lockMode !== LockMode::OPTIMISTIC && $lockMode !== LockMode::PESSIMISTIC_READ && $lockMode !== LockMode::PESSIMISTIC_WRITE) {
throw QueryException::invalidLockMode();
}

if ($lockMode === LockMode::PESSIMISTIC_READ) {
$sql .= ' ' . $this->getReadLockSQL($platform);
} elseif ($lockMode === LockMode::PESSIMISTIC_WRITE) {
$sql .= ' ' . $this->getWriteLockSQL($platform);
}

return $sql;
}

/** @return FinalizedSelectExecutor */
public function createExecutor(Query $query): AbstractSqlExecutor
{
return new FinalizedSelectExecutor($this->finalizeSql($query));
}
}
26 changes: 26 additions & 0 deletions src/Query/Exec/SqlFinalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM\Query\Exec;

use Doctrine\ORM\Query;

/**
* SqlFinalizers are created by OutputWalkers that traversed the DQL AST.
* The SqlFinalizer instance can be kept in the query cache and re-used
* at a later time.
*
* Once the SqlFinalizer has been created or retrieved from the query cache,
* it receives the Query object again in order to yield the AbstractSqlExecutor
* that will then be used to execute the query.
*
* The SqlFinalizer may assume that the DQL that was used to build the AST
* and run the OutputWalker (which created the SqlFinalizer) is equivalent to
* the query that will be passed to the createExecutor() method. Potential differences
* are the parameter values or firstResult/maxResult settings.
*/
interface SqlFinalizer
{
public function createExecutor(Query $query): AbstractSqlExecutor;
}
28 changes: 28 additions & 0 deletions src/Query/OutputWalker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM\Query;

use Doctrine\ORM\Query\Exec\SqlFinalizer;

/**
* Interface for output walkers
*
* Output walkers, like tree walkers, can traverse the DQL AST to perform
* their purpose.
beberlei marked this conversation as resolved.
Show resolved Hide resolved
*
* The goal of an OutputWalker is to ultimately provide the SqlFinalizer
* which produces the final, executable SQL statement in a "finalization" phase.
greg0ire marked this conversation as resolved.
Show resolved Hide resolved
*
* It must be possible to use the same SqlFinalizer for Queries with different
* firstResult/maxResult values. In other words, SQL produced by the
* output walker should not depend on those values, and any SQL generation/modification
* specific to them should happen in the finalizer's `\Doctrine\ORM\Query\Exec\SqlFinalizer::createExecutor()`
* method instead.
*/
interface OutputWalker
{
/** @param AST\DeleteStatement|AST\UpdateStatement|AST\SelectStatement $AST */
public function getFinalizer($AST): SqlFinalizer;
}
22 changes: 19 additions & 3 deletions src/Query/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\AST\Functions;
use Doctrine\ORM\Query\Exec\SqlFinalizer;
use LogicException;
use ReflectionClass;

Expand Down Expand Up @@ -396,11 +397,26 @@ public function parse()
$this->queryComponents = $treeWalkerChain->getQueryComponents();
}

$outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
$outputWalkerClass = $this->customOutputWalker ?: SqlOutputWalker::class;
$outputWalker = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);

// Assign an SQL executor to the parser result
$this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
if ($outputWalker instanceof OutputWalker) {
$finalizer = $outputWalker->getFinalizer($AST);
$this->parserResult->setSqlFinalizer($finalizer);
} else {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11188/',
'Your output walker class %s should implement %s in order to provide a %s. This also means the output walker should not use the query firstResult/maxResult values, which should be read from the query by the SqlFinalizer only.',
$outputWalkerClass,
OutputWalker::class,
SqlFinalizer::class
);
// @phpstan-ignore method.deprecated
$executor = $outputWalker->getExecutor($AST);
// @phpstan-ignore method.deprecated
$this->parserResult->setSqlExecutor($executor);
}

return $this->parserResult;
}
Expand Down
Loading