Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

IDENTITY identifier strategy for PostgreSQL breaks when inherited from MappedSuperclass #10927

Closed
DemoniacDeath opened this issue Sep 1, 2023 · 18 comments · Fixed by #11050
Closed
Labels

Comments

@DemoniacDeath
Copy link
Contributor

DemoniacDeath commented Sep 1, 2023

BC Break Report

Q A
BC Break yes
Version 2.16.0

Summary

Persisting an @Entity which has it's @Id defined in it's parent (@MappedSuperclass) with IDENTITY generator strategy on PostgreSQL platform.
It seems that the breaking change occured in this PR #10455, specifically changes in ClassMetadataFactory that removed the condtion of $rootEntityFound for calling inheritIdGeneratorMapping()

Previous behavior

In version 2.15.5 the IdentityGenerator used to retrieve inserted id came from the Entity itself with $sequenceName matching the schema. So the value was retrieved from the correct sequence.

Current behavior

Now the IdentityGenerator used to retrieve inserted id comes from the parent @MappedSuperclass and $sequenceName is wrong (name of parent class + sequence suffix) and non-existent in schema, so a PostgreSQL error about relation not existing is thrown.

How to reproduce

E.g. (irrelevant code and domain-specific terminology is ommited):

/**
 * @ORM\MappedSuperclass
 */
abstract class AbstractTaskOperatorHistoryRecord
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private ?int $id = null;
}
/**
 * @ORM\Entity()
 * @ORM\Table(name="foo_bar_task_operator_history")
 */
class FooBarTaskOperatorHistoryRecord extends AbstractTaskOperatorHistoryRecord
{
}
$this->_em->persist(new FooBarTaskOperatorHistoryRecord());
$this->_em->flush();

The PostgreSQL sequence used by id column in foo_bar_task_operator_history table is foo_bar_task_operator_history_id_seq.
The sequence used to retrieve inserted id was foo_bar_task_operator_history_id_seq before the breaking change. Now it is trying to get the value from abstracttaskoperatorhistoryrecord_id_seq which obviously does not and should not exist. The error thrown is

PDOException

SQLSTATE[42P01]: Undefined table: 7 ERROR: relation "abstracttaskoperatorhistoryrecord_id_seq" does not exist
@pkly
Copy link

pkly commented Sep 6, 2023

I've noticed a similar issue in one of our projects, running on MariaDB, similarly with a parent entity (DiscriminatorMap + Joined inheritance) but in our case I see:

An exception occurred while executing a query: SQLSTATE[HY093]: Invalid parameter number: number of bound variables does not match number of tokens

2.15.5 works as expected, 2.16.0 crashes on the same code.
We have a parent entity with a generated id value, the child entity also contains an automatically generated uuid value (via custom generator).

@julienbrunet21
Copy link

julienbrunet21 commented Oct 11, 2023

Hello I found a workaround which consists of changing the parent's id to protected instead of private

@DemoniacDeath
Copy link
Contributor Author

Hello I found a workaround which consists of changing the parent's id to protected instead of private

changing id property from private to protected did not change the result of failing test case #10928 for me

@mpdude
Copy link
Contributor

mpdude commented Oct 12, 2023

@DemoniacDeath did you opt into the new „report fields where declared“ setting?

@DemoniacDeath
Copy link
Contributor Author

DemoniacDeath commented Oct 13, 2023

@mpdude

OrmFunctionalTestCase.php:803

        $config->setMetadataDriverImpl(
            $mappingDriver ?? ORMSetup::createDefaultAnnotationDriver([
                realpath(__DIR__ . '/Models/Cache'),
                realpath(__DIR__ . '/Models/GeoNames'),
            ], null, true)
        );

it is set to true in the failing test case (by default). changing it to false did not change the outcome.

AFAIUI the issue is specifically with the code in \Doctrine\ORM\Mapping\ClassMetadataFactory::doLoadMetadata

@melroy89
Copy link

Mbin is also effected by this regression issue of Doctrine: MbinOrg/mbin#147

mpdude added a commit that referenced this issue Nov 6, 2023
 #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 `GH1927Test` 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.
@mpdude
Copy link
Contributor

mpdude commented Nov 6, 2023

I see two ways how this could be fixed.

The first one is in #11050. It reverts the way inheritance works for @SequenceGeneratorDefinition and mapped superclasses. But, with this change one cannot define @SequenceGeneratorDefinition on mapped superclasses and expect it to be inherited by child entity classes - at least not when using the new reportFieldsWhereDeclared mapping driver mode.

The second approach is in #11052. It will recognize when an explicit @SequenceGeneratorDefinition is used and pass that one on unchanged to child classes (= all use the same sequence name). Otherwise, the default one will be generated for every single class, so the sequence name is based on the table name.

@Chris53897
Copy link

@mpdude I have tested your solution. Thanks for your work. Looks like it works for me with 2.16.2 and report_fields_where_declared: true

@DemoniacDeath can you confirm this too?

mpdude added a commit to mpdude/doctrine2 that referenced this issue Nov 7, 2023
This is an alternative implementation to doctrine#11050. The difference is that with this PR here, once `@SequenceGeneratorDefinition` is used in an inheritance tree of entities and/or mapped superclasses, this definition (and in particular, the sequence name) will be inherited by all child entity/mapped superclasses.

Before doctrine#10455, the rules how `@SequenceGeneratorDefinition` is inherited from parent entity/mapped superclasses were as follows:

* 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.

But, this has to be considered together with the quirky mapping driver behaviour that was deprecated in doctrine#10455: The mapping driver would not report public and protected fields from mapped superclasses, so these were virtually "pushed down" to the next entity classes. 

That means `@SequenceGeneratorDefinition` on mapped superclasses would, in fact, be effective as-is for inheriting entity classes. This is what was covered by the tests in `BasicInheritanceMappingTest` that I marked with `@group doctrineGH-10927`.

My guess is that this PR will make it possible to opt-in to `reportFieldsWhereDeclared` (see doctrine#10455) and still get the same behaviour for mapped superclasses using `@SequenceGeneratorDefinition` as before. But maybe I don't see the full picture and all edge cases, so 👀 requested.

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.
@mpdude
Copy link
Contributor

mpdude commented Nov 7, 2023

Please also test #11052.

mpdude added a commit to mpdude/doctrine2 that referenced this issue Nov 7, 2023
This is an alternative implementation to doctrine#11050. The difference is that with this PR here, once `@SequenceGeneratorDefinition` is used in an inheritance tree of entities and/or mapped superclasses, this definition (and in particular, the sequence name) will be inherited by all child entity/mapped superclasses.

Before doctrine#10455, the rules how `@SequenceGeneratorDefinition` is inherited from parent entity/mapped superclasses were as follows:

* 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.

But, this has to be considered together with the quirky mapping driver behaviour that was deprecated in doctrine#10455: The mapping driver would not report public and protected fields from mapped superclasses, so these were virtually "pushed down" to the next entity classes. 

That means `@SequenceGeneratorDefinition` on mapped superclasses would, in fact, be effective as-is for inheriting entity classes. This is what was covered by the tests in `BasicInheritanceMappingTest` that I marked with `@group doctrineGH-10927`.

My guess is that this PR will make it possible to opt-in to `reportFieldsWhereDeclared` (see doctrine#10455) and still get the same behaviour for mapped superclasses using `@SequenceGeneratorDefinition` as before. But maybe I don't see the full picture and all edge cases, so 👀 requested.

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.

# Please enter the commit message for your changes. Lines starting
# with '#' will be kept; you may remove them yourself if you want to.
# An empty message aborts the commit.
#
# Date:      Mon Nov 6 23:01:25 2023 +0100
#
# On branch fix-10927-take2
# Your branch is up to date with 'mpdude/fix-10927-take2'.
#
# Changes to be committed:
#	modified:   lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
#	modified:   lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
#	modified:   phpstan-baseline.neon
#	new file:   tests/Doctrine/Tests/ORM/Functional/Ticket/GH10927Test.php
#	modified:   tests/Doctrine/Tests/ORM/Mapping/BasicInheritanceMappingTest.php
#
# Untracked files:
#	phpunit.xml
#	tests/Doctrine/Tests/ORM/Functional/Ticket/GH11040Test.php
#
@DemoniacDeath
Copy link
Contributor Author

@Chris53897 @mpdude indeed, both solutions have resolved the issue

@Arkemlar
Copy link

Same problem here but in my case I use Embeddable that contains id field:

#[ORM\Embeddable]
class AutoincrementId extends BaseId implements Stringable
{
    #[ORM\Id, ORM\GeneratedValue(strategy: 'IDENTITY'), ORM\Column(type: 'bigint', nullable: false, options: ['unsigned' => true])]
    protected int $id;
}


#[ORM\Entity]
class ProductUnit
{
    #[ORM\Embedded(columnPrefix: false)]
    public readonly ProductUnitId $id;
}

It worked fine with strategy "AUTO", but with strategy "IDENTITY" it throws exception:
SQLSTATE[42P01]: Undefined table: 7 ERROR: relation "product_unit_id_id_seq" does not exist CONTEXT: unnamed portal parameter $1 = '...'

Identity strategy so fucked, wasted lots of hours trying to make it work...

@greg0ire
Copy link
Member

@Arkemlar please stay polite, no one wants to hear you complain like that, especially not people providing you with free software .

@Arkemlar
Copy link

@greg0ire I didn't mean anyone in person or group of people at all, only Identity strategy feature because it makes a lot of pain working with it.

@mpdude
Copy link
Contributor

mpdude commented Dec 22, 2023

@Arkemlar do #11050 or #11052 fix your issue as well?

@Arkemlar
Copy link

@mpdude I tried both, but none fixed my issue :(
Doctrine generates incorrect sequence name product_unit_id_id_seq, while it must be product_unit_id_seq (doctrine:migration:diff generated name is product_unit_id_id_seq). I don't get how is it possible that migrations work properly while runtime not. I suppose it must use same mappings...

The error bumps up when doctrine's BigIntegerIdentityGenerator tries to get the id of freshly inserted row, see screenshot:

Debug panel screenshot

изображение

@mpdude
Copy link
Contributor

mpdude commented Dec 22, 2023

@Arkemlar does your code work with, say, 2.14.x, but broke at or after 2.15.1?

If not (does not work before either) you're seeing an issue different from this one here.

@Arkemlar
Copy link

@mpdude nope, it is not working in any version unless I use custom id generator that removes sequenceName argument at all. But my problem is similar to topic starter's issue because it caused by invalid sequence name.

@mpdude
Copy link
Contributor

mpdude commented Dec 22, 2023

Please create a different issue then so we can keep the discussion here focused.

mpdude added a commit that referenced this issue Jan 12, 2024
#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants