Skip to content

Commit 5a1241f

Browse files
authored
Merge pull request #62 from vimeo/support-null-value-comparision-in-min-max
Update Min & Max function to compare with null values.
2 parents 25c92ff + 6fda7af commit 5a1241f

File tree

2 files changed

+138
-4
lines changed

2 files changed

+138
-4
lines changed

src/Processor/Expression/FunctionEvaluator.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -435,14 +435,20 @@ private static function sqlMin(
435435

436436
$value = Evaluator::evaluate($conn, $scope, $expr, $row, $result);
437437

438-
if (!\is_scalar($value)) {
438+
if (!\is_scalar($value) && !\is_null($value)) {
439439
throw new \TypeError('Bad min value');
440440
}
441441

442442
$values[] = $value;
443443
}
444444

445-
return self::castAggregate(\min($values), $expr, $result);
445+
$min_value = \min($values);
446+
447+
if ($min_value === null) {
448+
return null;
449+
}
450+
451+
return self::castAggregate($min_value, $expr, $result);
446452
}
447453

448454
/**
@@ -470,14 +476,20 @@ private static function sqlMax(
470476

471477
$value = Evaluator::evaluate($conn, $scope, $expr, $row, $result);
472478

473-
if (!\is_scalar($value)) {
479+
if (!\is_scalar($value) && !\is_null($value)) {
474480
throw new \TypeError('Bad max value');
475481
}
476482

477483
$values[] = $value;
478484
}
479485

480-
return self::castAggregate(\max($values), $expr, $result);
486+
$max_value = \max($values);
487+
488+
if ($max_value === null) {
489+
return null;
490+
}
491+
492+
return self::castAggregate($max_value, $expr, $result);
481493
}
482494

483495
/**

tests/FunctionEvaluatorTest.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Vimeo\MysqlEngine\Tests;
6+
7+
use PHPUnit\Framework\TestCase;
8+
9+
class FunctionEvaluatorTest extends TestCase
10+
{
11+
12+
public function tearDown() : void
13+
{
14+
\Vimeo\MysqlEngine\Server::reset();
15+
}
16+
17+
/**
18+
* @dataProvider maxValueProvider
19+
*/
20+
public function testSqlMax(string $sql, ?string $expected, bool $is_db_number) : void
21+
{
22+
$query = self::getConnectionToFullDB()->prepare($sql);
23+
$query->execute();
24+
/** @var array<array<string, string|null>> $result */
25+
$result = $query->fetchAll(\PDO::FETCH_ASSOC);
26+
27+
if ($is_db_number) {
28+
$this->assertNotEmpty($result);
29+
$this->assertNotNull($result[0]['max']);
30+
} else {
31+
$this->assertSame([['max' => $expected]], $result);
32+
}
33+
}
34+
35+
public static function maxValueProvider(): array
36+
{
37+
return [
38+
'null when no rows' => [
39+
'sql' => 'SELECT MAX(null) as `max` FROM `video_game_characters`',
40+
'expected' => null,
41+
'is_db_number' => false,
42+
],
43+
'max of scalar values' => [
44+
'sql' => 'SELECT MAX(10) as `max` FROM `video_game_characters`',
45+
'expected' => '10',
46+
'is_db_number' => false,
47+
],
48+
'max in DB values' => [
49+
'sql' => 'SELECT MAX(id) as `max` FROM `video_game_characters`',
50+
'expected' => '',
51+
'is_db_number' => true,
52+
],
53+
];
54+
}
55+
56+
/**
57+
* @dataProvider minValueProvider
58+
*/
59+
public function testSqlMin(string $sql, ?string $expected, bool $is_db_number) : void
60+
{
61+
$query = self::getConnectionToFullDB()->prepare($sql);
62+
$query->execute();
63+
/** @var array<array<string, string|null>> $result */
64+
$result = $query->fetchAll(\PDO::FETCH_ASSOC);
65+
66+
if ($is_db_number) {
67+
$this->assertNotEmpty($result);
68+
$this->assertNotNull($result[0]['min']);
69+
} else {
70+
$this->assertSame([['min' => $expected]], $result);
71+
}
72+
}
73+
74+
public static function minValueProvider(): array
75+
{
76+
return [
77+
'null when no rows' => [
78+
'sql' => 'SELECT MIN(null) as `min` FROM `video_game_characters`',
79+
'expected' => null,
80+
'is_db_number' => false,
81+
],
82+
'min of scalar values' => [
83+
'sql' => 'SELECT MIN(10) as `min` FROM `video_game_characters`',
84+
'expected' => '10',
85+
'is_db_number' => false,
86+
],
87+
'min in DB values' => [
88+
'sql' => 'SELECT MIN(id) as `min` FROM `video_game_characters`',
89+
'expected' => '',
90+
'is_db_number' => true,
91+
],
92+
];
93+
}
94+
95+
private static function getPdo(string $connection_string, bool $strict_mode = false) : \PDO
96+
{
97+
$options = $strict_mode ? [\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET sql_mode="STRICT_ALL_TABLES"'] : [];
98+
99+
if (\PHP_MAJOR_VERSION === 8) {
100+
return new \Vimeo\MysqlEngine\Php8\FakePdo($connection_string, '', '', $options);
101+
}
102+
103+
return new \Vimeo\MysqlEngine\Php7\FakePdo($connection_string, '', '', $options);
104+
}
105+
106+
private static function getConnectionToFullDB(bool $emulate_prepares = true, bool $strict_mode = false) : \PDO
107+
{
108+
$pdo = self::getPdo('mysql:foo;dbname=test;', $strict_mode);
109+
110+
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, $emulate_prepares);
111+
112+
// create table
113+
$pdo->prepare(file_get_contents(__DIR__ . '/fixtures/create_table.sql'))->execute();
114+
115+
// insertData
116+
$pdo->prepare(file_get_contents(__DIR__ . '/fixtures/bulk_character_insert.sql'))->execute();
117+
$pdo->prepare(file_get_contents(__DIR__ . '/fixtures/bulk_enemy_insert.sql'))->execute();
118+
$pdo->prepare(file_get_contents(__DIR__ . '/fixtures/bulk_tag_insert.sql'))->execute();
119+
120+
return $pdo;
121+
}
122+
}

0 commit comments

Comments
 (0)