Skip to content

feat(tables): expand filter operators (not-contains, starts/ends-with, not-in, empty)#4827

Merged
waleedlatif1 merged 1 commit into
stagingfrom
waleedlatif1/tables-module-gaps
Jun 1, 2026
Merged

feat(tables): expand filter operators (not-contains, starts/ends-with, not-in, empty)#4827
waleedlatif1 merged 1 commit into
stagingfrom
waleedlatif1/tables-module-gaps

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

Summary

  • Add five filter operators end-to-end: does not contain ($ncontains), starts with ($startsWith), ends with ($endsWith), not in array ($nin — already executed server-side but unexposed in the UI), and is empty / is not empty ($empty).
  • Wired through the whole stack: SQL builder, ConditionOperators type, query-builder converters/constants, the filter UI (valueless operators hide the value input), the Table tools/block descriptions, and docs.
  • Fixed a same-column AND collision in the filter builder: two AND rules on one column (e.g. age > 18 AND age < 65, or name startsWith 'A' AND name endsWith 'Z') silently overwrote each other. They now merge into one operator object, which also makes Filter → rules → Filter round-trip losslessly for multi-operator columns.
  • Fixed two related converter issues: $nin values weren't split into an array like $in, and textual-match values like "123" were numeric-coerced (breaking the ILIKE path).
  • Hardened $empty: a non-boolean operand from the raw API silently inverted the check; it now coerces 'true'/'false' strings and otherwise returns a 400.

Type of Change

  • New feature
  • Bug fix

Testing

Added unit tests for the SQL builder (each pattern operator, wildcard escaping, $empty true/false + string coercion + non-boolean throw) and converter round-trips (per-operator serialization, same-column merge, bare-eq normalization, OR-boundary separation, lossless multi-operator round-trip). Note: this workspace has no node_modules, so the suite runs in CI rather than locally.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link
Copy Markdown

vercel Bot commented May 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment May 31, 2026 7:31pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 31, 2026

PR Summary

Medium Risk
Changes how bulk query/update/delete filters compile to SQL and which rows match; well covered by tests but incorrect filters could affect more or fewer rows than users expect.

Overview
This PR expands table row filtering with new MongoDB-style operators wired from the SQL builder through the visual filter UI, docs, Table block prompts, and agent tools.

New operators: case-insensitive $ncontains, $startsWith, $endsWith, and $empty (empty vs non-empty cells). $nin is exposed in the filter builder (it already worked in SQL). Docs note $contains is case-insensitive.

SQL behavior: pattern ops share an ILIKE path with wildcard escaping, rejection of empty patterns, and $ncontains treating null/empty cells as matches. $empty validates booleans (with lenient 'true'/'false' strings) instead of silently inverting on bad input.

Filter builder fixes: multiple AND rules on the same column merge into one operator object (e.g. age range) instead of the last rule overwriting. $nin values split like $in; text-match values stay strings so values like "123" are not coerced to numbers. Is empty / is not empty hide the value field and round-trip via $empty.

Tests cover SQL generation and converter round-trips for the new operators and merge behavior.

Reviewed by Cursor Bugbot for commit 8c41203. Configure here.

Comment thread apps/sim/lib/table/query-builder/converters.ts
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 31, 2026

Greptile Summary

This PR wires five new filter operators ($ncontains, $startsWith, $endsWith, $nin in UI, $empty) end-to-end across the SQL builder, query-builder converters, filter UI, block descriptions, tool descriptions, and docs. It also fixes two correctness bugs: a same-column AND collision that silently clobbered earlier rules in the same group, and $nin values not being split into an array like $in.

  • SQL layer: Adds buildLikeClause (replaces buildContainsClause) for four pattern operators with wildcard placement control, empty-pattern rejection, and IS NULL surfacing for negated matches; adds buildEmptyClause/coerceEmptyFlag for $empty boolean validation with 'true'/'false' string tolerance.
  • Converter layer: Fixes $nin array splitting, text-match operator coercion bypass (prevents numeric coercion of strings like "123"), same-column AND merge via mergeConditions, and maps $empty bool ↔ isEmpty/isNotEmpty UI operators bidirectionally.
  • UI layer: Hides the value input for valueless operators (isEmpty, isNotEmpty) and skips the value-required gate for them in handleApply; test suites for both the SQL builder patterns and converter round-trips are added.

Confidence Score: 5/5

This PR is safe to merge. All new operators are validated through the existing field-name and operator allowlists before reaching the SQL builder, and the empty-pattern and non-boolean operand guards follow the same defensive pattern already used for range and $empty operands.

The implementation is complete and internally consistent across all layers: SQL builder, converter, UI, types, tools, and docs. The same-column AND merge fix is correct and tested. The $empty coercion, empty-pattern rejection, and null-surfacing for negated matches are all covered by new unit tests. No existing behavior is altered — $contains is refactored through buildLikeClause but the generated SQL is identical.

No files require special attention. The converter and SQL builder changes are the most complex, but both are well-tested by the new test suites.

Important Files Changed

Filename Overview
apps/sim/lib/table/sql.ts Core SQL builder: adds buildLikeClause (wildcard control, empty-pattern rejection via TableQueryValidationError), buildEmptyClause, and coerceEmptyFlag; refactors buildContainsClause away. Field validation and operator allowlist remain intact.
apps/sim/lib/table/query-builder/converters.ts Fixes same-column AND collision via mergeConditions/toOperatorObject, $nin array splitting, text-match operator raw-string preservation, and bidirectional $empty ↔ isEmpty/isNotEmpty mapping; filterToRules handles string 'true'/'false' for $empty without predicate flip.
apps/sim/lib/table/query-builder/constants.ts Adds ncontains, startsWith, endsWith, nin, isEmpty, isNotEmpty to COMPARISON_OPERATORS and exports VALUELESS_OPERATORS Set for UI gating logic.
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx Hides value input for valueless operators (isEmpty/isNotEmpty) and skips value-required gate in handleApply; stale value field is harmless since toRuleValue ignores it for these operators.
apps/sim/lib/table/types.ts Extends ConditionOperators with $ncontains, $startsWith, $endsWith (string), and $empty (boolean); TypeScript types match runtime SQL builder expectations.
apps/sim/lib/table/tests/sql.test.ts Adds tests for all four pattern operators, wildcard escaping, empty-pattern rejection (all four operators), and $empty true/false/string coercion/non-boolean throw.
apps/sim/lib/table/query-builder/tests/converters.test.ts New test file covering filterRulesToFilter (per-operator serialization, array splitting, isEmpty/isNotEmpty, same-column AND merge, OR boundary) and filterToRules ($empty bool+string, pattern round-trips, multi-operator column lossless round-trip).

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    UI["Filter UI (table-filter.tsx)"]
    CONV["filterRulesToFilter (converters.ts)"]
    SQL["buildFilterClause (sql.ts)"]
    DB[(PostgreSQL JSONB)]

    UI -->|"FilterRule[]"| CONV
    CONV -->|"Filter object"| SQL

    CONV -->|"isEmpty/isNotEmpty → {$empty: bool}"| SQL
    CONV -->|"contains/ncontains/startsWith/endsWith → {$op: string}"| SQL
    CONV -->|"in/nin → {$op: array}"| SQL
    CONV -->|"same-column AND → mergeConditions()"| CONV

    SQL -->|"$contains / $ncontains → buildLikeClause()"| BL["ILIKE / NOT ILIKE\n(empty-pattern rejection)"]
    SQL -->|"$startsWith → buildLikeClause('startsWith')"| BL
    SQL -->|"$endsWith → buildLikeClause('endsWith')"| BL
    SQL -->|"$empty → coerceEmptyFlag() → buildEmptyClause()"| BE["IS NULL OR = ''\nIS NOT NULL AND <> ''"]

    BL --> DB
    BE --> DB

    FTRULES["filterToRules (converters.ts)"]
    DB -->|"stored Filter"| FTRULES
    FTRULES -->|"$empty: true → isEmpty\n$empty: false → isNotEmpty"| UI
    FTRULES -->|"$startsWith/etc → op.substring(1)"| UI
Loading

Reviews (2): Last reviewed commit: "feat(tables): expand filter operators (n..." | Re-trigger Greptile

Comment thread apps/sim/lib/table/query-builder/converters.ts
Comment thread apps/sim/lib/table/sql.ts
@waleedlatif1 waleedlatif1 force-pushed the waleedlatif1/tables-module-gaps branch from 145e18c to 5d81d54 Compare May 31, 2026 19:26
…, not-in, empty)

Add does-not-contain ($ncontains), starts-with ($startsWith), ends-with
($endsWith), not-in-array ($nin, previously executed server-side but unexposed
in the UI), and is-empty/is-not-empty ($empty) filter operators end-to-end —
SQL builder, condition types, query-builder converters/constants, the filter
UI, the Table tools/block descriptions, and docs.

Also fix correctness bugs in the filter builder surfaced by the wider operator
set:
- Same-column AND rules (e.g. age > 18 AND age < 65, or name startsWith 'A'
  AND name endsWith 'Z') silently overwrote each other because the AND group
  was keyed by column name. They now merge into one operator object, which
  also makes Filter -> rules -> Filter round-trip losslessly for multi-operator
  columns.
- $nin values were not split into an array like $in, and textual-match values
  like "123" were numeric-coerced (breaking the ILIKE path).
- A non-boolean $empty operand from the raw API silently inverted the check; it
  now coerces 'true'/'false' strings and otherwise returns a 400.
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 8c41203. Configure here.

@waleedlatif1 waleedlatif1 merged commit 919fa52 into staging Jun 1, 2026
14 checks passed
@waleedlatif1 waleedlatif1 deleted the waleedlatif1/tables-module-gaps branch June 1, 2026 05:49
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