Skip to content

Commit 13490d3

Browse files
authored
Merge pull request #225: add SubQuery injection
sub query
2 parents 0f46231 + 8579419 commit 13490d3

File tree

8 files changed

+209
-5
lines changed

8 files changed

+209
-5
lines changed

src/Driver/Compiler.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ protected function fragment(
126126

127127
return $this->selectQuery($params, $q, $tokens);
128128

129+
case self::SUBQUERY:
130+
return $this->subQuery($params, $q, $tokens);
131+
129132
case self::UPDATE_QUERY:
130133
return $this->updateQuery($params, $q, $tokens);
131134

@@ -198,6 +201,11 @@ protected function selectQuery(QueryParameters $params, Quoter $q, array $tokens
198201
);
199202
}
200203

204+
protected function subQuery(QueryParameters $params, Quoter $q, array $tokens): string
205+
{
206+
return \sprintf('( %s ) AS %s', $this->selectQuery($params, $q, $tokens), $q->quote($tokens['alias']));
207+
}
208+
201209
protected function distinct(QueryParameters $params, Quoter $q, string|bool|array $distinct): string
202210
{
203211
return $distinct === false ? '' : 'DISTINCT';

src/Driver/CompilerCache.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Cycle\Database\Injection\JsonExpression;
1818
use Cycle\Database\Injection\Parameter;
1919
use Cycle\Database\Injection\ParameterInterface;
20+
use Cycle\Database\Injection\SubQuery;
2021
use Cycle\Database\Query\QueryInterface;
2122
use Cycle\Database\Query\QueryParameters;
2223
use Cycle\Database\Query\SelectQuery;
@@ -162,6 +163,10 @@ protected function hashSelectQuery(QueryParameters $params, array $tokens): stri
162163
$hash .= 's_' . ($table->getPrefix() ?? '');
163164
$hash .= $this->hashSelectQuery($params, $table->getTokens());
164165
continue;
166+
} elseif ($table instanceof SubQuery) {
167+
$hash .= 'sb_';
168+
$hash .= $this->hashSelectQuery($params, $table->getTokens());
169+
continue;
165170
}
166171

167172
$hash .= $table;
@@ -330,7 +335,7 @@ protected function hashColumns(QueryParameters $params, array $columns): string
330335
{
331336
$hash = '';
332337
foreach ($columns as $column) {
333-
if ($column instanceof Expression || $column instanceof Fragment) {
338+
if ($column instanceof Expression || $column instanceof Fragment || $column instanceof SubQuery) {
334339
foreach ($column->getTokens()['parameters'] as $param) {
335340
$params->push($param);
336341
}

src/Driver/CompilerInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface CompilerInterface
2424
public const UPDATE_QUERY = 6;
2525
public const DELETE_QUERY = 7;
2626
public const JSON_EXPRESSION = 8;
27+
public const SUBQUERY = 9;
2728
public const TOKEN_AND = '@AND';
2829
public const TOKEN_OR = '@OR';
2930
public const TOKEN_AND_NOT = '@AND NOT';
@@ -33,7 +34,6 @@ public function quoteIdentifier(string $identifier): string;
3334

3435
/**
3536
* Compile the query fragment.
36-
*
3737
*/
3838
public function compile(
3939
QueryParameters $params,

src/Driver/Jsoner.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public static function toJson(mixed $value, bool $encode = true, bool $validate
3232

3333
$result = (string) $value;
3434

35-
if ($validate && !\json_validate($result)) {
35+
if ($validate && !json_validate($result)) {
3636
throw new BuilderException('Invalid JSON value.');
3737
}
3838

src/Injection/FragmentInterface.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,11 @@ interface FragmentInterface
1818
{
1919
/**
2020
* Return the fragment type.
21-
*
2221
*/
2322
public function getType(): int;
2423

2524
/**
2625
* Return the fragment tokens.
27-
*
2826
*/
2927
public function getTokens(): array;
3028
}

src/Injection/SubQuery.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
/**
4+
* This file is part of Cycle ORM package.
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Cycle\Database\Injection;
13+
14+
use Cycle\Database\Driver\CompilerInterface;
15+
use Cycle\Database\Query\Interpolator;
16+
use Cycle\Database\Query\QueryParameters;
17+
use Cycle\Database\Query\SelectQuery;
18+
19+
/**
20+
* This fragment is used to inject a whole select statement into
21+
* FROM and SELECT parts of the query.
22+
*
23+
* Examples:
24+
*
25+
* ```
26+
* $subQuery = new SubQuery($queryBuilder->select()->from(['users']),'u');
27+
* $query = $queryBuilder->select()->from($subQuery);
28+
* ```
29+
*
30+
* Will provide SQL like this: SELECT * FROM (SELECT * FROM users) AS u
31+
*
32+
* ```
33+
* $subQuery = new SubQuery($queryBuilder->select()->from(['users']),'u');
34+
* $query = $queryBuilder->select($subQuery)->from(['employee']);
35+
* ```
36+
*
37+
* Will provide SQL like this: SELECT *, (SELECT * FROM users) AS u FROM employee
38+
*/
39+
class SubQuery implements FragmentInterface
40+
{
41+
private SelectQuery $query;
42+
private string $alias;
43+
44+
/** @var ParameterInterface[] */
45+
private array $parameters;
46+
47+
public function __construct(SelectQuery $query, string $alias)
48+
{
49+
$this->query = $query;
50+
$this->alias = $alias;
51+
52+
$parameters = new QueryParameters();
53+
$this->query->sqlStatement($parameters);
54+
$this->parameters = $parameters->getParameters();
55+
}
56+
57+
public function getType(): int
58+
{
59+
return CompilerInterface::SUBQUERY;
60+
}
61+
62+
public function getTokens(): array
63+
{
64+
return \array_merge(
65+
[
66+
'alias' => $this->alias,
67+
'parameters' => $this->parameters,
68+
],
69+
$this->query->getTokens(),
70+
);
71+
}
72+
73+
public function getQuery(): SelectQuery
74+
{
75+
return $this->query;
76+
}
77+
78+
public function __toString(): string
79+
{
80+
$parameters = new QueryParameters();
81+
82+
return Interpolator::interpolate(
83+
$this->query->sqlStatement($parameters),
84+
$parameters->getParameters(),
85+
);
86+
}
87+
}

src/Query/SelectQuery.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Cycle\Database\Injection\Expression;
1515
use Cycle\Database\Injection\Fragment;
16+
use Cycle\Database\Injection\SubQuery;
1617
use Cycle\Database\Query\Traits\WhereJsonTrait;
1718
use Cycle\Database\Driver\CompilerInterface;
1819
use Cycle\Database\Injection\FragmentInterface;
@@ -84,6 +85,16 @@ public function distinct(bool|string|FragmentInterface $distinct = true): self
8485
/**
8586
* Set table names SELECT query should be performed for. Table names can be provided with
8687
* specified alias (AS construction).
88+
* Also, it is possible to use SubQuery.
89+
*
90+
* Following example will provide SQL like this: SELECT * FROM (SELECT * FROM users) AS u
91+
*
92+
* ```
93+
* $subQuery = new SubQuery($queryBuilder->select()->from(['users']),'u');
94+
* $query = $queryBuilder->select()->from($subQuery);
95+
* ```
96+
*
97+
* @see SubQuery
8798
*/
8899
public function from(mixed $tables): self
89100
{

tests/Database/Functional/Driver/Common/Query/SelectQueryTest.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Cycle\Database\Injection\Expression;
1111
use Cycle\Database\Injection\Fragment;
1212
use Cycle\Database\Injection\Parameter;
13+
use Cycle\Database\Injection\SubQuery;
1314
use Cycle\Database\Query\SelectQuery;
1415
use Cycle\Database\Tests\Functional\Driver\Common\BaseTest;
1516
use Spiral\Pagination\PaginableInterface;
@@ -2639,4 +2640,98 @@ public function testOrWhereNotWithArrayAnd(): void
26392640
$select,
26402641
);
26412642
}
2643+
2644+
public function testSelectFromSubQuery(): void
2645+
{
2646+
$innerSelect = $this->database
2647+
->select()
2648+
->from(['users'])
2649+
->where(['name' => 'John Doe']);
2650+
$injection = new SubQuery($innerSelect, 'u');
2651+
2652+
$outerSelect = $this->database
2653+
->select()
2654+
->from($injection)
2655+
->where('u.id', '>', 10);
2656+
2657+
$this->assertSameQuery(
2658+
<<<SQL
2659+
SELECT *
2660+
FROM (SELECT * FROM {users} WHERE {name} = ?) AS {u}
2661+
WHERE {u}.{id} > ?
2662+
SQL,
2663+
$outerSelect,
2664+
);
2665+
2666+
$this->assertSameParameters(
2667+
[
2668+
'John Doe',
2669+
10,
2670+
],
2671+
$outerSelect,
2672+
);
2673+
}
2674+
2675+
public function testSelectFromTwoSubQuery(): void
2676+
{
2677+
$innerSelect1 = $this->database
2678+
->select()
2679+
->from(['users'])
2680+
->where(['name' => 'John Doe']);
2681+
$injection1 = new SubQuery($innerSelect1, 'u');
2682+
2683+
$innerSelect2 = $this->database
2684+
->select()
2685+
->from(['apartments'])
2686+
->where(['dom' => 12]);
2687+
$injection2 = new SubQuery($innerSelect2, 'a');
2688+
2689+
2690+
$outerSelect = $this->database
2691+
->select()
2692+
->from($injection1, $injection2);
2693+
2694+
$this->assertSameQuery(
2695+
<<<SQL
2696+
SELECT *
2697+
FROM
2698+
(SELECT * FROM {users} WHERE {name} = ?) AS {u},
2699+
(SELECT * FROM {apartments} WHERE {dom} = ?) AS {a}
2700+
SQL,
2701+
$outerSelect,
2702+
);
2703+
2704+
$this->assertSameParameters(
2705+
[
2706+
'John Doe',
2707+
12,
2708+
],
2709+
$outerSelect,
2710+
);
2711+
}
2712+
2713+
public function testSelectSelectSubQuery(): void
2714+
{
2715+
$innerSelect = $this->database
2716+
->select()
2717+
->from(['users'])
2718+
->where(['name' => 'John Doe']);
2719+
$injection = new SubQuery($innerSelect, 'u');
2720+
2721+
$outerSelect = $this->database
2722+
->select(['*', $injection])
2723+
->from(['apartments']);
2724+
2725+
$this->assertSameQuery(
2726+
'SELECT *, (SELECT * FROM {users} WHERE {name} = ?) AS {u} FROM {apartments}',
2727+
$outerSelect,
2728+
);
2729+
2730+
$this->assertSameParameters(
2731+
[
2732+
'John Doe',
2733+
],
2734+
$outerSelect,
2735+
);
2736+
}
26422737
}

0 commit comments

Comments
 (0)