[00123] Implement the Rusty Filter Query Grammar Parser and AST Crate - #113
Merged
rorychatt merged 6 commits intoAug 2, 2026
Merged
Conversation
…aluator and printer A dependency-light Rust port of the filter query grammar the frontend's filter-query-editor 2.2.0 bundle implements, so a query can be authored on either side and mean the same thing. Only serde and serde_json; no ANTLR runtime, no build script. Every behaviour was cross-checked against the shipped bundle by replaying a corpus through both implementations: 42 valid queries matched on both AST JSON and formatQuery output, 46 error cases matched on first message and byte span, and 29 filters over 6 rows matched on evaluateFilter and countMatches. Divergences, all documented in the module docs: error spans are byte offsets rather than UTF-16 code units, and a syntax error stops the parse instead of recovering into an ANTLR error cascade.
Two integration suites, both measured against the shipped filter-query-editor 2.2.0 bundle rather than written from the grammar. frontend_ast_compat.rs pins 23 expectations that are the verbatim JSON the bundle emits, so serde key *presence* is significant: a comparison omits `negate`, a text operation emits `negate: false`, and existence operations carry empty args. The module doc records the capture recipe (import dist/index.js by absolute file:// URL, call parseQuery). round_trip.rs asserts what a cache key needs: 39 of the 47 corpus queries are exactly AST-stable across print/parse, printing is a fixed point from the second pass on, equivalent spellings collapse onto one key, and 14 differently-meaning queries produce 14 distinct keys. The 8 remaining queries are negated leaves. Printing lifts the negation onto a `NOT (...)` group, so re-parsing moves `negate` from the condition filter to a wrapping group filter. The reference bundle was probed at each of them and reshapes identically, so the plan's claim of universal AST stability does not hold for either implementation. Rather than drop the assertion, NOT_AST_STABLE lists the 8 exactly and the test fails if a ninth query joins them or a listed one starts passing; semantic equivalence over 7 rows is asserted for all 47. Cross-check totals, all with a mutation probe confirming the comparator discriminates: 42 valid queries with 0 AST or formatQuery mismatches, 46 error cases with 0 message or span mismatches, 29 filters x 6 rows with 0 evaluateFilter or countMatches mismatches. 195 tests pass; cargo fmt and clippy --all-targets -D warnings clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lude Additive only: nothing existing changes behaviour, and `to_json`, `register_events` and every field are untouched. `rusty/Cargo.toml` gains the path dependency. `data_table.rs` gains `From<ColType> for ColumnType` collapsing eight cell types onto the grammar's five, `filter_columns()` offering only `filterable && !hidden` columns, and `apply_filter(query)` returning a table of the matching rows or the parse and validation errors. Excluding non-filterable columns is what `DataTableFilterOption.tsx` does before handing columns to the browser's editor, so naming one gets the same `Column 'x' does not exist` error on both sides instead of working on one. `query_cache.rs` gains one associated function, `filtered_key`, keying on the canonical spelling so `[age] > 1` and `[age] greater than 1` share a cache entry. Associated rather than a method because it reads no cache state, which keeps it clear of the module's rule that the cache lock is never held across an `.await`. The SWR state machine is untouched. The prelude re-exports the flat list plus the crate itself. Docs: a new 02_concepts/07_filters.md covering operators, type compatibility, literals, precedence, evaluation and canonical keys; a "Server-side filtering" section on the DataTable page; and a Filters table in the prelude reference. Three grammar quirks the plan did not mention were measured and documented rather than guessed: bare `equal` is rejected though `not equal` is accepted, the `or equal` tail is singular so `greater than or equals` fails, and an unknown string escape keeps its backslash (`\t` is two characters, not a tab) — that last one re-probed against the bundle, which agrees. Both doc code samples were compiled and run, not just written. The widget's "DataTableConnection is not ported" comment now names what apply_filter does while saying plainly that the rendered filter box still posts to a gRPC DataTableService that does not exist, so nothing here implies typing in the browser reaches Rust. Gate: build, test (663 passed, 0 failed), clippy --workspace --all-targets -D warnings and fmt --check all exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two conflicts, both mechanical. `Cargo.toml` gained `rusty-desktop` on main while this branch added `rusty-filter`; the member list keeps both. `data_table.rs` was migrated to `#[derive(Widget)]` on main, deleting the hand-written `impl WidgetData`; the derive is kept and this plan's `filter_columns`, `apply_filter` and `From<ColType> for ColumnType` sit alongside it untouched, since none of them went through `to_json`. Main arrives red, independently of this plan: `cargo test --workspace` on a clean `git archive` of origin/main e6c4398 fails `shared::widget_names::every_widget_type_is_mapped` and `widget_type_scan_finds_known_widgets` (410 passed, 2 failed). The scan those tests use reads `"type": "..."` literals out of hand-written `to_json` bodies, and the derive migration deleted them, so it now finds 26 widget types where it demands 38. The merged tree fails exactly those two and nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main added the rusty-ivyml crate (#110) and repaired the widget_names scan and the rusty-macros clippy/rustfmt failures this branch had recorded as inherited. All three conflicts are additive: the workspace members list and rusty's dependency list each gain both crates, and Cargo.lock is regenerated rather than hand-merged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
23 commits, no conflicts. main's #112 and #111 removed the two failures this branch had documented as inherited: the empty-line-after-doc-comment clippy error at rusty/src/shared/types.rs:77 (the doc comment is gone), and the three pre-commit harness failures (.husky/pre-commit now guards and propagates both helper calls). Every reachable CI gate is exit 0 at this commit: build, test (734 passed, 0 failed), clippy, fmt, test inventory, both shell harnesses, the e2e job's harness parse and widget_harness build, and the desktop-shell job's two steps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rorychatt
deleted the
tendril/00123-ImplementTheRustyFilterQueryGrammarParserAndASTCrate
branch
August 2, 2026 11:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
rusty-filter, a new workspace member: a hand-written lexer and
recursive-descent parser for the Filters.g4 filter-query grammar, its AST, a
type-aware validator, an evaluator, and a canonical printer. Wired into
DataTable, the query cache and the prelude, and documented.
5972 insertions across 20 files, 6 commits, 208 new tests. Every CI gate exit 0.
Commits
786324d7149f0efda2fdbDataTable, the query cache and the prelude; docs93a4611#[derive(Widget)]migration,rusty-desktopa332547rusty-ivyml, plus main's repairs to two inherited gates64f4189What was built
rusty-filter(~5200 lines including tests)parser.rseval.rslexer.rsvalidate.rsprint.rsast.rsFilterGroup/Filter/Conditionwith wire-compatible serdecolumn.rsColumnDef,ColumnType, name normalizationlib.rsThe AST serializes to the same JSON the browser's editor produces, down to key
presence:
skip_serializing_if = "Option::is_none"keepsnegateabsent oncomparisons and
falseon text ops, because that is what the reference does.JavaScript's single number type is reproduced too (
007→7,1.000→1).Wiring
DataTable::apply_filter(query)→Result<Self, Vec<ParseError>>, andfilter_columns(), which excludes non-filterable and hidden columns.From<ColType> for ColumnType— eight cell types onto the validator's five,matching the normalization the browser applies to backend type names.
QueryCache::filtered_key(base, filter)— appends the canonical filter soequivalent spellings share one cache entry and different filters do not.
Docs
02_concepts/07_filters.md(new, 179 lines): the grammar, an operator-spellingstable, per-type compatibility, literals, precedence, evaluation, cache keys,
frontend compatibility, and a "Not implemented" list. Plus a "Server-side
filtering" section on the
DataTablepage and a "Filters" section in the preludereference. Both code samples were compiled and run before committing, via
throwaway examples that were then deleted.
Verification — all Pass
src/frontend/changedIncluding the steps outside the four named gates: test inventory ("intact: 533 at
1d58a57, 741 in the working tree"), both shell harnesses ("ALL 13 CASES PASS","ALL 6 CASES PASS"), the e2e job's harness-parse and
widget_harnessbuild, andthe
desktop-shelljob's two steps. Details inVerification/.What the plan got wrong
Two of the plan's stated expectations were falsified by probing the reference
bundle (
filter-query-editor2.2.0, imported fromsrc/frontend/node_modulesbyabsolute
file:///URL). The tests assert measured behaviour instead,documented at each assertion:
Orgroup evaluatesfalse. It returns true. The cratematches the bundle.
holds for 39 of 47. The other 8 are negated leaves — printing lifts the
negation onto a
NOT (...)group, so re-parsing attaches it to a wrapper. Thebundle was probed at each of the 8 and reshapes identically.
round_trip.rspins them inNOT_AST_STABLEand fails if a ninth joins themor a listed one starts passing. Evaluation equivalence over a 7-row corpus
is asserted separately for all 47, so a reshaped round trip is proved to still
mean the same thing.
Three grammar quirks the plan did not mention were measured rather than guessed:
bare
equalis rejected althoughnot equalis accepted; theor equaltail issingular, so
greater than or equalsfails; and an unknown string escape keepsits backslash. The last took two attempts — a bash heredoc silently collapsed the
backslashes in the first probe, which lied, and the wrong answer had already been
drafted into a doc page before the corrected probe caught it.
Merging against a moving target
origin/mainmoved three times during execution —df4e685→e6c4398→9b7c764→1d58a57, bringing a#[derive(Widget)]migration and two entirelynew crates (
rusty-desktop,rusty-ivyml). Each move was merged and every gatere-run.
It also repaired every failure this plan had documented as inherited: two
widget_namestest failures (9fb4213), tworusty-macrosclippy errors(
5d81694), arusty-macrosrustfmt failure (67e4cd8), thetypes.rs:77clippy error, and the pre-commit harness's three failures. Each had been proved
inherited at the time against a pristine
origin/mainprobe extracted withgit archiveunder its ownCARGO_TARGET_DIR. Deferring them rather than repairingthem was the right call — one of the fixes was 253 lines of parser work this plan
would have duplicated.
Left for others
Artifacts/recommendations.mdhas three items, all follow-on work rather thandefects: wiring the rendered filter box to the parser (High),
apply_filter'sconsuming signature (Small), and an explicit decision on the negated-leaf
round-trip reshape (Small).
Commits
786324d [00123] Add rusty-filter: lexer, parser, AST, validator, evaluator, printer
7149f0e [00123] Add differential and round-trip tests for rusty-filter
fda2fdb [00123] Wire rusty-filter into DataTable and query cache; add docs
93a4611 [00123] Merge main (df4e685)
a332547 [00123] Merge main (9b7c764)
64f4189 [00123] Merge main (1d58a57)
Created using Ivy Tendril.