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

3 Primary Keys with isCrossRef and ObjectCombinationCollection, ->diff() not working causing rows to be deleted and recreated #1968

Open
SpikedCola opened this issue Jun 7, 2023 · 3 comments

Comments

@SpikedCola
Copy link

SpikedCola commented Jun 7, 2023

Apologies in advance if this is hard to follow. This is with the latest version, dev-master 011aaa7.

My schema is using 3 primary keys on an isCrossRef table. In this case, 1 or more performers appearing at an event, with a flag to say who is a headliner. Schema is like this:

<table name="event_performers" idMethod="native" phpName="EventPerformer" isCrossRef="true">
    <column name="event_id" primaryKey="true" phpName="EventId" type="INTEGER" size="10" sqlType="int(10) unsigned" required="true"/>
    <column name="performer_id" primaryKey="true" phpName="PerformerId" type="INTEGER" size="10" sqlType="int(10) unsigned" required="true"/>
    <column name="is_headliner" primaryKey="true" phpName="IsHeadliner" type="BOOLEAN" size="1" sqlType="tinyint(1) unsigned" required="true"/>
    <column name="found_datetime" phpName="FoundDatetime" type="TIMESTAMP" required="true"/>

I am using an ObjectCombinationCollection to collect the perfomer & headliner flag, and calling the generated setPerformerHeadliner() function like this:

$performers = new \Propel\Runtime\Collection\ObjectCombinationCollection();
foreach ($headliners as $performerJson) {
	$performer = Performer::CreateFromJSON($performerJson);
	$performers->push($performer, true); // is headliner
}
foreach ($supportingActs as $performerJson) {
	$performer = Performer::CreateFromJSON($performerJson);
	$performers->push($performer, false); // is NOT headliner
}
$event->setPerformerIsHeadliners($performers);

The first pass works correctly, rows are inserted as expected.

On subsequent passes, however, the event_performers cross-reference rows are deleted and re-created on every pass.

Digging in to the generated setPerformerIsHeadliners() function, the existing records are fetched from the db, diff() is called, and then any records scheduled for deletion are removed:

public function setPerformerIsHeadliners(Collection $PerformerIsHeadliners, ?ConnectionInterface $con = null)
{
	$this->clearPerformerIsHeadliners();
	$currentPerformerIsHeadliners = $this->getPerformerIsHeadliners();

	$combinationCollPerformerIsHeadlinersScheduledForDeletion = $currentPerformerIsHeadliners->diff($PerformerIsHeadliners);

	foreach ($combinationCollPerformerIsHeadlinersScheduledForDeletion as $toDelete) {
		$this->removePerformerIsHeadliner(...$toDelete);
	}

	foreach ($PerformerIsHeadliners as $PerformerIsHeadliner) {
		if (!$currentPerformerIsHeadliners->contains(...$PerformerIsHeadliner)) {
			$this->doAddPerformerIsHeadliner(...$PerformerIsHeadliner);
		}
	}

If I print $combinationCollPerformerIsHeadlinersScheduledForDeletion, on every subsequent pass it lists all of my cross-references. So it appears diff() is not working as expected and all of my relationships are deleted.

Notice that if (!$currentPerformerIsHeadliners->contains(...$PerformerIsHeadliner)) { is using the spread operator.

$currentPerformerIsHeadliners returns an object of type ObjectCombinationCollection, but there is no overload for diff(). The version of diff() that is called is in the Collection class, which doesn't use the spread operator on the contains() call:

class Collection implements ArrayAccess, IteratorAggregate, Countable, Serializable
{
    public function diff(Collection $collection): self
    {
        $diff = clone $this;
        $diff->clear();

        foreach ($this as $object) {
            if (!$collection->contains($object)) {
                $diff[] = $object;
            }
        }

        return $diff;
    }

This results in the search() function of ObjectCombinationCollection being passed args that look like:

[ 0 => [ Performer_object, IsHeadliner_bool ] ]

You can see the foreach (func_get_args() as $pos => $obj) { loop is expecting to loop over the 2 args, checking if they are ActiveRecordInterface. But the double-array breaks that:

public function search($element)
{
	$hashes = [];
	$isActiveRecord = [];
	foreach (func_get_args() as $pos => $obj) {
		if ($obj instanceof ActiveRecordInterface) {
			$hashes[$pos] = $obj->hashCode();
			$isActiveRecord[$pos] = true;
		} else {
			$hashes[$pos] = $obj;
			$isActiveRecord[$pos] = false;
		}
	}

If I edit ObjectCombinationCollection and overload the diff() function, such that it uses the spread operator like in setPerformerIsHeadliners():

class ObjectCombinationCollection extends ObjectCollection
{
    public function diff(Collection $collection): self
    {
        $diff = clone $this;
        $diff->clear();

        foreach ($this as $object) {
            if (!$collection->contains(...$object)) {
                $diff[] = $object;
            }
        }

        return $diff;
    }

Then search() is passed args like [ Performer_object, IsHeadliner_bool ] which works correctly, and my generated setPerformerIsHeadliners() functions work properly. Rows aren't deleted erroneously on every pass.

I believe this is a bug.

@mringler
Copy link
Contributor

mringler commented Jun 8, 2023

That's a great report! I agree, it looks like a bug. Do you think you can write a test for your fix and create a pull request for that?

@SpikedCola
Copy link
Author

Hi @mringler,

I have almost no experience writing tests, but I suppose this is a good opportunity to learn! Let me see what I can come up with.

@mringler
Copy link
Contributor

mringler commented Jun 9, 2023

That's the spirit! If you haven't found it already, there is documentation how to run the tests locally.

To get you started, it looks like there are a bunch of tests for ObjectCombinationCollection, but they are not at the expected place, but in the files:

tests/Propel/Tests/Generator/Builder/Om/GeneratedObjectM2MRelationThreePKsTest.php
tests/Propel/Tests/Generator/Builder/Om/GeneratedObjectM2MRelationThreePKs2Test.php

and they look pretty messy, too.

You can still put the tests into those files, but the right place would be in

tests/Propel/Tests/Runtime/Collection/ObjectCombinationCollection.php

There are tests for the parent's diff() at the end of

tests/Propel/Tests/Runtime/Collection/CollectionTest.php

it might help to have a look at those.

So you could create the ObjectCombinationCollection.php file using CollectionTest.php as a template (or find another way of course). Let me know if you run into issues. Godspeed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants