From ed9f9fccd46a59fa2eed834eda8327546146eba8 Mon Sep 17 00:00:00 2001 From: dineshkrishnan24 Date: Wed, 3 Sep 2025 22:26:40 +0530 Subject: [PATCH 1/2] Update Min & Max function to compare with null values. --- .../Expression/FunctionEvaluator.php | 20 +- tests/FunctionEvaluatorTest.php | 179 ++++++++++++++++++ 2 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 tests/FunctionEvaluatorTest.php diff --git a/src/Processor/Expression/FunctionEvaluator.php b/src/Processor/Expression/FunctionEvaluator.php index 8294c504..16e2a28b 100644 --- a/src/Processor/Expression/FunctionEvaluator.php +++ b/src/Processor/Expression/FunctionEvaluator.php @@ -435,14 +435,20 @@ private static function sqlMin( $value = Evaluator::evaluate($conn, $scope, $expr, $row, $result); - if (!\is_scalar($value)) { + if (!\is_scalar($value) && !\is_null($value)) { throw new \TypeError('Bad min value'); } $values[] = $value; } - return self::castAggregate(\min($values), $expr, $result); + $min_value = \min($values); + + if ($min_value === null) { + return null; + } + + return self::castAggregate($min_value, $expr, $result); } /** @@ -470,14 +476,20 @@ private static function sqlMax( $value = Evaluator::evaluate($conn, $scope, $expr, $row, $result); - if (!\is_scalar($value)) { + if (!\is_scalar($value) && !\is_null($value)) { throw new \TypeError('Bad max value'); } $values[] = $value; } - return self::castAggregate(\max($values), $expr, $result); + $max_value = \max($values); + + if ($max_value === null) { + return null; + } + + return self::castAggregate($max_value, $expr, $result); } /** diff --git a/tests/FunctionEvaluatorTest.php b/tests/FunctionEvaluatorTest.php new file mode 100644 index 00000000..ea0b1a2f --- /dev/null +++ b/tests/FunctionEvaluatorTest.php @@ -0,0 +1,179 @@ + ['SELECT ?', 1], + ':field' => ['SELECT :field', ':field'], + 'field' => ['SELECT :field', 'field'], + ]; + } + + /** + * @dataProvider maxValueProvider + */ + public function testSqlMax(array $rows, ?int $expected) : void + { + $conn = $this->createMock(FakePdoInterface::class); + $scope = $this->createMock(Scope::class); + $queryResult = $this->createMock(QueryResult::class); + /** @var array> $rows */ + $queryResult->rows = $rows; + + $token = new \Vimeo\MysqlEngine\Parser\Token( + \Vimeo\MysqlEngine\TokenType::SQLFUNCTION, + 'MAX', + 'MAX', + 0 + ); + + $exp = new ColumnExpression( + new \Vimeo\MysqlEngine\Parser\Token(\Vimeo\MysqlEngine\TokenType::IDENTIFIER, "value", '', 0) + ); + + $functionExpr = new FunctionExpression( + $token, + [$exp], + false + ); + + $refMethod = new \ReflectionMethod(FunctionEvaluator::class, 'sqlMax'); + $refMethod->setAccessible(true); + + if ($expected === -1) { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Bad max value'); + } + + /** @var int|null $actual */ + $actual = $refMethod->invoke(null, $conn, $scope, $functionExpr, $queryResult); + + if ($expected !== -1) { + $this->assertSame($expected, $actual); + } + } + + /** + * @dataProvider minValueProvider + */ + public function testSqlMin(array $rows, ?int $expected) : void + { + $conn = $this->createMock(FakePdoInterface::class); + $scope = $this->createMock(Scope::class); + $queryResult = $this->createMock(QueryResult::class); + /** @var array> $rows */ + $queryResult->rows = $rows; + + $token = new \Vimeo\MysqlEngine\Parser\Token( + \Vimeo\MysqlEngine\TokenType::SQLFUNCTION, + 'MIN', + 'MIN', + 0 + ); + + $exp = new ColumnExpression( + new \Vimeo\MysqlEngine\Parser\Token(\Vimeo\MysqlEngine\TokenType::IDENTIFIER, "value", '', 0) + ); + + $functionExpr = new FunctionExpression( + $token, + [$exp], + false + ); + + $refMethod = new \ReflectionMethod(FunctionEvaluator::class, 'sqlMin'); + $refMethod->setAccessible(true); + + if ($expected === -1) { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Bad min value'); + } + + /** @var int|null $actual */ + $actual = $refMethod->invoke(null, $conn, $scope, $functionExpr, $queryResult); + + if ($expected !== -1) { + $this->assertSame($expected, $actual); + } + } + + + public static function maxValueProvider(): array + { + return [ + 'null when no rows' => [ + 'rows' => [], + 'expected' => null, + ], + 'max of scalar values' => [ + 'rows' => [ + ['value' => 10], + ['value' => 25], + ['value' => 5], + ], + 'expected' => 25, + ], + 'null values mixed in' => [ + 'rows' => [ + ['value' => null], + ['value' => 7], + ['value' => null], + ], + 'expected' => 7, + ], + 'non scalar values' => [ + 'rows' => [ + ['value' => ['test']], + ], + 'expected' => -1, + ], + ]; + } + + public static function minValueProvider(): array + { + return [ + 'null when no rows' => [ + 'rows' => [], + 'expected' => null, + ], + 'min of scalar values' => [ + 'rows' => [ + ['value' => 10], + ['value' => 25], + ['value' => 5], + ], + 'expected' => 5, + ], + 'null values mixed in' => [ + 'rows' => [ + ['value' => null], + ['value' => 7], + ['value' => null], + ], + 'expected' => null, + ], + 'non scalar values' => [ + 'rows' => [ + ['value' => ['test']], + ], + 'expected' => -1, + ], + ]; + } +} \ No newline at end of file From 6fda7afcff9ba755129fdf2ce50eaeb4ac507b91 Mon Sep 17 00:00:00 2001 From: dineshkrishnan24 Date: Tue, 9 Sep 2025 00:56:46 +0530 Subject: [PATCH 2/2] test case updated as per review suggestion --- tests/FunctionEvaluatorTest.php | 211 ++++++++++++-------------------- 1 file changed, 77 insertions(+), 134 deletions(-) diff --git a/tests/FunctionEvaluatorTest.php b/tests/FunctionEvaluatorTest.php index ea0b1a2f..473c47dc 100644 --- a/tests/FunctionEvaluatorTest.php +++ b/tests/FunctionEvaluatorTest.php @@ -5,175 +5,118 @@ namespace Vimeo\MysqlEngine\Tests; use PHPUnit\Framework\TestCase; -use Vimeo\MysqlEngine\FakePdoInterface; -use Vimeo\MysqlEngine\Processor\Expression\FunctionEvaluator; -use Vimeo\MysqlEngine\Processor\QueryResult; -use Vimeo\MysqlEngine\Processor\Scope; -use Vimeo\MysqlEngine\Query\Expression\ColumnExpression; -use Vimeo\MysqlEngine\Query\Expression\FunctionExpression; class FunctionEvaluatorTest extends TestCase { - public function dataFunction(): array + public function tearDown() : void { - return [ - 'numeric' => ['SELECT ?', 1], - ':field' => ['SELECT :field', ':field'], - 'field' => ['SELECT :field', 'field'], - ]; + \Vimeo\MysqlEngine\Server::reset(); } /** * @dataProvider maxValueProvider */ - public function testSqlMax(array $rows, ?int $expected) : void - { - $conn = $this->createMock(FakePdoInterface::class); - $scope = $this->createMock(Scope::class); - $queryResult = $this->createMock(QueryResult::class); - /** @var array> $rows */ - $queryResult->rows = $rows; - - $token = new \Vimeo\MysqlEngine\Parser\Token( - \Vimeo\MysqlEngine\TokenType::SQLFUNCTION, - 'MAX', - 'MAX', - 0 - ); - - $exp = new ColumnExpression( - new \Vimeo\MysqlEngine\Parser\Token(\Vimeo\MysqlEngine\TokenType::IDENTIFIER, "value", '', 0) - ); - - $functionExpr = new FunctionExpression( - $token, - [$exp], - false - ); - - $refMethod = new \ReflectionMethod(FunctionEvaluator::class, 'sqlMax'); - $refMethod->setAccessible(true); - - if ($expected === -1) { - $this->expectException(\TypeError::class); - $this->expectExceptionMessage('Bad max value'); - } - - /** @var int|null $actual */ - $actual = $refMethod->invoke(null, $conn, $scope, $functionExpr, $queryResult); - - if ($expected !== -1) { - $this->assertSame($expected, $actual); - } - } - - /** - * @dataProvider minValueProvider - */ - public function testSqlMin(array $rows, ?int $expected) : void + public function testSqlMax(string $sql, ?string $expected, bool $is_db_number) : void { - $conn = $this->createMock(FakePdoInterface::class); - $scope = $this->createMock(Scope::class); - $queryResult = $this->createMock(QueryResult::class); - /** @var array> $rows */ - $queryResult->rows = $rows; - - $token = new \Vimeo\MysqlEngine\Parser\Token( - \Vimeo\MysqlEngine\TokenType::SQLFUNCTION, - 'MIN', - 'MIN', - 0 - ); - - $exp = new ColumnExpression( - new \Vimeo\MysqlEngine\Parser\Token(\Vimeo\MysqlEngine\TokenType::IDENTIFIER, "value", '', 0) - ); - - $functionExpr = new FunctionExpression( - $token, - [$exp], - false - ); - - $refMethod = new \ReflectionMethod(FunctionEvaluator::class, 'sqlMin'); - $refMethod->setAccessible(true); - - if ($expected === -1) { - $this->expectException(\TypeError::class); - $this->expectExceptionMessage('Bad min value'); - } - - /** @var int|null $actual */ - $actual = $refMethod->invoke(null, $conn, $scope, $functionExpr, $queryResult); - - if ($expected !== -1) { - $this->assertSame($expected, $actual); + $query = self::getConnectionToFullDB()->prepare($sql); + $query->execute(); + /** @var array> $result */ + $result = $query->fetchAll(\PDO::FETCH_ASSOC); + + if ($is_db_number) { + $this->assertNotEmpty($result); + $this->assertNotNull($result[0]['max']); + } else { + $this->assertSame([['max' => $expected]], $result); } } - public static function maxValueProvider(): array { return [ 'null when no rows' => [ - 'rows' => [], + 'sql' => 'SELECT MAX(null) as `max` FROM `video_game_characters`', 'expected' => null, + 'is_db_number' => false, ], 'max of scalar values' => [ - 'rows' => [ - ['value' => 10], - ['value' => 25], - ['value' => 5], - ], - 'expected' => 25, - ], - 'null values mixed in' => [ - 'rows' => [ - ['value' => null], - ['value' => 7], - ['value' => null], - ], - 'expected' => 7, + 'sql' => 'SELECT MAX(10) as `max` FROM `video_game_characters`', + 'expected' => '10', + 'is_db_number' => false, ], - 'non scalar values' => [ - 'rows' => [ - ['value' => ['test']], - ], - 'expected' => -1, + 'max in DB values' => [ + 'sql' => 'SELECT MAX(id) as `max` FROM `video_game_characters`', + 'expected' => '', + 'is_db_number' => true, ], ]; } + /** + * @dataProvider minValueProvider + */ + public function testSqlMin(string $sql, ?string $expected, bool $is_db_number) : void + { + $query = self::getConnectionToFullDB()->prepare($sql); + $query->execute(); + /** @var array> $result */ + $result = $query->fetchAll(\PDO::FETCH_ASSOC); + + if ($is_db_number) { + $this->assertNotEmpty($result); + $this->assertNotNull($result[0]['min']); + } else { + $this->assertSame([['min' => $expected]], $result); + } + } + public static function minValueProvider(): array { return [ 'null when no rows' => [ - 'rows' => [], + 'sql' => 'SELECT MIN(null) as `min` FROM `video_game_characters`', 'expected' => null, + 'is_db_number' => false, ], 'min of scalar values' => [ - 'rows' => [ - ['value' => 10], - ['value' => 25], - ['value' => 5], - ], - 'expected' => 5, - ], - 'null values mixed in' => [ - 'rows' => [ - ['value' => null], - ['value' => 7], - ['value' => null], - ], - 'expected' => null, + 'sql' => 'SELECT MIN(10) as `min` FROM `video_game_characters`', + 'expected' => '10', + 'is_db_number' => false, ], - 'non scalar values' => [ - 'rows' => [ - ['value' => ['test']], - ], - 'expected' => -1, + 'min in DB values' => [ + 'sql' => 'SELECT MIN(id) as `min` FROM `video_game_characters`', + 'expected' => '', + 'is_db_number' => true, ], ]; } + + private static function getPdo(string $connection_string, bool $strict_mode = false) : \PDO + { + $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); + } + + private static function getConnectionToFullDB(bool $emulate_prepares = true, bool $strict_mode = false) : \PDO + { + $pdo = self::getPdo('mysql:foo;dbname=test;', $strict_mode); + + $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, $emulate_prepares); + + // create table + $pdo->prepare(file_get_contents(__DIR__ . '/fixtures/create_table.sql'))->execute(); + + // insertData + $pdo->prepare(file_get_contents(__DIR__ . '/fixtures/bulk_character_insert.sql'))->execute(); + $pdo->prepare(file_get_contents(__DIR__ . '/fixtures/bulk_enemy_insert.sql'))->execute(); + $pdo->prepare(file_get_contents(__DIR__ . '/fixtures/bulk_tag_insert.sql'))->execute(); + + return $pdo; + } } \ No newline at end of file