Compare commits

...

34 Commits

Author SHA1 Message Date
Grégoire Paris f58984c43f Merge pull request #11198 from jwage/port-lock-sql-changes
Fix calls to removed lock methods (#11061)
2024-01-31 08:50:41 +01:00
Alexander M. Turek 79c7c5087e Fix calls to removed lock methods (#11061) 2024-01-30 15:39:35 -06:00
Grégoire Paris 12c4560f1d Merge pull request #11197 from mpdude/update-limit-subquery-output-walker-test
Cover limit/offset values in `LimitSubqueryOutputWalkerTest`
2024-01-30 11:26:54 +01:00
Matthias Pigulla 152ebd756c Cover limit/offset values in LimitSubqueryOutputWalkerTest
This will help to make sure we don't lose those parts of the SQL when working on #11188.
2024-01-30 09:02:43 +01:00
Grégoire Paris 82533af893 Merge pull request #11191 from greg0ire/ignore-depr
Ignore deprecations handled in next major
2024-01-28 16:40:46 +01:00
Grégoire Paris b988137378 Ignore deprecations handled in next major
These deprecations have been handled on 4.0.x in
https://github.com/doctrine/orm/pull/11061, it is safe to ignore them.
2024-01-28 16:30:02 +01:00
Grégoire Paris ccfc97c32f Merge pull request #11187 from jwage/remove-xml-validation-disabling-deprecation
Remove XML validation disabling deprecation.
2024-01-26 20:41:16 +01:00
Jonathan H. Wage d386b43be3 Remove XML validation disabling deprecation. 2024-01-26 09:59:03 -06:00
Grégoire Paris 0970ce7072 Merge pull request #11186 from derrabus/chore/readme-versions
Update branches in README
2024-01-26 08:34:24 +01:00
Alexander M. Turek 624c56be72 Update branches in README 2024-01-26 00:52:35 +01:00
Grégoire Paris fbc8e6741e Merge pull request #11176 from thePanz/fix-11173-get-name-on-null-non-backed-enum
Throw exception when trying to use non-backed enum types
2024-01-23 07:50:20 +01:00
thePanz 7151db3cb8 Throw exception when trying to use non-backed enum types 2024-01-22 13:04:58 +01:00
Grégoire Paris ac24c11808 Modernize code in documentation (#11179)
Somehow, there still were code samples relying on annotations.
2024-01-20 21:53:48 +01:00
Grégoire Paris dd478d8662 Merge pull request #11178 from greg0ire/remove-ref-jira
Remove references to JIRA
2024-01-20 14:06:15 +01:00
Grégoire Paris 0b3cd72609 Remove references to JIRA 2024-01-20 13:45:04 +01:00
Grégoire Paris 85034699cb Merge pull request #11171 from greg0ire/extract-class
Make Doctrine\Tests\ORM\Internal\Node autoloadable
2024-01-18 21:33:06 +01:00
Grégoire Paris d98186e2c4 Make Doctrine\Tests\ORM\Internal\Node autoloadable
It is used in several tests.
2024-01-18 21:19:28 +01:00
Grégoire Paris a0ed37954b Merge pull request #11167 from bobvandevijver/fix-eager-iterable-loading-test
Use foreach on iterable to prevent table locks during tests
2024-01-18 16:47:11 +01:00
Bob van de Vijver 4875f4c878 Use foreach on iterable to prevent table locks during tests 2024-01-18 10:24:40 +01:00
Grégoire Paris 398ab0547a Merge pull request #11162 from greg0ire/fix-libxml-compat
Remove redundant tags
2024-01-16 22:32:04 +01:00
Grégoire Paris 8f15337b03 Remove redundant tags
The "any" tags inside the definition for mapped superclasses and
embeddables duplicate what is already done for entities.

The other removed "any" tags are also redundant, as they duplicate
what's already done inside the grandparent "choice" tag.

Starting with version libxml 2.12, such redundant tags cause errors
about the content model not being "determinist".

Fixes #11117
2024-01-16 22:01:16 +01:00
Matthias Pigulla a8632aca8f Keep the declared mapping information when using attribute overrides (#11135)
When using `AttributeOverride` to override mapping information inherited from a parent class (a mapped superclass), make sure to keep information about where the field was originally declared.

This is important for `private` fields: Without the correct `declared` information, it will lead to errors when cached mapping information is loaded, reflection wakes up and looks for the private field in the wrong class.
2024-01-13 00:06:34 +01:00
Matthias Pigulla 3dd3d38857 Fix @SequenceGeneratorDefinition inheritance, take 1 (#11050)
#10927 reported that #10455 broke the way how the default `@SequenceGeneratorDefinition` is created and inherited by subclasses for ID columns using `@GeneratedValue(strategy="SEQUENCE")`.

First, I had to understand how `@SequenceGeneratorDefinition` has been handled before #10455 when entity inheritance comes into play:

* Entity and mapped superclasses inherit the ID generator type (as given by `@GeneratedValue`) from their parent classes
* `@SequenceGeneratorDefinition`, however, is not generally inherited
* ... instead, a default sequence generator definition is created for every class when no explicit configuration is given. In this case, sequence names are based on the current class' table name.
* Once a root entity has been identified, all subclasses inherit its sequence generator definition unchanged.

#### Why did #10455 break this?

When I implemented #10455, I was mislead by two tests `BasicInheritanceMappingTest::testGeneratedValueFromMappedSuperclass` and `BasicInheritanceMappingTest::testMultipleMappedSuperclasses`.

These tests check the sequence generator definition that is inherited by an entity class from a mapped superclass, either directly or through an additional (intermediate) mapped superclass.

The tests expect the sequence generator definition on the entity _to be the same_ as on the base mapped superclass.

The reason why the tests worked before was the quirky behaviour of the annotation and attribute drivers that #10455 was aiming at: The drivers did not report the `@SequenceGeneratorDefinition` on the base mapped superclass where it was actually defined. Instead, they reported this `@SequenceGeneratorDefinition` for the entity class only.

This means the inheritance rules stated above did not take effect, since the ID field with the sequence generator was virtually pushed down to the entity class.

In #10455, I did not realize that these failing tests had to do with the quirky and changed mapping driver behaviour. Instead, I tried to "fix" the inheritance rules by passing along the sequence generator definition unchanged once the ID column had been defined.

#### Consequences of the change suggested here

This PR reverts the changes made to `@SequenceGeneratorDefinition` inheritance behaviour that were done in #10455.

This means that with the new "report fields where declared" driver mode (which is active in our functional tests) we can not expect the sequence generator definition to be inherited from mapped superclasses. The two test cases from `BasicInheritanceMappingTest` are removed.

I will leave a notice in #10455 to indicate that the new driver mode also affects sequence generator definitions.

The `GH10927Test` test case validates the sequence names generated in a few cases. In fact, I wrote this test against the `2.15.x` branch to make sure we get results that are consistent with the previous behaviour.

This also means `@SequenceGeneratorDefinition` on mapped superclasses is pointless: The mapped superclass does not make use of the definition itself (it has no table), and the setting is never inherited to child classes.
 
Fixes #10927. There is another implementation with slightly different inheritance semantics in #11052, in case the fix is not good enough and we'd need to review the topic later on.
2024-01-12 22:59:14 +01:00
Matthias Pigulla c6b3509aa9 Include ON DELETE CASCADE associations in the delete order computation (#10913)
In order to resolve #10348, some changes were included in #10547 to improve the computed _delete_ order for entities. 

One assumption was that foreign key references with `ON DELETE SET NULL` or `... CASCADE` need not need to be taken into consideration when planning the deletion order, since the RDBMS would unset or cascade-delete such associations by itself when necessary. Only associations that do _not_ use RDBMS-level cascade handling would be sequenced, to make sure the referring entity is deleted before the referred-to one.

This assumption is wrong for `ON DELETE CASCADE`. The following examples give reasons why we need to also consider such associations, and in addition, we need to be able to deal with cycles formed by them.

In the following diagrams, `odc` means `ON DELETE CASCADE`, and `ref` is a regular foreign key with no extra `ON DELETE` semantics.

```mermaid
graph LR;
C-->|ref| B;
B-->|odc| A;
```

In this example, C must be removed before B and A. If we ignore the B->A dependency in the delete order computation, the result may not to be correct. ACB is not a working solution.

```mermaid
graph LR;
A-->|odc| B;
B-->|odc| A;
C-->|ref| B;
```

This is the situation in #10912. We have to deal with a cycle in the graph. C must be removed before A as well as B. If we ignore the B->A dependency (e.g. because we set it to "optional" to get away with the cycle), we might end up with an incorrect order ACB.

```mermaid
graph LR;
A-->|odc| B;
B-->|odc| A;
A-->|ref| C;
C-->|ref| B;
```

This example has no possible remove order. But, if we treat `odc` edges as optional, A -> C -> B would wrongly be deemed suitable.

```mermaid
graph LR;
A-->|ref| B;
B-->|odc| C;
C-->|odc| B;
D-->|ref| C;
```

Here, we must first remove A and D in any order; then, B and C in any order. If we treat one of the `odc` edges as optional, we might find the invalid solutions ABDC or DCAB.

#### Solution implemented in this PR

First, build a graph with a node for every to-be-removed entity, and edges for `ON DELETE CASCADE` associations between those entities. Then, use [Tarjan's algorithm](https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm) to find strongly connected components (SCCs) in this graph. The significance of SCCs is that whenever we remove one of the entities in a SCC from the database (no matter which one), the DBMS will immediately remove _all_ the other entities of that group as well.

For every SCC, pick one (arbitrary) entity from the group to represent all entities of that group. 

Then, build a second graph. Again we have nodes for all entities that are to be removed. This time, we insert edges for all regular (foreign key) associations and those with `ON DELETE CASCADE`. `ON DELETE SET NULL` can be left out. The edges are not added between the entities themselves, but between the entities representing the respective SCCs.

Also, for all non-trivial SCCs (those containing more than a single entity), add dependency edges to indicate that all entities of the SCC shall be processed _after_ the entity representing the group. This is to make sure we do not remove a SCC inadvertedly by removing one of its entities too early.

Run a topological sort on the second graph to get the actual delete order. Cycles in this second graph are a problem, there is no delete order.

Fixes #10912.
2024-01-12 22:44:07 +01:00
Grégoire Paris a32578b7ea Merge pull request #11082 from bobvandevijver/eager-collection-iterable
Do not defer eager collection loading when in iteration context
2024-01-10 10:03:40 +01:00
Matthias Pigulla e585a92763 Mention that `postRemove` may still see removed entities in in-memory collections (#11146)
... plus minor tweaks.
2024-01-02 21:31:28 +01:00
Grégoire Paris 26f47cb8d3 Merge pull request #11142 from greg0ire/remove-inheritance
Remove inheritance
2024-01-02 08:24:50 +01:00
Grégoire Paris ebb101009c Remove inheritance
Spotted while trying to merge https://github.com/doctrine/orm/pull/11076
(among other things) up into 3.0.x. On that branch, it is no longer
possible for an entity to extend another entity without specifying an
inheritance mapping type.

I think the goal of that inheritance was just to reuse the identifier
anyway, so let's just duplicate the identifier declaration instead.
2023-12-28 19:59:02 +01:00
Grégoire Paris f80ef66ffb Merge pull request #11134 from doctrine/no-private-fields-duplicate
Mention in the limitations that private field names cannot be reused
2023-12-22 17:35:27 +01:00
Matthias Pigulla 85d78f8b0d Mention in the limitations that private field names cannot be reused 2023-12-22 17:13:11 +01:00
Grégoire Paris c2886478e8 Merge pull request #11086 from mpdude/11058-revisited
Avoid an inconsistency in topological sort result order
2023-12-21 22:51:39 +01:00
Matthias Pigulla 108fa30db2 Improve topological sort result order
This PR changes a detail in the commit order computation for depended-upon entities.

We have a parent-child relationship between two entity classes. The association is parent one-to-many children, with the child entities containing the (owning side) back-reference.

Cascade-persist is not used, so all entities have to be passed to `EntityManager::persist()`.

Before v2.16.0, two child entities C1 and C2 will be inserted in the same order in which they are passed to `persist()`, and that is regardless of whether the parent entity was passed to `persist()` before or after the child entities.

As of v2.16.0, passing the parent entity to `persist()` _after_ the child entities will lead to an insert order that is _reversed_ compared to the order of `persist()` calls.

This PR makes the order consistent in both cases, as it was before v2.16.0.

 #### Cause

When the parent is passed to `persist()` after the children, commit order computation has to re-arrange the entities. The parent must be inserted first since it is referred to by the children.

The implementation of the topological sort from #10547 processed entities in reverse `persist()` order and unshifted finished nodes to an array to obtain the final result. That leads to dependencies (parent → before c1, parent → before c2) showing up in the result in the reverse order of which they were added.

This PR changes the topological sort to produce a result in the opposite order ("all edges pointing left"), which helps to avoid the duplicate array order reversal.

 #### Discussion

* This PR _does not_ change semantics of the `persist()` so that entities would (under all ciscumstances) be inserted in the order of `persist()` calls.
* It fixes an unnecessary inconsistency between versions before 2.16.0 and after. In particular, it may be surprising that the insert order for the child entities depends on whether another referred-to entity (the parent) was added before or after them.
* _Both_ orders (c1 before or after c2) are technically and logically correct with regard to the agreement that `commit()` is free to arrange entities in a way that allows for efficient insertion into the database.

Fixes #11058.
2023-12-21 16:26:20 +01:00
Bob van de Vijver e5ab18ff80 Do not defer eager loading when iterable hint is set 2023-11-23 13:04:13 +01:00
Bob van de Vijver 665ccf1376 Add failing test
This test show that eager collections are broken when used in conjuction
with iterating over a result.
2023-11-23 12:42:38 +01:00
42 changed files with 1592 additions and 447 deletions
+11 -11
View File
@@ -1,7 +1,7 @@
| [3.0.x][3.0] | [2.16.x][2.16] | [2.15.x][2.15] |
| [3.0.x][3.0] | [2.18.x][2.18] | [2.17.x][2.17] |
|:----------------:|:----------------:|:----------:|
| [![Build status][3.0 image]][3.0] | [![Build status][2.16 image]][2.16] | [![Build status][2.15 image]][2.15] |
| [![Coverage Status][3.0 coverage image]][3.0 coverage]| [![Coverage Status][2.16 coverage image]][2.16 coverage] | [![Coverage Status][2.15 coverage image]][2.15 coverage] |
| [![Build status][3.0 image]][3.0] | [![Build status][2.18 image]][2.18] | [![Build status][2.17 image]][2.17] |
| [![Coverage Status][3.0 coverage image]][3.0 coverage]| [![Coverage Status][2.18 coverage image]][2.18 coverage] | [![Coverage Status][2.17 coverage image]][2.17 coverage] |
[<h1 align="center">🇺🇦 UKRAINE NEEDS YOUR HELP NOW!</h1>](https://www.doctrine-project.org/stop-war.html)
@@ -22,11 +22,11 @@ without requiring unnecessary code duplication.
[3.0]: https://github.com/doctrine/orm/tree/3.0.x
[3.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.0.x/graph/badge.svg
[3.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.0.x
[2.16 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.16.x
[2.16]: https://github.com/doctrine/orm/tree/2.16.x
[2.16 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.16.x/graph/badge.svg
[2.16 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.16.x
[2.15 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.15.x
[2.15]: https://github.com/doctrine/orm/tree/2.15.x
[2.15 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.15.x/graph/badge.svg
[2.15 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.15.x
[2.18 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.18.x
[2.18]: https://github.com/doctrine/orm/tree/2.18.x
[2.18 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.18.x/graph/badge.svg
[2.18 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.18.x
[2.17 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.17.x
[2.17]: https://github.com/doctrine/orm/tree/2.17.x
[2.17 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.17.x/graph/badge.svg
[2.17 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.17.x
+2 -3
View File
@@ -13,6 +13,5 @@ understand the assumptions we make.
- [DBAL Security Page](https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/security.html)
- [ORM Security Page](https://www.doctrine-project.org/projects/doctrine-orm/en/stable/reference/security.html)
If you find a Security bug in Doctrine, please report it on Jira and change the
Security Level to "Security Issues". It will be visible to Doctrine Core
developers and you only.
If you find a Security bug in Doctrine, please follow our
[Security reporting guidelines](https://www.doctrine-project.org/policies/security.html#reporting).
+50 -77
View File
@@ -36,71 +36,50 @@ Our entities look like:
namespace Bank\Entities;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
#[ORM\Entity]
class Account
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id;
/**
* @ORM\Column(type="string", unique=true)
*/
private string $no;
/**
* @ORM\OneToMany(targetEntity="Entry", mappedBy="account", cascade={"persist"})
*/
private array $entries;
/**
* @ORM\Column(type="integer")
*/
private int $maxCredit = 0;
public function __construct(string $no, int $maxCredit = 0)
{
$this->no = $no;
$this->maxCredit = $maxCredit;
$this->entries = new \Doctrine\Common\Collections\ArrayCollection();
#[ORM\OneToMany(targetEntity: Entry::class, mappedBy: 'account', cascade: ['persist'])]
private Collection $entries;
public function __construct(
#[ORM\Column(type: 'string', unique: true)]
private string $no,
#[ORM\Column(type: 'integer')]
private int $maxCredit = 0,
) {
$this->entries = new ArrayCollection();
}
}
/**
* @ORM\Entity
*/
#[ORM\Entity]
class Entry
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id;
/**
* @ORM\ManyToOne(targetEntity="Account", inversedBy="entries")
*/
private Account $account;
/**
* @ORM\Column(type="integer")
*/
private int $amount;
public function __construct(Account $account, int $amount)
{
$this->account = $account;
$this->amount = $amount;
public function __construct(
#[ORM\ManyToOne(targetEntity: Account::class, inversedBy: 'entries')]
private Account $account,
#[ORM\Column(type: 'integer')]
private int $amount,
) {
// more stuff here, from/to whom, stated reason, execution date and such
}
public function getAmount(): Amount
{
return $this->amount;
@@ -193,9 +172,8 @@ relation with this method:
public function addEntry(int $amount): void
{
$this->assertAcceptEntryAllowed($amount);
$e = new Entry($this, $amount);
$this->entries[] = $e;
$this->entries[] = new Entry($this, $amount);
}
}
@@ -213,18 +191,18 @@ Now look at the following test-code for our entities:
{
$account = new Account("123456", maxCredit: 200);
$this->assertEquals(0, $account->getBalance());
$account->addEntry(500);
$this->assertEquals(500, $account->getBalance());
$account->addEntry(-700);
$this->assertEquals(-200, $account->getBalance());
}
public function testExceedMaxLimit()
{
$account = new Account("123456", maxCredit: 200);
$this->expectException(Exception::class);
$account->addEntry(-1000);
}
@@ -285,22 +263,19 @@ entries collection) we want to add an aggregate field called
<?php
class Account
{
/**
* @ORM\Column(type="integer")
*/
#[ORM\Column(type: 'integer')]
private int $balance = 0;
public function getBalance(): int
{
return $this->balance;
}
public function addEntry(int $amount): void
{
$this->assertAcceptEntryAllowed($amount);
$e = new Entry($this, $amount);
$this->entries[] = $e;
$this->entries[] = new Entry($this, $amount);
$this->balance += $amount;
}
}
@@ -331,13 +306,13 @@ potentially lead to inconsistent state. See this example:
// The Account $accId has a balance of 0 and a max credit limit of 200:
// request 1 account
$account1 = $em->find(Account::class, $accId);
// request 2 account
$account2 = $em->find(Account::class, $accId);
$account1->addEntry(-200);
$account2->addEntry(-200);
// now request 1 and 2 both flush the changes.
The aggregate field ``Account::$balance`` is now -200, however the
@@ -357,10 +332,8 @@ Optimistic locking is as easy as adding a version column:
class Account
{
/**
* @ORM\Column(type="integer")
* @ORM\Version
*/
#[ORM\Column(type: 'integer')]
#[ORM\Version]
private int $version;
}
@@ -47,10 +47,8 @@ A Customer entity
use Acme\CustomerModule\Entity\Customer as BaseCustomer;
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
/**
* @ORM\Entity
* @ORM\Table(name="customer")
*/
#[ORM\Entity]
#[ORM\Table(name: 'customer')]
class Customer extends BaseCustomer implements InvoiceSubjectInterface
{
// In our example, any methods defined in the InvoiceSubjectInterface
@@ -69,19 +67,12 @@ An Invoice entity
use Doctrine\ORM\Mapping AS ORM;
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
/**
* Represents an Invoice.
*
* @ORM\Entity
* @ORM\Table(name="invoice")
*/
#[ORM\Entity]
#[ORM\Table(name: 'invoice')]
class Invoice
{
/**
* @ORM\ManyToOne(targetEntity="Acme\InvoiceModule\Model\InvoiceSubjectInterface")
* @var InvoiceSubjectInterface
*/
protected $subject;
#[ORM\ManyToOne(targetEntity: InvoiceSubjectInterface::class)]
protected InvoiceSubjectInterface $subject;
}
An InvoiceSubjectInterface
+25 -22
View File
@@ -173,6 +173,19 @@ Events Overview
| :ref:`onClear<reference-events-on-clear>` | ``$em->clear()`` | No | `OnClearEventArgs`_ |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
.. warning::
Making changes to entities and calling ``EntityManager::flush()`` from within
event handlers dispatched by ``EntityManager::flush()`` itself is strongly
discouraged, and might be deprecated and eventually prevented in the future.
The reason is that it causes re-entrance into ``UnitOfWork::commit()`` while a commit
is currently being processed. The ``UnitOfWork`` was never designed to support this,
and its behavior in this situation is not covered by any tests.
This may lead to entity or collection updates being missed, applied only in parts and
changes being lost at the end of the commit phase.
Naming convention
~~~~~~~~~~~~~~~~~
@@ -699,30 +712,33 @@ Restrictions for this event:
postUpdate, postRemove, postPersist
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
These three post* events are called inside ``EntityManager::flush()``.
These three ``post*`` events are called inside ``EntityManager::flush()``.
Changes in here are not relevant to the persistence in the
database, but you can use these events to alter non-persistable items,
like non-mapped fields, logging or even associated classes that are
not directly mapped by Doctrine.
- The ``postUpdate`` event occurs after the database
update operations to entity data. It is not called for a DQL
``UPDATE`` statement.
update operations to entity data, but before the database transaction
has been committed. It is not called for a DQL ``UPDATE`` statement.
- The ``postPersist`` event occurs for an entity after the entity has
been made persistent. It will be invoked after all database insert
operations for new entities have been performed. Generated primary
key values will be available for all entities at the time this
event is triggered.
operations for new entities have been performed, but before the database
transaction has been committed. Generated primary key values will be
available for all entities at the time this event is triggered.
- The ``postRemove`` event occurs for an entity after the
entity has been deleted. It will be invoked after all database
delete operations for entity rows have been executed. This event is
not called for a DQL ``DELETE`` statement.
delete operations for entity rows have been executed, but before the
database transaction has been committed. This event is not called for
a DQL ``DELETE`` statement.
.. note::
At the time ``postPersist`` is called, there may still be collection and/or
"extra" updates pending. The database may not yet be completely in
sync with the entity states in memory, not even for the new entities.
sync with the entity states in memory, not even for the new entities. Similarly,
also at the time ``postUpdate`` and ``postRemove`` are called, in-memory collections
may still be in a "dirty" state or still contain removed entities.
.. warning::
@@ -731,19 +747,6 @@ not directly mapped by Doctrine.
cascade remove relations. In this case, you should load yourself the proxy in
the associated ``pre*`` event.
.. warning::
Making changes to entities and calling ``EntityManager::flush()`` from within
``post*`` event handlers is strongly discouraged, and might be deprecated and
eventually prevented in the future.
The reason is that it causes re-entrance into ``UnitOfWork::commit()`` while a commit
is currently being processed. The ``UnitOfWork`` was never designed to support this,
and its behavior in this situation is not covered by any tests.
This may lead to entity or collection updates being missed, applied only in parts and
changes being lost at the end of the commit phase.
.. _reference-events-post-load:
postLoad
@@ -1,10 +1,10 @@
Limitations and Known Issues
============================
We try to make using Doctrine2 a very pleasant experience.
We try to make using Doctrine ORM a very pleasant experience.
Therefore we think it is very important to be honest about the
current limitations to our users. Much like every other piece of
software Doctrine2 is not perfect and far from feature complete.
software the ORM is not perfect and far from feature complete.
This section should give you an overview of current limitations of
Doctrine ORM as well as critical known issues that you should know
about.
@@ -175,6 +175,18 @@ due to complexity.
XML mapping configuration probably needs to completely re-configure or otherwise
copy-and-paste configuration for fields used from traits.
Mapping multiple private fields of the same name
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When two classes, say a mapped superclass and an entity inheriting from it,
both contain a ``private`` field of the same name, this will lead to a ``MappingException``.
Since the fields are ``private``, both are technically separate and can contain
different values at the same time. However, the ``ClassMetadata`` configuration used
internally by the ORM currently refers to fields by their name only, without taking the
class containing the field into consideration. This makes it impossible to keep separate
mapping configuration for both fields.
Known Issues
------------
+2 -3
View File
@@ -12,9 +12,8 @@ page only handles Security issues in the ORM.
- `DBAL Security Page <https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/security.html>`
If you find a Security bug in Doctrine, please report it on Jira and change the
Security Level to "Security Issues". It will be visible to Doctrine Core
developers and you only.
If you find a Security bug in Doctrine, please follow our
`Security reporting guidelines <https://www.doctrine-project.org/policies/security.html#reporting>`_.
User input and Doctrine ORM
---------------------------
+2 -14
View File
@@ -148,7 +148,6 @@
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="entity-result" type="orm:entity-result"/>
<xs:element name="column-result" type="orm:column-result"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
@@ -226,22 +225,13 @@
<xs:complexType name="mapped-superclass" >
<xs:complexContent>
<xs:extension base="orm:entity">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:anyAttribute namespace="##other"/>
</xs:extension>
<xs:extension base="orm:entity"/>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="embeddable">
<xs:complexContent>
<xs:extension base="orm:entity">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
</xs:extension>
<xs:extension base="orm:entity"/>
</xs:complexContent>
</xs:complexType>
@@ -565,7 +555,6 @@
<xs:choice minOccurs="0" maxOccurs="1">
<xs:element name="join-column" type="orm:join-column"/>
<xs:element name="join-columns" type="orm:join-columns"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
@@ -583,7 +572,6 @@
<xs:choice minOccurs="0" maxOccurs="1">
<xs:element name="join-column" type="orm:join-column"/>
<xs:element name="join-columns" type="orm:join-columns"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal;
use InvalidArgumentException;
use function array_keys;
use function array_pop;
use function array_push;
use function min;
use function spl_object_id;
/**
* StronglyConnectedComponents implements Tarjan's algorithm to find strongly connected
* components (SCC) in a directed graph. This algorithm has a linear running time based on
* nodes (V) and edges between the nodes (E), resulting in a computational complexity
* of O(V + E).
*
* See https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
* for an explanation and the meaning of the DFS and lowlink numbers.
*
* @internal
*/
final class StronglyConnectedComponents
{
private const NOT_VISITED = 1;
private const IN_PROGRESS = 2;
private const VISITED = 3;
/**
* Array of all nodes, indexed by object ids.
*
* @var array<int, object>
*/
private $nodes = [];
/**
* DFS state for the different nodes, indexed by node object id and using one of
* this class' constants as value.
*
* @var array<int, self::*>
*/
private $states = [];
/**
* Edges between the nodes. The first-level key is the object id of the outgoing
* node; the second array maps the destination node by object id as key.
*
* @var array<int, array<int, bool>>
*/
private $edges = [];
/**
* DFS numbers, by object ID
*
* @var array<int, int>
*/
private $dfs = [];
/**
* lowlink numbers, by object ID
*
* @var array<int, int>
*/
private $lowlink = [];
/** @var int */
private $maxdfs = 0;
/**
* Nodes representing the SCC another node is in, indexed by lookup-node object ID
*
* @var array<int, object>
*/
private $representingNodes = [];
/**
* Stack with OIDs of nodes visited in the current state of the DFS
*
* @var list<int>
*/
private $stack = [];
/** @param object $node */
public function addNode($node): void
{
$id = spl_object_id($node);
$this->nodes[$id] = $node;
$this->states[$id] = self::NOT_VISITED;
$this->edges[$id] = [];
}
/** @param object $node */
public function hasNode($node): bool
{
return isset($this->nodes[spl_object_id($node)]);
}
/**
* Adds a new edge between two nodes to the graph
*
* @param object $from
* @param object $to
*/
public function addEdge($from, $to): void
{
$fromId = spl_object_id($from);
$toId = spl_object_id($to);
$this->edges[$fromId][$toId] = true;
}
public function findStronglyConnectedComponents(): void
{
foreach (array_keys($this->nodes) as $oid) {
if ($this->states[$oid] === self::NOT_VISITED) {
$this->tarjan($oid);
}
}
}
private function tarjan(int $oid): void
{
$this->dfs[$oid] = $this->lowlink[$oid] = $this->maxdfs++;
$this->states[$oid] = self::IN_PROGRESS;
array_push($this->stack, $oid);
foreach ($this->edges[$oid] as $adjacentId => $ignored) {
if ($this->states[$adjacentId] === self::NOT_VISITED) {
$this->tarjan($adjacentId);
$this->lowlink[$oid] = min($this->lowlink[$oid], $this->lowlink[$adjacentId]);
} elseif ($this->states[$adjacentId] === self::IN_PROGRESS) {
$this->lowlink[$oid] = min($this->lowlink[$oid], $this->dfs[$adjacentId]);
}
}
$lowlink = $this->lowlink[$oid];
if ($lowlink === $this->dfs[$oid]) {
$representingNode = null;
do {
$unwindOid = array_pop($this->stack);
if (! $representingNode) {
$representingNode = $this->nodes[$unwindOid];
}
$this->representingNodes[$unwindOid] = $representingNode;
$this->states[$unwindOid] = self::VISITED;
} while ($unwindOid !== $oid);
}
}
/**
* @param object $node
*
* @return object
*/
public function getNodeRepresentingStronglyConnectedComponent($node)
{
$oid = spl_object_id($node);
if (! isset($this->representingNodes[$oid])) {
throw new InvalidArgumentException('unknown node');
}
return $this->representingNodes[$oid];
}
}
+5 -11
View File
@@ -7,8 +7,6 @@ namespace Doctrine\ORM\Internal;
use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException;
use function array_keys;
use function array_reverse;
use function array_unshift;
use function spl_object_id;
/**
@@ -93,18 +91,14 @@ final class TopologicalSort
/**
* Returns a topological sort of all nodes. When we have an edge A->B between two nodes
* A and B, then A will be listed before B in the result.
* A and B, then B will be listed before A in the result. Visually speaking, when ordering
* the nodes in the result order from left to right, all edges point to the left.
*
* @return list<object>
*/
public function sort(): array
{
/*
* When possible, keep objects in the result in the same order in which they were added as nodes.
* Since nodes are unshifted into $this->>sortResult (see the visit() method), that means we
* need to work them in array_reverse order here.
*/
foreach (array_reverse(array_keys($this->nodes)) as $oid) {
foreach (array_keys($this->nodes) as $oid) {
if ($this->states[$oid] === self::NOT_VISITED) {
$this->visit($oid);
}
@@ -147,7 +141,7 @@ final class TopologicalSort
}
// We have found a cycle and cannot break it at $edge. Best we can do
// is to retreat from the current vertex, hoping that somewhere up the
// is to backtrack from the current vertex, hoping that somewhere up the
// stack this can be salvaged.
$this->states[$oid] = self::NOT_VISITED;
$exception->addToCycle($this->nodes[$oid]);
@@ -160,6 +154,6 @@ final class TopologicalSort
// So we're done with this vertex as well.
$this->states[$oid] = self::VISITED;
array_unshift($this->sortResult, $this->nodes[$oid]);
$this->sortResult[] = $this->nodes[$oid];
}
}
@@ -123,7 +123,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
if ($parent) {
$class->setInheritanceType($parent->inheritanceType);
$class->setDiscriminatorColumn($parent->discriminatorColumn);
$this->inheritIdGeneratorMapping($class, $parent);
$class->setIdGeneratorType($parent->generatorType);
$this->addInheritedFields($class, $parent);
$this->addInheritedRelations($class, $parent);
$this->addInheritedEmbeddedClasses($class, $parent);
@@ -151,8 +151,12 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
throw MappingException::reflectionFailure($class->getName(), $e);
}
// Complete id generator mapping when the generator was declared/added in this class
if ($class->identifier && (! $parent || ! $parent->identifier)) {
// If this class has a parent the id generator strategy is inherited.
// However this is only true if the hierarchy of parents contains the root entity,
// if it consists of mapped superclasses these don't necessarily include the id field.
if ($parent && $rootEntityFound) {
$this->inheritIdGeneratorMapping($class, $parent);
} else {
$this->completeIdGeneratorMapping($class);
}
@@ -2558,6 +2558,10 @@ class ClassMetadataInfo implements ClassMetadata
$overrideMapping['id'] = $mapping['id'];
}
if (isset($mapping['declared'])) {
$overrideMapping['declared'] = $mapping['declared'];
}
if (! isset($overrideMapping['type'])) {
$overrideMapping['type'] = $mapping['type'];
}
@@ -57,7 +57,15 @@ final class DefaultTypedFieldMapper implements TypedFieldMapper
$mapping['enumType'] = $type->getName();
$reflection = new ReflectionEnum($type->getName());
$type = $reflection->getBackingType();
if (! $reflection->isBacked()) {
throw MappingException::backedEnumTypeRequired(
$field->class,
$mapping['fieldName'],
$mapping['enumType']
);
}
$type = $reflection->getBackingType();
assert($type instanceof ReflectionNamedType);
}
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
@@ -56,15 +55,6 @@ class XmlDriver extends FileDriver
);
}
if (! $isXsdValidationEnabled) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/6728',
'Using XML mapping driver with XSD validation disabled is deprecated'
. ' and will not be supported in Doctrine ORM 3.0.'
);
}
if ($isXsdValidationEnabled && ! extension_loaded('dom')) {
throw new LogicException(
'XSD validation cannot be enabled because the DOM extension is missing.'
@@ -954,6 +954,16 @@ class MappingException extends ORMException
return new self(sprintf('Enum types require PHP 8.1 in %s::$%s', $className, $fieldName));
}
public static function backedEnumTypeRequired(string $className, string $fieldName, string $enumType): self
{
return new self(sprintf(
'Attempting to map a non-backed enum type %s in entity %s::$%s. Please use backed enums only',
$enumType,
$className,
$fieldName
));
}
public static function nonEnumTypeMapped(string $className, string $fieldName, string $enumType): self
{
return new self(sprintf(
@@ -31,6 +31,7 @@ use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Repository\Exception\InvalidFindByCall;
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\Utility\IdentifierFlattener;
use Doctrine\ORM\Utility\LockSqlHelper;
use Doctrine\ORM\Utility\PersisterHelper;
use LengthException;
@@ -92,6 +93,8 @@ use function trim;
*/
class BasicEntityPersister implements EntityPersister
{
use LockSqlHelper;
/** @var array<string,string> */
private static $comparisonMap = [
Comparison::EQ => '= %s',
@@ -1116,11 +1119,11 @@ class BasicEntityPersister implements EntityPersister
switch ($lockMode) {
case LockMode::PESSIMISTIC_READ:
$lockSql = ' ' . $this->platform->getReadLockSQL();
$lockSql = ' ' . $this->getReadLockSQL($this->platform);
break;
case LockMode::PESSIMISTIC_WRITE:
$lockSql = ' ' . $this->platform->getWriteLockSQL();
$lockSql = ' ' . $this->getWriteLockSQL($this->platform);
break;
}
@@ -1578,11 +1581,11 @@ class BasicEntityPersister implements EntityPersister
switch ($lockMode) {
case LockMode::PESSIMISTIC_READ:
$lockSql = $this->platform->getReadLockSQL();
$lockSql = $this->getReadLockSQL($this->platform);
break;
case LockMode::PESSIMISTIC_WRITE:
$lockSql = $this->platform->getWriteLockSQL();
$lockSql = $this->getWriteLockSQL($this->platform);
break;
}
@@ -10,6 +10,7 @@ use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Internal\SQLResultCasing;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Utility\LockSqlHelper;
use Doctrine\ORM\Utility\PersisterHelper;
use LengthException;
@@ -26,6 +27,7 @@ use function implode;
*/
class JoinedSubclassPersister extends AbstractEntityInheritancePersister
{
use LockSqlHelper;
use SQLResultCasing;
/**
@@ -316,12 +318,12 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
switch ($lockMode) {
case LockMode::PESSIMISTIC_READ:
$lockSql = ' ' . $this->platform->getReadLockSQL();
$lockSql = ' ' . $this->getReadLockSQL($this->platform);
break;
case LockMode::PESSIMISTIC_WRITE:
$lockSql = ' ' . $this->platform->getWriteLockSQL();
$lockSql = ' ' . $this->getWriteLockSQL($this->platform);
break;
}
+5 -2
View File
@@ -16,6 +16,7 @@ use Doctrine\ORM\Mapping\QuoteStrategy;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\Query;
use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver;
use Doctrine\ORM\Utility\LockSqlHelper;
use Doctrine\ORM\Utility\PersisterHelper;
use InvalidArgumentException;
use LogicException;
@@ -48,6 +49,8 @@ use function trim;
*/
class SqlWalker implements TreeWalker
{
use LockSqlHelper;
public const HINT_DISTINCT = 'doctrine.distinct';
/**
@@ -577,11 +580,11 @@ class SqlWalker implements TreeWalker
}
if ($lockMode === LockMode::PESSIMISTIC_READ) {
return $sql . ' ' . $this->platform->getReadLockSQL();
return $sql . ' ' . $this->getReadLockSQL($this->platform);
}
if ($lockMode === LockMode::PESSIMISTIC_WRITE) {
return $sql . ' ' . $this->platform->getWriteLockSQL();
return $sql . ' ' . $this->getWriteLockSQL($this->platform);
}
if ($lockMode !== LockMode::OPTIMISTIC) {
@@ -257,7 +257,7 @@ class LimitSubqueryOutputWalker extends SqlWalker
$innerSql
);
// http://www.doctrine-project.org/jira/browse/DDC-1958
// https://github.com/doctrine/orm/issues/2630
$sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause);
// Apply the limit and offset.
+81 -16
View File
@@ -28,6 +28,7 @@ use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Exception\UnexpectedAssociationValue;
use Doctrine\ORM\Id\AssignedGenerator;
use Doctrine\ORM\Internal\HydrationCompleteHandler;
use Doctrine\ORM\Internal\StronglyConnectedComponents;
use Doctrine\ORM\Internal\TopologicalSort;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
@@ -1378,9 +1379,10 @@ class UnitOfWork implements PropertyChangedListener
$joinColumns = reset($assoc['joinColumns']);
$isNullable = ! isset($joinColumns['nullable']) || $joinColumns['nullable'];
// Add dependency. The dependency direction implies that "$targetEntity has to go before $entity",
// so we can work through the topo sort result from left to right (with all edges pointing right).
$sort->addEdge($targetEntity, $entity, $isNullable);
// Add dependency. The dependency direction implies that "$entity depends on $targetEntity". The
// topological sort result will output the depended-upon nodes first, which means we can insert
// entities in that order.
$sort->addEdge($entity, $targetEntity, $isNullable);
}
}
@@ -1390,14 +1392,19 @@ class UnitOfWork implements PropertyChangedListener
/** @return list<object> */
private function computeDeleteExecutionOrder(): array
{
$sort = new TopologicalSort();
$stronglyConnectedComponents = new StronglyConnectedComponents();
$sort = new TopologicalSort();
// First make sure we have all the nodes
foreach ($this->entityDeletions as $entity) {
$stronglyConnectedComponents->addNode($entity);
$sort->addNode($entity);
}
// Now add edges
// First, consider only "on delete cascade" associations between entities
// and find strongly connected groups. Once we delete any one of the entities
// in such a group, _all_ of the other entities will be removed as well. So,
// we need to treat those groups like a single entity when performing delete
// order topological sorting.
foreach ($this->entityDeletions as $entity) {
$class = $this->em->getClassMetadata(get_class($entity));
@@ -1409,16 +1416,65 @@ class UnitOfWork implements PropertyChangedListener
continue;
}
// For associations that implement a database-level cascade/set null operation,
assert(isset($assoc['joinColumns']));
$joinColumns = reset($assoc['joinColumns']);
if (! isset($joinColumns['onDelete'])) {
continue;
}
$onDeleteOption = strtolower($joinColumns['onDelete']);
if ($onDeleteOption !== 'cascade') {
continue;
}
$targetEntity = $class->getFieldValue($entity, $assoc['fieldName']);
// If the association does not refer to another entity or that entity
// is not to be deleted, there is no ordering problem and we can
// skip this particular association.
if ($targetEntity === null || ! $stronglyConnectedComponents->hasNode($targetEntity)) {
continue;
}
$stronglyConnectedComponents->addEdge($entity, $targetEntity);
}
}
$stronglyConnectedComponents->findStronglyConnectedComponents();
// Now do the actual topological sorting to find the delete order.
foreach ($this->entityDeletions as $entity) {
$class = $this->em->getClassMetadata(get_class($entity));
// Get the entities representing the SCC
$entityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($entity);
// When $entity is part of a non-trivial strongly connected component group
// (a group containing not only those entities alone), make sure we process it _after_ the
// entity representing the group.
// The dependency direction implies that "$entity depends on $entityComponent
// being deleted first". The topological sort will output the depended-upon nodes first.
if ($entityComponent !== $entity) {
$sort->addEdge($entity, $entityComponent, false);
}
foreach ($class->associationMappings as $assoc) {
// We only need to consider the owning sides of to-one associations,
// since many-to-many associations can always be (and have already been)
// deleted in a preceding step.
if (! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
continue;
}
// For associations that implement a database-level set null operation,
// we do not have to follow a particular order: If the referred-to entity is
// deleted first, the DBMS will either delete the current $entity right away
// (CASCADE) or temporarily set the foreign key to NULL (SET NULL).
// Either way, we can skip it in the computation.
// deleted first, the DBMS will temporarily set the foreign key to NULL (SET NULL).
// So, we can skip it in the computation.
assert(isset($assoc['joinColumns']));
$joinColumns = reset($assoc['joinColumns']);
if (isset($joinColumns['onDelete'])) {
$onDeleteOption = strtolower($joinColumns['onDelete']);
if ($onDeleteOption === 'cascade' || $onDeleteOption === 'set null') {
if ($onDeleteOption === 'set null') {
continue;
}
}
@@ -1432,9 +1488,17 @@ class UnitOfWork implements PropertyChangedListener
continue;
}
// Add dependency. The dependency direction implies that "$entity has to be removed before $targetEntity",
// so we can work through the topo sort result from left to right (with all edges pointing right).
$sort->addEdge($entity, $targetEntity, false);
// Get the entities representing the SCC
$targetEntityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($targetEntity);
// When we have a dependency between two different groups of strongly connected nodes,
// add it to the computation.
// The dependency direction implies that "$targetEntityComponent depends on $entityComponent
// being deleted first". The topological sort will output the depended-upon nodes first,
// so we can work through the result in the returned order.
if ($targetEntityComponent !== $entityComponent) {
$sort->addEdge($targetEntityComponent, $entityComponent, false);
}
}
}
@@ -3104,9 +3168,10 @@ EXCEPTION
$reflField->setValue($entity, $pColl);
if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
if ($assoc['type'] === ClassMetadata::ONE_TO_MANY) {
$isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION];
if (! $isIteration && $assoc['type'] === ClassMetadata::ONE_TO_MANY) {
$this->scheduleCollectionForBatchLoading($pColl, $class);
} elseif ($assoc['type'] === ClassMetadata::MANY_TO_MANY) {
} elseif (($isIteration && $assoc['type'] === ClassMetadata::ONE_TO_MANY) || $assoc['type'] === ClassMetadata::MANY_TO_MANY) {
$this->loadCollection($pColl);
$pColl->takeSnapshot();
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Utility;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\DB2Platform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Platforms\SQLServerPlatform;
/** @internal */
trait LockSqlHelper
{
private function getReadLockSQL(AbstractPlatform $platform): string
{
if ($platform instanceof AbstractMySQLPlatform || $platform instanceof MySQLPlatform) {
return 'LOCK IN SHARE MODE';
}
if ($platform instanceof PostgreSQLPlatform) {
return 'FOR SHARE';
}
return $this->getWriteLockSQL($platform);
}
private function getWriteLockSQL(AbstractPlatform $platform): string
{
if ($platform instanceof DB2Platform) {
return 'WITH RR USE AND KEEP UPDATE LOCKS';
}
if ($platform instanceof SqlitePlatform) {
return '';
}
if ($platform instanceof SQLServerPlatform) {
return '';
}
return 'FOR UPDATE';
}
}
+2
View File
@@ -10,6 +10,8 @@ parameters:
- '/Call to an undefined method Doctrine\\DBAL\\Connection::createSchemaManager\(\)\./'
# Class name will change in DBAL 3.
- '/^Class Doctrine\\DBAL\\Platforms\\PostgreSQLPlatform not found\.$/'
- '/^Class Doctrine\\DBAL\\Platforms\\AbstractMySQLPlatform not found\.$/'
- '/^Class Doctrine\\DBAL\\Platforms\\MySQLPlatform not found\.$/'
-
message: '/Doctrine\\DBAL\\Platforms\\MyS(ql|QL)Platform/'
path: lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
+3
View File
@@ -27,6 +27,9 @@ parameters:
-
message: '/^Call to static method ensure\(\) on an unknown class Doctrine\\DBAL\\ForwardCompatibility\\Result\.$/'
path: lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php
-
message: '/^Instanceof between Doctrine\\DBAL\\Platforms\\AbstractPlatform and Doctrine\\DBAL\\Platforms\\MySQLPlatform will always evaluate to false\.$/'
path: lib/Doctrine/ORM/Utility/LockSqlHelper.php
# False positive
-
+3
View File
@@ -27,6 +27,9 @@ parameters:
-
message: '/^Call to static method ensure\(\) on an unknown class Doctrine\\DBAL\\ForwardCompatibility\\Result\.$/'
path: lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php
-
message: '/^Instanceof between Doctrine\\DBAL\\Platforms\\AbstractPlatform and Doctrine\\DBAL\\Platforms\\MySQLPlatform will always evaluate to false\.$/'
path: lib/Doctrine/ORM/Utility/LockSqlHelper.php
# False positive
-
+4
View File
@@ -88,6 +88,8 @@
<referencedMethod name="Doctrine\DBAL\Platforms\AbstractPlatform::supportsForeignKeyConstraints"/>
<!-- Remove on 3.0.x -->
<referencedMethod name="Doctrine\DBAL\Connection::getEventManager"/>
<referencedMethod name="Doctrine\DBAL\Platforms\AbstractPlatform::getReadLockSQL"/>
<referencedMethod name="Doctrine\DBAL\Platforms\AbstractPlatform::getWriteLockSQL"/>
<referencedMethod name="Doctrine\DBAL\Schema\Schema::visit"/>
<referencedMethod name="Doctrine\DBAL\Schema\SchemaDiff::toSaveSql"/>
<referencedMethod name="Doctrine\DBAL\Schema\SchemaDiff::toSql"/>
@@ -158,6 +160,7 @@
<errorLevel type="suppress">
<!-- Class name changes in DBAL 3. -->
<referencedClass name="Doctrine\DBAL\Platforms\PostgreSQLPlatform" />
<referencedClass name="Doctrine\DBAL\Platforms\MySQLPlatform" />
</errorLevel>
</InvalidClass>
<InvalidParamDefault>
@@ -256,6 +259,7 @@
<errorLevel type="suppress">
<file name="lib/Doctrine/ORM/Internal/SQLResultCasing.php"/>
<file name="lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php"/>
<file name="lib/Doctrine/ORM/Utility/LockSqlHelper.php"/>
</errorLevel>
</TypeDoesNotContainType>
<UndefinedClass>
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Enums;
use Doctrine\ORM\Mapping\Column;
class FaultySwitch
{
#[Column(type: 'string')]
public string $value;
/**
* The following line is ignored on psalm and phpstan so that we can test
* that the mapping is throwing an exception when a non-backed enum is used.
*
* @psalm-suppress InvalidArgument
*/
#[Column(enumType: SwitchStatus::class)]
public SwitchStatus $status;
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Enums;
enum SwitchStatus
{
case ON;
case OFF;
}
@@ -18,6 +18,10 @@ class EagerFetchCollectionTest extends OrmFunctionalTestCase
parent::setUp();
$this->createSchemaForModels(EagerFetchOwner::class, EagerFetchChild::class);
// Ensure tables are empty
$this->_em->getRepository(EagerFetchChild::class)->createQueryBuilder('o')->delete()->getQuery()->execute();
$this->_em->getRepository(EagerFetchOwner::class)->createQueryBuilder('o')->delete()->getQuery()->execute();
}
public function testEagerFetchMode(): void
@@ -84,6 +88,20 @@ class EagerFetchCollectionTest extends OrmFunctionalTestCase
$query->getResult();
}
public function testEagerFetchWithIterable(): void
{
$this->createOwnerWithChildren(2);
$this->_em->flush();
$this->_em->clear();
$iterable = $this->_em->getRepository(EagerFetchOwner::class)->createQueryBuilder('o')->getQuery()->toIterable();
// There is only a single record, but use a foreach to ensure the iterator is marked as finished and the table lock is released
foreach ($iterable as $owner) {
$this->assertCount(2, $owner->children);
}
}
protected function createOwnerWithChildren(int $children): EagerFetchOwner
{
$owner = new EagerFetchOwner();
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Locking;
use Doctrine\DBAL\LockMode;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\Query;
use Doctrine\ORM\TransactionRequiredException;
@@ -168,9 +169,7 @@ class LockTest extends OrmFunctionalTestCase
*/
public function testLockPessimisticWrite(): void
{
$writeLockSql = $this->_em->getConnection()->getDatabasePlatform()->getWriteLockSQL();
if (! $writeLockSql) {
if ($this->_em->getConnection()->getDatabasePlatform() instanceof SQLitePlatform) {
self::markTestSkipped('Database Driver has no Write Lock support.');
}
@@ -195,7 +194,7 @@ class LockTest extends OrmFunctionalTestCase
$lastLoggedQuery = $this->getLastLoggedQuery(1)['sql'];
}
self::assertStringContainsString($writeLockSql, $lastLoggedQuery);
self::assertStringContainsString('FOR UPDATE', $lastLoggedQuery);
}
/**
@@ -203,9 +202,7 @@ class LockTest extends OrmFunctionalTestCase
*/
public function testRefreshWithLockPessimisticWrite(): void
{
$writeLockSql = $this->_em->getConnection()->getDatabasePlatform()->getWriteLockSQL();
if (! $writeLockSql) {
if ($this->_em->getConnection()->getDatabasePlatform() instanceof SQLitePlatform) {
self::markTestSkipped('Database Driver has no Write Lock support.');
}
@@ -230,15 +227,13 @@ class LockTest extends OrmFunctionalTestCase
$lastLoggedQuery = $this->getLastLoggedQuery(1)['sql'];
}
self::assertStringContainsString($writeLockSql, $lastLoggedQuery);
self::assertStringContainsString('FOR UPDATE', $lastLoggedQuery);
}
/** @group DDC-178 */
public function testLockPessimisticRead(): void
{
$readLockSql = $this->_em->getConnection()->getDatabasePlatform()->getReadLockSQL();
if (! $readLockSql) {
if ($this->_em->getConnection()->getDatabasePlatform() instanceof SQLitePlatform) {
self::markTestSkipped('Database Driver has no Write Lock support.');
}
@@ -264,7 +259,11 @@ class LockTest extends OrmFunctionalTestCase
$lastLoggedQuery = $this->getLastLoggedQuery(1)['sql'];
}
self::assertStringContainsString($readLockSql, $lastLoggedQuery);
self::assertThat($lastLoggedQuery, self::logicalOr(
self::stringContains('FOR UPDATE'),
self::stringContains('FOR SHARE'),
self::stringContains('LOCK IN SHARE MODE')
));
}
/** @group DDC-1693 */
@@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
use function array_filter;
use function array_values;
use function strpos;
class GH10912Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH10912User::class,
GH10912Profile::class,
GH10912Room::class,
]);
}
public function testIssue(): void
{
$user = new GH10912User();
$profile = new GH10912Profile();
$room = new GH10912Room();
$user->rooms->add($room);
$user->profile = $profile;
$profile->user = $user;
$room->user = $user;
$this->_em->persist($room);
$this->_em->persist($user);
$this->_em->persist($profile);
$this->_em->flush();
/*
* This issue is about finding a special deletion order:
* $user and $profile cross-reference each other with ON DELETE CASCADE.
* So, whichever one gets deleted first, the DBMS will immediately dispose
* of the other one as well.
*
* $user -> $room is the unproblematic (irrelevant) inverse side of
* a OneToMany association.
*
* $room -> $user is a not-nullable, no DBMS-level-cascade, owning side
* of ManyToOne. We *must* remove the $room _before_ the $user can be
* deleted. And remember, $user deletion happens either when we DELETE the
* user (direct deletion), or when we delete the $profile (ON DELETE CASCADE
* propagates to the user).
*
* In the original bug report, the ordering of fields in the entities was
* relevant, in combination with a cascade=persist configuration.
*
* But, for the sake of clarity, let's put these features away and create
* the problematic sequence in UnitOfWork::$entityDeletions directly:
*/
$this->_em->remove($profile);
$this->_em->remove($user);
$this->_em->remove($room);
$queryLog = $this->getQueryLog();
$queryLog->reset()->enable();
$this->_em->flush();
$queries = array_values(array_filter($queryLog->queries, static function (array $entry): bool {
return strpos($entry['sql'], 'DELETE') === 0;
}));
self::assertCount(3, $queries);
// we do not care about the order of $user vs. $profile, so do not check them.
self::assertSame('DELETE FROM GH10912Room WHERE id = ?', $queries[0]['sql'], '$room deletion is the first query');
// The EntityManager is aware that all three entities have been deleted (sanity check)
$im = $this->_em->getUnitOfWork()->getIdentityMap();
self::assertEmpty($im[GH10912Profile::class]);
self::assertEmpty($im[GH10912User::class]);
self::assertEmpty($im[GH10912Room::class]);
}
}
/** @ORM\Entity */
class GH10912User
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
/**
* @ORM\OneToMany(targetEntity=GH10912Room::class, mappedBy="user")
*
* @var Collection<int, GH10912Room>
*/
public $rooms;
/**
* @ORM\OneToOne(targetEntity=GH10912Profile::class)
* @ORM\JoinColumn(onDelete="cascade")
*
* @var GH10912Profile
*/
public $profile;
public function __construct()
{
$this->rooms = new ArrayCollection();
}
}
/** @ORM\Entity */
class GH10912Profile
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
/**
* @ORM\OneToOne(targetEntity=GH10912User::class)
* @ORM\JoinColumn(onDelete="cascade")
*
* @var GH10912User
*/
public $user;
}
/** @ORM\Entity */
class GH10912Room
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity=GH10912User::class, inversedBy="rooms")
* @ORM\JoinColumn(nullable=false)
*
* @var GH10912User
*/
public $user;
}
@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
use function array_filter;
use function array_values;
use function strpos;
class GH10913Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH10913Entity::class,
]);
}
public function testExample1(): void
{
[$a, $b, $c] = $this->createEntities(3);
$c->ref = $b;
$b->odc = $a;
$this->_em->persist($a);
$this->_em->persist($b);
$this->_em->persist($c);
$this->_em->flush();
$this->_em->remove($a);
$this->_em->remove($b);
$this->_em->remove($c);
$this->flushAndAssertNumberOfDeleteQueries(3);
}
public function testExample2(): void
{
[$a, $b, $c] = $this->createEntities(3);
$a->odc = $b;
$b->odc = $a;
$c->ref = $b;
$this->_em->persist($a);
$this->_em->persist($b);
$this->_em->persist($c);
$this->_em->flush();
$this->_em->remove($a);
$this->_em->remove($b);
$this->_em->remove($c);
$this->flushAndAssertNumberOfDeleteQueries(3);
}
public function testExample3(): void
{
[$a, $b, $c] = $this->createEntities(3);
$a->odc = $b;
$a->ref = $c;
$c->ref = $b;
$b->odc = $a;
$this->_em->persist($a);
$this->_em->persist($b);
$this->_em->persist($c);
$this->_em->flush();
$this->_em->remove($a);
$this->_em->remove($b);
$this->_em->remove($c);
self::expectException(CycleDetectedException::class);
$this->_em->flush();
}
public function testExample4(): void
{
[$a, $b, $c, $d] = $this->createEntities(4);
$a->ref = $b;
$b->odc = $c;
$c->odc = $b;
$d->ref = $c;
$this->_em->persist($a);
$this->_em->persist($b);
$this->_em->persist($c);
$this->_em->persist($d);
$this->_em->flush();
$this->_em->remove($b);
$this->_em->remove($c);
$this->_em->remove($d);
$this->_em->remove($a);
$this->flushAndAssertNumberOfDeleteQueries(4);
}
private function flushAndAssertNumberOfDeleteQueries(int $expectedCount): void
{
$queryLog = $this->getQueryLog();
$queryLog->reset()->enable();
$this->_em->flush();
$queries = array_values(array_filter($queryLog->queries, static function (array $entry): bool {
return strpos($entry['sql'], 'DELETE') === 0;
}));
self::assertCount($expectedCount, $queries);
}
/**
* @return list<GH10913Entity>
*/
private function createEntities(int $count = 1): array
{
$result = [];
for ($i = 0; $i < $count; $i++) {
$result[] = new GH10913Entity();
}
return $result;
}
}
/** @ORM\Entity */
class GH10913Entity
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity=GH10913Entity::class)
* @ORM\JoinColumn(nullable=true, onDelete="CASCADE")
*
* @var GH10913Entity
*/
public $odc;
/**
* @ORM\ManyToOne(targetEntity=GH10913Entity::class)
* @ORM\JoinColumn(nullable=true)
*
* @var GH10913Entity
*/
public $ref;
}
@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @group GH-10927
*/
class GH10927Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$platform = $this->_em->getConnection()->getDatabasePlatform();
if (! $platform instanceof PostgreSQLPlatform) {
self::markTestSkipped('The ' . self::class . ' requires the use of postgresql.');
}
$this->setUpEntitySchema([
GH10927RootMappedSuperclass::class,
GH10927InheritedMappedSuperclass::class,
GH10927EntityA::class,
GH10927EntityB::class,
GH10927EntityC::class,
]);
}
public function testSequenceGeneratorDefinitionForRootMappedSuperclass(): void
{
$metadata = $this->_em->getClassMetadata(GH10927RootMappedSuperclass::class);
self::assertNull($metadata->sequenceGeneratorDefinition);
}
public function testSequenceGeneratorDefinitionForEntityA(): void
{
$metadata = $this->_em->getClassMetadata(GH10927EntityA::class);
self::assertSame('GH10927EntityA_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
}
public function testSequenceGeneratorDefinitionForInheritedMappedSuperclass(): void
{
$metadata = $this->_em->getClassMetadata(GH10927InheritedMappedSuperclass::class);
self::assertSame('GH10927InheritedMappedSuperclass_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
}
public function testSequenceGeneratorDefinitionForEntityB(): void
{
$metadata = $this->_em->getClassMetadata(GH10927EntityB::class);
self::assertSame('GH10927EntityB_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
}
public function testSequenceGeneratorDefinitionForEntityC(): void
{
$metadata = $this->_em->getClassMetadata(GH10927EntityC::class);
self::assertSame('GH10927EntityB_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
}
}
/**
* @ORM\MappedSuperclass()
*/
class GH10927RootMappedSuperclass
{
}
/**
* @ORM\Entity()
*/
class GH10927EntityA extends GH10927RootMappedSuperclass
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="SEQUENCE")
* @ORM\Column(type="integer")
*
* @var int|null
*/
private $id = null;
}
/**
* @ORM\MappedSuperclass()
*/
class GH10927InheritedMappedSuperclass extends GH10927RootMappedSuperclass
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="SEQUENCE")
* @ORM\Column(type="integer")
*
* @var int|null
*/
private $id = null;
}
/**
* @ORM\Entity()
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorColumn(name="discr", type="string")
* @ORM\DiscriminatorMap({"B" = "GH10927EntityB", "C" = "GH10927EntityC"})
*/
class GH10927EntityB extends GH10927InheritedMappedSuperclass
{
}
/**
* @ORM\Entity()
*/
class GH10927EntityC extends GH10927EntityB
{
}
@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH11058Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH11058Parent::class,
GH11058Child::class,
]);
}
public function testChildrenInsertedInOrderOfPersistCalls1WhenParentPersistedLast(): void
{
[$parent, $child1, $child2] = $this->createParentWithTwoChildEntities();
$this->_em->persist($child1);
$this->_em->persist($child2);
$this->_em->persist($parent);
$this->_em->flush();
self::assertTrue($child1->id < $child2->id);
}
public function testChildrenInsertedInOrderOfPersistCalls2WhenParentPersistedLast(): void
{
[$parent, $child1, $child2] = $this->createParentWithTwoChildEntities();
$this->_em->persist($child2);
$this->_em->persist($child1);
$this->_em->persist($parent);
$this->_em->flush();
self::assertTrue($child2->id < $child1->id);
}
public function testChildrenInsertedInOrderOfPersistCalls1WhenParentPersistedFirst(): void
{
[$parent, $child1, $child2] = $this->createParentWithTwoChildEntities();
$this->_em->persist($parent);
$this->_em->persist($child1);
$this->_em->persist($child2);
$this->_em->flush();
self::assertTrue($child1->id < $child2->id);
}
public function testChildrenInsertedInOrderOfPersistCalls2WhenParentPersistedFirst(): void
{
[$parent, $child1, $child2] = $this->createParentWithTwoChildEntities();
$this->_em->persist($parent);
$this->_em->persist($child2);
$this->_em->persist($child1);
$this->_em->flush();
self::assertTrue($child2->id < $child1->id);
}
private function createParentWithTwoChildEntities(): array
{
$parent = new GH11058Parent();
$child1 = new GH11058Child();
$child2 = new GH11058Child();
$parent->addChild($child1);
$parent->addChild($child2);
return [$parent, $child1, $child2];
}
}
/**
* @ORM\Entity()
*/
class GH11058Parent
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
/**
* @ORM\OneToMany(targetEntity="GH11058Child", mappedBy="parent")
*
* @var Collection<int, GH11058Child>
*/
public $children;
public function __construct()
{
$this->children = new ArrayCollection();
}
public function addChild(GH11058Child $child): void
{
if (! $this->children->contains($child)) {
$this->children->add($child);
$child->setParent($this);
}
}
}
/**
* @ORM\Entity()
*/
class GH11058Child
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
public $id;
/**
* @ORM\ManyToOne(targetEntity="GH11058Parent", inversedBy="children")
*
* @var GH11058Parent
*/
public $parent;
public function setParent(GH11058Parent $parent): void
{
$this->parent = $parent;
$parent->addChild($this);
}
}
@@ -6,12 +6,22 @@ namespace Doctrine\Tests\ORM\Functional\Ticket\GH11072;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
/**
* @Entity
*/
class GH11072EntityAdvanced extends GH11072EntityBasic
class GH11072EntityAdvanced
{
/**
* @Id
* @Column(type="integer")
* @GeneratedValue
* @var int
*/
public $id;
/** @Column(type="json") */
public mixed $anything;
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH11135Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH11135MappedSuperclass::class,
GH11135EntityWithOverride::class,
GH11135EntityWithoutOverride::class,
]);
}
public function testOverrideInheritsDeclaringClass(): void
{
$cm1 = $this->_em->getClassMetadata(GH11135EntityWithOverride::class);
$cm2 = $this->_em->getClassMetadata(GH11135EntityWithoutOverride::class);
self::assertSame($cm1->getFieldMapping('id')['declared'], $cm2->getFieldMapping('id')['declared']);
self::assertSame($cm1->getAssociationMapping('ref')['declared'], $cm2->getAssociationMapping('ref')['declared']);
}
}
/**
* @ORM\MappedSuperclass
*/
class GH11135MappedSuperclass
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*
* @var int
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="GH11135EntityWithoutOverride")
*
* @var GH11135EntityWithoutOverride
*/
private $ref;
}
/**
* @ORM\Entity()
* @ORM\AttributeOverrides({
* @ORM\AttributeOverride(name="id", column=@ORM\Column(name="id_overridden"))
* })
* @ORM\AssociationOverrides({
* @ORM\AssociationOverride(name="ref", joinColumns=@ORM\JoinColumn(name="ref_overridden", referencedColumnName="id"))
* })
*/
class GH11135EntityWithOverride extends GH11135MappedSuperclass
{
}
/**
* @ORM\Entity()
*/
class GH11135EntityWithoutOverride extends GH11135MappedSuperclass
{
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Internal;
class Node
{
/** @var string */
public $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Internal;
use Doctrine\ORM\Internal\StronglyConnectedComponents;
use Doctrine\Tests\OrmTestCase;
class StronglyConnectedComponentsTest extends OrmTestCase
{
/** @var array<string, Node> */
private $nodes = [];
/** @var StronglyConnectedComponents */
private $stronglyConnectedComponents;
protected function setUp(): void
{
$this->stronglyConnectedComponents = new StronglyConnectedComponents();
}
public function testFindStronglyConnectedComponents(): void
{
// A -> B <-> C -> D <-> E
$this->addNodes('A', 'B', 'C', 'D', 'E');
$this->addEdge('A', 'B');
$this->addEdge('B', 'C');
$this->addEdge('C', 'B');
$this->addEdge('C', 'D');
$this->addEdge('D', 'E');
$this->addEdge('E', 'D');
$this->stronglyConnectedComponents->findStronglyConnectedComponents();
$this->assertNodesAreInSameComponent('B', 'C');
$this->assertNodesAreInSameComponent('D', 'E');
$this->assertNodesAreNotInSameComponent('A', 'B');
$this->assertNodesAreNotInSameComponent('A', 'D');
}
public function testFindStronglyConnectedComponents2(): void
{
// A -> B -> C -> D -> B
$this->addNodes('A', 'B', 'C', 'D');
$this->addEdge('A', 'B');
$this->addEdge('B', 'C');
$this->addEdge('C', 'D');
$this->addEdge('D', 'B');
$this->stronglyConnectedComponents->findStronglyConnectedComponents();
$this->assertNodesAreInSameComponent('B', 'C');
$this->assertNodesAreInSameComponent('C', 'D');
$this->assertNodesAreNotInSameComponent('A', 'B');
}
public function testFindStronglyConnectedComponents3(): void
{
// v---------.
// A -> B -> C -> D -> E
// ^--------´
$this->addNodes('A', 'B', 'C', 'D', 'E');
$this->addEdge('A', 'B');
$this->addEdge('B', 'C');
$this->addEdge('C', 'D');
$this->addEdge('D', 'E');
$this->addEdge('E', 'C');
$this->addEdge('D', 'B');
$this->stronglyConnectedComponents->findStronglyConnectedComponents();
$this->assertNodesAreInSameComponent('B', 'C');
$this->assertNodesAreInSameComponent('C', 'D');
$this->assertNodesAreInSameComponent('D', 'E');
$this->assertNodesAreInSameComponent('E', 'B');
$this->assertNodesAreNotInSameComponent('A', 'B');
}
private function addNodes(string ...$names): void
{
foreach ($names as $name) {
$node = new Node($name);
$this->nodes[$name] = $node;
$this->stronglyConnectedComponents->addNode($node);
}
}
private function addEdge(string $from, string $to, bool $optional = false): void
{
$this->stronglyConnectedComponents->addEdge($this->nodes[$from], $this->nodes[$to], $optional);
}
private function assertNodesAreInSameComponent(string $first, string $second): void
{
self::assertSame(
$this->stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($this->nodes[$first]),
$this->stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($this->nodes[$second])
);
}
private function assertNodesAreNotInSameComponent(string $first, string $second): void
{
self::assertNotSame(
$this->stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($this->nodes[$first]),
$this->stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($this->nodes[$second])
);
}
}
@@ -34,7 +34,7 @@ class TopologicalSortTest extends OrmTestCase
$this->addEdge('E', 'A');
// There is only 1 valid ordering for this constellation
self::assertSame(['E', 'A', 'B', 'C'], $this->computeResult());
self::assertSame(['C', 'B', 'A', 'E'], $this->computeResult());
}
public function testSkipOptionalEdgeToBreakCycle(): void
@@ -44,7 +44,7 @@ class TopologicalSortTest extends OrmTestCase
$this->addEdge('A', 'B', true);
$this->addEdge('B', 'A', false);
self::assertSame(['B', 'A'], $this->computeResult());
self::assertSame(['A', 'B'], $this->computeResult());
}
public function testBreakCycleByBacktracking(): void
@@ -57,7 +57,7 @@ class TopologicalSortTest extends OrmTestCase
$this->addEdge('D', 'A'); // closes the cycle
// We can only break B -> C, so the result must be C -> D -> A -> B
self::assertSame(['C', 'D', 'A', 'B'], $this->computeResult());
self::assertSame(['B', 'A', 'D', 'C'], $this->computeResult());
}
public function testCycleRemovedByEliminatingLastOptionalEdge(): void
@@ -75,7 +75,7 @@ class TopologicalSortTest extends OrmTestCase
$this->addEdge('B', 'D', true);
$this->addEdge('D', 'A');
self::assertSame(['C', 'D', 'A', 'B'], $this->computeResult());
self::assertSame(['B', 'A', 'C', 'D'], $this->computeResult());
}
public function testGH7180Example(): void
@@ -89,7 +89,7 @@ class TopologicalSortTest extends OrmTestCase
$this->addEdge('F', 'E');
$this->addEdge('E', 'D');
self::assertSame(['F', 'E', 'D', 'G'], $this->computeResult());
self::assertSame(['G', 'D', 'E', 'F'], $this->computeResult());
}
public function testCommitOrderingFromGH7259Test(): void
@@ -106,9 +106,9 @@ class TopologicalSortTest extends OrmTestCase
// the D -> A -> B ordering is important to break the cycle
// on the nullable link.
$correctOrders = [
['D', 'A', 'B', 'C'],
['D', 'A', 'C', 'B'],
['D', 'C', 'A', 'B'],
['C', 'B', 'A', 'D'],
['B', 'C', 'A', 'D'],
['B', 'A', 'C', 'D'],
];
self::assertContains($this->computeResult(), $correctOrders);
@@ -124,12 +124,12 @@ class TopologicalSortTest extends OrmTestCase
$this->addEdge('B', 'C', true);
$this->addEdge('C', 'D', true);
// Many orderings are possible here, but the bottom line is D must be before A (it's the only hard requirement).
// Many orderings are possible here, but the bottom line is A must be before D (it's the only hard requirement).
$result = $this->computeResult();
$indexA = array_search('A', $result, true);
$indexD = array_search('D', $result, true);
self::assertTrue($indexD < $indexA);
self::assertTrue($indexD > $indexA);
}
public function testCommitOrderingFromGH8349Case2Test(): void
@@ -141,7 +141,7 @@ class TopologicalSortTest extends OrmTestCase
$this->addEdge('A', 'B', true);
// The B -> A requirement determines the result here
self::assertSame(['B', 'A'], $this->computeResult());
self::assertSame(['A', 'B'], $this->computeResult());
}
public function testNodesMaintainOrderWhenNoDepencency(): void
@@ -153,6 +153,58 @@ class TopologicalSortTest extends OrmTestCase
self::assertSame(['A', 'B', 'C'], $this->computeResult());
}
public function testNodesReturnedInDepthFirstOrder(): void
{
$this->addNodes('A', 'B', 'C');
$this->addEdge('A', 'B');
$this->addEdge('A', 'C');
// We start on A and find that it has two dependencies on B and C,
// added (as dependencies) in that order.
// So, first we continue the DFS on B, because that edge was added first.
// This gives the result order B, C, A.
self::assertSame(['B', 'C', 'A'], $this->computeResult());
}
public function testNodesReturnedInDepthFirstOrderWithEdgesInDifferentOrderThanNodes(): void
{
$this->addNodes('A', 'B', 'C');
$this->addEdge('A', 'C');
$this->addEdge('A', 'B');
// This is like testNodesReturnedInDepthFirstOrder, but it shows that for the two
// nodes B and C that A depends upon, the result will follow the order in which
// the edges were added.
self::assertSame(['C', 'B', 'A'], $this->computeResult());
}
public function testNodesReturnedInDepthFirstOrderWithDependingNodeLast(): void
{
$this->addNodes('B', 'C', 'A');
$this->addEdge('A', 'B');
$this->addEdge('A', 'C');
// This again is like testNodesReturnedInDepthFirstOrder, but this
// time the node A that depends on B and C is added as the last node.
// That means processing can go over B and C in the order they were given.
// The order in which edges are added is not relevant (!), since at the time
// the edges are evaluated, the nodes they point to have already been finished.
self::assertSame(['B', 'C', 'A'], $this->computeResult());
}
public function testNodesReturnedInDepthFirstOrderWithDependingNodeLastAndEdgeOrderInversed(): void
{
$this->addNodes('B', 'C', 'A');
$this->addEdge('A', 'C');
$this->addEdge('A', 'B');
// This again is like testNodesReturnedInDepthFirstOrderWithDependingNodeLast, but adds
// the edges in the opposing order. Still, the result order is the same (!).
// This may be surprising when comparing with testNodesReturnedInDepthFirstOrderWithEdgesInDifferentOrderThanNodes,
// where the result order depends upon the _edge_ order.
self::assertSame(['B', 'C', 'A'], $this->computeResult());
}
public function testDetectSmallCycle(): void
{
$this->addNodes('A', 'B');
@@ -205,7 +257,7 @@ class TopologicalSortTest extends OrmTestCase
$this->computeResult();
} catch (CycleDetectedException $exception) {
self::assertEquals(
[$this->nodes['D'], $this->nodes['B'], $this->nodes['C'], $this->nodes['D']],
[$this->nodes['B'], $this->nodes['C'], $this->nodes['D'], $this->nodes['B']],
$exception->getCycle()
);
}
@@ -235,14 +287,3 @@ class TopologicalSortTest extends OrmTestCase
}, array_values($this->topologicalSort->sort()));
}
}
class Node
{
/** @var string */
public $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
@@ -163,22 +163,7 @@ class BasicInheritanceMappingTest extends OrmTestCase
/**
* @group DDC-1156
* @group DDC-1218
*/
public function testGeneratedValueFromMappedSuperclass(): void
{
$class = $this->cmf->getMetadataFor(SuperclassEntity::class);
assert($class instanceof ClassMetadata);
self::assertInstanceOf(IdSequenceGenerator::class, $class->idGenerator);
self::assertEquals(
['allocationSize' => 1, 'initialValue' => 10, 'sequenceName' => 'foo'],
$class->sequenceGeneratorDefinition
);
}
/**
* @group DDC-1156
* @group DDC-1218
* @group GH-10927
*/
public function testSequenceDefinitionInHierarchyWithSandwichMappedSuperclass(): void
{
@@ -192,22 +177,6 @@ class BasicInheritanceMappingTest extends OrmTestCase
);
}
/**
* @group DDC-1156
* @group DDC-1218
*/
public function testMultipleMappedSuperclasses(): void
{
$class = $this->cmf->getMetadataFor(MediumSuperclassEntity::class);
assert($class instanceof ClassMetadata);
self::assertInstanceOf(IdSequenceGenerator::class, $class->idGenerator);
self::assertEquals(
['allocationSize' => 1, 'initialValue' => 10, 'sequenceName' => 'foo'],
$class->sequenceGeneratorDefinition
);
}
/**
* Ensure indexes are inherited from the mapped superclass.
*
@@ -29,6 +29,8 @@ use Doctrine\Tests\Models\DDC117\DDC117ArticleDetails;
use Doctrine\Tests\Models\DDC6412\DDC6412File;
use Doctrine\Tests\Models\DDC964\DDC964Admin;
use Doctrine\Tests\Models\DDC964\DDC964Guest;
use Doctrine\Tests\Models\DirectoryTree\AbstractContentItem;
use Doctrine\Tests\Models\DirectoryTree\Directory;
use Doctrine\Tests\Models\Routing\RoutingLeg;
use Doctrine\Tests\Models\TypedProperties;
use Doctrine\Tests\ORM\Mapping\TypedFieldMapper\CustomIntAsStringTypedFieldMapper;
@@ -1186,6 +1188,30 @@ class ClassMetadataTest extends OrmTestCase
$cm->setAttributeOverride('name', ['type' => 'date']);
}
public function testAttributeOverrideKeepsDeclaringClass(): void
{
$cm = new ClassMetadata(Directory::class);
$cm->mapField(['fieldName' => 'id', 'type' => 'integer', 'declared' => AbstractContentItem::class]);
$cm->setAttributeOverride('id', ['columnName' => 'new_id']);
$mapping = $cm->getFieldMapping('id');
self::assertArrayHasKey('declared', $mapping);
self::assertSame(AbstractContentItem::class, $mapping['declared']);
}
public function testAssociationOverrideKeepsDeclaringClass(): void
{
$cm = new ClassMetadata(Directory::class);
$cm->mapManyToOne(['fieldName' => 'parentDirectory', 'targetEntity' => Directory::class, 'cascade' => ['remove'], 'declared' => Directory::class]);
$cm->setAssociationOverride('parentDirectory', ['cascade' => '']);
$mapping = $cm->getAssociationMapping('parentDirectory');
self::assertArrayHasKey('declared', $mapping);
self::assertSame(Directory::class, $mapping['declared']);
}
/** @group DDC-1955 */
public function testInvalidEntityListenerClassException(): void
{
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Mapping;
use Doctrine\ORM\Mapping\DefaultTypedFieldMapper;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\Tests\Models\Enums\FaultySwitch;
use Doctrine\Tests\OrmTestCase;
use ReflectionClass;
/**
* @requires PHP >= 8.1
*/
class TypedEnumFieldMapperTest extends OrmTestCase
{
private static function defaultTypedFieldMapper(): DefaultTypedFieldMapper
{
return new DefaultTypedFieldMapper();
}
public function testNotBackedEnumThrows(): void
{
$reflectionClass = new ReflectionClass(FaultySwitch::class);
$this->expectException(MappingException::class);
$this->expectExceptionMessage(
'Attempting to map a non-backed enum type Doctrine\Tests\Models\Enums\SwitchStatus in entity Doctrine\Tests\Models\Enums\FaultySwitch::$status. Please use backed enums only'
);
self::defaultTypedFieldMapper()->validateAndComplete(['fieldName' => 'status'], $reflectionClass->getProperty('status'));
}
}
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Tools\Pagination;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
@@ -18,273 +19,212 @@ class_exists('Doctrine\DBAL\Platforms\PostgreSqlPlatform');
final class LimitSubqueryOutputWalkerTest extends PaginationTestCase
{
/**
* @var AbstractPlatform|null
*/
private $originalDatabasePlatform;
protected function setUp(): void
{
parent::setUp();
$this->originalDatabasePlatform = $this->entityManager->getConnection()->getDatabasePlatform();
}
protected function tearDown(): void
{
if ($this->originalDatabasePlatform) {
$this->entityManager->getConnection()->setDatabasePlatform($this->originalDatabasePlatform);
}
parent::tearDown();
}
private function replaceDatabasePlatform(AbstractPlatform $platform): void
{
$this->entityManager->getConnection()->setDatabasePlatform($platform);
}
public function testLimitSubquery(): void
{
$query = $this->entityManager->createQuery(
'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a'
);
$query->expireQueryCache(true);
$limitQuery = clone $query;
$limitQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a');
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT m0_.id AS id_0, m0_.title AS title_1, c1_.id AS id_2, a2_.id AS id_3, a2_.name AS name_4, m0_.author_id AS author_id_5, m0_.category_id AS category_id_6 FROM MyBlogPost m0_ INNER JOIN Category c1_ ON m0_.category_id = c1_.id INNER JOIN Author a2_ ON m0_.author_id = a2_.id) dctrn_result',
$limitQuery->getSQL()
'SELECT DISTINCT id_0 FROM (SELECT m0_.id AS id_0, m0_.title AS title_1, c1_.id AS id_2, a2_.id AS id_3, a2_.name AS name_4, m0_.author_id AS author_id_5, m0_.category_id AS category_id_6 FROM MyBlogPost m0_ INNER JOIN Category c1_ ON m0_.category_id = c1_.id INNER JOIN Author a2_ ON m0_.author_id = a2_.id) dctrn_result LIMIT 20 OFFSET 10',
$query->getSQL()
);
}
public function testLimitSubqueryWithSortPg(): void
{
$odp = $this->entityManager->getConnection()->getDatabasePlatform();
$this->entityManager->getConnection()->setDatabasePlatform(new PostgreSQLPlatform());
$this->replaceDatabasePlatform(new PostgreSQLPlatform());
$query = $this->entityManager->createQuery(
'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a ORDER BY p.title'
);
$limitQuery = clone $query;
$limitQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a ORDER BY p.title');
self::assertSame(
'SELECT DISTINCT id_0, MIN(sclr_5) AS dctrn_minrownum FROM (SELECT m0_.id AS id_0, m0_.title AS title_1, c1_.id AS id_2, a2_.id AS id_3, a2_.name AS name_4, ROW_NUMBER() OVER(ORDER BY m0_.title ASC) AS sclr_5, m0_.author_id AS author_id_6, m0_.category_id AS category_id_7 FROM MyBlogPost m0_ INNER JOIN Category c1_ ON m0_.category_id = c1_.id INNER JOIN Author a2_ ON m0_.author_id = a2_.id) dctrn_result GROUP BY id_0 ORDER BY dctrn_minrownum ASC',
$limitQuery->getSQL()
'SELECT DISTINCT id_0, MIN(sclr_5) AS dctrn_minrownum FROM (SELECT m0_.id AS id_0, m0_.title AS title_1, c1_.id AS id_2, a2_.id AS id_3, a2_.name AS name_4, ROW_NUMBER() OVER(ORDER BY m0_.title ASC) AS sclr_5, m0_.author_id AS author_id_6, m0_.category_id AS category_id_7 FROM MyBlogPost m0_ INNER JOIN Category c1_ ON m0_.category_id = c1_.id INNER JOIN Author a2_ ON m0_.author_id = a2_.id) dctrn_result GROUP BY id_0 ORDER BY dctrn_minrownum ASC LIMIT 20 OFFSET 10',
$query->getSQL()
);
$this->entityManager->getConnection()->setDatabasePlatform($odp);
}
public function testLimitSubqueryWithScalarSortPg(): void
{
$odp = $this->entityManager->getConnection()->getDatabasePlatform();
$this->entityManager->getConnection()->setDatabasePlatform(new PostgreSQLPlatform());
$this->replaceDatabasePlatform(new PostgreSQLPlatform());
$query = $this->entityManager->createQuery(
'SELECT u, g, COUNT(g.id) AS g_quantity FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g ORDER BY g_quantity'
);
$limitQuery = clone $query;
$limitQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT u, g, COUNT(g.id) AS g_quantity FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g ORDER BY g_quantity');
self::assertSame(
'SELECT DISTINCT id_1, MIN(sclr_3) AS dctrn_minrownum FROM (SELECT COUNT(g0_.id) AS sclr_0, u1_.id AS id_1, g0_.id AS id_2, ROW_NUMBER() OVER(ORDER BY COUNT(g0_.id) ASC) AS sclr_3 FROM User u1_ INNER JOIN user_group u2_ ON u1_.id = u2_.user_id INNER JOIN groups g0_ ON g0_.id = u2_.group_id) dctrn_result GROUP BY id_1 ORDER BY dctrn_minrownum ASC',
$limitQuery->getSQL()
'SELECT DISTINCT id_1, MIN(sclr_3) AS dctrn_minrownum FROM (SELECT COUNT(g0_.id) AS sclr_0, u1_.id AS id_1, g0_.id AS id_2, ROW_NUMBER() OVER(ORDER BY COUNT(g0_.id) ASC) AS sclr_3 FROM User u1_ INNER JOIN user_group u2_ ON u1_.id = u2_.user_id INNER JOIN groups g0_ ON g0_.id = u2_.group_id) dctrn_result GROUP BY id_1 ORDER BY dctrn_minrownum ASC LIMIT 20 OFFSET 10',
$query->getSQL()
);
$this->entityManager->getConnection()->setDatabasePlatform($odp);
}
public function testLimitSubqueryWithMixedSortPg(): void
{
$odp = $this->entityManager->getConnection()->getDatabasePlatform();
$this->entityManager->getConnection()->setDatabasePlatform(new PostgreSQLPlatform());
$this->replaceDatabasePlatform(new PostgreSQLPlatform());
$query = $this->entityManager->createQuery(
'SELECT u, g, COUNT(g.id) AS g_quantity FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g ORDER BY g_quantity, u.id DESC'
);
$limitQuery = clone $query;
$limitQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT u, g, COUNT(g.id) AS g_quantity FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g ORDER BY g_quantity, u.id DESC');
self::assertSame(
'SELECT DISTINCT id_1, MIN(sclr_3) AS dctrn_minrownum FROM (SELECT COUNT(g0_.id) AS sclr_0, u1_.id AS id_1, g0_.id AS id_2, ROW_NUMBER() OVER(ORDER BY COUNT(g0_.id) ASC, u1_.id DESC) AS sclr_3 FROM User u1_ INNER JOIN user_group u2_ ON u1_.id = u2_.user_id INNER JOIN groups g0_ ON g0_.id = u2_.group_id) dctrn_result GROUP BY id_1 ORDER BY dctrn_minrownum ASC',
$limitQuery->getSQL()
'SELECT DISTINCT id_1, MIN(sclr_3) AS dctrn_minrownum FROM (SELECT COUNT(g0_.id) AS sclr_0, u1_.id AS id_1, g0_.id AS id_2, ROW_NUMBER() OVER(ORDER BY COUNT(g0_.id) ASC, u1_.id DESC) AS sclr_3 FROM User u1_ INNER JOIN user_group u2_ ON u1_.id = u2_.user_id INNER JOIN groups g0_ ON g0_.id = u2_.group_id) dctrn_result GROUP BY id_1 ORDER BY dctrn_minrownum ASC LIMIT 20 OFFSET 10',
$query->getSQL()
);
$this->entityManager->getConnection()->setDatabasePlatform($odp);
}
public function testLimitSubqueryWithHiddenScalarSortPg(): void
{
$odp = $this->entityManager->getConnection()->getDatabasePlatform();
$this->entityManager->getConnection()->setDatabasePlatform(new PostgreSQLPlatform());
$this->replaceDatabasePlatform(new PostgreSQLPlatform());
$query = $this->entityManager->createQuery(
'SELECT u, g, COUNT(g.id) AS hidden g_quantity FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g ORDER BY g_quantity, u.id DESC'
);
$limitQuery = clone $query;
$limitQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT u, g, COUNT(g.id) AS hidden g_quantity FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g ORDER BY g_quantity, u.id DESC');
self::assertSame(
'SELECT DISTINCT id_1, MIN(sclr_3) AS dctrn_minrownum FROM (SELECT COUNT(g0_.id) AS sclr_0, u1_.id AS id_1, g0_.id AS id_2, ROW_NUMBER() OVER(ORDER BY COUNT(g0_.id) ASC, u1_.id DESC) AS sclr_3 FROM User u1_ INNER JOIN user_group u2_ ON u1_.id = u2_.user_id INNER JOIN groups g0_ ON g0_.id = u2_.group_id) dctrn_result GROUP BY id_1 ORDER BY dctrn_minrownum ASC',
$limitQuery->getSQL()
'SELECT DISTINCT id_1, MIN(sclr_3) AS dctrn_minrownum FROM (SELECT COUNT(g0_.id) AS sclr_0, u1_.id AS id_1, g0_.id AS id_2, ROW_NUMBER() OVER(ORDER BY COUNT(g0_.id) ASC, u1_.id DESC) AS sclr_3 FROM User u1_ INNER JOIN user_group u2_ ON u1_.id = u2_.user_id INNER JOIN groups g0_ ON g0_.id = u2_.group_id) dctrn_result GROUP BY id_1 ORDER BY dctrn_minrownum ASC LIMIT 20 OFFSET 10',
$query->getSQL()
);
$this->entityManager->getConnection()->setDatabasePlatform($odp);
}
public function testLimitSubqueryPg(): void
{
$odp = $this->entityManager->getConnection()->getDatabasePlatform();
$this->entityManager->getConnection()->setDatabasePlatform(new PostgreSQLPlatform());
$this->replaceDatabasePlatform(new PostgreSQLPlatform());
$this->testLimitSubquery();
$this->entityManager->getConnection()->setDatabasePlatform($odp);
}
public function testLimitSubqueryWithSortOracle(): void
{
$odp = $this->entityManager->getConnection()->getDatabasePlatform();
$this->entityManager->getConnection()->setDatabasePlatform(new OraclePlatform());
$this->replaceDatabasePlatform(new OraclePlatform());
$query = $this->entityManager->createQuery(
'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a ORDER BY p.title'
);
$query->expireQueryCache(true);
$limitQuery = clone $query;
$limitQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a ORDER BY p.title');
self::assertSame(
'SELECT DISTINCT ID_0, MIN(SCLR_5) AS dctrn_minrownum FROM (SELECT m0_.id AS ID_0, m0_.title AS TITLE_1, c1_.id AS ID_2, a2_.id AS ID_3, a2_.name AS NAME_4, ROW_NUMBER() OVER(ORDER BY m0_.title ASC) AS SCLR_5, m0_.author_id AS AUTHOR_ID_6, m0_.category_id AS CATEGORY_ID_7 FROM MyBlogPost m0_ INNER JOIN Category c1_ ON m0_.category_id = c1_.id INNER JOIN Author a2_ ON m0_.author_id = a2_.id) dctrn_result GROUP BY ID_0 ORDER BY dctrn_minrownum ASC',
$limitQuery->getSQL()
'SELECT * FROM (SELECT a.*, ROWNUM AS doctrine_rownum FROM (SELECT DISTINCT ID_0, MIN(SCLR_5) AS dctrn_minrownum FROM (SELECT m0_.id AS ID_0, m0_.title AS TITLE_1, c1_.id AS ID_2, a2_.id AS ID_3, a2_.name AS NAME_4, ROW_NUMBER() OVER(ORDER BY m0_.title ASC) AS SCLR_5, m0_.author_id AS AUTHOR_ID_6, m0_.category_id AS CATEGORY_ID_7 FROM MyBlogPost m0_ INNER JOIN Category c1_ ON m0_.category_id = c1_.id INNER JOIN Author a2_ ON m0_.author_id = a2_.id) dctrn_result GROUP BY ID_0 ORDER BY dctrn_minrownum ASC) a WHERE ROWNUM <= 30) WHERE doctrine_rownum >= 11',
$query->getSQL()
);
$this->entityManager->getConnection()->setDatabasePlatform($odp);
}
public function testLimitSubqueryWithScalarSortOracle(): void
{
$odp = $this->entityManager->getConnection()->getDatabasePlatform();
$this->entityManager->getConnection()->setDatabasePlatform(new OraclePlatform());
$this->replaceDatabasePlatform(new OraclePlatform());
$query = $this->entityManager->createQuery(
'SELECT u, g, COUNT(g.id) AS g_quantity FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g ORDER BY g_quantity'
);
$query->expireQueryCache(true);
$limitQuery = clone $query;
$limitQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT u, g, COUNT(g.id) AS g_quantity FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g ORDER BY g_quantity');
self::assertSame(
'SELECT DISTINCT ID_1, MIN(SCLR_3) AS dctrn_minrownum FROM (SELECT COUNT(g0_.id) AS SCLR_0, u1_.id AS ID_1, g0_.id AS ID_2, ROW_NUMBER() OVER(ORDER BY COUNT(g0_.id) ASC) AS SCLR_3 FROM User u1_ INNER JOIN user_group u2_ ON u1_.id = u2_.user_id INNER JOIN groups g0_ ON g0_.id = u2_.group_id) dctrn_result GROUP BY ID_1 ORDER BY dctrn_minrownum ASC',
$limitQuery->getSQL()
'SELECT * FROM (SELECT a.*, ROWNUM AS doctrine_rownum FROM (SELECT DISTINCT ID_1, MIN(SCLR_3) AS dctrn_minrownum FROM (SELECT COUNT(g0_.id) AS SCLR_0, u1_.id AS ID_1, g0_.id AS ID_2, ROW_NUMBER() OVER(ORDER BY COUNT(g0_.id) ASC) AS SCLR_3 FROM User u1_ INNER JOIN user_group u2_ ON u1_.id = u2_.user_id INNER JOIN groups g0_ ON g0_.id = u2_.group_id) dctrn_result GROUP BY ID_1 ORDER BY dctrn_minrownum ASC) a WHERE ROWNUM <= 30) WHERE doctrine_rownum >= 11',
$query->getSQL()
);
$this->entityManager->getConnection()->setDatabasePlatform($odp);
}
public function testLimitSubqueryWithMixedSortOracle(): void
{
$odp = $this->entityManager->getConnection()->getDatabasePlatform();
$this->entityManager->getConnection()->setDatabasePlatform(new OraclePlatform());
$this->replaceDatabasePlatform(new OraclePlatform());
$query = $this->entityManager->createQuery(
'SELECT u, g, COUNT(g.id) AS g_quantity FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g ORDER BY g_quantity, u.id DESC'
);
$query->expireQueryCache(true);
$limitQuery = clone $query;
$limitQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT u, g, COUNT(g.id) AS g_quantity FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g ORDER BY g_quantity, u.id DESC');
self::assertSame(
'SELECT DISTINCT ID_1, MIN(SCLR_3) AS dctrn_minrownum FROM (SELECT COUNT(g0_.id) AS SCLR_0, u1_.id AS ID_1, g0_.id AS ID_2, ROW_NUMBER() OVER(ORDER BY COUNT(g0_.id) ASC, u1_.id DESC) AS SCLR_3 FROM User u1_ INNER JOIN user_group u2_ ON u1_.id = u2_.user_id INNER JOIN groups g0_ ON g0_.id = u2_.group_id) dctrn_result GROUP BY ID_1 ORDER BY dctrn_minrownum ASC',
$limitQuery->getSQL()
'SELECT * FROM (SELECT a.*, ROWNUM AS doctrine_rownum FROM (SELECT DISTINCT ID_1, MIN(SCLR_3) AS dctrn_minrownum FROM (SELECT COUNT(g0_.id) AS SCLR_0, u1_.id AS ID_1, g0_.id AS ID_2, ROW_NUMBER() OVER(ORDER BY COUNT(g0_.id) ASC, u1_.id DESC) AS SCLR_3 FROM User u1_ INNER JOIN user_group u2_ ON u1_.id = u2_.user_id INNER JOIN groups g0_ ON g0_.id = u2_.group_id) dctrn_result GROUP BY ID_1 ORDER BY dctrn_minrownum ASC) a WHERE ROWNUM <= 30) WHERE doctrine_rownum >= 11',
$query->getSQL()
);
$this->entityManager->getConnection()->setDatabasePlatform($odp);
}
public function testLimitSubqueryOracle(): void
{
$odp = $this->entityManager->getConnection()->getDatabasePlatform();
$this->entityManager->getConnection()->setDatabasePlatform(new OraclePlatform());
$this->replaceDatabasePlatform(new OraclePlatform());
$query = $this->entityManager->createQuery(
'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a'
);
$query->expireQueryCache(true);
$limitQuery = clone $query;
$limitQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a');
self::assertSame(
'SELECT DISTINCT ID_0 FROM (SELECT m0_.id AS ID_0, m0_.title AS TITLE_1, c1_.id AS ID_2, a2_.id AS ID_3, a2_.name AS NAME_4, m0_.author_id AS AUTHOR_ID_5, m0_.category_id AS CATEGORY_ID_6 FROM MyBlogPost m0_ INNER JOIN Category c1_ ON m0_.category_id = c1_.id INNER JOIN Author a2_ ON m0_.author_id = a2_.id) dctrn_result',
$limitQuery->getSQL()
'SELECT * FROM (SELECT a.*, ROWNUM AS doctrine_rownum FROM (SELECT DISTINCT ID_0 FROM (SELECT m0_.id AS ID_0, m0_.title AS TITLE_1, c1_.id AS ID_2, a2_.id AS ID_3, a2_.name AS NAME_4, m0_.author_id AS AUTHOR_ID_5, m0_.category_id AS CATEGORY_ID_6 FROM MyBlogPost m0_ INNER JOIN Category c1_ ON m0_.category_id = c1_.id INNER JOIN Author a2_ ON m0_.author_id = a2_.id) dctrn_result) a WHERE ROWNUM <= 30) WHERE doctrine_rownum >= 11',
$query->getSQL()
);
$this->entityManager->getConnection()->setDatabasePlatform($odp);
}
public function testCountQueryMixedResultsWithName(): void
{
$query = $this->entityManager->createQuery(
'SELECT a, sum(a.name) as foo FROM Doctrine\Tests\ORM\Tools\Pagination\Author a'
);
$limitQuery = clone $query;
$limitQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT a, sum(a.name) as foo FROM Doctrine\Tests\ORM\Tools\Pagination\Author a');
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT a0_.id AS id_0, a0_.name AS name_1, sum(a0_.name) AS sclr_2 FROM Author a0_) dctrn_result',
$limitQuery->getSQL()
'SELECT DISTINCT id_0 FROM (SELECT a0_.id AS id_0, a0_.name AS name_1, sum(a0_.name) AS sclr_2 FROM Author a0_) dctrn_result LIMIT 20 OFFSET 10',
$query->getSQL()
);
}
/** @group DDC-3336 */
public function testCountQueryWithArithmeticOrderByCondition(): void
{
$query = $this->entityManager->createQuery(
'SELECT a FROM Doctrine\Tests\ORM\Tools\Pagination\Author a ORDER BY (1 - 1000) * 1 DESC'
);
$this->entityManager->getConnection()->setDatabasePlatform(new MySQLPlatform());
$this->replaceDatabasePlatform(new MySQLPlatform());
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT a FROM Doctrine\Tests\ORM\Tools\Pagination\Author a ORDER BY (1 - 1000) * 1 DESC');
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, (1 - 1000) * 1 FROM (SELECT a0_.id AS id_0, a0_.name AS name_1 FROM Author a0_) dctrn_result_inner ORDER BY (1 - 1000) * 1 DESC) dctrn_result',
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, (1 - 1000) * 1 FROM (SELECT a0_.id AS id_0, a0_.name AS name_1 FROM Author a0_) dctrn_result_inner ORDER BY (1 - 1000) * 1 DESC) dctrn_result LIMIT 20 OFFSET 10',
$query->getSQL()
);
}
public function testCountQueryWithComplexScalarOrderByItemWithoutJoin(): void
{
$query = $this->entityManager->createQuery(
'SELECT a FROM Doctrine\Tests\ORM\Tools\Pagination\Avatar a ORDER BY a.imageHeight * a.imageWidth DESC'
);
$this->entityManager->getConnection()->setDatabasePlatform(new MySQLPlatform());
$this->replaceDatabasePlatform(new MySQLPlatform());
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT a FROM Doctrine\Tests\ORM\Tools\Pagination\Avatar a ORDER BY a.imageHeight * a.imageWidth DESC');
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, imageHeight_2 * imageWidth_3 FROM (SELECT a0_.id AS id_0, a0_.image AS image_1, a0_.imageHeight AS imageHeight_2, a0_.imageWidth AS imageWidth_3, a0_.imageAltDesc AS imageAltDesc_4, a0_.user_id AS user_id_5 FROM Avatar a0_) dctrn_result_inner ORDER BY imageHeight_2 * imageWidth_3 DESC) dctrn_result',
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, imageHeight_2 * imageWidth_3 FROM (SELECT a0_.id AS id_0, a0_.image AS image_1, a0_.imageHeight AS imageHeight_2, a0_.imageWidth AS imageWidth_3, a0_.imageAltDesc AS imageAltDesc_4, a0_.user_id AS user_id_5 FROM Avatar a0_) dctrn_result_inner ORDER BY imageHeight_2 * imageWidth_3 DESC) dctrn_result LIMIT 20 OFFSET 10',
$query->getSQL()
);
}
public function testCountQueryWithComplexScalarOrderByItemJoinedWithoutPartial(): void
{
$query = $this->entityManager->createQuery(
'SELECT u FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.avatar a ORDER BY a.imageHeight * a.imageWidth DESC'
);
$this->entityManager->getConnection()->setDatabasePlatform(new MySQLPlatform());
$this->replaceDatabasePlatform(new MySQLPlatform());
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT u FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.avatar a ORDER BY a.imageHeight * a.imageWidth DESC');
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, imageHeight_3 * imageWidth_4 FROM (SELECT u0_.id AS id_0, a1_.id AS id_1, a1_.image AS image_2, a1_.imageHeight AS imageHeight_3, a1_.imageWidth AS imageWidth_4, a1_.imageAltDesc AS imageAltDesc_5, a1_.user_id AS user_id_6 FROM User u0_ INNER JOIN Avatar a1_ ON u0_.id = a1_.user_id) dctrn_result_inner ORDER BY imageHeight_3 * imageWidth_4 DESC) dctrn_result',
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, imageHeight_3 * imageWidth_4 FROM (SELECT u0_.id AS id_0, a1_.id AS id_1, a1_.image AS image_2, a1_.imageHeight AS imageHeight_3, a1_.imageWidth AS imageWidth_4, a1_.imageAltDesc AS imageAltDesc_5, a1_.user_id AS user_id_6 FROM User u0_ INNER JOIN Avatar a1_ ON u0_.id = a1_.user_id) dctrn_result_inner ORDER BY imageHeight_3 * imageWidth_4 DESC) dctrn_result LIMIT 20 OFFSET 10',
$query->getSQL()
);
}
public function testCountQueryWithComplexScalarOrderByItemJoinedWithPartial(): void
{
$query = $this->entityManager->createQuery(
'SELECT u, partial a.{id, imageAltDesc} FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.avatar a ORDER BY a.imageHeight * a.imageWidth DESC'
);
$this->entityManager->getConnection()->setDatabasePlatform(new MySQLPlatform());
$this->replaceDatabasePlatform(new MySQLPlatform());
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT u, partial a.{id, imageAltDesc} FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.avatar a ORDER BY a.imageHeight * a.imageWidth DESC');
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, imageHeight_5 * imageWidth_6 FROM (SELECT u0_.id AS id_0, a1_.id AS id_1, a1_.imageAltDesc AS imageAltDesc_2, a1_.id AS id_3, a1_.image AS image_4, a1_.imageHeight AS imageHeight_5, a1_.imageWidth AS imageWidth_6, a1_.imageAltDesc AS imageAltDesc_7, a1_.user_id AS user_id_8 FROM User u0_ INNER JOIN Avatar a1_ ON u0_.id = a1_.user_id) dctrn_result_inner ORDER BY imageHeight_5 * imageWidth_6 DESC) dctrn_result',
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, imageHeight_5 * imageWidth_6 FROM (SELECT u0_.id AS id_0, a1_.id AS id_1, a1_.imageAltDesc AS imageAltDesc_2, a1_.id AS id_3, a1_.image AS image_4, a1_.imageHeight AS imageHeight_5, a1_.imageWidth AS imageWidth_6, a1_.imageAltDesc AS imageAltDesc_7, a1_.user_id AS user_id_8 FROM User u0_ INNER JOIN Avatar a1_ ON u0_.id = a1_.user_id) dctrn_result_inner ORDER BY imageHeight_5 * imageWidth_6 DESC) dctrn_result LIMIT 20 OFFSET 10',
$query->getSQL()
);
}
public function testCountQueryWithComplexScalarOrderByItemOracle(): void
{
$query = $this->entityManager->createQuery(
'SELECT a FROM Doctrine\Tests\ORM\Tools\Pagination\Avatar a ORDER BY a.imageHeight * a.imageWidth DESC'
);
$this->entityManager->getConnection()->setDatabasePlatform(new OraclePlatform());
$this->replaceDatabasePlatform(new OraclePlatform());
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT a FROM Doctrine\Tests\ORM\Tools\Pagination\Avatar a ORDER BY a.imageHeight * a.imageWidth DESC');
self::assertSame(
'SELECT DISTINCT ID_0, MIN(SCLR_5) AS dctrn_minrownum FROM (SELECT a0_.id AS ID_0, a0_.image AS IMAGE_1, a0_.imageHeight AS IMAGEHEIGHT_2, a0_.imageWidth AS IMAGEWIDTH_3, a0_.imageAltDesc AS IMAGEALTDESC_4, ROW_NUMBER() OVER(ORDER BY a0_.imageHeight * a0_.imageWidth DESC) AS SCLR_5, a0_.user_id AS USER_ID_6 FROM Avatar a0_) dctrn_result GROUP BY ID_0 ORDER BY dctrn_minrownum ASC',
'SELECT * FROM (SELECT a.*, ROWNUM AS doctrine_rownum FROM (SELECT DISTINCT ID_0, MIN(SCLR_5) AS dctrn_minrownum FROM (SELECT a0_.id AS ID_0, a0_.image AS IMAGE_1, a0_.imageHeight AS IMAGEHEIGHT_2, a0_.imageWidth AS IMAGEWIDTH_3, a0_.imageAltDesc AS IMAGEALTDESC_4, ROW_NUMBER() OVER(ORDER BY a0_.imageHeight * a0_.imageWidth DESC) AS SCLR_5, a0_.user_id AS USER_ID_6 FROM Avatar a0_) dctrn_result GROUP BY ID_0 ORDER BY dctrn_minrownum ASC) a WHERE ROWNUM <= 30) WHERE doctrine_rownum >= 11',
$query->getSQL()
);
}
@@ -292,75 +232,65 @@ final class LimitSubqueryOutputWalkerTest extends PaginationTestCase
/** @group DDC-3434 */
public function testLimitSubqueryWithHiddenSelectionInOrderBy(): void
{
$query = $this->entityManager->createQuery(
$query = $this->createQuery(
'SELECT a, a.name AS HIDDEN ord FROM Doctrine\Tests\ORM\Tools\Pagination\Author a ORDER BY ord DESC'
);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, name_2 FROM (SELECT a0_.id AS id_0, a0_.name AS name_1, a0_.name AS name_2 FROM Author a0_) dctrn_result_inner ORDER BY name_2 DESC) dctrn_result',
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, name_2 FROM (SELECT a0_.id AS id_0, a0_.name AS name_1, a0_.name AS name_2 FROM Author a0_) dctrn_result_inner ORDER BY name_2 DESC) dctrn_result LIMIT 20 OFFSET 10',
$query->getSQL()
);
}
public function testLimitSubqueryWithColumnWithSortDirectionInName(): void
{
$query = $this->entityManager->createQuery(
'SELECT a FROM Doctrine\Tests\ORM\Tools\Pagination\Avatar a ORDER BY a.imageAltDesc DESC'
);
$this->entityManager->getConnection()->setDatabasePlatform(new MySQLPlatform());
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$this->replaceDatabasePlatform(new MySQLPlatform());
$query = $this->createQuery('SELECT a FROM Doctrine\Tests\ORM\Tools\Pagination\Avatar a ORDER BY a.imageAltDesc DESC');
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, imageAltDesc_4 FROM (SELECT a0_.id AS id_0, a0_.image AS image_1, a0_.imageHeight AS imageHeight_2, a0_.imageWidth AS imageWidth_3, a0_.imageAltDesc AS imageAltDesc_4, a0_.user_id AS user_id_5 FROM Avatar a0_) dctrn_result_inner ORDER BY imageAltDesc_4 DESC) dctrn_result',
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, imageAltDesc_4 FROM (SELECT a0_.id AS id_0, a0_.image AS image_1, a0_.imageHeight AS imageHeight_2, a0_.imageWidth AS imageWidth_3, a0_.imageAltDesc AS imageAltDesc_4, a0_.user_id AS user_id_5 FROM Avatar a0_) dctrn_result_inner ORDER BY imageAltDesc_4 DESC) dctrn_result LIMIT 20 OFFSET 10',
$query->getSQL()
);
}
public function testLimitSubqueryWithOrderByInnerJoined(): void
{
$query = $this->entityManager->createQuery(
'SELECT b FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost b JOIN b.author a ORDER BY a.name ASC'
);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query = $this->createQuery('SELECT b FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost b JOIN b.author a ORDER BY a.name ASC');
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, name_2 FROM (SELECT b0_.id AS id_0, a1_.id AS id_1, a1_.name AS name_2, b0_.author_id AS author_id_3, b0_.category_id AS category_id_4 FROM BlogPost b0_ INNER JOIN Author a1_ ON b0_.author_id = a1_.id) dctrn_result_inner ORDER BY name_2 ASC) dctrn_result',
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, name_2 FROM (SELECT b0_.id AS id_0, a1_.id AS id_1, a1_.name AS name_2, b0_.author_id AS author_id_3, b0_.category_id AS category_id_4 FROM BlogPost b0_ INNER JOIN Author a1_ ON b0_.author_id = a1_.id) dctrn_result_inner ORDER BY name_2 ASC) dctrn_result LIMIT 20 OFFSET 10',
$query->getSQL()
);
}
public function testLimitSubqueryWithOrderByAndSubSelectInWhereClauseMySql(): void
{
$this->entityManager->getConnection()->setDatabasePlatform(new MySQLPlatform());
$query = $this->entityManager->createQuery(
$this->replaceDatabasePlatform(new MySQLPlatform());
$query = $this->createQuery(
'SELECT b FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost b
WHERE ((SELECT COUNT(simple.id) FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost simple) = 1)
ORDER BY b.id DESC'
);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0 FROM (SELECT b0_.id AS id_0, b0_.author_id AS author_id_1, b0_.category_id AS category_id_2 FROM BlogPost b0_ WHERE ((SELECT COUNT(b1_.id) AS sclr_3 FROM BlogPost b1_) = 1)) dctrn_result_inner ORDER BY id_0 DESC) dctrn_result',
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0 FROM (SELECT b0_.id AS id_0, b0_.author_id AS author_id_1, b0_.category_id AS category_id_2 FROM BlogPost b0_ WHERE ((SELECT COUNT(b1_.id) AS sclr_3 FROM BlogPost b1_) = 1)) dctrn_result_inner ORDER BY id_0 DESC) dctrn_result LIMIT 20 OFFSET 10',
$query->getSQL()
);
}
public function testLimitSubqueryWithOrderByAndSubSelectInWhereClausePgSql(): void
{
$this->entityManager->getConnection()->setDatabasePlatform(new PostgreSQLPlatform());
$query = $this->entityManager->createQuery(
$this->replaceDatabasePlatform(new PostgreSQLPlatform());
$query = $this->createQuery(
'SELECT b FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost b
WHERE ((SELECT COUNT(simple.id) FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost simple) = 1)
ORDER BY b.id DESC'
);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
self::assertSame(
'SELECT DISTINCT id_0, MIN(sclr_1) AS dctrn_minrownum FROM (SELECT b0_.id AS id_0, ROW_NUMBER() OVER(ORDER BY b0_.id DESC) AS sclr_1, b0_.author_id AS author_id_2, b0_.category_id AS category_id_3 FROM BlogPost b0_ WHERE ((SELECT COUNT(b1_.id) AS sclr_4 FROM BlogPost b1_) = 1)) dctrn_result GROUP BY id_0 ORDER BY dctrn_minrownum ASC',
'SELECT DISTINCT id_0, MIN(sclr_1) AS dctrn_minrownum FROM (SELECT b0_.id AS id_0, ROW_NUMBER() OVER(ORDER BY b0_.id DESC) AS sclr_1, b0_.author_id AS author_id_2, b0_.category_id AS category_id_3 FROM BlogPost b0_ WHERE ((SELECT COUNT(b1_.id) AS sclr_4 FROM BlogPost b1_) = 1)) dctrn_result GROUP BY id_0 ORDER BY dctrn_minrownum ASC LIMIT 20 OFFSET 10',
$query->getSQL()
);
}
@@ -370,16 +300,15 @@ ORDER BY b.id DESC'
*/
public function testLimitSubqueryOrderByFieldFromMappedSuperclass(): void
{
$this->entityManager->getConnection()->setDatabasePlatform(new MySQLPlatform());
$this->replaceDatabasePlatform(new MySQLPlatform());
// now use the third one in query
$query = $this->entityManager->createQuery(
$query = $this->createQuery(
'SELECT b FROM Doctrine\Tests\ORM\Tools\Pagination\Banner b ORDER BY b.id DESC'
);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0 FROM (SELECT b0_.id AS id_0, b0_.name AS name_1 FROM Banner b0_) dctrn_result_inner ORDER BY id_0 DESC) dctrn_result',
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0 FROM (SELECT b0_.id AS id_0, b0_.name AS name_1 FROM Banner b0_) dctrn_result_inner ORDER BY id_0 DESC) dctrn_result LIMIT 20 OFFSET 10',
$query->getSQL()
);
}
@@ -389,9 +318,9 @@ ORDER BY b.id DESC'
*/
public function testLimitSubqueryOrderBySubSelectOrderByExpression(): void
{
$this->entityManager->getConnection()->setDatabasePlatform(new MySQLPlatform());
$this->replaceDatabasePlatform(new MySQLPlatform());
$query = $this->entityManager->createQuery(
$query = $this->createQuery(
'SELECT a,
(
SELECT MIN(bp.title)
@@ -401,10 +330,9 @@ ORDER BY b.id DESC'
FROM Doctrine\Tests\ORM\Tools\Pagination\Author a
ORDER BY first_blog_post DESC'
);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
self::assertSame(
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, sclr_2 FROM (SELECT a0_.id AS id_0, a0_.name AS name_1, (SELECT MIN(m1_.title) AS sclr_3 FROM MyBlogPost m1_ WHERE m1_.author_id = a0_.id) AS sclr_2 FROM Author a0_) dctrn_result_inner ORDER BY sclr_2 DESC) dctrn_result',
'SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, sclr_2 FROM (SELECT a0_.id AS id_0, a0_.name AS name_1, (SELECT MIN(m1_.title) AS sclr_3 FROM MyBlogPost m1_ WHERE m1_.author_id = a0_.id) AS sclr_2 FROM Author a0_) dctrn_result_inner ORDER BY sclr_2 DESC) dctrn_result LIMIT 20 OFFSET 10',
$query->getSQL()
);
}
@@ -414,9 +342,9 @@ ORDER BY b.id DESC'
*/
public function testLimitSubqueryOrderBySubSelectOrderByExpressionPg(): void
{
$this->entityManager->getConnection()->setDatabasePlatform(new PostgreSQLPlatform());
$this->replaceDatabasePlatform(new PostgreSQLPlatform());
$query = $this->entityManager->createQuery(
$query = $this->createQuery(
'SELECT a,
(
SELECT MIN(bp.title)
@@ -426,10 +354,9 @@ ORDER BY b.id DESC'
FROM Doctrine\Tests\ORM\Tools\Pagination\Author a
ORDER BY first_blog_post DESC'
);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
self::assertSame(
'SELECT DISTINCT id_0, MIN(sclr_4) AS dctrn_minrownum FROM (SELECT a0_.id AS id_0, a0_.name AS name_1, (SELECT MIN(m1_.title) AS sclr_3 FROM MyBlogPost m1_ WHERE m1_.author_id = a0_.id) AS sclr_2, ROW_NUMBER() OVER(ORDER BY (SELECT MIN(m1_.title) AS sclr_5 FROM MyBlogPost m1_ WHERE m1_.author_id = a0_.id) DESC) AS sclr_4 FROM Author a0_) dctrn_result GROUP BY id_0 ORDER BY dctrn_minrownum ASC',
'SELECT DISTINCT id_0, MIN(sclr_4) AS dctrn_minrownum FROM (SELECT a0_.id AS id_0, a0_.name AS name_1, (SELECT MIN(m1_.title) AS sclr_3 FROM MyBlogPost m1_ WHERE m1_.author_id = a0_.id) AS sclr_2, ROW_NUMBER() OVER(ORDER BY (SELECT MIN(m1_.title) AS sclr_5 FROM MyBlogPost m1_ WHERE m1_.author_id = a0_.id) DESC) AS sclr_4 FROM Author a0_) dctrn_result GROUP BY id_0 ORDER BY dctrn_minrownum ASC LIMIT 20 OFFSET 10',
$query->getSQL()
);
}
@@ -439,9 +366,9 @@ ORDER BY b.id DESC'
*/
public function testLimitSubqueryOrderBySubSelectOrderByExpressionOracle(): void
{
$this->entityManager->getConnection()->setDatabasePlatform(new OraclePlatform());
$this->replaceDatabasePlatform(new OraclePlatform());
$query = $this->entityManager->createQuery(
$query = $this->createQuery(
'SELECT a,
(
SELECT MIN(bp.title)
@@ -451,11 +378,20 @@ ORDER BY b.id DESC'
FROM Doctrine\Tests\ORM\Tools\Pagination\Author a
ORDER BY first_blog_post DESC'
);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
self::assertSame(
'SELECT DISTINCT ID_0, MIN(SCLR_4) AS dctrn_minrownum FROM (SELECT a0_.id AS ID_0, a0_.name AS NAME_1, (SELECT MIN(m1_.title) AS SCLR_3 FROM MyBlogPost m1_ WHERE m1_.author_id = a0_.id) AS SCLR_2, ROW_NUMBER() OVER(ORDER BY (SELECT MIN(m1_.title) AS SCLR_5 FROM MyBlogPost m1_ WHERE m1_.author_id = a0_.id) DESC) AS SCLR_4 FROM Author a0_) dctrn_result GROUP BY ID_0 ORDER BY dctrn_minrownum ASC',
'SELECT * FROM (SELECT a.*, ROWNUM AS doctrine_rownum FROM (SELECT DISTINCT ID_0, MIN(SCLR_4) AS dctrn_minrownum FROM (SELECT a0_.id AS ID_0, a0_.name AS NAME_1, (SELECT MIN(m1_.title) AS SCLR_3 FROM MyBlogPost m1_ WHERE m1_.author_id = a0_.id) AS SCLR_2, ROW_NUMBER() OVER(ORDER BY (SELECT MIN(m1_.title) AS SCLR_5 FROM MyBlogPost m1_ WHERE m1_.author_id = a0_.id) DESC) AS SCLR_4 FROM Author a0_) dctrn_result GROUP BY ID_0 ORDER BY dctrn_minrownum ASC) a WHERE ROWNUM <= 30) WHERE doctrine_rownum >= 11',
$query->getSQL()
);
}
private function createQuery(string $dql): Query
{
$query = $this->entityManager->createQuery($dql);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class);
$query->setFirstResult(10);
$query->setMaxResults(20);
return $query;
}
}