From 765d33467481b89fd29ac3a83d6267041a1f97d2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 5 Feb 2026 22:08:43 +1300 Subject: [PATCH] fix: Return null instead of empty array for non-matching queries RuntimeQuery::filter() now returns null when the query doesn't match, instead of an empty array. This distinguishes between "no match" and "match with empty payload", fixing the issue where subscriptions with empty payloads weren't being delivered. Updated Realtime::getSubscribers() to check for null instead of using !empty(), and updated all tests to expect null for non-matches. Co-Authored-By: Claude Opus 4.5 --- src/Appwrite/Messaging/Adapter/Realtime.php | 2 +- src/Appwrite/Utopia/Database/RuntimeQuery.php | 8 ++-- .../Database/Query/RuntimeQueryTest.php | 48 +++++++++---------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index 05de941cb3..3149785c4c 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -286,7 +286,7 @@ class Realtime extends MessagingAdapter $compiled = $data['compiled'] ?? ['type' => 'selectAll']; $strings = $data['strings'] ?? []; - if (!empty(RuntimeQuery::filter($compiled, $payload))) { + if (RuntimeQuery::filter($compiled, $payload) !== null) { $matched[$subscriptionId] = $strings; } } diff --git a/src/Appwrite/Utopia/Database/RuntimeQuery.php b/src/Appwrite/Utopia/Database/RuntimeQuery.php index cfc7fbfd7d..1db750b7ef 100644 --- a/src/Appwrite/Utopia/Database/RuntimeQuery.php +++ b/src/Appwrite/Utopia/Database/RuntimeQuery.php @@ -156,9 +156,9 @@ class RuntimeQuery extends Query * * @param array $compiled Result from compile() * @param array $payload Event payload - * @return array Empty array if no match, payload if match + * @return array|null Null if no match, payload if match */ - public static function filter(array $compiled, array $payload): array + public static function filter(array $compiled, array $payload): ?array { // Fast path for select("*") subscriptions if ($compiled['type'] === 'selectAll') { @@ -168,14 +168,14 @@ class RuntimeQuery extends Query // Quick rejection: if payload is missing any required attribute, fail fast foreach ($compiled['attributes'] as $attr) { if (!isset($payload[$attr]) && !\array_key_exists($attr, $payload)) { - return []; + return null; } } // Evaluate all conditions (AND logic at top level) foreach ($compiled['conditions'] as $condition) { if (!self::evaluateCondition($condition, $payload)) { - return []; + return null; } } diff --git a/tests/unit/Utopia/Database/Query/RuntimeQueryTest.php b/tests/unit/Utopia/Database/Query/RuntimeQueryTest.php index 7745d535cf..4078e8e2c4 100644 --- a/tests/unit/Utopia/Database/Query/RuntimeQueryTest.php +++ b/tests/unit/Utopia/Database/Query/RuntimeQueryTest.php @@ -37,7 +37,7 @@ class RuntimeQueryTest extends TestCase $queries = [Query::equal('name', ['Jane'])]; $payload = ['name' => 'John', 'age' => 30]; $result = $this->compileAndFilter($queries, $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testFilterWithMatchingQuery(): void @@ -62,7 +62,7 @@ class RuntimeQueryTest extends TestCase $query = Query::equal('name', ['Jane']); $payload = ['name' => 'John']; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testEqualMultipleValuesMatch(): void @@ -78,7 +78,7 @@ class RuntimeQueryTest extends TestCase $query = Query::equal('status', ['active', 'pending', 'approved']); $payload = ['status' => 'rejected']; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testEqualNumericValues(): void @@ -102,7 +102,7 @@ class RuntimeQueryTest extends TestCase $query = Query::equal('missing', ['value']); $payload = ['name' => 'John']; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } // TYPE_NOT_EQUAL tests @@ -119,7 +119,7 @@ class RuntimeQueryTest extends TestCase $query = Query::notEqual('name', ['John']); $payload = ['name' => 'John']; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testNotEqualMultipleValues(): void @@ -134,7 +134,7 @@ class RuntimeQueryTest extends TestCase $query = Query::notEqual('status', ['active', 'pending']); $payload = ['status' => 'active']; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } // TYPE_LESSER tests @@ -151,7 +151,7 @@ class RuntimeQueryTest extends TestCase $query = Query::lessThan('age', 30); $payload = ['age' => 35]; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testLesserEqualValue(): void @@ -159,7 +159,7 @@ class RuntimeQueryTest extends TestCase $query = Query::lessThan('age', 30); $payload = ['age' => 30]; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testLesserMultipleValues(): void @@ -202,7 +202,7 @@ class RuntimeQueryTest extends TestCase $query = Query::lessThanEqual('age', 30); $payload = ['age' => 35]; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testLesserEqualMultipleValues(): void @@ -228,7 +228,7 @@ class RuntimeQueryTest extends TestCase $query = Query::greaterThan('age', 30); $payload = ['age' => 25]; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testGreaterEqualValue(): void @@ -236,7 +236,7 @@ class RuntimeQueryTest extends TestCase $query = Query::greaterThan('age', 30); $payload = ['age' => 30]; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testGreaterMultipleValues(): void @@ -270,7 +270,7 @@ class RuntimeQueryTest extends TestCase $query = Query::greaterThanEqual('age', 30); $payload = ['age' => 25]; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testGreaterEqualMultipleValues(): void @@ -296,7 +296,7 @@ class RuntimeQueryTest extends TestCase $query = Query::isNull('description'); $payload = ['description' => 'Some text']; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testIsNullMissingAttribute(): void @@ -304,7 +304,7 @@ class RuntimeQueryTest extends TestCase $query = Query::isNull('missing'); $payload = ['name' => 'John']; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } // TYPE_IS_NOT_NULL tests @@ -321,7 +321,7 @@ class RuntimeQueryTest extends TestCase $query = Query::isNotNull('description'); $payload = ['description' => null]; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testIsNotNullMissingAttribute(): void @@ -329,7 +329,7 @@ class RuntimeQueryTest extends TestCase $query = Query::isNotNull('missing'); $payload = ['name' => 'John']; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } // TYPE_AND tests @@ -352,7 +352,7 @@ class RuntimeQueryTest extends TestCase ]); $payload = ['name' => 'John', 'age' => 30]; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testAndAllFail(): void @@ -363,7 +363,7 @@ class RuntimeQueryTest extends TestCase ]); $payload = ['name' => 'John', 'age' => 30]; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testAndMultipleConditions(): void @@ -423,7 +423,7 @@ class RuntimeQueryTest extends TestCase ]); $payload = ['name' => 'John', 'age' => 30]; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testOrMultipleConditions(): void @@ -516,7 +516,7 @@ class RuntimeQueryTest extends TestCase $payload = ['name' => 'John', 'age' => 30]; $result = $this->compileAndFilter($queries, $payload); // With AND logic, if first matches but second doesn't, should return empty - $this->assertEquals([], $result); + $this->assertNull($result); } public function testMultipleQueriesSecondMatches(): void @@ -528,7 +528,7 @@ class RuntimeQueryTest extends TestCase $payload = ['name' => 'John', 'age' => 30]; $result = $this->compileAndFilter($queries, $payload); // With AND logic, if second matches but first doesn't, should return empty - $this->assertEquals([], $result); + $this->assertNull($result); } public function testMultipleQueriesNoneMatch(): void @@ -539,7 +539,7 @@ class RuntimeQueryTest extends TestCase ]; $payload = ['name' => 'John', 'age' => 30]; $result = $this->compileAndFilter($queries, $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testEmptyPayload(): void @@ -547,7 +547,7 @@ class RuntimeQueryTest extends TestCase $query = Query::equal('name', ['John']); $payload = []; $result = $this->compileAndFilter([$query], $payload); - $this->assertEquals([], $result); + $this->assertNull($result); } public function testEmptyAndQuery(): void @@ -565,7 +565,7 @@ class RuntimeQueryTest extends TestCase $payload = ['name' => 'John']; $result = $this->compileAndFilter([$query], $payload); // Empty OR should return false (no conditions match) - $this->assertEquals([], $result); + $this->assertNull($result); } // Type-specific edge cases