From 0593ff4d343b9666291a4682c6d91fa66fe75f7c Mon Sep 17 00:00:00 2001 From: Henry Robinson Date: Tue, 5 Sep 2023 16:10:25 +0100 Subject: [PATCH] Fix evaluation of =NULL (#107) * Fix evaluation of =NULL * Tests --- src/Expressions/BinaryOperatorExpression.php | 6 ++++ tests/SelectExpressionTest.php | 34 +++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/Expressions/BinaryOperatorExpression.php b/src/Expressions/BinaryOperatorExpression.php index 24e8fbb..69c99f9 100644 --- a/src/Expressions/BinaryOperatorExpression.php +++ b/src/Expressions/BinaryOperatorExpression.php @@ -154,6 +154,12 @@ public function evaluateImpl(row $row, AsyncMysqlConnection $conn): mixed { case Operator::OR: invariant(false, 'impossible to arrive here'); case Operator::EQUALS: + // foo = NULL is always NULL, no matter what the value of foo + // (specifically, if foo is also NULL!). Negation of NULL is always + // NULL, so we ignore that. + if ($l_value is null || $r_value is null) { + return null; + } // maybe do some stuff with data types here // comparing strings: gotta think about collation and case sensitivity! return (bool)(\HH\Lib\Legacy_FIXME\eq($l_value, $r_value) ? 1 : 0 ^ $this->negatedInt); diff --git a/tests/SelectExpressionTest.php b/tests/SelectExpressionTest.php index c997e4c..8774a5f 100644 --- a/tests/SelectExpressionTest.php +++ b/tests/SelectExpressionTest.php @@ -46,8 +46,9 @@ final class SelectExpressionTest extends HackTest { public async function testSelectExpressions(): Awaitable { $conn = static::$conn as nonnull; - $results = - await $conn->query('SELECT id, group_id as my_fav_group_id, id*1000 as math FROM table3 WHERE group_id=6'); + $results = await $conn->query( + 'SELECT id, group_id as my_fav_group_id, id*1000 as math FROM table3 WHERE group_id=6', + ); expect($results->rows())->toBeSame( vec[ dict['id' => 4, 'my_fav_group_id' => 6, 'math' => 4000], @@ -156,6 +157,12 @@ final class SelectExpressionTest extends HackTest { ], 'adding a column reference still parses', ); + + $results = await $conn->query('SELECT id FROM table4 WHERE id IN (NULL)'); + expect($results->rows())->toBeEmpty(); + + $results = await $conn->query('SELECT id FROM table4 WHERE id IN (NULL=NULL)'); + expect($results->rows())->toBeEmpty(); } public async function testNotIn(): Awaitable { @@ -199,7 +206,9 @@ final class SelectExpressionTest extends HackTest { // weird this is even valid SQL, and possibly pedantic, but this demonstrates a lot of how // case statements are implemented such that it doesn't blow up on the second THEN or second CASE - $results = await $conn->query("SELECT CASE WHEN 4 = CASE WHEN 1 = 2 THEN 3 ELSE 4 END THEN 'yes' ELSE 'no' END"); + $results = await $conn->query( + "SELECT CASE WHEN 4 = CASE WHEN 1 = 2 THEN 3 ELSE 4 END THEN 'yes' ELSE 'no' END", + ); expect($results->rows())->toBeSame( vec[ dict["CASE WHEN 4 = CASE WHEN 1 = 2 THEN 3 ELSE 4 END THEN 'yes' ELSE 'no' END" => 'yes'], @@ -321,6 +330,21 @@ final class SelectExpressionTest extends HackTest { ]); } + public async function testEqualsNull(): Awaitable { + $conn = static::$conn as nonnull; + $results = await $conn->query('SELECT id FROM table3 WHERE NULL=NULL'); + expect($results->rows())->toBeEmpty(); + + $results = await $conn->query('SELECT id FROM table3 WHERE NULL!=NULL'); + expect($results->rows())->toBeEmpty(); + + $results = await $conn->query('SELECT id FROM table3 WHERE table_3_id=NULL'); + expect($results->rows())->toBeEmpty(); + + $results = await $conn->query('SELECT id FROM table3 WHERE table_3_id!=NULL'); + expect($results->rows())->toBeEmpty(); + } + public async function testIsNotNull(): Awaitable { $conn = static::$conn as nonnull; $results = await $conn->query( @@ -338,7 +362,9 @@ final class SelectExpressionTest extends HackTest { public async function testNotParens(): Awaitable { $conn = static::$conn as nonnull; - $results = await $conn->query("SELECT id FROM table3 WHERE group_id=12345 AND NOT (name='name1' OR name='name3')"); + $results = await $conn->query( + "SELECT id FROM table3 WHERE group_id=12345 AND NOT (name='name1' OR name='name3')", + ); expect($results->rows())->toBeSame(vec[ dict['id' => 2], ]);