Skip to content

fix(sql): render negated filter operators null-inclusively - #758

Merged
tada5hi merged 2 commits into
masterfrom
fix/752-sql-complement-law
Jul 14, 2026
Merged

fix(sql): render negated filter operators null-inclusively#758
tada5hi merged 2 commits into
masterfrom
fix/752-sql-complement-law

Conversation

@tada5hi

@tada5hi tada5hi commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

Negated filter operators evaluated through @rapiq/sql / @rapiq/typeorm silently excluded records whose column is NULL: ne('name', 'Peter') rendered a bare name <> ?, which drops NULL rows under SQL three-valued logic. Every negated operator is now the exact logical complement of its positive twin — matching the semantics @rapiq/memory already pins with its complement-law spec.

A shared whereComplement() helper in the filters visitor wraps every negated rendering:

Filter SQL (before) SQL (after)
ne(field, a) "field" <> $1 ("field" <> $1 or "field" is null)
nin(field, [a, b]) "field" not in($1, $2) ("field" not in($1, $2) or "field" is null)
notContains family (regexp dialects) "field" ~* $1 ("field" ~* $1 or "field" is null)
notContains family (LIKE fallback) [field] not like ? escape '\' ([field] not like ? escape '\' or [field] is null)

The already-complement-correct renderings are unchanged: ne(null)is not null, nin with a null element → (not in(...) and is not null), only-null / empty-list nin, and exists(false).

The TypeORM adapter contains no rendering logic of its own, so it inherits the fix 1:1 through the shared visitor.

Tests

  • Updated the SQL interpreter specs that pinned the old renderings (ne, nin, negated anchored operators on both the regexp path and the LIKE fallback); the existing null-semantics spec stayed green throughout.
  • New TypeORM live-DB specs seed a record with a NULL value in the filtered column and assert ne, nin and notContains return it while the positive twins do not.
  • New cross-adapter agreement spec (packages/typeorm/test/unit/complement.spec.ts): the complement-law matrix (9 positive/negative pairs, including null-element / empty-list in/nin shapes) evaluated via @rapiq/memory and via the TypeORM adapter on a live better-sqlite3 database yields identical record sets, and each pair partitions the table. @rapiq/memory resolves through the workspace symlink — no dependency declaration needed for the test-only import.

Docs

  • @rapiq/sql page: null-semantics section documents the null-inclusive negations; string-matching section notes the LIKE-fallback form.
  • @rapiq/memory page: removed the "alignment planned" divergence row.
  • Filters guide: states the complement law under null semantics.

Closes #752

Summary by CodeRabbit

  • Bug Fixes

    • Fixed SQL generation for negated filters (ne, nin, and negated string matching) so they are null-inclusive and correctly match records with missing or NULL field values.
    • Ensured consistent “complement” behavior across SQL adapters, aligning SQL results with in-memory filtering.
  • Documentation

    • Updated null semantics documentation and added examples showing how negated operators behave with NULL/absent fields.
  • Tests

    • Added and expanded unit coverage to verify complement behavior for null scenarios across adapters.

Copilot AI review requested due to automatic review settings July 13, 2026 13:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c197b8ac-d41b-43e3-87b2-42290c96877d

📥 Commits

Reviewing files that changed from the base of the PR and between cb1a28f and 4042cf0.

📒 Files selected for processing (1)
  • packages/typeorm/test/unit/complement.spec.ts

📝 Walkthrough

Walkthrough

Negated SQL filters now include NULL fields as exact complements of positive operators. SQL rendering, TypeORM integration tests, unit expectations, and filter documentation were updated to reflect the new semantics.

Changes

Negated filter complement semantics

Layer / File(s) Summary
Null-inclusive SQL rendering
packages/sql/src/visitor/filters.ts, packages/sql/test/unit/interpreters/*
Negated comparisons, IN, regular-expression, and LIKE predicates now render OR field IS NULL complements, with unit tests updated for the generated SQL.
Cross-adapter TypeORM validation
packages/typeorm/test/unit/complement.spec.ts, packages/typeorm/test/unit/filters.spec.ts
TypeORM tests seed and validate NULL rows, compare database results with compileFilters, and verify complement coverage across filter pairs.
Documented complement-law behavior
packages/docs/guide/filters.md, packages/docs/packages/memory.md, packages/docs/packages/sql.md
Documentation describes NULL-inclusive negations and removes the previous memory-package divergence entry.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FilterInput
  participant TypeormAdapter
  participant FiltersVisitor
  participant Database
  participant compileFilters
  FilterInput->>TypeormAdapter: submit positive and negated filters
  TypeormAdapter->>FiltersVisitor: compile SQL predicates
  FiltersVisitor->>Database: execute NULL-inclusive conditions
  Database-->>TypeormAdapter: return matching record IDs
  FilterInput->>compileFilters: evaluate the same filters in memory
  compileFilters-->>FilterInput: return expected record IDs
Loading

Possibly related PRs

  • tada5hi/rapiq#741: Related SQL filter and NULL-handling changes, including MSSQL string-operator fallbacks.
  • tada5hi/rapiq#742: Related updates to shared SQL visitor helpers for in/nin and anchored string operators.
🚥 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 concisely states the main change: null-inclusive rendering for negated SQL filters.
Linked Issues check ✅ Passed The changes address the linked issue by updating SQL rendering, TypeORM behavior, tests, and docs for null-inclusive negation semantics.
Out of Scope Changes check ✅ Passed The PR stays focused on the requested filter-semantics fix, related tests, and documentation updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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/752-sql-complement-law

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

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

Negated filter operators (ne, nin, notContains, notStartsWith,
notEndsWith) rendered as bare SQL negations, which drop NULL rows
under three-valued logic. They now render null-inclusively
((field <> ? or field is null)), making every negated operator the
exact logical complement of its positive twin — the semantics
@rapiq/memory already implements.

The TypeORM adapter inherits the fix through the shared visitor;
live-DB specs pin the inheritance and a cross-adapter spec replays
the complement-law matrix via both @rapiq/memory and the TypeORM
adapter, asserting identical record sets.

Closes #752
@tada5hi
tada5hi force-pushed the fix/752-sql-complement-law branch from 653e07e to cb1a28f Compare July 13, 2026 13:11
@tada5hi

tada5hi commented Jul 13, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/sql/test/unit/interpreters/regex.spec.ts (1)

157-175: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a params assertion to the regex-dialect cases loop.

The pattern variable is destructured from each case tuple but never used. Adding expect(params).toEqual([pattern]) would verify that createFilterRegex produces the expected regex source, strengthening coverage beyond SQL shape alone.

♻️ Proposed addition
         const [sql, params] = adapter.getQueryAndParameters();
         expect(sql, operator).toEqual(negated ?
             '("name" ~* $1 or "name" is null)' :
             '"name" ~* $1');
+        expect(params, operator).toEqual([pattern]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sql/test/unit/interpreters/regex.spec.ts` around lines 157 - 175,
Add a parameter assertion inside the regex cases loop after retrieving adapter
output, verifying that params equals the destructured pattern for each operator.
Keep the existing SQL assertion unchanged and use the existing pattern variable
to validate createFilterRegex output.
packages/docs/packages/sql.md (1)

97-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider cross-referencing the two nin table entries.

The existing null-semantics table (L93) shows nin(field, [a, null])(field NOT IN (...) AND field IS NOT NULL), while the new complement table (L102) shows nin(field, [a, b])(field NOT IN (...) OR field IS NULL). Both are correct but represent different scenarios. A brief note distinguishing the null-element case from the all-non-null case would prevent reader confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/docs/packages/sql.md` around lines 97 - 104, Clarify the
negated-operator documentation by cross-referencing the existing null-semantics
table entry for nin(field, [a, null]) with the complement table’s all-non-null
nin(field, [a, b]) example. Add a brief note distinguishing filters containing a
NULL element from filters whose values are all non-NULL, preserving both SQL
behaviors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/typeorm/test/unit/complement.spec.ts`:
- Around line 36-66: Add an afterAll cleanup hook in the complement test suite
that calls dataSource.destroy() after the beforeAll initialization, ensuring the
initialized DataSource connection is released once all tests complete.

---

Nitpick comments:
In `@packages/docs/packages/sql.md`:
- Around line 97-104: Clarify the negated-operator documentation by
cross-referencing the existing null-semantics table entry for nin(field, [a,
null]) with the complement table’s all-non-null nin(field, [a, b]) example. Add
a brief note distinguishing filters containing a NULL element from filters whose
values are all non-NULL, preserving both SQL behaviors.

In `@packages/sql/test/unit/interpreters/regex.spec.ts`:
- Around line 157-175: Add a parameter assertion inside the regex cases loop
after retrieving adapter output, verifying that params equals the destructured
pattern for each operator. Keep the existing SQL assertion unchanged and use the
existing pattern variable to validate createFilterRegex output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7296b72d-2fb0-4c08-9737-20ec604648f8

📥 Commits

Reviewing files that changed from the base of the PR and between 1bea2df and cb1a28f.

📒 Files selected for processing (9)
  • packages/docs/guide/filters.md
  • packages/docs/packages/memory.md
  • packages/docs/packages/sql.md
  • packages/sql/src/visitor/filters.ts
  • packages/sql/test/unit/interpreters/in.spec.ts
  • packages/sql/test/unit/interpreters/primitves.spec.ts
  • packages/sql/test/unit/interpreters/regex.spec.ts
  • packages/typeorm/test/unit/complement.spec.ts
  • packages/typeorm/test/unit/filters.spec.ts
💤 Files with no reviewable changes (1)
  • packages/docs/packages/memory.md

Comment thread packages/typeorm/test/unit/complement.spec.ts
@tada5hi
tada5hi merged commit 374a2d9 into master Jul 14, 2026
3 of 4 checks passed
@tada5hi
tada5hi deleted the fix/752-sql-complement-law branch July 27, 2026 07:53
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.

Negated filter operators should match NULL rows in the SQL/TypeORM adapters (complement law)

2 participants