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

Fix invalid sql generated for CTI JOINs when INNER JOIN'ing with a WITH clause #6475

Closed
wants to merge 3 commits into from
Closed
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
28 changes: 23 additions & 5 deletions lib/Doctrine/ORM/Query/SqlWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,19 @@ public function walkIndexBy($indexBy)
* @return string
*/
public function walkRangeVariableDeclaration($rangeVariableDeclaration)
{
return $this->generateRangeVariableDeclarationSQL($rangeVariableDeclaration, false);
}

/**
* Generate appropriate SQL for RangeVariableDeclaration AST node
*
* @param AST\RangeVariableDeclaration $rangeVariableDeclaration
* @param bool $buildNestedJoins
*
* @return string
*/
private function generateRangeVariableDeclarationSQL($rangeVariableDeclaration, $buildNestedJoins)
{
$class = $this->em->getClassMetadata($rangeVariableDeclaration->abstractSchemaName);
$dqlAlias = $rangeVariableDeclaration->aliasIdentificationVariable;
Expand All @@ -891,7 +904,11 @@ public function walkRangeVariableDeclaration($rangeVariableDeclaration)
);

if ($class->isInheritanceTypeJoined()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this block be moved to a private method too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt that aids readability since the decision is made by the caller (actually even that one's caller) and not the extracted function itself ... and it just modifies local state (i.e. $sql).

$sql .= $this->_generateClassTableInheritanceJoins($class, $dqlAlias);
if ($buildNestedJoins) {
$sql = '(' . $sql . $this->_generateClassTableInheritanceJoins($class, $dqlAlias) . ')';
} else {
$sql .= $this->_generateClassTableInheritanceJoins($class, $dqlAlias);
}
}

return $sql;
Expand Down Expand Up @@ -1127,7 +1144,7 @@ public function walkJoin($join)
: ' INNER JOIN ';

switch (true) {
case ($joinDeclaration instanceof \Doctrine\ORM\Query\AST\RangeVariableDeclaration):
case ($joinDeclaration instanceof AST\RangeVariableDeclaration):
$class = $this->em->getClassMetadata($joinDeclaration->abstractSchemaName);
$dqlAlias = $joinDeclaration->aliasIdentificationVariable;
$tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias);
Expand All @@ -1137,11 +1154,12 @@ public function walkJoin($join)
$conditions[] = '(' . $this->walkConditionalExpression($join->conditionalExpression) . ')';
}

$condExprConjunction = ($class->isInheritanceTypeJoined() && $joinType != AST\Join::JOIN_TYPE_LEFT && $joinType != AST\Join::JOIN_TYPE_LEFTOUTER)
$isUnconditionalJoin = empty($conditions);
$condExprConjunction = ($class->isInheritanceTypeJoined() && $joinType != AST\Join::JOIN_TYPE_LEFT && $joinType != AST\Join::JOIN_TYPE_LEFTOUTER && $isUnconditionalJoin)
? ' AND '
: ' ON ';

$sql .= $this->walkRangeVariableDeclaration($joinDeclaration);
$sql .= $this->generateRangeVariableDeclarationSQL($joinDeclaration, !$isUnconditionalJoin);

// Apply remaining inheritance restrictions
$discrSql = $this->_generateDiscriminatorColumnConditionSQL([$dqlAlias]);
Expand All @@ -1163,7 +1181,7 @@ public function walkJoin($join)

break;

case ($joinDeclaration instanceof \Doctrine\ORM\Query\AST\JoinAssociationDeclaration):
case ($joinDeclaration instanceof AST\JoinAssociationDeclaration):
$sql .= $this->walkJoinAssociationDeclaration($joinDeclaration, $joinType, $join->conditionalExpression);
break;
}
Expand Down
78 changes: 78 additions & 0 deletions tests/Doctrine/Tests/ORM/Functional/Ticket/GH6464Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace Doctrine\Tests\ORM\Functional\Ticket;

use Doctrine\Tests\OrmFunctionalTestCase;

/**
* @group GH-6464
*/
class GH6464Test extends OrmFunctionalTestCase
{
/**
* {@inheritDoc}
*/
protected function setUp()
{
parent::setUp();

$this->_schemaTool->createSchema(
[
$this->_em->getClassMetadata(GH6464Post::class),
$this->_em->getClassMetadata(GH6464User::class),
$this->_em->getClassMetadata(GH6464Author::class),
]
);
}

/**
* Verifies that SqlWalker generates valid SQL for an INNER JOIN to CTI table
*
* SqlWalker needs to generate nested INNER JOIN statements, otherwise there would be INNER JOIN
* statements without an ON clause, which are valid on e.g. MySQL but rejected by PostgreSQL.
*/
public function testIssue()
{
$query = $this->_em->createQueryBuilder()
->select('p', 'a')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sort of select is invalid - removing a from the selection, as you should NEVER fetch-join results over an association that was filtered.

->from(GH6464Post::class, 'p')
->innerJoin(GH6464Author::class, 'a', 'WITH', 'p.authorId = a.id')
->getQuery();

$this->assertNotRegExp(
'/INNER JOIN \w+ \w+ INNER JOIN/',
$query->getSQL(),
'As of GH-6464, every INNER JOIN should have an ON clause, which is missing here'
);

// Query shouldn't yield a result, yet it shouldn't crash (anymore)
$this->assertEquals([], $query->getResult());
}
}

/** @Entity */
class GH6464Post
{
/** @Id @Column(type="integer") @GeneratedValue */
public $id;

/** @Column(type="integer") */
public $authorId;
}

/**
* @Entity
* @InheritanceType("JOINED")
* @DiscriminatorColumn(name="discr", type="string")
* @DiscriminatorMap({"author" = "GH6464Author"})
*/
abstract class GH6464User
{
/** @Id @Column(type="integer") @GeneratedValue */
public $id;
}

/** @Entity */
class GH6464Author extends GH6464User
{
}
6 changes: 3 additions & 3 deletions tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,12 @@ public function testSupportsJoinOnMultipleComponentsWithJoinedInheritanceType()
{
$this->assertSqlGeneration(
'SELECT e FROM Doctrine\Tests\Models\Company\CompanyEmployee e JOIN Doctrine\Tests\Models\Company\CompanyManager m WITH e.id = m.id',
'SELECT c0_.id AS id_0, c0_.name AS name_1, c1_.salary AS salary_2, c1_.department AS department_3, c1_.startDate AS startDate_4, c0_.discr AS discr_5 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id INNER JOIN company_managers c2_ INNER JOIN company_employees c4_ ON c2_.id = c4_.id INNER JOIN company_persons c3_ ON c2_.id = c3_.id AND (c0_.id = c3_.id)'
'SELECT c0_.id AS id_0, c0_.name AS name_1, c1_.salary AS salary_2, c1_.department AS department_3, c1_.startDate AS startDate_4, c0_.discr AS discr_5 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id INNER JOIN (company_managers c2_ INNER JOIN company_employees c4_ ON c2_.id = c4_.id INNER JOIN company_persons c3_ ON c2_.id = c3_.id) ON (c0_.id = c3_.id)'
);

$this->assertSqlGeneration(
'SELECT e FROM Doctrine\Tests\Models\Company\CompanyEmployee e LEFT JOIN Doctrine\Tests\Models\Company\CompanyManager m WITH e.id = m.id',
'SELECT c0_.id AS id_0, c0_.name AS name_1, c1_.salary AS salary_2, c1_.department AS department_3, c1_.startDate AS startDate_4, c0_.discr AS discr_5 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id LEFT JOIN company_managers c2_ INNER JOIN company_employees c4_ ON c2_.id = c4_.id INNER JOIN company_persons c3_ ON c2_.id = c3_.id ON (c0_.id = c3_.id)'
'SELECT c0_.id AS id_0, c0_.name AS name_1, c1_.salary AS salary_2, c1_.department AS department_3, c1_.startDate AS startDate_4, c0_.discr AS discr_5 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id LEFT JOIN (company_managers c2_ INNER JOIN company_employees c4_ ON c2_.id = c4_.id INNER JOIN company_persons c3_ ON c2_.id = c3_.id) ON (c0_.id = c3_.id)'
);
}

Expand Down Expand Up @@ -2211,7 +2211,7 @@ public function testSingleTableInheritanceLeftJoinNonAssociationWithConditionAnd
// the where clause when not joining onto that table
$this->assertSqlGeneration(
'SELECT c FROM Doctrine\Tests\Models\Company\CompanyContract c LEFT JOIN Doctrine\Tests\Models\Company\CompanyEmployee e WITH e.id = c.salesPerson WHERE c.completed = true',
"SELECT c0_.id AS id_0, c0_.completed AS completed_1, c0_.fixPrice AS fixPrice_2, c0_.hoursWorked AS hoursWorked_3, c0_.pricePerHour AS pricePerHour_4, c0_.maxPrice AS maxPrice_5, c0_.discr AS discr_6 FROM company_contracts c0_ LEFT JOIN company_employees c1_ INNER JOIN company_persons c2_ ON c1_.id = c2_.id ON (c2_.id = c0_.salesPerson_id) WHERE (c0_.completed = 1) AND c0_.discr IN ('fix', 'flexible', 'flexultra')"
"SELECT c0_.id AS id_0, c0_.completed AS completed_1, c0_.fixPrice AS fixPrice_2, c0_.hoursWorked AS hoursWorked_3, c0_.pricePerHour AS pricePerHour_4, c0_.maxPrice AS maxPrice_5, c0_.discr AS discr_6 FROM company_contracts c0_ LEFT JOIN (company_employees c1_ INNER JOIN company_persons c2_ ON c1_.id = c2_.id) ON (c2_.id = c0_.salesPerson_id) WHERE (c0_.completed = 1) AND c0_.discr IN ('fix', 'flexible', 'flexultra')"
);
}

Expand Down