From 6aebcaed9779c40f64fe52e7cc9d8893748eeb15 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 7 Jun 2025 20:34:31 +0300 Subject: [PATCH] lateral join --- application/Espo/ORM/Query/Part/Join.php | 28 ++++++++++++++++ .../Espo/ORM/Query/SelectingBuilderTrait.php | 7 ++++ .../ORM/QueryComposer/BaseQueryComposer.php | 28 +++++++++++----- .../unit/Espo/ORM/MysqlQueryComposerTest.php | 33 +++++++++++++++++++ 4 files changed, 87 insertions(+), 9 deletions(-) diff --git a/application/Espo/ORM/Query/Part/Join.php b/application/Espo/ORM/Query/Part/Join.php index 282c181659..b18f413478 100644 --- a/application/Espo/ORM/Query/Part/Join.php +++ b/application/Espo/ORM/Query/Part/Join.php @@ -47,6 +47,7 @@ class Join private ?WhereItem $conditions = null; private bool $onlyMiddle = false; + private bool $isLateral = false; private function __construct( private string|Select $target, @@ -132,6 +133,16 @@ class Join return $this->onlyMiddle; } + /** + * Is LATERAL. + * + * @since 9.1.6 + */ + public function isLateral(): bool + { + return $this->isLateral; + } + /** * Create. * @@ -216,4 +227,21 @@ class Join return $obj; } + + /** + * With LATERAL. Only for a sub-query join. + * + * @since 9.1.6 + */ + public function withLateral(bool $isLateral = true): self + { + if (!$this->isSubQuery()) { + throw new LogicException("Lateral can be used only with sub-query joins."); + } + + $obj = clone $this; + $obj->isLateral = $isLateral; + + return $obj; + } } diff --git a/application/Espo/ORM/Query/SelectingBuilderTrait.php b/application/Espo/ORM/Query/SelectingBuilderTrait.php index cd1513e26a..e078c62ef8 100644 --- a/application/Espo/ORM/Query/SelectingBuilderTrait.php +++ b/application/Espo/ORM/Query/SelectingBuilderTrait.php @@ -201,6 +201,7 @@ trait SelectingBuilderTrait ): self { $onlyMiddle = false; + $isLateral = false; /** @var string|Join|array $target */ @@ -208,6 +209,8 @@ trait SelectingBuilderTrait $alias = $alias ?? $target->getAlias(); $conditions = $conditions ?? $target->getConditions(); $onlyMiddle = $target->isOnlyMiddle(); + $isLateral = $target->isLateral(); + $target = $target->getTarget(); } @@ -256,6 +259,10 @@ trait SelectingBuilderTrait $params['onlyMiddle'] = true; } + if ($isLateral) { + $params['isLateral'] = true; + } + if ($params !== []) { $this->params[$type][] = [$target, $alias, $conditions, $params]; diff --git a/application/Espo/ORM/QueryComposer/BaseQueryComposer.php b/application/Espo/ORM/QueryComposer/BaseQueryComposer.php index 5300531736..116a49318a 100644 --- a/application/Espo/ORM/QueryComposer/BaseQueryComposer.php +++ b/application/Espo/ORM/QueryComposer/BaseQueryComposer.php @@ -3140,6 +3140,18 @@ abstract class BaseQueryComposer implements QueryComposer $value = $right; + if ($isNotValue) { + if (is_null($value)) { + return $sql; + } + + $rightPart = $this->convertComplexExpression($entity, $value, false, $params); + + $sql .= " " . $operator . " " . $rightPart; + + return $sql; + } + if (is_null($value)) { if ($operator === '=') { $sql .= " IS NULL"; @@ -3150,14 +3162,6 @@ abstract class BaseQueryComposer implements QueryComposer return $sql; } - if ($isNotValue) { - $rightPart = $this->convertComplexExpression($entity, $value, false, $params); - - $sql .= " " . $operator . " " . $rightPart; - - return $sql; - } - $sql .= " " . $operator . " " . $this->quote($value); return $sql; @@ -3206,7 +3210,13 @@ abstract class BaseQueryComposer implements QueryComposer $aliasPart = $this->quoteIdentifier($alias); - $sql = $prefixPart . "JOIN $targetPart AS $aliasPart"; + $sql = $prefixPart . "JOIN "; + + if (!empty($joinParams['isLateral'])) { + $sql .= "LATERAL "; + } + + $sql .= "$targetPart AS $aliasPart"; if ($conditions === []) { return $sql; diff --git a/tests/unit/Espo/ORM/MysqlQueryComposerTest.php b/tests/unit/Espo/ORM/MysqlQueryComposerTest.php index 055f1e9939..3a6120115f 100644 --- a/tests/unit/Espo/ORM/MysqlQueryComposerTest.php +++ b/tests/unit/Espo/ORM/MysqlQueryComposerTest.php @@ -1057,6 +1057,39 @@ class MysqlQueryComposerTest extends \PHPUnit\Framework\TestCase ); } + public function testJoinSubQueryLateral(): void + { + $sql = + "SELECT post.id AS `id` FROM `post` " . + "JOIN LATERAL (SELECT post.id AS `id` FROM `post` LIMIT 0, 1) AS `a` ON TRUE"; + + $select = SelectBuilder::create() + ->select('id') + ->from('Post') + ->join( + Join::createWithSubQuery( + SelectBuilder::create() + ->select('id') + ->from('Post') + ->limit(0, 1) + ->withDeleted() + ->build(), + 'a', + ) + ->withLateral() + ->withConditions( + Expression::value(true) + ) + ) + ->withDeleted() + ->build(); + + $this->assertEquals( + $sql, + $this->query->composeSelect($select) + ); + } + public function testJoinSubQueryException1(): void { $this->expectException(LogicException::class);