From 4cd356012074bb6668720e06c28fa82d4d1c8863 Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Fri, 4 Aug 2023 15:38:28 +1200 Subject: [PATCH] NEW Add sql UNION abstraction --- src/ORM/Connect/DBQueryBuilder.php | 43 +++++++++++++++++++++++++++++- src/ORM/DataQuery.php | 14 ++++++++++ src/ORM/Queries/SQLSelect.php | 32 ++++++++++++++++++++++ tests/php/ORM/SQLSelectTest.php | 41 ++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 1 deletion(-) diff --git a/src/ORM/Connect/DBQueryBuilder.php b/src/ORM/Connect/DBQueryBuilder.php index c0f1e63f033..19bfe2b9ff9 100644 --- a/src/ORM/Connect/DBQueryBuilder.php +++ b/src/ORM/Connect/DBQueryBuilder.php @@ -68,13 +68,23 @@ public function buildSQL(SQLExpression $query, &$parameters) */ protected function buildSelectQuery(SQLSelect $query, array &$parameters) { - $sql = $this->buildSelectFragment($query, $parameters); + $needsParenthisis = count($query->getUnions()) > 0; + $newLine = $this->getSeparator(); + $sql = ''; + if ($needsParenthisis) { + $sql .= "({$newLine}"; + } + $sql .= $this->buildSelectFragment($query, $parameters); $sql .= $this->buildFromFragment($query, $parameters); $sql .= $this->buildWhereFragment($query, $parameters); $sql .= $this->buildGroupByFragment($query, $parameters); $sql .= $this->buildHavingFragment($query, $parameters); $sql .= $this->buildOrderByFragment($query, $parameters); $sql .= $this->buildLimitFragment($query, $parameters); + if ($needsParenthisis) { + $sql .= "{$newLine})"; + } + $sql .= $this->buildUnionFragment($query, $parameters); return $sql; } @@ -269,6 +279,37 @@ public function buildWhereFragment(SQLConditionalExpression $query, array &$para return "{$nl}WHERE (" . implode("){$nl}{$connective} (", $where) . ")"; } + /** + * Return the UNION clause(s) ready for inserting into a query. + */ + protected function buildUnionFragment(SQLSelect $query, array &$parameters): string + { + $unions = $query->getUnions(); + if (empty($unions)) { + return ''; + } + + $nl = $this->getSeparator(); + $clauses = []; + + foreach ($unions as $union) { + $unionQuery = $union['query']; + $unionType = $union['type']; + + $clause = "{$nl}UNION"; + + if ($unionType) { + $clause .= " $unionType"; + } + + $clause .= "$nl($nl" . $this->buildSelectQuery($unionQuery, $parameters) . "$nl)"; + + $clauses[] = $clause; + } + + return implode('', $clauses); + } + /** * Returns the ORDER BY clauses ready for inserting into a query. * diff --git a/src/ORM/DataQuery.php b/src/ORM/DataQuery.php index b37f1669938..328363de0ec 100644 --- a/src/ORM/DataQuery.php +++ b/src/ORM/DataQuery.php @@ -655,6 +655,20 @@ public function having($having) return $this; } + /** + * Add a query to UNION with. + * + * @param string|null $type One of the SQLSelect::UNION_ALL or SQLSelect::UNION_DISTINCT constants - or null for a default union + */ + public function union(self|SQLSelect $query, ?string $type = null): static + { + if ($query instanceof self) { + $query = $query->query(); + } + $this->query->addUnion($query, $type); + return $this; + } + /** * Create a disjunctive subgroup. * diff --git a/src/ORM/Queries/SQLSelect.php b/src/ORM/Queries/SQLSelect.php index fa38005aaf3..497767e0fc5 100644 --- a/src/ORM/Queries/SQLSelect.php +++ b/src/ORM/Queries/SQLSelect.php @@ -5,6 +5,7 @@ use SilverStripe\Core\Injector\Injector; use SilverStripe\ORM\DB; use InvalidArgumentException; +use LogicException; /** * Object representing a SQL SELECT query. @@ -12,6 +13,9 @@ */ class SQLSelect extends SQLConditionalExpression { + public const UNION_ALL = 'ALL'; + + public const UNION_DISTINCT = 'DISTINCT'; /** * An array of SELECT fields, keyed by an optional alias. @@ -36,6 +40,11 @@ class SQLSelect extends SQLConditionalExpression */ protected $having = []; + /** + * An array of subqueries to union with this one. + */ + protected array $union = []; + /** * If this is true DISTINCT will be added to the SQL. * @@ -529,6 +538,29 @@ public function getHavingParameterised(&$parameters) return $conditions; } + /** + * Add a select query to UNION with. + * + * @param string|null $type One of the UNION_ALL or UNION_DISTINCT constants - or null for a default union + */ + public function addUnion(self $query, ?string $type = null): static + { + if ($type && $type !== self::UNION_ALL && $type !== self::UNION_DISTINCT) { + throw new LogicException('Union $type must be one of the constants UNION_ALL or UNION_DISTINCT.'); + } + + $this->union[] = ['query' => $query, 'type' => $type]; + return $this; + } + + /** + * Get all of the queries that will be UNIONed with this one. + */ + public function getUnions(): array + { + return $this->union; + } + /** * Return a list of GROUP BY clauses used internally. * diff --git a/tests/php/ORM/SQLSelectTest.php b/tests/php/ORM/SQLSelectTest.php index be70ccb3a93..fceefb461c9 100755 --- a/tests/php/ORM/SQLSelectTest.php +++ b/tests/php/ORM/SQLSelectTest.php @@ -9,6 +9,7 @@ use SilverStripe\SQLite\SQLite3Database; use SilverStripe\PostgreSQL\PostgreSQLDatabase; use SilverStripe\Dev\SapphireTest; +use SilverStripe\ORM\ArrayList; class SQLSelectTest extends SapphireTest { @@ -791,6 +792,46 @@ public function testParameterisedLeftJoins() $query->execute(); } + public function provideUnion() + { + return [ + [ + 'unionQuery' => new SQLSelect([1, 2]), + 'type' => null, + 'expected' => [ + [1 => 1, 2 => 2], + ], + ], + [ + 'unionQuery' => new SQLSelect([3, 4]), + 'type' => SQLSelect::UNION_DISTINCT, + 'expected' => [ + [1 => 1, 2 => 2], + [1 => 3, 2 => 4], + ], + ], + [ + 'unionQuery' => new SQLSelect([1, 2]), + 'type' => SQLSelect::UNION_ALL, + 'expected' => [ + [1 => 1, 2 => 2], + [1 => 1, 2 => 2], + ], + ], + ]; + } + + /** + * @dataProvider provideUnion + */ + public function testUnion(SQLSelect $unionQuery, ?string $type, array $expected) + { + $query = new SQLSelect([1, 2]); + $query->addUnion($unionQuery, $type); + + $this->assertSame($expected, iterator_to_array($query->execute(), true)); + } + public function testBaseTableAliases() { $query = SQLSelect::create('*', ['"MyTableAlias"' => '"MyTable"']);