diff --git a/src/Parser/ExpressionParser.php b/src/Parser/ExpressionParser.php index 9abb2b0b..7b719e47 100644 --- a/src/Parser/ExpressionParser.php +++ b/src/Parser/ExpressionParser.php @@ -127,7 +127,12 @@ public function __construct( /** * @param array $tokens * - * @return array{0: bool, 1: array} + * @return array{ + * 0: bool, + * 1: list, + * 2: ?array, + * 3: ?Expression + * } */ private function getListExpression(array $tokens) { @@ -137,10 +142,17 @@ private function getListExpression(array $tokens) $needs_comma = false; $args = []; + $order_by = null; + $separator = null; + + if (isset($tokens[0]) && $tokens[0]->value == "DISTINCT") { + $distinct = true; + $pos++; + } + while ($pos < $token_count) { $arg = $tokens[$pos]; - if ($arg->value === ',') { if ($needs_comma) { $needs_comma = false; @@ -151,6 +163,20 @@ private function getListExpression(array $tokens) } } + if ($arg->value === 'ORDER') { + $p = new OrderByParser($pos, $tokens); + [$pos, $order_by] = $p->parse(); + $pos++; // ORDER BY の次の式の先頭に position を合わせる + continue; + } + + if ($arg->value === 'SEPARATOR') { + $p = new ExpressionParser($tokens, $pos); + list(, $expr) = $p->buildWithPointer(); + $separator = $expr; + break; + } + $p = new ExpressionParser($tokens, $pos - 1); list($pos, $expr) = $p->buildWithPointer(); $args[] = $expr; @@ -158,7 +184,7 @@ private function getListExpression(array $tokens) $needs_comma = true; } - return [$distinct, $args]; + return [$distinct, $args, $order_by, $separator]; } /** @@ -272,8 +298,8 @@ function ($token) { $fn = new CastExpression($token, $expr, $type); } else { - list($distinct, $args) = $this->getListExpression($arg_tokens); - $fn = new FunctionExpression($token, $args, $distinct); + list($distinct, $args, $order, $separator) = $this->getListExpression($arg_tokens); + $fn = new FunctionExpression($token, $args, $distinct, $order, $separator); } $this->pointer = $closing_paren_pointer; diff --git a/src/Processor/Expression/BinaryOperatorEvaluator.php b/src/Processor/Expression/BinaryOperatorEvaluator.php index 3770a951..d0e512e3 100644 --- a/src/Processor/Expression/BinaryOperatorEvaluator.php +++ b/src/Processor/Expression/BinaryOperatorEvaluator.php @@ -66,7 +66,9 @@ public static function evaluate( $left, $right, ], - false + false, + null, + null ), $row, $result diff --git a/src/Processor/Expression/FunctionEvaluator.php b/src/Processor/Expression/FunctionEvaluator.php index 8294c504..a8aef58f 100644 --- a/src/Processor/Expression/FunctionEvaluator.php +++ b/src/Processor/Expression/FunctionEvaluator.php @@ -1,6 +1,7 @@ $row */ private static function sqlGroupConcat( - FakePdoInterface $conn, - Scope $scope, + FakePdoInterface $conn, + Scope $scope, FunctionExpression $expr, - array $row, - QueryResult $result - ): string { + QueryResult $result + ): string + { $args = $expr->args; - $final_concat = ""; - foreach ($args as $arg) { - $val = (string) Evaluator::evaluate($conn, $scope, $arg, $row, $result); - $final_concat .= $val; + $items = []; + foreach ($result->rows as $row) { + $tmp_str = ""; + /** @var Closure(array{direction: "ASC"|"DESC", expression: Expression}): ?scalar $func */ + /** @psalm-suppress MissingClosureReturnType */ + $func = function (array $order) use ($result, $row, $scope, $conn) { + /** @var array{expression: Expression} $order */ + return Evaluator::evaluate($conn, $scope, $order["expression"], $row, $result); + }; + $orders = array_map( + $func, + $expr->order ?? [] + ); + foreach ($args as $arg) { + $val = (string)Evaluator::evaluate($conn, $scope, $arg, $row, $result); + $tmp_str .= $val; + } + if ($tmp_str !== "" && (!$expr->distinct || !isset($items[$tmp_str]))) { + $items[$tmp_str] = ["val" => $tmp_str, "orders" => $orders]; + } } - return $final_concat; + usort($items, function ($a, $b) use ($expr): int { + /** + * @var array{val: string, orders: array} $a + * @var array{val: string, orders: array} $b + */ + for ($i = 0; $i < count($expr->order ?? []); $i++) { + $direction = $expr->order[$i]["direction"] ?? 'ASC'; + $a_val = $a["orders"][$i]; + $b_val = $b["orders"][$i]; + + if ($a_val < $b_val) { + return ($direction === 'ASC') ? -1 : 1; + } elseif ($a_val > $b_val) { + return ($direction === 'ASC') ? 1 : -1; + } + } + return 0; + }); + + if (isset($expr->separator)) { + $separator = (string)(Evaluator::evaluate($conn, $scope, $expr->separator, [], $result)); + } else { + $separator = ","; + } + + return implode($separator, array_column($items, 'val')); } /** diff --git a/src/Query/Expression/FunctionExpression.php b/src/Query/Expression/FunctionExpression.php index e573ffca..180760dd 100644 --- a/src/Query/Expression/FunctionExpression.php +++ b/src/Query/Expression/FunctionExpression.php @@ -1,9 +1,8 @@ $order */ + public $order; + /** @var ?Expression $separator */ + public $separator; /** * @param Token $token - * @param array $args + * @param array $args + * @param ?array $order */ - public function __construct(Token $token, array $args, bool $distinct) + public function __construct( + Token $token, + array $args, + bool $distinct, + ?array $order, + ?Expression $separator + ) { $this->token = $token; $this->args = $args; @@ -45,8 +55,10 @@ public function __construct(Token $token, array $args, bool $distinct) $this->precedence = 0; $this->functionName = $token->value; $this->name = $token->value; - $this->operator = (string) $this->type; + $this->operator = $this->type; $this->start = $token->start; + $this->separator = $separator; + $this->order = $order; } /** @@ -57,7 +69,7 @@ public function functionName() return $this->functionName; } - public function hasAggregate() : bool + public function hasAggregate(): bool { if ($this->functionName === 'COUNT' || $this->functionName === 'SUM' diff --git a/tests/EndToEndTest.php b/tests/EndToEndTest.php index d2cd95a3..87b96c77 100644 --- a/tests/EndToEndTest.php +++ b/tests/EndToEndTest.php @@ -1,4 +1,5 @@ prepare( + 'SELECT `type`, GROUP_CONCAT(DISTINCT `profession` ORDER BY `name` SEPARATOR \' \') as `profession_list` + FROM `video_game_characters` + GROUP BY `type`' + ); + + $query->execute(); + + $this->assertSame( + [ + [ + "type" => "hero", + "profession_list" => "monkey sure earthworm not sure boxer plumber yellow circle pokemon princess hedgehog dinosaur" + ], + [ + "type" => "villain", + "profession_list" => "evil dinosaur evil chain dude evil doctor throwing shit from clouds" + ], + ], + $query->fetchAll(\PDO::FETCH_ASSOC) + ); + } + public function testOrderBySecondDimensionAliased() { $pdo = self::getConnectionToFullDB(false);