Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
- Enh #350: Adapt to `Like` changes in `yiisoft/db` package (@vjik)
- Enh #352: Support column's collation (@Tigrov)
- New #358: Add `Connection::getColumnBuilderClass()` method (@Tigrov)
- New #357: Implement `ArrayMergeBuilder`, `LongestBuilder` and `ShortestBuilder` classes (@Tigrov)

## 1.3.0 March 21, 2024

Expand Down
1 change: 1 addition & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
<issueHandlers>
<MixedAssignment errorLevel="suppress" />
<RiskyTruthyFalsyComparison errorLevel="suppress" />
<MoreSpecificImplementedParamType errorLevel="suppress" />
</issueHandlers>
</psalm>
75 changes: 75 additions & 0 deletions src/Builder/ArrayMergeBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Oracle\Builder;

use Yiisoft\Db\Expression\Function\ArrayMerge;
use Yiisoft\Db\Expression\Function\Builder\MultiOperandFunctionBuilder;
use Yiisoft\Db\Expression\Function\MultiOperandFunction;
use Yiisoft\Db\Schema\Column\AbstractArrayColumn;
use Yiisoft\Db\Schema\Column\ColumnInterface;

use function implode;
use function is_string;
use function rtrim;

/**
* Builds SQL expressions which merge arrays for {@see ArrayMerge} objects.
*
* ```sql
* (SELECT JSON_ARRAYAGG(value) AS value FROM (
* SELECT value FROM JSON_TABLE(operand1, '$[*]' COLUMNS(value int PATH '$'))
* UNION
* SELECT value FROM JSON_TABLE(operand2, '$[*]' COLUMNS(value int PATH '$'))
* ))
* ```
*
* @extends MultiOperandFunctionBuilder<ArrayMerge>
*/
final class ArrayMergeBuilder extends MultiOperandFunctionBuilder
{
private const DEFAULT_OPERAND_TYPE = '';

/**
* Builds a SQL expression which merges arrays from the given {@see ArrayMerge} object.
*
* @param ArrayMerge $expression The expression to build.
* @param array $params The parameters to bind.
*
* @return string The SQL expression.
*/
protected function buildFromExpression(MultiOperandFunction $expression, array &$params): string
{
$selects = [];
$operandType = $this->buildOperandType($expression->getType());

foreach ($expression->getOperands() as $operand) {
$builtOperand = $this->buildOperand($operand, $params);
$selects[] = "SELECT value FROM JSON_TABLE($builtOperand, '$[*]' COLUMNS(value $operandType PATH '$'))";
}

return '(SELECT JSON_ARRAYAGG(value) AS value FROM (' . implode(' UNION ', $selects) . '))';
}

private function buildOperandType(string|ColumnInterface $type): string
{
if (is_string($type)) {
return $type === '' ? self::DEFAULT_OPERAND_TYPE : rtrim($type, '[]');
}

if ($type instanceof AbstractArrayColumn) {
if ($type->getDimension() > 1) {
return self::DEFAULT_OPERAND_TYPE;
}

$type = $type->getColumn();

if ($type === null) {
return self::DEFAULT_OPERAND_TYPE;
}
}

return $this->queryBuilder->getColumnDefinitionBuilder()->buildType($type);
}
}
47 changes: 47 additions & 0 deletions src/Builder/LongestBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Oracle\Builder;

use Yiisoft\Db\Expression\Function\Builder\MultiOperandFunctionBuilder;
use Yiisoft\Db\Expression\Function\Greatest;
use Yiisoft\Db\Expression\Function\Longest;
use Yiisoft\Db\Expression\Function\MultiOperandFunction;

/**
* Builds SQL representation of function expressions which returns the longest string from a set of operands.
*
* ```SQL
* (SELECT value FROM (
* SELECT operand1 AS value FROM DUAL
* UNION
* SELECT operand2 AS value FROM DUAL
* ) ORDER BY LENGTH(value) DESC FETCH FIRST 1 ROWS ONLY)
* ```
*
* @extends MultiOperandFunctionBuilder<Longest>
*/
final class LongestBuilder extends MultiOperandFunctionBuilder
{
/**
* Builds a SQL expression to represent the function which returns the longest string.
*
* @param Greatest $expression The expression to build.
* @param array $params The parameters to bind.
*
* @return string The SQL expression.
*/
protected function buildFromExpression(MultiOperandFunction $expression, array &$params): string
{
$selects = [];

foreach ($expression->getOperands() as $operand) {
$selects[] = 'SELECT ' . $this->buildOperand($operand, $params) . ' AS value FROM DUAL';
}

$unions = implode(' UNION ', $selects);

return "(SELECT value FROM ($unions) ORDER BY LENGTH(value) DESC FETCH FIRST 1 ROWS ONLY)";
}
}
46 changes: 46 additions & 0 deletions src/Builder/ShortestBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Oracle\Builder;

use Yiisoft\Db\Expression\Function\Builder\MultiOperandFunctionBuilder;
use Yiisoft\Db\Expression\Function\MultiOperandFunction;
use Yiisoft\Db\Expression\Function\Shortest;

/**
* Builds SQL representation of function expressions which return the shortest string from a set of operands.
*
* ```SQL
* (SELECT value FROM (
* SELECT operand1 AS value FROM DUAL
* UNION
* SELECT operand2 AS value FROM DUAL
* ) ORDER BY LENGTH(value) ASC FETCH FIRST 1 ROWS ONLY)
* ```
*
* @extends MultiOperandFunctionBuilder<Shortest>
*/
final class ShortestBuilder extends MultiOperandFunctionBuilder
{
/**
* Builds a SQL expression to represent the function which returns the shortest string.
*
* @param Shortest $expression The expression to build.
* @param array $params The parameters to bind.
*
* @return string The SQL expression.
*/
protected function buildFromExpression(MultiOperandFunction $expression, array &$params): string
{
$selects = [];

foreach ($expression->getOperands() as $operand) {
$selects[] = 'SELECT ' . $this->buildOperand($operand, $params) . ' AS value FROM DUAL';
}

$unions = implode(' UNION ', $selects);

return "(SELECT value FROM ($unions) ORDER BY LENGTH(value) ASC FETCH FIRST 1 ROWS ONLY)";
}
}
9 changes: 9 additions & 0 deletions src/DQLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
namespace Yiisoft\Db\Oracle;

use Yiisoft\Db\Expression\ExpressionInterface;
use Yiisoft\Db\Expression\Function\ArrayMerge;
use Yiisoft\Db\Expression\Function\Longest;
use Yiisoft\Db\Expression\Function\Shortest;
use Yiisoft\Db\Oracle\Builder\ArrayMergeBuilder;
use Yiisoft\Db\Oracle\Builder\InBuilder;
use Yiisoft\Db\Oracle\Builder\LikeBuilder;
use Yiisoft\Db\Oracle\Builder\LongestBuilder;
use Yiisoft\Db\Oracle\Builder\ShortestBuilder;
use Yiisoft\Db\Query\Query;
use Yiisoft\Db\QueryBuilder\AbstractDQLQueryBuilder;
use Yiisoft\Db\QueryBuilder\Condition\In;
Expand Down Expand Up @@ -89,6 +95,9 @@ protected function defaultExpressionBuilders(): array
NotIn::class => InBuilder::class,
Like::class => LikeBuilder::class,
NotLike::class => LikeBuilder::class,
ArrayMerge::class => ArrayMergeBuilder::class,
Longest::class => LongestBuilder::class,
Shortest::class => ShortestBuilder::class,
];
}
}
76 changes: 76 additions & 0 deletions tests/Provider/QueryBuilderProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
use Yiisoft\Db\Constant\PseudoType;
use Yiisoft\Db\Constant\ReferentialAction;
use Yiisoft\Db\Constraint\ForeignKey;
use Yiisoft\Db\Expression\ArrayExpression;
use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\Expression\Function\ArrayMerge;
use Yiisoft\Db\Expression\Param;
use Yiisoft\Db\Oracle\Column\ColumnBuilder;
use Yiisoft\Db\Oracle\Tests\Support\TestTrait;
Expand Down Expand Up @@ -453,4 +455,78 @@ public static function delete(): array
$values['base'][2] = 'DELETE FROM "user" WHERE ("is_enabled" = \'0\') AND ("power" = WRONG_POWER())';
return $values;
}

public static function lengthBuilder(): array
{
$data = parent::lengthBuilder();

$data['query'][1] = "LENGTH((SELECT 'four' FROM DUAL))";

return $data;
}

public static function multiOperandFunctionClasses(): array
{
return [
...parent::multiOperandFunctionClasses(),
ArrayMerge::class => [ArrayMerge::class],
];
}

public static function multiOperandFunctionBuilder(): array
{
$data = parent::multiOperandFunctionBuilder();

$data['Greatest with 4 operands'][2] = 'GREATEST(1, 1.5, 1 + 2, (SELECT 10 FROM DUAL))';
$data['Least with 4 operands'][2] = 'LEAST(1, 1.5, 1 + 2, (SELECT 10 FROM DUAL))';
$data['Longest with 2 operands'][2] = <<<SQL
(SELECT value FROM (SELECT 'short' AS value FROM DUAL UNION SELECT :qp0 AS value FROM DUAL) ORDER BY LENGTH(value) DESC FETCH FIRST 1 ROWS ONLY)
SQL;
$data['Longest with 3 operands'][2] = <<<SQL
(SELECT value FROM (SELECT 'short' AS value FROM DUAL UNION SELECT (SELECT 'longest' FROM DUAL) AS value FROM DUAL UNION SELECT :qp0 AS value FROM DUAL) ORDER BY LENGTH(value) DESC FETCH FIRST 1 ROWS ONLY)
SQL;
$data['Shortest with 2 operands'][2] = <<<SQL
(SELECT value FROM (SELECT 'short' AS value FROM DUAL UNION SELECT :qp0 AS value FROM DUAL) ORDER BY LENGTH(value) ASC FETCH FIRST 1 ROWS ONLY)
SQL;
$data['Shortest with 3 operands'][2] = <<<SQL
(SELECT value FROM (SELECT 'short' AS value FROM DUAL UNION SELECT (SELECT 'longest' FROM DUAL) AS value FROM DUAL UNION SELECT :qp0 AS value FROM DUAL) ORDER BY LENGTH(value) ASC FETCH FIRST 1 ROWS ONLY)
SQL;

$stringParam = new Param('[3,4,5]', DataType::STRING);

return [
...$data,
'ArrayMerge with 1 operand' => [
ArrayMerge::class,
["'[1,2,3]'"],
"('[1,2,3]')",
[1, 2, 3],
],
'ArrayMerge with 2 operands' => [
ArrayMerge::class,
["'[1,2,3]'", $stringParam],
'(SELECT JSON_ARRAYAGG(value) AS value FROM ('
. "SELECT value FROM JSON_TABLE('[1,2,3]', '$[*]' COLUMNS(value PATH '$'))"
. " UNION SELECT value FROM JSON_TABLE(:qp0, '$[*]' COLUMNS(value PATH '$'))))",
[1, 2, 3, 4, 5],
[':qp0' => $stringParam],
],
'ArrayMerge with 4 operands' => [
ArrayMerge::class,
["'[1,2,3]'", [5, 6, 7], $stringParam, self::getDb()->select(new ArrayExpression([9, 10]))],
'(SELECT JSON_ARRAYAGG(value) AS value FROM ('
. "SELECT value FROM JSON_TABLE('[1,2,3]', '$[*]' COLUMNS(value PATH '$'))"
. " UNION SELECT value FROM JSON_TABLE(:qp0, '$[*]' COLUMNS(value PATH '$'))"
. " UNION SELECT value FROM JSON_TABLE(:qp1, '$[*]' COLUMNS(value PATH '$'))"
. " UNION SELECT value FROM JSON_TABLE((SELECT :qp2 FROM DUAL), '$[*]' COLUMNS(value PATH '$'))"
. '))',
[1, 2, 3, 4, 5, 6, 7, 9, 10],
[
':qp0' => new Param('[5,6,7]', DataType::STRING),
':qp1' => $stringParam,
':qp2' => new Param('[9,10]', DataType::STRING),
],
],
];
}
}
Loading
Loading