Skip to content

Commit

Permalink
Merge pull request codeigniter4#5510 from iRedds/qb-from-subquery
Browse files Browse the repository at this point in the history
Feature: Subqueries in the FROM section
  • Loading branch information
kenjis committed Jan 13, 2022
2 parents 1ca7d77 + 013e374 commit e0ce16c
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 19 deletions.
56 changes: 38 additions & 18 deletions system/Database/BaseBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -519,35 +519,47 @@ public function distinct(bool $val = true)
*
* @return $this
*/
public function from($from, bool $overwrite = false)
public function from($from, bool $overwrite = false): self
{
if ($overwrite === true) {
$this->QBFrom = [];
$this->db->setAliasedTables([]);
}

foreach ((array) $from as $val) {
if (strpos($val, ',') !== false) {
foreach (explode(',', $val) as $v) {
$v = trim($v);
$this->trackAliases($v);

$this->QBFrom[] = $this->db->protectIdentifiers($v, true, null, false);
}
foreach ((array) $from as $table) {
if (strpos($table, ',') !== false) {
$this->from(explode(',', $table));
} else {
$val = trim($val);
$table = trim($table);

// Extract any aliases that might exist. We use this information
// in the protectIdentifiers to know whether to add a table prefix
$this->trackAliases($val);
if ($table === '') {
continue;
}

$this->QBFrom[] = $this->db->protectIdentifiers($val, true, null, false);
$this->trackAliases($table);
$this->QBFrom[] = $this->db->protectIdentifiers($table, true, null, false);
}
}

return $this;
}

/**
* @param BaseBuilder $from Expected subquery
* @param string $alias Subquery alias
*
* @return $this
*/
public function fromSubquery(BaseBuilder $from, string $alias): self
{
$table = $this->buildSubquery($from, true, $alias);

$this->trackAliases($table);
$this->QBFrom[] = $table;

return $this;
}

/**
* Generates the JOIN portion of the query
*
Expand Down Expand Up @@ -2743,16 +2755,24 @@ protected function isSubquery($value): bool
/**
* @param BaseBuilder|Closure $builder
* @param bool $wrapped Wrap the subquery in brackets
* @param string $alias Subquery alias
*/
protected function buildSubquery($builder, bool $wrapped = false): string
protected function buildSubquery($builder, bool $wrapped = false, string $alias = ''): string
{
if ($builder instanceof Closure) {
$instance = (clone $this)->from([], true)->resetQuery();
$builder = $builder($instance);
$builder($builder = $this->db->newQuery());
}

$subquery = strtr($builder->getCompiledSelect(), "\n", ' ');

return $wrapped ? '(' . $subquery . ')' : $subquery;
if ($wrapped) {
$subquery = '(' . $subquery . ')';

if ($alias !== '') {
$subquery .= " AS {$alias}";
}
}

return $subquery;
}
}
8 changes: 8 additions & 0 deletions system/Database/BaseConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,14 @@ public function table($tableName)
return new $className($tableName, $this);
}

/**
* Returns a new instance of the BaseBuilder class with a cleared FROM clause.
*/
public function newQuery(): BaseBuilder
{
return $this->table(',')->from([], true);
}

/**
* Creates a prepared statement with the database that can then
* be used to execute multiple statements against. Within the
Expand Down
2 changes: 1 addition & 1 deletion system/Database/SQLSRV/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ protected function _fromTables(): string
$from = [];

foreach ($this->QBFrom as $value) {
$from[] = $this->getFullName($value);
$from[] = strpos($value, '(SELECT') === 0 ? $value : $this->getFullName($value);
}

return implode(', ', $from);
Expand Down
36 changes: 36 additions & 0 deletions tests/system/Database/Builder/FromTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,27 @@ public function testFromReset()
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
}

public function testFromSubquery()
{
$expectedSQL = 'SELECT * FROM (SELECT * FROM "users") AS alias';
$subquery = new BaseBuilder('users', $this->db);
$builder = $this->db->newQuery()->fromSubquery($subquery, 'alias');

$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));

$expectedSQL = 'SELECT * FROM (SELECT "id", "name" FROM "users") AS users_1';
$subquery = (new BaseBuilder('users', $this->db))->select('id, name');
$builder = $this->db->newQuery()->fromSubquery($subquery, 'users_1');

$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));

$expectedSQL = 'SELECT * FROM (SELECT * FROM "users") AS alias, "some_table"';
$subquery = new BaseBuilder('users', $this->db);
$builder = $this->db->newQuery()->fromSubquery($subquery, 'alias')->from('some_table');

$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
}

public function testFromWithMultipleTablesAsStringWithSQLSRV()
{
$this->db = new MockConnection(['DBDriver' => 'SQLSRV', 'database' => 'test', 'schema' => 'dbo']);
Expand All @@ -113,4 +134,19 @@ public function testFromWithMultipleTablesAsStringWithSQLSRV()

$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
}

public function testFromSubqueryWithSQLSRV()
{
$this->db = new MockConnection(['DBDriver' => 'SQLSRV', 'database' => 'test', 'schema' => 'dbo']);

$subquery = new SQLSRVBuilder('users', $this->db);

$builder = new SQLSRVBuilder('jobs', $this->db);

$builder->fromSubquery($subquery, 'users_1');

$expectedSQL = 'SELECT * FROM "test"."dbo"."jobs", (SELECT * FROM "test"."dbo"."users") AS users_1';

$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
}
}
31 changes: 31 additions & 0 deletions user_guide_src/source/database/query_builder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,28 @@ Permits you to write the FROM portion of your query::
in the ``$db->table()`` function. Additional calls to ``from()`` will add more tables
to the FROM portion of your query.

**$builder->fromSubquery()**

Permits you to write part of a FROM query as a subquery.

This is where we add a subquery to an existing table.::

$subquery = $db->table('users');
$builder = $db->table('jobs')->fromSubquery($subquery, 'alias');
$query = $builder->get();

// Produces: SELECT * FROM `jobs`, (SELECT * FROM `users`) AS alias

Use the ``$db->newQuery()`` method to make a subquery the main table.::

$subquery = $db->table('users')->select('id, name');
$builder = $db->newQuery()->fromSubquery($subquery, 't');
$query = $builder->get();

// Produces: SELECT * FROM (SELECT `id`, `name` FROM users) AS t

.. note:: Only one subquery can be passed to a method.

**$builder->join()**

Permits you to write the JOIN portion of your query::
Expand Down Expand Up @@ -1401,6 +1423,15 @@ Class Reference

Specifies the ``FROM`` clause of a query.

.. php:method:: fromSubquery($from, $alias)
:param BaseBuilder $from: Instance of the BaseBuilder class
:param string $alias: Subquery alias
:returns: ``BaseBuilder`` instance (method chaining)
:rtype: ``BaseBuilder``

Specifies the ``FROM`` clause of a query using a subquery.

.. php:method:: join($table, $cond[, $type = ''[, $escape = null]])
:param string $table: Table name to join
Expand Down

0 comments on commit e0ce16c

Please sign in to comment.