Skip to content

Commit 320fe4e

Browse files
authored
Implement ArrayMergeBuilder, LongestBuilder and ShortestBuilder (#420)
1 parent 6b40636 commit 320fe4e

File tree

8 files changed

+340
-0
lines changed

8 files changed

+340
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
- Enh #414: Remove `TableSchema` class and refactor `Schema` class (@Tigrov)
5151
- Enh #415: Support column's collation (@Tigrov)
5252
- New #421: Add `Connection::getColumnBuilderClass()` method (@Tigrov)
53+
- New #420: Implement `ArrayMergeBuilder`, `LongestBuilder` and `ShortestBuilder` classes (@Tigrov)
5354

5455
## 1.2.0 March 21, 2024
5556

psalm.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@
1818
<issueHandlers>
1919
<RiskyTruthyFalsyComparison errorLevel="suppress" />
2020
<MixedAssignment errorLevel="suppress" />
21+
<MoreSpecificImplementedParamType errorLevel="suppress" />
2122
</issueHandlers>
2223
</psalm>

src/Builder/ArrayMergeBuilder.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Db\Mysql\Builder;
6+
7+
use Yiisoft\Db\Expression\Function\ArrayMerge;
8+
use Yiisoft\Db\Expression\Function\Builder\MultiOperandFunctionBuilder;
9+
use Yiisoft\Db\Expression\Function\MultiOperandFunction;
10+
use Yiisoft\Db\Schema\Column\AbstractArrayColumn;
11+
use Yiisoft\Db\Schema\Column\ColumnInterface;
12+
13+
use function implode;
14+
use function is_string;
15+
use function rtrim;
16+
17+
/**
18+
* Builds SQL expressions which merge arrays for {@see ArrayMerge} objects.
19+
*
20+
* ```sql
21+
* (SELECT JSON_ARRAYAGG(value) AS value FROM (
22+
* SELECT value FROM JSON_TABLE(operand1, '$[*]' COLUMNS(value json PATH '$')) AS t
23+
* UNION
24+
* SELECT value FROM JSON_TABLE(operand2, '$[*]' COLUMNS(value json PATH '$')) AS t
25+
* ) t)
26+
* ```
27+
*
28+
* @extends MultiOperandFunctionBuilder<ArrayMerge>
29+
*/
30+
final class ArrayMergeBuilder extends MultiOperandFunctionBuilder
31+
{
32+
private const DEFAULT_OPERAND_TYPE = 'json';
33+
34+
/**
35+
* Builds a SQL expression which merges arrays from the given {@see ArrayMerge} object.
36+
*
37+
* @param ArrayMerge $expression The expression to build.
38+
* @param array $params The parameters to bind.
39+
*
40+
* @return string The SQL expression.
41+
*/
42+
protected function buildFromExpression(MultiOperandFunction $expression, array &$params): string
43+
{
44+
$operandType = $this->buildOperandType($expression->getType());
45+
$selects = [];
46+
47+
foreach ($expression->getOperands() as $operand) {
48+
$builtOperand = $this->buildOperand($operand, $params);
49+
50+
$selects[] = "SELECT value FROM JSON_TABLE($builtOperand, '$[*]' COLUMNS(value $operandType PATH '$')) AS t";
51+
}
52+
53+
return '(SELECT JSON_ARRAYAGG(value) AS value FROM (' . implode(' UNION ', $selects) . ') AS t)';
54+
}
55+
56+
private function buildOperandType(string|ColumnInterface $type): string
57+
{
58+
if (is_string($type)) {
59+
return $type === '' ? self::DEFAULT_OPERAND_TYPE : rtrim($type, '[]');
60+
}
61+
62+
if ($type instanceof AbstractArrayColumn) {
63+
if ($type->getDimension() > 1) {
64+
return self::DEFAULT_OPERAND_TYPE;
65+
}
66+
67+
$type = $type->getColumn();
68+
69+
if ($type === null) {
70+
return self::DEFAULT_OPERAND_TYPE;
71+
}
72+
}
73+
74+
return $this->queryBuilder->getColumnDefinitionBuilder()->buildType($type);
75+
}
76+
}

src/Builder/LongestBuilder.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Db\Mysql\Builder;
6+
7+
use Yiisoft\Db\Expression\Function\Builder\MultiOperandFunctionBuilder;
8+
use Yiisoft\Db\Expression\Function\Longest;
9+
use Yiisoft\Db\Expression\Function\MultiOperandFunction;
10+
11+
/**
12+
* Builds SQL representation of function expressions which returns the longest string from a set of operands.
13+
*
14+
* ```SQL
15+
* (SELECT operand1 AS value
16+
* UNION SELECT operand2 AS value
17+
* ORDER BY LENGTH(value) DESC LIMIT 1)
18+
* ```
19+
*
20+
* @extends MultiOperandFunctionBuilder<Longest>
21+
*/
22+
final class LongestBuilder extends MultiOperandFunctionBuilder
23+
{
24+
/**
25+
* Builds a SQL expression to represent the function which returns the longest string.
26+
*
27+
* @param Longest $expression The expression to build.
28+
* @param array $params The parameters to bind.
29+
*
30+
* @return string The SQL expression.
31+
*/
32+
protected function buildFromExpression(MultiOperandFunction $expression, array &$params): string
33+
{
34+
$selects = [];
35+
36+
foreach ($expression->getOperands() as $operand) {
37+
$selects[] = 'SELECT ' . $this->buildOperand($operand, $params) . ' AS value';
38+
}
39+
40+
$unions = implode(' UNION ', $selects);
41+
42+
return "($unions ORDER BY LENGTH(value) DESC LIMIT 1)";
43+
}
44+
}

src/Builder/ShortestBuilder.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Db\Mysql\Builder;
6+
7+
use Yiisoft\Db\Expression\Function\Builder\MultiOperandFunctionBuilder;
8+
use Yiisoft\Db\Expression\Function\MultiOperandFunction;
9+
use Yiisoft\Db\Expression\Function\Shortest;
10+
11+
/**
12+
* Builds SQL representation of function expressions which return the shortest string from a set of operands.
13+
*
14+
* ```SQL
15+
* (SELECT operand1 AS value
16+
* UNION SELECT operand2 AS value
17+
* ORDER BY LENGTH(value) ASC LIMIT 1)
18+
* ```
19+
*
20+
* @extends MultiOperandFunctionBuilder<Shortest>
21+
*/
22+
final class ShortestBuilder extends MultiOperandFunctionBuilder
23+
{
24+
/**
25+
* Builds a SQL expression to represent the function which returns the shortest string.
26+
*
27+
* @param Shortest $expression The expression to build.
28+
* @param array $params The parameters to bind.
29+
*
30+
* @return string The SQL expression.
31+
*/
32+
protected function buildFromExpression(MultiOperandFunction $expression, array &$params): string
33+
{
34+
$selects = [];
35+
36+
foreach ($expression->getOperands() as $operand) {
37+
$selects[] = 'SELECT ' . $this->buildOperand($operand, $params) . ' AS value';
38+
}
39+
40+
$unions = implode(' UNION ', $selects);
41+
42+
return "($unions ORDER BY LENGTH(value) ASC LIMIT 1)";
43+
}
44+
}

src/DQLQueryBuilder.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@
55
namespace Yiisoft\Db\Mysql;
66

77
use Yiisoft\Db\Expression\ExpressionInterface;
8+
use Yiisoft\Db\Expression\Function\ArrayMerge;
9+
use Yiisoft\Db\Expression\Function\Longest;
10+
use Yiisoft\Db\Expression\Function\Shortest;
11+
use Yiisoft\Db\Mysql\Builder\ArrayMergeBuilder;
812
use Yiisoft\Db\Mysql\Builder\JsonOverlapsBuilder;
913
use Yiisoft\Db\Mysql\Builder\LikeBuilder;
14+
use Yiisoft\Db\Mysql\Builder\LongestBuilder;
15+
use Yiisoft\Db\Mysql\Builder\ShortestBuilder;
1016
use Yiisoft\Db\QueryBuilder\AbstractDQLQueryBuilder;
1117
use Yiisoft\Db\QueryBuilder\Condition\JsonOverlaps;
1218
use Yiisoft\Db\QueryBuilder\Condition\Like;
@@ -78,6 +84,9 @@ protected function defaultExpressionBuilders(): array
7884
JsonOverlaps::class => JsonOverlapsBuilder::class,
7985
Like::class => LikeBuilder::class,
8086
NotLike::class => LikeBuilder::class,
87+
ArrayMerge::class => ArrayMergeBuilder::class,
88+
Longest::class => LongestBuilder::class,
89+
Shortest::class => ShortestBuilder::class,
8190
];
8291
}
8392
}

tests/Provider/QueryBuilderProvider.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@
55
namespace Yiisoft\Db\Mysql\Tests\Provider;
66

77
use Yiisoft\Db\Constant\ColumnType;
8+
use Yiisoft\Db\Constant\DataType;
89
use Yiisoft\Db\Constant\PseudoType;
10+
use Yiisoft\Db\Expression\ArrayExpression;
911
use Yiisoft\Db\Expression\Expression;
12+
use Yiisoft\Db\Expression\Function\ArrayMerge;
13+
use Yiisoft\Db\Expression\Param;
1014
use Yiisoft\Db\Mysql\Column\ColumnBuilder;
1115
use Yiisoft\Db\Mysql\Tests\Support\TestTrait;
1216

1317
use function array_replace;
18+
use function str_contains;
19+
use function version_compare;
1420

1521
final class QueryBuilderProvider extends \Yiisoft\Db\Tests\Provider\QueryBuilderProvider
1622
{
@@ -301,4 +307,76 @@ public static function prepareValue(): array
301307

302308
return $values;
303309
}
310+
311+
public static function multiOperandFunctionClasses(): array
312+
{
313+
return [
314+
...parent::multiOperandFunctionClasses(),
315+
ArrayMerge::class => [ArrayMerge::class],
316+
];
317+
}
318+
319+
public static function multiOperandFunctionBuilder(): array
320+
{
321+
$data = parent::multiOperandFunctionBuilder();
322+
323+
$stringParam = new Param('[3,4,5]', DataType::STRING);
324+
325+
$data['Longest with 2 operands'][2] = "(SELECT 'short' AS value UNION SELECT :qp0 AS value ORDER BY LENGTH(value) DESC LIMIT 1)";
326+
$data['Longest with 3 operands'][2] = "(SELECT 'short' AS value UNION SELECT (SELECT 'longest') AS value UNION SELECT :qp0 AS value ORDER BY LENGTH(value) DESC LIMIT 1)";
327+
$data['Shortest with 2 operands'][2] = "(SELECT 'short' AS value UNION SELECT :qp0 AS value ORDER BY LENGTH(value) ASC LIMIT 1)";
328+
$data['Shortest with 3 operands'][2] = "(SELECT 'short' AS value UNION SELECT (SELECT 'longest') AS value UNION SELECT :qp0 AS value ORDER BY LENGTH(value) ASC LIMIT 1)";
329+
330+
$db = self::getDb();
331+
$serverVersion = $db->getServerInfo()->getVersion();
332+
$db->close();
333+
334+
$isMariadb = str_contains($serverVersion, 'MariaDB');
335+
336+
if (
337+
$isMariadb && version_compare($serverVersion, '10.6', '<')
338+
|| !$isMariadb && version_compare($serverVersion, '8.0.0', '<')
339+
) {
340+
// MariaDB < 10.6 and MySQL < 8 does not support JSON_TABLE() function.
341+
return $data;
342+
}
343+
344+
$data['ArrayMerge with 1 operand'] = [
345+
ArrayMerge::class,
346+
["'[1,2,3]'"],
347+
"('[1,2,3]')",
348+
[1, 2, 3],
349+
];
350+
$data['ArrayMerge with 2 operands'] = [
351+
ArrayMerge::class,
352+
["'[1,2,3]'", $stringParam],
353+
'(SELECT JSON_ARRAYAGG(value) AS value FROM ('
354+
. "SELECT value FROM JSON_TABLE('[1,2,3]', '$[*]' COLUMNS(value json PATH '$')) AS t"
355+
. " UNION SELECT value FROM JSON_TABLE(:qp0, '$[*]' COLUMNS(value json PATH '$')) AS t) AS t)",
356+
[1, 2, 3, 4, 5],
357+
[':qp0' => $stringParam],
358+
];
359+
360+
if ($isMariadb) {
361+
// MySQL does not support query parameters in JSON_TABLE() function.
362+
$data['ArrayMerge with 4 operands'] = [
363+
ArrayMerge::class,
364+
["'[1,2,3]'", [5, 6, 7], $stringParam, self::getDb()->select(new ArrayExpression([9, 10]))],
365+
'(SELECT JSON_ARRAYAGG(value) AS value FROM ('
366+
. "SELECT value FROM JSON_TABLE('[1,2,3]', '$[*]' COLUMNS(value json PATH '$')) AS t"
367+
. " UNION SELECT value FROM JSON_TABLE(:qp0, '$[*]' COLUMNS(value json PATH '$')) AS t"
368+
. " UNION SELECT value FROM JSON_TABLE(:qp1, '$[*]' COLUMNS(value json PATH '$')) AS t"
369+
. " UNION SELECT value FROM JSON_TABLE((SELECT :qp2), '$[*]' COLUMNS(value json PATH '$')) AS t"
370+
. ') AS t)',
371+
[1, 2, 3, 4, 5, 6, 7, 9, 10],
372+
[
373+
':qp0' => new Param('[5,6,7]', DataType::STRING),
374+
':qp1' => $stringParam,
375+
':qp2' => new Param('[9,10]', DataType::STRING),
376+
],
377+
];
378+
}
379+
380+
return $data;
381+
}
304382
}

0 commit comments

Comments
 (0)