Skip to content

Commit

Permalink
Merge pull request #10956 from creative-commoners/pulls/5/sql-union
Browse files Browse the repository at this point in the history
NEW Add sql UNION abstraction
  • Loading branch information
GuySartorelli authored Sep 24, 2023
2 parents fac3356 + 76da701 commit 157317d
Show file tree
Hide file tree
Showing 4 changed files with 140 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;
$nl = $this->getSeparator();
$sql = '';
if ($needsParenthisis) {
$sql .= "({$nl}";
}
$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 .= "{$nl})";
}
$sql .= $this->buildUnionFragment($query, $parameters);
return $sql;
}

Expand Down Expand Up @@ -285,6 +295,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 @@ -667,6 +667,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(DataQuery|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 @@ -532,6 +541,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
52 changes: 52 additions & 0 deletions tests/php/ORM/SQLSelectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
namespace SilverStripe\ORM\Tests;

use InvalidArgumentException;
use LogicException;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\Connect\MySQLDatabase;
use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\SQLite\SQLite3Database;
use SilverStripe\PostgreSQL\PostgreSQLDatabase;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\ArrayList;

class SQLSelectTest extends SapphireTest
{
Expand Down Expand Up @@ -858,6 +860,56 @@ public function testParameterisedJoinSQL($joinMethod, $joinType)
$query->execute();
}

public function provideUnion()
{
return [
// Note that a default (null) UNION is identical to a DISTINCT UNION
[
'unionQuery' => new SQLSelect([1, 2]),
'type' => null,
'expected' => [
[1 => 1, 2 => 2],
],
],
[
'unionQuery' => new SQLSelect([1, 2]),
'type' => SQLSelect::UNION_DISTINCT,
'expected' => [
[1 => 1, 2 => 2],
],
],
[
'unionQuery' => new SQLSelect([1, 2]),
'type' => SQLSelect::UNION_ALL,
'expected' => [
[1 => 1, 2 => 2],
[1 => 1, 2 => 2],
],
],
[
'unionQuery' => new SQLSelect([1, 2]),
'type' => 'tulips',
'expected' => LogicException::class,
],
];
}

/**
* @dataProvider provideUnion
*/
public function testUnion(SQLSelect $unionQuery, ?string $type, string|array $expected)
{
if (is_string($expected)) {
$this->expectException($expected);
$this->expectExceptionMessage('Union $type must be one of the constants UNION_ALL or UNION_DISTINCT.');
}

$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 157317d

Please sign in to comment.