diff --git a/src/Driver/Compiler.php b/src/Driver/Compiler.php index 2a664ec5..93ba54a9 100644 --- a/src/Driver/Compiler.php +++ b/src/Driver/Compiler.php @@ -126,6 +126,9 @@ protected function fragment( return $this->selectQuery($params, $q, $tokens); + case self::SUBQUERY: + return $this->subQuery($params, $q, $tokens); + case self::UPDATE_QUERY: return $this->updateQuery($params, $q, $tokens); @@ -198,6 +201,11 @@ protected function selectQuery(QueryParameters $params, Quoter $q, array $tokens ); } + protected function subQuery(QueryParameters $params, Quoter $q, array $tokens): string + { + return \sprintf('( %s ) AS %s', $this->selectQuery($params, $q, $tokens), $q->quote($tokens['alias'])); + } + protected function distinct(QueryParameters $params, Quoter $q, string|bool|array $distinct): string { return $distinct === false ? '' : 'DISTINCT'; diff --git a/src/Driver/CompilerCache.php b/src/Driver/CompilerCache.php index a2684549..7aecba87 100644 --- a/src/Driver/CompilerCache.php +++ b/src/Driver/CompilerCache.php @@ -17,6 +17,7 @@ use Cycle\Database\Injection\JsonExpression; use Cycle\Database\Injection\Parameter; use Cycle\Database\Injection\ParameterInterface; +use Cycle\Database\Injection\SubQuery; use Cycle\Database\Query\QueryInterface; use Cycle\Database\Query\QueryParameters; use Cycle\Database\Query\SelectQuery; @@ -162,6 +163,10 @@ protected function hashSelectQuery(QueryParameters $params, array $tokens): stri $hash .= 's_' . ($table->getPrefix() ?? ''); $hash .= $this->hashSelectQuery($params, $table->getTokens()); continue; + } elseif ($table instanceof SubQuery) { + $hash .= 'sb_'; + $hash .= $this->hashSelectQuery($params, $table->getTokens()); + continue; } $hash .= $table; @@ -330,7 +335,7 @@ protected function hashColumns(QueryParameters $params, array $columns): string { $hash = ''; foreach ($columns as $column) { - if ($column instanceof Expression || $column instanceof Fragment) { + if ($column instanceof Expression || $column instanceof Fragment || $column instanceof SubQuery) { foreach ($column->getTokens()['parameters'] as $param) { $params->push($param); } diff --git a/src/Driver/CompilerInterface.php b/src/Driver/CompilerInterface.php index c227f523..281c124b 100644 --- a/src/Driver/CompilerInterface.php +++ b/src/Driver/CompilerInterface.php @@ -24,6 +24,7 @@ interface CompilerInterface public const UPDATE_QUERY = 6; public const DELETE_QUERY = 7; public const JSON_EXPRESSION = 8; + public const SUBQUERY = 9; public const TOKEN_AND = '@AND'; public const TOKEN_OR = '@OR'; public const TOKEN_AND_NOT = '@AND NOT'; @@ -33,7 +34,6 @@ public function quoteIdentifier(string $identifier): string; /** * Compile the query fragment. - * */ public function compile( QueryParameters $params, diff --git a/src/Driver/Jsoner.php b/src/Driver/Jsoner.php index fe08c02c..7bc50eaa 100644 --- a/src/Driver/Jsoner.php +++ b/src/Driver/Jsoner.php @@ -32,7 +32,7 @@ public static function toJson(mixed $value, bool $encode = true, bool $validate $result = (string) $value; - if ($validate && !\json_validate($result)) { + if ($validate && !json_validate($result)) { throw new BuilderException('Invalid JSON value.'); } diff --git a/src/Injection/FragmentInterface.php b/src/Injection/FragmentInterface.php index 6f74f20a..80c2302c 100644 --- a/src/Injection/FragmentInterface.php +++ b/src/Injection/FragmentInterface.php @@ -18,13 +18,11 @@ interface FragmentInterface { /** * Return the fragment type. - * */ public function getType(): int; /** * Return the fragment tokens. - * */ public function getTokens(): array; } diff --git a/src/Injection/SubQuery.php b/src/Injection/SubQuery.php new file mode 100644 index 00000000..2cf03274 --- /dev/null +++ b/src/Injection/SubQuery.php @@ -0,0 +1,87 @@ +select()->from(['users']),'u'); + * $query = $queryBuilder->select()->from($subQuery); + * ``` + * + * Will provide SQL like this: SELECT * FROM (SELECT * FROM users) AS u + * + * ``` + * $subQuery = new SubQuery($queryBuilder->select()->from(['users']),'u'); + * $query = $queryBuilder->select($subQuery)->from(['employee']); + * ``` + * + * Will provide SQL like this: SELECT *, (SELECT * FROM users) AS u FROM employee + */ +class SubQuery implements FragmentInterface +{ + private SelectQuery $query; + private string $alias; + + /** @var ParameterInterface[] */ + private array $parameters; + + public function __construct(SelectQuery $query, string $alias) + { + $this->query = $query; + $this->alias = $alias; + + $parameters = new QueryParameters(); + $this->query->sqlStatement($parameters); + $this->parameters = $parameters->getParameters(); + } + + public function getType(): int + { + return CompilerInterface::SUBQUERY; + } + + public function getTokens(): array + { + return \array_merge( + [ + 'alias' => $this->alias, + 'parameters' => $this->parameters, + ], + $this->query->getTokens(), + ); + } + + public function getQuery(): SelectQuery + { + return $this->query; + } + + public function __toString(): string + { + $parameters = new QueryParameters(); + + return Interpolator::interpolate( + $this->query->sqlStatement($parameters), + $parameters->getParameters(), + ); + } +} diff --git a/src/Query/SelectQuery.php b/src/Query/SelectQuery.php index 52a39271..818dd6de 100644 --- a/src/Query/SelectQuery.php +++ b/src/Query/SelectQuery.php @@ -13,6 +13,7 @@ use Cycle\Database\Injection\Expression; use Cycle\Database\Injection\Fragment; +use Cycle\Database\Injection\SubQuery; use Cycle\Database\Query\Traits\WhereJsonTrait; use Cycle\Database\Driver\CompilerInterface; use Cycle\Database\Injection\FragmentInterface; @@ -84,6 +85,16 @@ public function distinct(bool|string|FragmentInterface $distinct = true): self /** * Set table names SELECT query should be performed for. Table names can be provided with * specified alias (AS construction). + * Also, it is possible to use SubQuery. + * + * Following example will provide SQL like this: SELECT * FROM (SELECT * FROM users) AS u + * + * ``` + * $subQuery = new SubQuery($queryBuilder->select()->from(['users']),'u'); + * $query = $queryBuilder->select()->from($subQuery); + * ``` + * + * @see SubQuery */ public function from(mixed $tables): self { diff --git a/tests/Database/Functional/Driver/Common/Query/SelectQueryTest.php b/tests/Database/Functional/Driver/Common/Query/SelectQueryTest.php index baf70cb2..d4a37ae1 100644 --- a/tests/Database/Functional/Driver/Common/Query/SelectQueryTest.php +++ b/tests/Database/Functional/Driver/Common/Query/SelectQueryTest.php @@ -10,6 +10,7 @@ use Cycle\Database\Injection\Expression; use Cycle\Database\Injection\Fragment; use Cycle\Database\Injection\Parameter; +use Cycle\Database\Injection\SubQuery; use Cycle\Database\Query\SelectQuery; use Cycle\Database\Tests\Functional\Driver\Common\BaseTest; use Spiral\Pagination\PaginableInterface; @@ -2639,4 +2640,98 @@ public function testOrWhereNotWithArrayAnd(): void $select, ); } + + public function testSelectFromSubQuery(): void + { + $innerSelect = $this->database + ->select() + ->from(['users']) + ->where(['name' => 'John Doe']); + $injection = new SubQuery($innerSelect, 'u'); + + $outerSelect = $this->database + ->select() + ->from($injection) + ->where('u.id', '>', 10); + + $this->assertSameQuery( + << ? + SQL, + $outerSelect, + ); + + $this->assertSameParameters( + [ + 'John Doe', + 10, + ], + $outerSelect, + ); + } + + public function testSelectFromTwoSubQuery(): void + { + $innerSelect1 = $this->database + ->select() + ->from(['users']) + ->where(['name' => 'John Doe']); + $injection1 = new SubQuery($innerSelect1, 'u'); + + $innerSelect2 = $this->database + ->select() + ->from(['apartments']) + ->where(['dom' => 12]); + $injection2 = new SubQuery($innerSelect2, 'a'); + + + $outerSelect = $this->database + ->select() + ->from($injection1, $injection2); + + $this->assertSameQuery( + <<assertSameParameters( + [ + 'John Doe', + 12, + ], + $outerSelect, + ); + } + + public function testSelectSelectSubQuery(): void + { + $innerSelect = $this->database + ->select() + ->from(['users']) + ->where(['name' => 'John Doe']); + $injection = new SubQuery($innerSelect, 'u'); + + $outerSelect = $this->database + ->select(['*', $injection]) + ->from(['apartments']); + + $this->assertSameQuery( + 'SELECT *, (SELECT * FROM {users} WHERE {name} = ?) AS {u} FROM {apartments}', + $outerSelect, + ); + + $this->assertSameParameters( + [ + 'John Doe', + ], + $outerSelect, + ); + } }