Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/Collection/Functions/FetchPropertyFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,24 @@ private function processTokens(
foreach ($tokens as $tokenIndex => $token) {
$property = $currentEntityMetadata->getProperty($token);
if ($property->relationship !== null) {
$relType = $property->relationship->type;

// The foreign key lives on the current table (m:1 or the owning side of 1:1), so the
// related entity's primary key can be read straight from that column.
$isForeignKeyOnCurrentTable = $relType === Relationship::MANY_HAS_ONE
|| ($relType === Relationship::ONE_HAS_ONE && $property->relationship->isMain);

// Optimization: the expression ends with `<relation>-><primaryKey>` and the foreign key
// is local, so we can read the key from the current table and skip the JOIN to the target.
// e.g. `book->author->id` reads `books.author_id` without JOINing the `authors` table.
$isRelationToOwnPrimaryKey = $isForeignKeyOnCurrentTable
&& $tokenIndex === count($tokens) - 1
&& in_array($lastToken, $property->relationship->entityMetadata->getPrimaryKey(), strict: true);
if ($isRelationToOwnPrimaryKey) {
$lastToken = $token;
break;
}

[
$currentAlias,
$currentConventions,
Expand Down Expand Up @@ -241,6 +259,43 @@ private function processTokens(
throw new InvalidArgumentException("Property expression '$propertyExpression' does not fetch specific property.");
}

if ($propertyMetadata->relationship !== null) {
$relType = $propertyMetadata->relationship->type;

// The final property is itself a relationship whose foreign key is *not* on the current
// table (non-owning side of 1:1, 1:m, m:m), so there is no local column to read: JOIN to the
// target table and compare against its primary key instead.
// e.g. `ean->book`, where Ean is the non-owning side, JOINs `books` and compares `books.id`.
// (A terminal main-side relationship such as `book->author` is not handled here; toColumnExpr()
// maps it directly onto the local `author_id` column.)
$isForeignKeyOnTargetTable =
($relType === Relationship::ONE_HAS_ONE && !$propertyMetadata->relationship->isMain)
|| $relType === Relationship::ONE_HAS_MANY
|| $relType === Relationship::MANY_HAS_MANY;
if ($isForeignKeyOnTargetTable) {
$allTokens = [...$tokens, $lastToken];
[
$currentAlias,
$currentConventions,
$currentEntityMetadata,
] = $this->processRelationship(
$allTokens,
$joins,
$propertyMetadata,
$aggregator,
$currentConventions,
$currentMapper,
$currentAlias,
$lastToken,
count($allTokens) - 1,
$makeDistinct,
);
$primaryKey = $currentEntityMetadata->getPrimaryKey();
$lastToken = $primaryKey[0];
$propertyMetadata = $currentEntityMetadata->getProperty($lastToken);
}
}

$column = $this->toColumnExpr(
$currentEntityMetadata,
$propertyMetadata,
Expand Down
34 changes: 34 additions & 0 deletions tests/cases/integration/Collection/collection.where.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,40 @@ class CollectionWhereTest extends DataTestCase
}


public function testFilterByNonMainSideRelationship(): void
{
// Non-main side of 1:1 relationship (Ean.book is non-main, FK ean_id is in books table)
$ean = new Ean();
$ean->code = '1234';
$ean->book = $this->orm->books->getByIdChecked(1);
$this->orm->eans->persistAndFlush($ean);
$this->orm->clear();

$fetched = $this->orm->eans->findBy(['book' => 1])->fetch();
Assert::notNull($fetched);
Assert::equal('1234', $fetched->code);

Assert::null($this->orm->eans->findBy(['book' => 2])->fetch());
}


public function testFilterByNonMainSideRelationshipNull(): void
{
// Non-main side of 1:1 self-referential relationship (Book.previousPart is non-main side)
// Book 4 has next_part = 3, so Book 3 has a previousPart (= Book 4)
// Books 1, 2, 4 have no previousPart

$booksWithNoPrevious = $this->orm->books->findBy(['previousPart' => null]);
Assert::count(3, $booksWithNoPrevious);

$booksWithPrevious = $this->orm->books->findBy(['previousPart!=' => null]);
Assert::count(1, $booksWithPrevious);
$book = $booksWithPrevious->fetch();
Assert::notNull($book);
Assert::equal(3, $book->id);
}


private function moveToDifferentZone(DateTimeImmutable $dateTime): DateTimeImmutable
{
return $dateTime->setTimezone(new DateTimeZone("UTC"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@ FROM
LEFT JOIN "books" AS "books_any" ON (
"authors"."id" = "books_any"."author_id"
)
LEFT JOIN "public"."authors" AS "books_translator_any" ON (
"books_any"."translator_id" = "books_translator_any"."id"
)
WHERE
("books_any"."title" = 'Book 1')
AND (
"books_translator_any"."id" IS NULL
"books_any"."translator_id" IS NULL
)
GROUP BY
"authors"."id";
Expand All @@ -27,13 +24,10 @@ FROM
LEFT JOIN "books" AS "books_any" ON (
"authors"."id" = "books_any"."author_id"
)
LEFT JOIN "public"."authors" AS "books_translator_any" ON (
"books_any"."translator_id" = "books_translator_any"."id"
)
WHERE
("books_any"."title" = 'Book 1')
AND (
"books_translator_any"."id" IS NULL
"books_any"."translator_id" IS NULL
)
GROUP BY
"authors"."id"
Expand All @@ -45,7 +39,10 @@ FROM
"public"."authors" AS "authors"
LEFT JOIN "books" AS "books_count" ON (
(
"authors"."id" = "books_count"."author_id"
(
"authors"."id" = "books_count"."author_id"
)
AND "books_count"."translator_id" IS NOT NULL
)
OR (
(
Expand All @@ -54,18 +51,12 @@ FROM
AND "books_count"."price" < 100
)
)
LEFT JOIN "public"."authors" AS "books_translator_count" ON (
(
"books_count"."translator_id" = "books_translator_count"."id"
)
AND "books_translator_count"."id" IS NOT NULL
)
GROUP BY
"authors"."id"
HAVING
(
COUNT("books_translator_count"."id") >= 1
AND COUNT("books_translator_count"."id") <= 1
COUNT("books_count"."id") >= 1
AND COUNT("books_count"."id") <= 1
)
OR (
COUNT("books_count"."id") >= 1
Expand All @@ -82,7 +73,10 @@ FROM
"public"."authors" AS "authors"
LEFT JOIN "books" AS "books_count" ON (
(
"authors"."id" = "books_count"."author_id"
(
"authors"."id" = "books_count"."author_id"
)
AND "books_count"."translator_id" IS NOT NULL
)
OR (
(
Expand All @@ -91,18 +85,12 @@ FROM
AND "books_count"."price" < 100
)
)
LEFT JOIN "public"."authors" AS "books_translator_count" ON (
(
"books_count"."translator_id" = "books_translator_count"."id"
)
AND "books_translator_count"."id" IS NOT NULL
)
GROUP BY
"authors"."id"
HAVING
(
COUNT("books_translator_count"."id") >= 1
AND COUNT("books_translator_count"."id") <= 1
COUNT("books_count"."id") >= 1
AND COUNT("books_count"."id") <= 1
)
OR (
COUNT("books_count"."id") >= 1
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,2 @@
SELECT
COUNT(*) AS count
FROM
(
SELECT
"books"."id"
FROM
"books" AS "books"
LEFT JOIN "public"."authors" AS "author" ON (
"books"."author_id" = "author"."id"
)
WHERE
"author"."id" > 0
) temp;

SELECT
"books".*
FROM
"books" AS "books"
LEFT JOIN "public"."authors" AS "author" ON (
"books"."author_id" = "author"."id"
)
WHERE
"author"."id" > 0
ORDER BY
"author"."id" ASC;
SELECT COUNT(*) AS count FROM (SELECT "books"."id" FROM "books" AS "books" WHERE "books"."author_id" > 0) temp;
SELECT "books".* FROM "books" AS "books" WHERE "books"."author_id" > 0 ORDER BY "books"."author_id" ASC;
Original file line number Diff line number Diff line change
@@ -1,21 +1,2 @@
SELECT
"books".*
FROM
"books" AS "books"
LEFT JOIN "public"."authors" AS "author" ON (
"books"."author_id" = "author"."id"
)
ORDER BY
"author"."id" DESC,
"books"."title" ASC;

SELECT
"books".*
FROM
"books" AS "books"
LEFT JOIN "public"."authors" AS "author" ON (
"books"."author_id" = "author"."id"
)
ORDER BY
"author"."id" DESC,
"books"."title" DESC;
SELECT "books".* FROM "books" AS "books" ORDER BY "books"."author_id" DESC, "books"."title" ASC;
SELECT "books".* FROM "books" AS "books" ORDER BY "books"."author_id" DESC, "books"."title" DESC;
Original file line number Diff line number Diff line change
@@ -1,21 +1,2 @@
SELECT
"books".*
FROM
"books" AS "books"
LEFT JOIN "public"."authors" AS "author" ON (
"books"."author_id" = "author"."id"
)
ORDER BY
"author"."id" DESC,
"books"."title" ASC;

SELECT
"books".*
FROM
"books" AS "books"
LEFT JOIN "public"."authors" AS "author" ON (
"books"."author_id" = "author"."id"
)
ORDER BY
"author"."id" DESC,
"books"."title" DESC;
SELECT "books".* FROM "books" AS "books" ORDER BY "books"."author_id" DESC, "books"."title" ASC;
SELECT "books".* FROM "books" AS "books" ORDER BY "books"."author_id" DESC, "books"."title" DESC;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
SELECT "books".* FROM "books" AS "books" WHERE "books"."id" = 1;
START TRANSACTION;
INSERT INTO "eans" ("code", "type") VALUES ('1234', 2);
SELECT CURRVAL('public.eans_id_seq');
UPDATE "books" SET "ean_id" = 1 WHERE "id" = 1;
COMMIT;
SELECT
"eans".*
FROM
"eans" AS "eans"
LEFT JOIN "books" AS "book" ON ("eans"."id" = "book"."ean_id")
WHERE
"book"."id" = 1;

SELECT
"eans".*
FROM
"eans" AS "eans"
LEFT JOIN "books" AS "book" ON ("eans"."id" = "book"."ean_id")
WHERE
"book"."id" = 2;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
SELECT
"books".*
FROM
"books" AS "books"
LEFT JOIN "books" AS "previousPart" ON (
"books"."id" = "previousPart"."next_part"
)
WHERE
"previousPart"."id" IS NULL;

SELECT
"books".*
FROM
"books" AS "books"
LEFT JOIN "books" AS "previousPart" ON (
"books"."id" = "previousPart"."next_part"
)
WHERE
"previousPart"."id" IS NOT NULL;
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@ SELECT
"books_x_tags"."tag_id"
FROM
"books" AS "books"
LEFT JOIN "public"."authors" AS "author" ON (
"books"."author_id" = "author"."id"
)
LEFT JOIN "books_x_tags" AS "books_x_tags" ON (
"books_x_tags"."book_id" = "books"."id"
)
WHERE
("author"."id" = 1)
("books"."author_id" = 1)
AND (
"books_x_tags"."tag_id" IN (1)
);
Expand All @@ -24,14 +21,11 @@ SELECT
) AS "count"
FROM
"books" AS "books"
LEFT JOIN "public"."authors" AS "author" ON (
"books"."author_id" = "author"."id"
)
LEFT JOIN "books_x_tags" AS "books_x_tags" ON (
"books_x_tags"."book_id" = "books"."id"
)
WHERE
("author"."id" = 1)
("books"."author_id" = 1)
AND (
"books_x_tags"."tag_id" IN (1)
)
Expand All @@ -53,15 +47,12 @@ FROM
LEFT JOIN "tag_followers" AS "author_tagFollowers_any" ON (
"author"."id" = "author_tagFollowers_any"."author_id"
)
LEFT JOIN "public"."authors" AS "author_tagFollowers_author_any" ON (
"author_tagFollowers_any"."author_id" = "author_tagFollowers_author_any"."id"
)
LEFT JOIN "books_x_tags" AS "books_x_tags" ON (
"books_x_tags"."book_id" = "books"."id"
)
WHERE
(
"author_tagFollowers_author_any"."id" = 1
"author_tagFollowers_any"."author_id" = 1
)
AND (
"books_x_tags"."tag_id" IN (1)
Expand Down Expand Up @@ -89,15 +80,12 @@ FROM
LEFT JOIN "tag_followers" AS "author_tagFollowers_any" ON (
"author"."id" = "author_tagFollowers_any"."author_id"
)
LEFT JOIN "public"."authors" AS "author_tagFollowers_author_any" ON (
"author_tagFollowers_any"."author_id" = "author_tagFollowers_author_any"."id"
)
LEFT JOIN "books_x_tags" AS "books_x_tags" ON (
"books_x_tags"."book_id" = "books"."id"
)
WHERE
(
"author_tagFollowers_author_any"."id" = 1
"author_tagFollowers_any"."author_id" = 1
)
AND (
"books_x_tags"."tag_id" IN (1)
Expand Down