Skip to content

Commit

Permalink
NEW Add sql UNION abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli committed Sep 24, 2023
1 parent b8665a7 commit 4cd3560
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 1 deletion.
43 changes: 42 additions & 1 deletion src/ORM/Connect/DBQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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.
*
Expand Down
14 changes: 14 additions & 0 deletions src/ORM/DataQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
32 changes: 32 additions & 0 deletions src/ORM/Queries/SQLSelect.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\DB;
use InvalidArgumentException;
use LogicException;

/**
* Object representing a SQL SELECT query.
* The various parts of the SQL query can be manipulated individually.
*/
class SQLSelect extends SQLConditionalExpression
{
public const UNION_ALL = 'ALL';

public const UNION_DISTINCT = 'DISTINCT';

/**
* An array of SELECT fields, keyed by an optional alias.
Expand All @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
41 changes: 41 additions & 0 deletions tests/php/ORM/SQLSelectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use SilverStripe\SQLite\SQLite3Database;
use SilverStripe\PostgreSQL\PostgreSQLDatabase;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\ArrayList;

class SQLSelectTest extends SapphireTest
{
Expand Down Expand Up @@ -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"']);
Expand Down

0 comments on commit 4cd3560

Please sign in to comment.