From 3fd1612d2612d135ffd691507a162a542d2fd36c Mon Sep 17 00:00:00 2001 From: tarasom Date: Wed, 24 Sep 2025 22:37:16 +0300 Subject: [PATCH 1/3] feat: Add support for `@@session.time_zone` system variable --- .../Expression/VariableEvaluator.php | 23 +++++++ tests/VariableEvaluatorTest.php | 66 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 tests/VariableEvaluatorTest.php diff --git a/src/Processor/Expression/VariableEvaluator.php b/src/Processor/Expression/VariableEvaluator.php index c029e606..cccf6e0e 100644 --- a/src/Processor/Expression/VariableEvaluator.php +++ b/src/Processor/Expression/VariableEvaluator.php @@ -1,6 +1,8 @@ variableName, '@') === 0) { + return self::getSystemVariable(substr($expr->variableName, 1)); + } + if (\array_key_exists($expr->variableName, $scope->variables)) { return $scope->variables[$expr->variableName]; } return null; } + + /** + * @param string $variableName + * + * @return string + * @throws ProcessorException + */ + private static function getSystemVariable(string $variableName): string + { + switch ($variableName) { + case 'session.time_zone': + return date_default_timezone_get(); + default: + throw new ProcessorException("System variable $variableName is not supported yet!"); + } + } } diff --git a/tests/VariableEvaluatorTest.php b/tests/VariableEvaluatorTest.php new file mode 100644 index 00000000..adb33543 --- /dev/null +++ b/tests/VariableEvaluatorTest.php @@ -0,0 +1,66 @@ +getPdo() + ->prepare('SELECT @@session.time_zone'); + $query->execute(); + $result = $query->fetch(\PDO::FETCH_COLUMN); + + $this->assertSame(date_default_timezone_get(), $result); + } + + public function testNotImplementedGlobalVariable(): void + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage("The SQL code SELECT @@collation_server; could not be evaluated"); + + $query = $this->getPdo() + ->prepare('SELECT @@collation_server;'); + $query->execute(); + $result = $query->fetch(\PDO::FETCH_COLUMN); + + $this->assertSame(date_default_timezone_get(), $result); + } + + public function testVariable(): void + { + $sql = " + SELECT (@var := @var + 2) AS `counter` + FROM (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) AS `virtual_rows` + CROSS JOIN (SELECT @var := 0) AS `vars`; + "; + + $query = $this->getPdo() + ->prepare($sql); + $query->execute(); + $result = $query->fetchAll(\PDO::FETCH_ASSOC); + $counters = array_map('intval', array_column($result, 'counter')); + $this->assertSame([2,4,6], $counters); + } + + private function getPdo(bool $strict_mode = false): \PDO + { + $connection_string = 'mysql:foo;dbname=test;'; + $options = $strict_mode ? [\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET sql_mode="STRICT_ALL_TABLES"'] : []; + + if (\PHP_MAJOR_VERSION === 8) { + return new \Vimeo\MysqlEngine\Php8\FakePdo($connection_string, '', '', $options); + } + + return new \Vimeo\MysqlEngine\Php7\FakePdo($connection_string, '', '', $options); + } +} From 4dccefa689482af930c3f00fe86c9df6291a8b5d Mon Sep 17 00:00:00 2001 From: Taras Omelianchuk Date: Thu, 25 Sep 2025 14:46:27 +0300 Subject: [PATCH 2/3] fix: Resolve evaluation issues in SUM() and CONVERT_TZ() functions --- .../Expression/FunctionEvaluator.php | 16 +++++++- tests/EndToEndTest.php | 40 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/Processor/Expression/FunctionEvaluator.php b/src/Processor/Expression/FunctionEvaluator.php index 60c81b6d..c6db5e8f 100644 --- a/src/Processor/Expression/FunctionEvaluator.php +++ b/src/Processor/Expression/FunctionEvaluator.php @@ -353,9 +353,13 @@ private static function sqlCount( } /** - * @param array $columns + * @param FakePdoInterface $conn + * @param Scope $scope + * @param FunctionExpression $expr + * @param QueryResult $result * - * @return ?numeric + * @return float|int|mixed|string|null + * @throws ProcessorException */ private static function sqlSum( FakePdoInterface $conn, @@ -368,6 +372,10 @@ private static function sqlSum( $sum = 0; if (!$result->rows) { + if ($expr instanceof FunctionExpression) { + return self::evaluate($conn, $scope, $expr, [], $result); + } + return null; } @@ -1575,6 +1583,10 @@ private static function sqlConvertTz( throw new \InvalidArgumentException("CONVERT_TZ() requires exactly 3 arguments"); } + if ($args[0] instanceof ColumnExpression && empty($row)) { + return null; + } + /** @var string|null $dtValue */ $dtValue = Evaluator::evaluate($conn, $scope, $args[0], $row, $result); /** @var string|null $fromTzValue */ diff --git a/tests/EndToEndTest.php b/tests/EndToEndTest.php index bd6118d1..f59be4f0 100644 --- a/tests/EndToEndTest.php +++ b/tests/EndToEndTest.php @@ -1516,4 +1516,44 @@ public function leastWithExceptionProvider(): iterable yield ['Should fail with single argument' => [1]]; yield ['Should fail without any arguments' => []]; } + + public function testNestedFunctions() + { + $pdo = self::getConnectionToFullDB(); + + $query = $pdo->prepare(" + SELECT + SUM( + TIMESTAMPDIFF( + SECOND, + CONVERT_TZ('2025-12-31 22:59:59', 'Europe/Kyiv', 'Europe/Kyiv'), + CONVERT_TZ('2025-12-31 23:59:59', 'Europe/Kyiv', 'Europe/Kyiv') + ) + ) + "); + $query->execute(); + + $this->assertSame(3600, (int)$query->fetchColumn()); + } + + public function testNestedFunctionsFromDB() + { + $pdo = self::getConnectionToFullDB(); + $count = $pdo->query("SELECT COUNT(*) FROM video_game_characters")->fetchColumn(); + + $query = $pdo->prepare(" + SELECT SUM( + TIMESTAMPDIFF( + SECOND, + CONVERT_TZ(`created_on`, 'Europe/Kyiv', 'Europe/Kyiv'), + CONVERT_TZ(`created_on` + INTERVAL 1 SECOND, 'Europe/Kyiv', 'Europe/Kyiv') + ) + ) + FROM `video_game_characters` + "); + + $query->execute(); + + $this->assertSame((int)$count, (int)$query->fetchColumn()); + } } From bfa5473f6b0156c39e5f13f8f7434d6f929f8487 Mon Sep 17 00:00:00 2001 From: Taras Omelianchuk Date: Thu, 25 Sep 2025 19:50:24 +0300 Subject: [PATCH 3/3] fix: Correct SUM() evaluation for empty result sets --- .../Expression/FunctionEvaluator.php | 3 ++- tests/EndToEndTest.php | 26 ++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Processor/Expression/FunctionEvaluator.php b/src/Processor/Expression/FunctionEvaluator.php index c6db5e8f..b82d27a5 100644 --- a/src/Processor/Expression/FunctionEvaluator.php +++ b/src/Processor/Expression/FunctionEvaluator.php @@ -372,7 +372,8 @@ private static function sqlSum( $sum = 0; if (!$result->rows) { - if ($expr instanceof FunctionExpression) { + $isQueryWithoutFromClause = empty($result->columns); + if ($expr instanceof FunctionExpression && $isQueryWithoutFromClause) { return self::evaluate($conn, $scope, $expr, [], $result); } diff --git a/tests/EndToEndTest.php b/tests/EndToEndTest.php index f59be4f0..1d5352dc 100644 --- a/tests/EndToEndTest.php +++ b/tests/EndToEndTest.php @@ -2,9 +2,6 @@ namespace Vimeo\MysqlEngine\Tests; use PDOException; -use Vimeo\MysqlEngine\Parser\Token; -use Vimeo\MysqlEngine\Query\Expression\ColumnExpression; -use Vimeo\MysqlEngine\TokenType; class EndToEndTest extends \PHPUnit\Framework\TestCase { @@ -13,6 +10,29 @@ public function tearDown() : void \Vimeo\MysqlEngine\Server::reset(); } + public function testSumWithEmptyResultSetReturnsNull() + { + $sql = " + SELECT + SUM( + IF( + id > 1, + 0, + 1 + ) + ) + FROM + video_game_characters + WHERE + id > 100; + "; + + $pdo = self::getConnectionToFullDB(); + $query= $pdo->query($sql); + + $this->assertNull($query->fetchColumn()); + } + public function testSelectEmptyResults() { $pdo = self::getConnectionToFullDB();