Skip to content

Commit 94994d1

Browse files
committed
fix: Support ORDER AND SEPARATOR in GROUP_CONCAT
1 parent 95cf197 commit 94994d1

File tree

5 files changed

+87
-22
lines changed

5 files changed

+87
-22
lines changed

src/Parser/ExpressionParser.php

+20-5
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,12 @@ public function __construct(
127127
/**
128128
* @param array<int, Token> $tokens
129129
*
130-
* @return array{0: bool, 1: array<int, Expression>}
130+
* @return array{
131+
* 0: bool,
132+
* 1: list<Expression>,
133+
* 2: ?array<int, array{direction: 'ASC'|'DESC', expression: Expression}>,
134+
* 3: ?Expression
135+
* }
131136
*/
132137
private function getListExpression(array $tokens)
133138
{
@@ -137,6 +142,9 @@ private function getListExpression(array $tokens)
137142
$needs_comma = false;
138143
$args = [];
139144

145+
$order_by = null;
146+
$separator = null;
147+
140148
if (isset($tokens[0]) && $tokens[0]->value == "DISTINCT") {
141149
$distinct = true;
142150
$pos++;
@@ -155,21 +163,28 @@ private function getListExpression(array $tokens)
155163
}
156164
}
157165

158-
159166
if ($arg->value === 'ORDER') {
160167
$p = new OrderByParser($pos, $tokens);
161168
[$pos, $order_by] = $p->parse();
169+
$pos++; // ORDER BY の次の式の先頭に position を合わせる
162170
continue;
163171
}
164172

173+
if ($arg->value === 'SEPARATOR') {
174+
$p = new ExpressionParser($tokens, $pos);
175+
list(, $expr) = $p->buildWithPointer();
176+
$separator = $expr;
177+
break;
178+
}
179+
165180
$p = new ExpressionParser($tokens, $pos - 1);
166181
list($pos, $expr) = $p->buildWithPointer();
167182
$args[] = $expr;
168183
$pos++;
169184
$needs_comma = true;
170185
}
171186

172-
return [$distinct, $args, $order_by ?? null];
187+
return [$distinct, $args, $order_by, $separator];
173188
}
174189

175190
/**
@@ -283,8 +298,8 @@ function ($token) {
283298

284299
$fn = new CastExpression($token, $expr, $type);
285300
} else {
286-
list($distinct, $args) = $this->getListExpression($arg_tokens);
287-
$fn = new FunctionExpression($token, $args, $distinct);
301+
list($distinct, $args, $order, $separator) = $this->getListExpression($arg_tokens);
302+
$fn = new FunctionExpression($token, $args, $distinct, $order, $separator);
288303
}
289304

290305
$this->pointer = $closing_paren_pointer;

src/Processor/Expression/BinaryOperatorEvaluator.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ public static function evaluate(
6666
$left,
6767
$right,
6868
],
69-
false
69+
false,
70+
null,
71+
null
7072
),
7173
$row,
7274
$result

src/Processor/Expression/FunctionEvaluator.php

+43-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22
namespace Vimeo\MysqlEngine\Processor\Expression;
33

4+
use Closure;
45
use Vimeo\MysqlEngine\FakePdoInterface;
56
use Vimeo\MysqlEngine\Processor\ProcessorException;
67
use Vimeo\MysqlEngine\Processor\QueryResult;
@@ -938,26 +939,61 @@ private static function sqlConcat(
938939
* @param array<string, mixed> $row
939940
*/
940941
private static function sqlGroupConcat(
941-
FakePdoInterface $conn,
942-
Scope $scope,
942+
FakePdoInterface $conn,
943+
Scope $scope,
943944
FunctionExpression $expr,
944-
QueryResult $result
945-
): string {
945+
QueryResult $result
946+
): string
947+
{
946948
$args = $expr->args;
947949

948950
$items = [];
949951
foreach ($result->rows as $row) {
950952
$tmp_str = "";
953+
/** @var Closure(array{direction: "ASC"|"DESC", expression: Expression}): ?scalar $func */
954+
$func = function (array $order) use ($result, $row, $scope, $conn) { // @phpstan-ignore-line
955+
/** @var array{expression: Expression} $order */
956+
return Evaluator::evaluate($conn, $scope, $order["expression"], $row, $result);
957+
};
958+
$orders = array_map(
959+
$func,
960+
$expr->order ?? []
961+
);
951962
foreach ($args as $arg) {
952-
$val = (string) Evaluator::evaluate($conn, $scope, $arg, $row, $result);
963+
$val = (string)Evaluator::evaluate($conn, $scope, $arg, $row, $result);
953964
$tmp_str .= $val;
954965
}
955966
if ($tmp_str !== "" && (!$expr->distinct || !isset($items[$tmp_str]))) {
956-
$items[$tmp_str] = $tmp_str;
967+
$items[$tmp_str] = ["val" => $tmp_str, "orders" => $orders];
957968
}
958969
}
959970

960-
return implode(",", array_values($items));
971+
usort($items, function ($a, $b) use ($expr): int {
972+
/**
973+
* @var array{val: string, orders: array<int, scalar>} $a
974+
* @var array{val: string, orders: array<int, scalar>} $b
975+
*/
976+
for ($i = 0; $i < count($expr->order ?? []); $i++) {
977+
$direction = $expr->order[$i]["direction"] ?? 'ASC';
978+
$a_val = $a["orders"][$i];
979+
$b_val = $b["orders"][$i];
980+
981+
if ($a_val < $b_val) {
982+
return ($direction === 'ASC') ? -1 : 1;
983+
} elseif ($a_val > $b_val) {
984+
return ($direction === 'ASC') ? 1 : -1;
985+
}
986+
}
987+
return 0;
988+
});
989+
990+
if (isset($expr->separator)) {
991+
$separator = (string)(Evaluator::evaluate($conn, $scope, $expr->separator, [], $result));
992+
} else {
993+
$separator = ",";
994+
}
995+
996+
return implode($separator, array_column($items, 'val'));
961997
}
962998

963999
/**

src/Query/Expression/FunctionExpression.php

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
<?php
2+
23
namespace Vimeo\MysqlEngine\Query\Expression;
34

45
use Vimeo\MysqlEngine\Parser\Token;
5-
use Vimeo\MysqlEngine\TokenType;
6-
use Vimeo\MysqlEngine\Processor\ProcessorException;
76

87
final class FunctionExpression extends Expression
98
{
@@ -31,12 +30,23 @@ final class FunctionExpression extends Expression
3130
* @var bool
3231
*/
3332
public $distinct;
33+
/** @var ?array<int, array{expression: Expression, direction: 'ASC'|'DESC'}> $order */
34+
public $order;
35+
/** @var ?Expression $separator */
36+
public $separator;
3437

3538
/**
3639
* @param Token $token
37-
* @param array<int, Expression> $args
40+
* @param array<int, Expression> $args
41+
* @param ?array<int, array{expression: Expression, direction: 'ASC'|'DESC'}> $order
3842
*/
39-
public function __construct(Token $token, array $args, bool $distinct)
43+
public function __construct(
44+
Token $token,
45+
array $args,
46+
bool $distinct,
47+
?array $order,
48+
?Expression $separator
49+
)
4050
{
4151
$this->token = $token;
4252
$this->args = $args;
@@ -45,8 +55,10 @@ public function __construct(Token $token, array $args, bool $distinct)
4555
$this->precedence = 0;
4656
$this->functionName = $token->value;
4757
$this->name = $token->value;
48-
$this->operator = (string) $this->type;
58+
$this->operator = $this->type;
4959
$this->start = $token->start;
60+
$this->separator = $separator;
61+
$this->order = $order;
5062
}
5163

5264
/**
@@ -57,7 +69,7 @@ public function functionName()
5769
return $this->functionName;
5870
}
5971

60-
public function hasAggregate() : bool
72+
public function hasAggregate(): bool
6173
{
6274
if ($this->functionName === 'COUNT'
6375
|| $this->functionName === 'SUM'

tests/EndToEndTest.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,7 @@ public function testGroupConcat()
917917
$pdo = self::getConnectionToFullDB(false);
918918

919919
$query = $pdo->prepare(
920-
'SELECT `type`, GROUP_CONCAT(DISTINCT `profession`) as `profession_list`
920+
'SELECT `type`, GROUP_CONCAT(DISTINCT `profession` ORDER BY `name` SEPARATOR \' \') as `profession_list`
921921
FROM `video_game_characters`
922922
GROUP BY `type`'
923923
);
@@ -928,11 +928,11 @@ public function testGroupConcat()
928928
[
929929
[
930930
"type" => "hero",
931-
"profession_list" => "plumber,hedgehog,earthworm,monkey,pokemon,princess,boxer,yellow circle,dinosaur,not sure,sure"
931+
"profession_list" => "monkey sure earthworm not sure boxer plumber yellow circle pokemon princess hedgehog dinosaur"
932932
],
933933
[
934934
"type" => "villain",
935-
"profession_list" => "evil dinosaur,evil doctor,throwing shit from clouds,evil chain dude"
935+
"profession_list" => "evil dinosaur evil chain dude evil doctor throwing shit from clouds"
936936
],
937937
],
938938
$query->fetchAll(\PDO::FETCH_ASSOC)

0 commit comments

Comments
 (0)