Skip to content

fix(memory): exclude null rows from negation operators#869

Merged
abnegate merged 1 commit intomainfrom
fix/memory-negation-null-rows
Apr 29, 2026
Merged

fix(memory): exclude null rows from negation operators#869
abnegate merged 1 commit intomainfrom
fix/memory-negation-null-rows

Conversation

@abnegate
Copy link
Copy Markdown
Member

@abnegate abnegate commented Apr 29, 2026

Summary

Greptile P1 follow-up to #860. The Memory adapter included null-valued rows in negation operator results, diverging from every SQL adapter:

  • notEqual, notBetween, notStartsWith, notEndsWith (scalar)
  • notContains, notSearch (scalar)
  • notEqual, notContains (object/JSONB context)

In MariaDB / MySQL / Postgres / SQLite, WHERE col != x evaluates to NULL for null rows under three-valued logic, so they are excluded. The Memory adapter was returning true (or relying on ! matches(...) flipping false to true), causing null rows to leak into negation results.

Each branch now short-circuits on $value === null (or $haystack === null in the object path) and returns false, matching SQL semantics.

Test plan

  • testNegationOperatorsExcludeNullRows — verifies all six scalar negation operators exclude a row with null-valued attributes
  • CI pipeline (lint, phpstan, full test suite across MariaDB/MySQL/Postgres/SQLite/Mongo/Memory)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed query matching behavior for negated operators (notEqual, notBetween, notStartsWith, notEndsWith, notContains, notSearch) to properly handle null values—these operators now return false when the queried value is null.
  • Tests

    • Added regression test coverage for negation operators with null values.

Memory adapter's negation operators (notEqual, notBetween, notStartsWith,
notEndsWith, notContains, notSearch) previously matched rows where the
attribute was null, diverging from MariaDB / MySQL / Postgres / SQLite
which treat NULL under three-valued logic — `WHERE col != x` evaluates to
NULL for null rows and excludes them from the result set.

Same applies to object-typed notEqual / notContains: Postgres'
NOT (NULL @> x) evaluates to NULL, excluding the row.

Adds testNegationOperatorsExcludeNullRows covering all six scalar
operators against a row with null-valued attributes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

📝 Walkthrough

Walkthrough

The Memory adapter's query matching logic for negated operators is updated to align with SQL three-valued logic, where null values return false for negation conditions instead of being treated as satisfying the negation. A comprehensive regression test ensures this behavior is maintained.

Changes

Cohort / File(s) Summary
Memory Adapter Implementation
src/Database/Adapter/Memory.php
Updated negation operator semantics (NOT_EQUAL, NOT_BETWEEN, NOT_STARTS_WITH, NOT_ENDS_WITH, NOT_CONTAINS, NOT_SEARCH) to return false when scalar attribute values are null. For object/JSON attributes, TYPE_NOT_EQUAL and TYPE_NOT_CONTAINS now return false instead of true when the decoded JSON is null, aligning with SQL three-valued logic.
Regression Testing
tests/e2e/Adapter/MemoryTest.php
Added new end-to-end test method testNegationOperatorsExcludeNullRows() that verifies all negation operators (notEqual, notBetween, notStartsWith, notEndsWith, notContains, notSearch) exclude rows where the queried column value is null.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PR #860: Introduced the Memory adapter and MemoryTest suite; directly affected by these null-handling logic updates.
  • PR #652: Added support for negated query types (notContains, notSearch, notBetween, notStartsWith, notEndsWith); this PR aligns their semantic behavior with SQL three-valued logic.

Suggested reviewers

  • fogelito

Poem

🐰 Hops through the nulls with care so bright,
Negations now align just right,
SQL's three-valued dance so true,
No more surprises, logic through and through!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main fix: Memory adapter now excludes null rows from negation operators, which directly aligns with the primary objective of aligning Memory adapter behavior with SQL three-valued logic.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/memory-negation-null-rows

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.1.51)

PHPStan was skipped because the sandbox runner could not parse its output.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 29, 2026

Greptile Summary

This PR fixes the Memory adapter's handling of null-valued rows under negation operators (notEqual, notBetween, notStartsWith, notEndsWith, notContains, notSearch) to match SQL three-valued logic, where NULL != x evaluates to NULL and is excluded from results. The scalar path fixes are all correct and the new regression test is well-structured; the only gap is that the JSONB/object-path notEqual and notContains corrections (where return true was changed to return false) are not covered by the new test.

Confidence Score: 4/5

Safe to merge; all scalar negation fixes are correct and the behavioural regression is well-targeted.

Only P2 findings (missing test coverage for two JSONB-path cases). The production code changes are logically sound and consistent with SQL semantics across all touched operators.

tests/e2e/Adapter/MemoryTest.php — JSONB/object-path branches for notEqual and notContains in matchesObjectQuery lack regression tests.

Important Files Changed

Filename Overview
src/Database/Adapter/Memory.php Adds null-short-circuit guards to all six scalar negation operators and corrects the two object/JSONB-path cases from return true to return false; logic matches SQL three-valued semantics.
tests/e2e/Adapter/MemoryTest.php New regression test covers all six scalar negation operators against null rows, but the JSONB/object-path notEqual and notContains fixes from Memory.php are not exercised.

Reviews (1): Last reviewed commit: "fix(memory): exclude null rows from nega..." | Re-trigger Greptile

Comment on lines +933 to 957
$assertOnlyValueRow('notEqual', $database->find('nullable', [
Query::notEqual('name', 'bob'),
]));

$assertOnlyValueRow('notBetween', $database->find('nullable', [
Query::notBetween('score', 100, 200),
]));

$assertOnlyValueRow('notStartsWith', $database->find('nullable', [
Query::notStartsWith('name', 'zz'),
]));

$assertOnlyValueRow('notEndsWith', $database->find('nullable', [
Query::notEndsWith('name', 'zz'),
]));

$assertOnlyValueRow('notContains', $database->find('nullable', [
Query::notContains('bio', ['nope']),
]));

$assertOnlyValueRow('notSearch', $database->find('nullable', [
Query::notSearch('bio', 'unrelated'),
]));
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing coverage for JSONB/object-path negation fixes

The PR also changes matchesObjectQuery for TYPE_NOT_EQUAL and TYPE_NOT_CONTAINS in the object/JSONB path (lines ~2945 and ~2984 of Memory.php), but no test in testNegationOperatorsExcludeNullRows exercises those branches. A document with a null JSON/array-typed attribute filtered with notEqual or notContains through the object path would go untested, leaving those two fixes without regression coverage.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/e2e/Adapter/MemoryTest.php (1)

878-956: Add regression coverage for object/JSON negation paths changed in this PR.

This test validates scalar negation semantics well, but it does not exercise matchesObject() paths updated for TYPE_NOT_EQUAL and TYPE_NOT_CONTAINS. Please add at least one null-vs-non-null object attribute case so both changed surfaces are guarded.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/Adapter/MemoryTest.php` around lines 878 - 956, Add a
null-vs-non-null object/JSON attribute case in
testNegationOperatorsExcludeNullRows so the updated matchesObject() paths for
TYPE_NOT_EQUAL and TYPE_NOT_CONTAINS are exercised: extend the 'nullable'
collection schema with an object/JSON field (e.g., "meta" or "settings"), insert
one document with that field set to a non-null object and one with it set to
null, then add assertions using Query::notEqual('meta', ...) and
Query::notContains('meta', ...) (mirroring the existing notEqual/notContains
calls) to assert only the non-null document is returned; this will cover the
matchesObject() branches touched by TYPE_NOT_EQUAL and TYPE_NOT_CONTAINS.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/e2e/Adapter/MemoryTest.php`:
- Around line 878-956: Add a null-vs-non-null object/JSON attribute case in
testNegationOperatorsExcludeNullRows so the updated matchesObject() paths for
TYPE_NOT_EQUAL and TYPE_NOT_CONTAINS are exercised: extend the 'nullable'
collection schema with an object/JSON field (e.g., "meta" or "settings"), insert
one document with that field set to a non-null object and one with it set to
null, then add assertions using Query::notEqual('meta', ...) and
Query::notContains('meta', ...) (mirroring the existing notEqual/notContains
calls) to assert only the non-null document is returned; this will cover the
matchesObject() branches touched by TYPE_NOT_EQUAL and TYPE_NOT_CONTAINS.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 303ba14d-c74b-45e5-bbd4-970c693af995

📥 Commits

Reviewing files that changed from the base of the PR and between dc008bb and 66747e4.

📒 Files selected for processing (2)
  • src/Database/Adapter/Memory.php
  • tests/e2e/Adapter/MemoryTest.php

@abnegate abnegate merged commit 688d942 into main Apr 29, 2026
31 of 32 checks passed
@abnegate abnegate deleted the fix/memory-negation-null-rows branch April 29, 2026 07:33
@claude claude Bot mentioned this pull request Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant