Skip to content

Merge release 3.7.6 into 3.8.x - #12721

Merged
greg0ire merged 21 commits into
3.8.xfrom
3.7.x-merge-up-into-3.8.x_Kpnl50BO
Oct 31, 2026
Merged

greg0ire merged 21 commits into
3.8.xfrom
3.7.x-merge-up-into-3.8.x_Kpnl50BO

Conversation

SulimanAbdulrazzaq and others added 21 commits September 23, 2026 10:52
Unnamed indexes and unique constraints only get a positional key, so
addInheritedIndexes() dropped the first unnamed index of a mapped superclass
as soon as the entity declared one of its own. Append unnamed entries of the
superclass unless the entity already declares one on the same columns or
fields, and keep overriding named entries by name.
paginate() now calls useQueryCache(false) on the cloned sub-query, so
each page still runs CursorWalker and the cursor walk does not write a
cache entry. The walker has to keep running because the order-by
metadata used to build the next cursor is only available when it runs.
The new test paginates the same DQL with a null cursor and three Cursor
values against an ArrayAdapter, asserts the cache stays empty, then
calls getSQL() on the original query twice and asserts that a single
entry remains.

Fixes #12649
Name the two cases a cached ParserResult would break (a repeated cursor
and a repeated first page) instead of "later pages".

The second getSQL() call in the test returned the already parsed
ParserResult without touching the query cache, so its assertion could
never fail.
The LimitSubqueryWalker replaces the select clause with the root identifier,
so a parameter only used in the SELECT clause is no longer used by the id
query, which then fails with "Too many parameters". Unbind the unused
parameters like the other paginators do, keeping the cursor parameters.
The cursor parameters are bound when the query is executed, so the parse
detecting unused parameters does not need the cursor hints.
The cursor condition for a multi-column ORDER BY is expanded into an OR
chain, (a < :a) OR (a = :a AND b < :b). PostgreSQL cannot derive an
index range from that form, so it applies it as a filter and the cost
of fetching a page grows with its depth.
Add a leading bound to the cursor pagination predicate
…ery-cache

Stop CursorPaginator writing a query-cache entry per cursor
…r-unbind-unused-parameters

Unbind unused parameters in CursorPaginator without output walkers
When a custom ResultSetMapping is set on a query, as the Paginator
does for its count query, the discriminator parameters recorded by
the SqlWalker in the parsed RSM were ignored: a ClassMetadata bound
to an INSTANCE OF expression was bound as its FQCN instead of the
discriminator values, so count() always returned 0 while iterating
the very same Paginator returned the expected results.

Resolve discriminator parameters against the parser result RSM,
which is always populated once the query has been parsed, keeping
the user-provided RSM as a fallback.

Fixes #7039

Co-authored-by: Si-gi <hidji@protonmail.com>
ProxyFactory::getProxy() assigns an embedded identifier through
EmbeddablePropertyAccessor::setValue(), which first reads the embeddable.
TypedNoDefaultPropertyAccessor::getValue() called isInitialized() on the
lazy property, which initializes the whole object, so the proxy ended up
registered as initialized with an empty snapshot and fully rewritten on
the next flush.

Fixes #12674
The contract was implicit. Reading or writing a mapped property must not
trigger the initialization of a lazy object, because the UnitOfWork uses
these accessors to compute changesets and identifiers. State it on the
interface, so that an implementation relying on isInitialized() or
getValue() on a lazy object stands out as a violation.
…rties

Unsetting a property that is still lazy initializes the whole object.
The property is already unset in that case, so the call can be skipped.
Cover getValue() and setValue(null) on a lazy ghost, and assert that the
identifier of a to-one proxy with an embedded identifier stays readable
without initializing the proxy.
…itialize them

The hydrator reads the current value of a collection property through the
property accessor before merging the fetched elements. On a partial lazy
object, that read went through isInitialized(), which initialized the object
and ran one extra query per row: 4 queries on PHP 8.4, 2 on PHP 8.5.

Test written by George Tudoran, who reported the case on #12675.

Co-Authored-By: George Tudoran <george.tudoran@zitec.com>
Use DBAL's configured folding for unquoted identifiers while preserving legacy casing on DBAL platforms without the folding API.

Fixes #12272.
…typed properties (#12675)

* Do not initialize a lazy object when reading one of its typed properties

ProxyFactory::getProxy() assigns an embedded identifier through
EmbeddablePropertyAccessor::setValue(), which first reads the embeddable.
TypedNoDefaultPropertyAccessor::getValue() called isInitialized() on the
lazy property, which initializes the whole object, so the proxy ended up
registered as initialized with an empty snapshot and fully rewritten on
the next flush.

Fixes #12674

* Document that property accessors must not initialize lazy objects

The contract was implicit. Reading or writing a mapped property must not
trigger the initialization of a lazy object, because the UnitOfWork uses
these accessors to compute changesets and identifiers. State it on the
interface, so that an implementation relying on isInitialized() or
getValue() on a lazy object stands out as a violation.

* Do not initialize a lazy object when unsetting one of its typed properties

Unsetting a property that is still lazy initializes the whole object.
The property is already unset in that case, so the call can be skipped.

* Test that property accessors do not initialize lazy objects

Cover getValue() and setValue(null) on a lazy ghost, and assert that the
identifier of a to-one proxy with an embedded identifier stays readable
without initializing the proxy.

* Test that fetch-joining a collection into partial objects does not initialize them

The hydrator reads the current value of a collection property through the
property accessor before merging the fetched elements. On a partial lazy
object, that read went through isInitialized(), which initialized the object
and ran one extra query per row: 4 queries on PHP 8.4, 2 on PHP 8.5.

Test written by George Tudoran, who reported the case on #12675.

Co-Authored-By: George Tudoran <george.tudoran@zitec.com>

---------

Co-authored-by: Jérôme Tamarelle <jerome@tamarelle.net>
Co-authored-by: George Tudoran <george.tudoran@zitec.com>
…ed-indexes-3.7.x

Inherit unnamed indexes from mapped superclasses
…fier-folding

Respect configured identifier folding
…3.8.x_Kpnl50BO

# Conflicts:
#	tests/Tests/ORM/Functional/PaginationTest.php
@greg0ire
greg0ire merged commit c3a9290 into 3.8.x Oct 31, 2026
131 checks passed
@greg0ire
greg0ire deleted the 3.7.x-merge-up-into-3.8.x_Kpnl50BO branch October 31, 2026 13:20
@greg0ire
greg0ire restored the 3.7.x-merge-up-into-3.8.x_Kpnl50BO branch October 31, 2026 14:59
@greg0ire
greg0ire deleted the 3.7.x-merge-up-into-3.8.x_Kpnl50BO branch October 31, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants