diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 2a431accd2f5..8bbaf764dcf1 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -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 * @@ -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; } } diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 53e5dad1b351..c3761322aa84 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -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 diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index 9b5e9ea88a01..55da29b7bf85 100755 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -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); diff --git a/tests/system/Database/Builder/FromTest.php b/tests/system/Database/Builder/FromTest.php index b7c62a2c748c..fee171e9ca42 100644 --- a/tests/system/Database/Builder/FromTest.php +++ b/tests/system/Database/Builder/FromTest.php @@ -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']); @@ -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())); + } } diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 2c85a58e63e6..aaefbe9d4630 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -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:: @@ -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