Skip to content

refactor: one operator-semantics authority for all backends - #792

Merged
tada5hi merged 4 commits into
masterfrom
feat/operator-semantics-plan
Jul 20, 2026
Merged

refactor: one operator-semantics authority for all backends#792
tada5hi merged 4 commits into
masterfrom
feat/operator-semantics-plan

Conversation

@tada5hi

@tada5hi tada5hi commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Closes #791

What

Filter-operator semantics now have one home. @rapiq/core gains a declarative operator-semantics table plus a lowering pass, and both backends become thin interpreters over pre-decided primitives:

  • FILTER_OPERATOR_SEMANTICS (core/parameter/filters/plan/constants.ts) — one row per operator: family, negation twin (complementOf), visitor dispatch method, anchor placement, comparison range, case-fold participation. Filter.accept derives its per-operator dispatch from this table (runtime behavior identical; the 19-branch if-chain is gone).
  • planCondition(condition, { caseSensitive }) lowers any condition tree into ~9 plan-node kinds with every policy decision already made: negation twins resolved to negated leaf flags (complement law), eq/ne(null) → null checks, in/nin decomposed (empty list → constant, null members extracted, non-array → constant), fold verdicts computed (string value + caseSensitive opt-out, composed through elemMatch prefixes), anchored operators derived into escaped positive patterns, mod/size values validated, ITSELF placement checked, nor/not mapped to negated groups.
  • interpretPlan(plan, interpreter) is the single support-enforcement point: optional handler presence (mod/size/elemMatch) and the itself declaration are the support matrix; the typed featureUnsupported throw lives here and nowhere else.
  • @rapiq/sql: visitor/filters.ts drops whereIn/whereAnchored/isCaseInsensitive and all 19 visit methods for one handler per plan node (350 → ~250 lines, pure rendering). The adapter surface is untouched — @rapiq/typeorm needed zero source changes.
  • @rapiq/memory: compiler.ts same treatment (398 → ~270 lines); binding.ts (quantification) untouched.
  • Parity suite (typeorm/test/unit/parity.spec.ts): 31 hand-computed cases asserting the same condition selects the same rows on typeorm-over-better-sqlite3 and @rapiq/memory; a coverage test forces at least one case per table operator, so a future operator cannot ship without parity coverage. Declared-unsupported conditions assert the typed error.

Deliberate behavior changes

  • Negated anchored rendering (sql): notContains & co. now render (not (<regexp positive>) or <field> is null) instead of a negative-lookahead pattern. Semantically identical; lookaheads are gone entirely (one snapshot assertion updated).
  • @rapiq/memory gains nor/not compound support (plain boolean negation, sql parity) — it previously threw operatorUnsupported while sql supported them.
  • Uniform degenerate-input laws (previously divergent or broken): a malformed mod value renders a never-match constant on sql (was: invalid SQL) exactly as memory always did; a non-array in/nin value lowers to a constant (sql previously crashed with a TypeError); an invalid elemMatch interior throws the typed error on sql too (previously a crash); eq/ne(undefined) is unified to null everywhere.
  • The per-operator IFilterVisitor methods are kept — removing them in favor of the plan is a later, separately-deliberate step once downstream consumers migrate.

Not in scope

  • Slimming IFilterVisitor / removing createFilterRegex's negation flags from the public surface.
  • Dialect-level size support (JSON array length) — the plan node + support matrix make that a per-dialect follow-up.
  • Docs: guide/filters.md's support statements (size/ITSELF) remain accurate as written.

Testing

  • New: core lowering + table-law spec (filters-plan.spec.ts, 32 tests), cross-backend parity suite (31 cases + coverage guard).
  • Updated: one sql regex snapshot (rendering change above), one memory compound spec (nor now supported; unknown operators still throw typed).
  • All 8 packages green: 1,248 tests; lint clean; full nx run-many build passes.

Summary by CodeRabbit

  • New Features

    • Added a unified filter planning system for consistent condition handling across memory and SQL backends.
    • Expanded support for negation, null values, membership, pattern matching, arithmetic, array sizes, and nested element matching.
    • Added centralized validation and clearer errors for unsupported filter capabilities.
  • Bug Fixes

    • Improved consistency of filter behavior and case handling across backends.
    • Added cross-backend parity coverage to detect differing results.
  • Tests

    • Expanded coverage for compound filters, regular expressions, operator semantics, and backend parity.

tada5hi added 4 commits July 20, 2026 11:52
One authority for filter-operator meaning: a declarative
FILTER_OPERATOR_SEMANTICS table (family, negation twin, anchor,
comparison range, fold participation) drives a planCondition()
lowering that resolves every policy decision - complement law,
null equality, in/nin decomposition, case-fold verdicts, anchored
pattern derivation, value-shape validation and ITSELF legality -
into a small closed plan-node vocabulary. Backends consume it via
interpretPlan(); optional handler presence is the declared support
matrix and the single featureUnsupported throw site.

Filter.accept now derives its per-operator visitor dispatch from
the same table (behavior identical); the per-operator
IFilterVisitor methods stay untouched.

Refs #791
The visitor interprets pre-decided plan primitives instead of
re-deriving operator semantics: whereIn, whereAnchored and the
per-operator visit methods collapse into one handler per plan
node. Rendering is unchanged except negated anchored operators,
which now emit a plain not() over the positive pattern (with the
null-inclusive complement arm) instead of a negative-lookahead
regex. A malformed mod value and a non-array in/nin value now
render a never-match constant instead of invalid SQL.

Refs #791
The compiler interprets plan primitives; negation, IN
decomposition, fold verdicts and value validation arrive
pre-decided from core. Binding/quantification stays untouched.
nor/not compound groups now compile (plain boolean negation,
sql parity) instead of throwing.

Refs #791
The same condition must select the same rows on
typeorm-over-sqlite and @rapiq/memory; expectations are computed
by hand from the fixtures. A coverage test forces at least one
parity case per operator of the semantics table, and declared-
unsupported conditions (regex on sqlite, size, ITSELF) assert the
typed error.

Refs #791
Copilot AI review requested due to automatic review settings July 20, 2026 09:53

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 20, 2026

Copy link
Copy Markdown

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Review Change Stack

📝 Walkthrough

Walkthrough

Filter semantics are centralized in core through operator metadata, condition-plan lowering, and interpreter dispatch. Memory and SQL backends now consume the shared plans, with updated negation and matching behavior plus TypeORM parity tests.

Changes

Filter planning pipeline

Layer / File(s) Summary
Core plan contracts and dispatch
packages/core/src/parameter/filters/plan/*, packages/core/src/parameter/filters/index.ts, packages/core/src/parameter/filters/record/module.ts
Adds typed plan nodes, interpreter contracts, operator semantics metadata, public exports, and table-driven visitor dispatch.
Condition lowering and interpretation
packages/core/src/parameter/filters/plan/module.ts, packages/core/test/unit/parameter/filters-plan.spec.ts
Lowers filter trees into normalized plans, handles nulls, membership, matching, arithmetic, nesting, and compounds, and validates interpreter capabilities.
Memory and SQL plan execution
packages/memory/src/parameter/filters/*, packages/sql/src/visitor/filters.ts, packages/sql/test/unit/interpreters/regex.spec.ts, packages/memory/test/unit/filters/compound.spec.ts
Replaces backend-specific operator visitors with plan interpreters for predicate evaluation and SQL rendering.
Cross-backend parity validation
packages/typeorm/test/unit/parity.spec.ts
Compares memory and TypeORM results across operator cases and verifies unsupported SQL operations raise AdapterError.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Query
  participant planCondition
  participant interpretPlan
  participant MemoryCompiler
  participant SQLFiltersVisitor
  Query->>planCondition: filter condition
  planCondition-->>interpretPlan: ConditionPlan
  interpretPlan->>MemoryCompiler: interpret predicate plan
  interpretPlan->>SQLFiltersVisitor: interpret SQL plan
  MemoryCompiler-->>Query: memory predicate
  SQLFiltersVisitor-->>Query: SQL condition
Loading

Possibly related PRs

  • tada5hi/rapiq#742: Overlaps with SQL handling for anchored, regex, and membership operators.
  • tada5hi/rapiq#758: Overlaps with null-inclusive negation rendering in the SQL visitor.
  • tada5hi/rapiq#770: Overlaps with ITSELF and nested elemMatch planning and evaluation.

Suggested reviewers: copilot

🚥 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 summarizes the main change: centralizing filter-operator semantics for all backends.
Linked Issues check ✅ Passed The PR adds core plan lowering, a semantics table, backend interpreters, and parity tests, matching the linked issue's scope.
Out of Scope Changes check ✅ Passed The file changes all support the operator-semantics refactor; no unrelated code paths are introduced.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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 feat/operator-semantics-plan

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.

@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

🤖 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/memory/src/parameter/filters/compiler.ts`:
- Around line 133-140: Update the regex branch in match to prevent ReDoS during
record evaluation: validate or reject unsafe plan.pattern.source patterns before
constructing the RegExp, or replace execution with a linear-time/timeout-bounded
evaluator. Preserve the existing featureUnsupported error behavior for patterns
that cannot be safely evaluated.
🪄 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: 3e3972b5-bd79-457b-9530-6bbd126dd9c8

📥 Commits

Reviewing files that changed from the base of the PR and between 644311a and 99cc58b.

📒 Files selected for processing (13)
  • packages/core/src/parameter/filters/index.ts
  • packages/core/src/parameter/filters/plan/constants.ts
  • packages/core/src/parameter/filters/plan/index.ts
  • packages/core/src/parameter/filters/plan/module.ts
  • packages/core/src/parameter/filters/plan/types.ts
  • packages/core/src/parameter/filters/record/module.ts
  • packages/core/test/unit/parameter/filters-plan.spec.ts
  • packages/memory/src/parameter/filters/compiler.ts
  • packages/memory/src/parameter/filters/module.ts
  • packages/memory/test/unit/filters/compound.spec.ts
  • packages/sql/src/visitor/filters.ts
  • packages/sql/test/unit/interpreters/regex.spec.ts
  • packages/typeorm/test/unit/parity.spec.ts

Comment on lines +133 to +140
match(plan: MatchPlan) : ConditionEval {
let regex : RegExp;
if (plan.pattern.mode === 'regex') {
try {
regex = new RegExp(plan.pattern.source, plan.pattern.flags);
} catch {
throw AdapterError.featureUnsupported('filters:regex:value');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

recheck library validate regex ReDoS safety usage

💡 Result:

The recheck library is a specialized tool designed to detect Regular Expression Denial of Service (ReDoS) vulnerabilities by identifying patterns susceptible to catastrophic backtracking [1][2]. It is primarily used to analyze regular expressions during development and CI/CD, rather than as a runtime security shield for untrusted patterns [2][3]. Key capabilities of recheck include: - Multi-engine analysis: It combines static analysis (automaton-based) and fuzzing to identify vulnerabilities, supporting complex features like backreferences and look-arounds [2][4]. - Diagnostics: It returns analysis results (safe, vulnerable, or unknown) and provides specific attack patterns (strings that trigger the exponential complexity) when a vulnerability is found [5][4][6]. - Integration: It offers a programmatic API (asynchronous and synchronous), a CLI, and an ESLint plugin to automate detection in project codebases [5][2][4]. Best practices for ReDoS safety include: 1. Use linear-time engines: For processing untrusted input, the most robust defense is using a non-backtracking regex engine that guarantees linear-time performance (e.g., Google's RE2, or built-in non-backtracking modes in languages like.NET or Rust) [3][7]. 2. Harden regex patterns: If you must use a backtracking engine, ensure patterns are unambiguous. Avoid nested quantifiers (e.g., (a+)+), overlapping alternation, and ensure quantifiers are bounded (e.g., {1,64} instead of + or *) [8][3][7]. 3. Input validation and timeouts: Always restrict the length of input processed by regexes. If you cannot rewrite a pattern to be linear, run it in an isolated environment (worker thread or subprocess) with a strict, enforced execution timeout [3][7][9]. 4. Defense in depth: Treat regexes that touch attacker-controlled input as security boundaries. Integrate tools like recheck into your CI pipeline to catch vulnerable patterns before they reach production [3]. Note: Timeouts and library checks are aids for developer-authored patterns; they are not intended to be a secure sandbox for executing completely arbitrary, untrusted regular expression patterns provided by end users [3][9].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and nearby symbols first.
ast-grep outline packages/memory/src/parameter/filters/compiler.ts --view expanded || true

echo "----"
wc -l packages/memory/src/parameter/filters/compiler.ts

echo "---- relevant excerpt ----"
sed -n '1,240p' packages/memory/src/parameter/filters/compiler.ts

echo "---- search for MatchPlan / pattern.mode / regex mode usage ----"
rg -n "pattern\.mode|MatchPlan|filters:regex:value|new RegExp\(" packages -g '*.ts' -g '*.tsx'

Repository: tada5hi/rapiq

Length of output: 11572


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- MatchPlan lowering ----"
sed -n '330,430p' packages/core/src/parameter/filters/plan/module.ts

echo "---- MatchPlan type ----"
sed -n '150,230p' packages/core/src/parameter/filters/plan/types.ts

echo "---- regex helper ----"
sed -n '1,120p' packages/core/src/parameter/filters/regex.ts

echo "---- parser-mongo regex handling ----"
sed -n '660,740p' packages/parser-mongo/src/parameter/filters/module.ts

echo "---- search for any regex safety checks / limits / alternative engines ----"
rg -n "recheck|RE2|timeout|safe regex|regex.*limit|pattern.*limit|catastrophic|backtracking" packages -g '*.ts' -g '*.tsx'

Repository: tada5hi/rapiq

Length of output: 9434


Bound regex evaluation in the memory backend. plan.pattern.source is compiled directly and then run against every record, so a crafted pattern can trigger catastrophic backtracking. The try/catch only rejects invalid syntax; it does not address ReDoS. Consider rejecting unsafe regexes here or enforcing a linear-time/timeout-limited evaluator.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 136-136: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(plan.pattern.source, plan.pattern.flags)
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)


[warning] 136-136: Do not use variable for regular expressions
Context: new RegExp(plan.pattern.source, plan.pattern.flags)
Note: [CWE-1333] Inefficient Regular Expression Complexity. Security best practice.

(regexp-non-literal-typescript)

🤖 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/memory/src/parameter/filters/compiler.ts` around lines 133 - 140,
Update the regex branch in match to prevent ReDoS during record evaluation:
validate or reject unsafe plan.pattern.source patterns before constructing the
RegExp, or replace execution with a linear-time/timeout-bounded evaluator.
Preserve the existing featureUnsupported error behavior for patterns that cannot
be safely evaluated.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Pre-existing trust model, not introduced here: master's buildRegex compiled the same variable input, and the plan lowering only moved the construction point. Exposure is limited to applications parsing untrusted mongo-style documents ($regex via @rapiq/parser-mongo) and evaluating with @rapiq/memory — the URL dialects have no wire spelling for the regex operator, and SQL backends receive the same pass-through pattern by settled design. Runtime recheck/timeout evaluation is disproportionate for this layer; the designed gating point is the schema filters.validate hook. Tracking documentation + a possible first-class operator allow-list in #794.

@tada5hi

tada5hi commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai pause

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews paused.

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.

Unify filter-operator semantics: plan lowering + descriptor table in core

2 participants