10 KiB
Session handoff — 2026-05-18 (17)
Seventeenth handover. This session was an implementation run: it built ADR-0026 (complex WHERE expressions) — the keystone the handoff-16 design run set up. Steps 1–4 and 6 of the ADR's build order are done, committed, tested, and clippy- clean. Step 5 (the §7 diagnostic flagging) is deferred to ADR-0027 — a deliberate sequencing call; see §3, which is the one thing in this handoff that may want your input.
Headline: complex WHERE expressions work end to end.
update / delete / show data take a full boolean
expression — AND / OR / NOT, the six comparisons,
LIKE / IN / BETWEEN / IS [NOT] NULL, parentheses; it
compiles to parameterised SQL; show data gained where and
limit. The functional C5a requirement is satisfied.
State at handoff
Branch: main. Working tree clean. Four commits since
handoff-16 (ac41938), all local — push asynchronously, do
not treat as blocking:
a50c6cd WHERE expressions: matrix cells + predicate_tail grammar fix (ADR-0026 step 6)
f75f71b WHERE expressions: wire into update/delete/show data + SQL gen (ADR-0026 steps 3-4)
59e6a54 grammar: WHERE-expression fragment + Expr AST + build_expr (ADR-0026 step 2)
f0b2043 walker: add Subgrammar node + recursion-depth cap (ADR-0026 step 1)
Tests: 1079 passing, 0 failing, 1 ignored (cargo test — up from 1039). The ignored test is the long-standing
```ignore doc-test in src/friendly/mod.rs. The
typing-surface matrix is 161 cells (was 152 — +9 in the
new tests/typing_surface/where_expression.rs).
Clippy: clean (cargo clippy --all-targets -- -D warnings, nursery group).
§1. What shipped — ADR-0026 steps 1–4 + 6
Step 1 — Subgrammar node + depth cap (f0b2043)
New Node::Subgrammar(&'static Node) variant: a named
static grammar fragment recurses through a reference (a
Seq / Choice embeds children by value and cannot close a
cycle). WalkContext::subgrammar_depth counts active frames;
past MAX_SUBGRAMMAR_DEPTH (64) the walk fails friendly
(parse.custom.expression_too_deep) instead of overflowing
the stack. Depth is saved/restored per frame so a
rolled-back Choice branch leaves no residue.
Step 2 — expression grammar + Expr AST + build_expr (59e6a54)
src/dsl/grammar/expr.rs (new): the stratified
WHERE-expression grammar — or / and / not /
bool_primary / predicate tiers as named static Node
fragments, recursing through Subgrammar. New recursive
Expr / Predicate / Operand / CompareOp AST in
dsl::command. build_expr folds the flat matched-terminal
slice into an Expr.
Steps 3–4 — wiring + SQL generation (f75f71b)
- Grammar (
grammar/data.rs):update/deletewhereis nowSubgrammar(&expr::OR_EXPR);show datagained optionalwhere <expr>andlimit <n>. - AST:
RowFilter::Where{column,value}→RowFilter::Where(Expr);ShowDatagainedfilter: Option<Expr>+limit: Option<u64>. ARowFilter::eqconvenience constructor keeps simple-equality call sites readable. - SQL (
db.rs):compile_exprlowersExprto a parameterised WHERE (every literal a?placeholder,<>for inequality).show data … limit nemitsLIMIT ?with an implicit primary-keyORDER BY. - §8 hints: the expression's right-hand operands resolve
through a schema-aware
DynamicSubgrammar(where_rhs_operand) so the hint panel narrows to the compared column's type — but the operand grammar carries no validators, so a type-mismatched literal parses (§7 permissive). - 11 db integration tests cover AND/OR/NOT, BETWEEN, IN,
LIKE, filtered
show data, limit ordering.
Step 6 — matrix cells + a grammar fix (a50c6cd)
tests/typing_surface/where_expression.rs (9 cells). Writing
them caught a real bug: predicate_tail's [NOT] negatable
branch started with Optional(not), and an Optional-first
Seq always "commits" — so the walker's Choice returned
that branch's Incomplete early and discarded the sibling
branches' expected sets, dropping is and the comparison
operators from completion after a column. Fixed by splitting
into explicit NOT negatable + bare negatable branches;
the matched terminal sequence is unchanged so build_expr
was untouched.
§2. The §3 builder realization (settled with the user)
ADR-0026 §3 admitted two realizations of "the fragment-builder the walker invokes". The project owner chose option 1 ("reconstruct in builder") before implementation:
- The stratified grammar is walked normally; terminals flow
into the flat
MatchedPathunchanged (driving highlight / completion / the expected-set).build_exprthen folds that flat slice into theExpr— a deterministic recursive descent, run only at submit-time dispatch. - Two honest deviations from §3's wording, recorded in
ADR-0026's new "As-built notes" section: there is no
MatchedKind::Exprvariant (MatchedPathstays purely terminals; theExpris built in the commandast_builders), and there is a second structural pass (scoped to dispatch — "no second parse" read as "no separate parser framework").
Do not re-litigate; build to the as-built notes.
§3. ⚠️ Step 5 is deferred — this is the one open call
ADR-0026 step 5 is "schema-aware type-mismatch and = NULL
flagging in the walker". It splits in two:
- The §7 behaviour relaxation — DONE. A type-mismatched
WHERE literal (
where Age = 3.14,Agean int) used to be rejected at bind time. It no longer is:bind_where_literalbinds it by its syntactic shape and the command runs. Test:walker::tests::phase_d_delete_where_permits_decimal_at_int_column. - The §7 diagnostic flagging — DEFERRED to ADR-0027.
Surfacing a type-mismatched comparison or
= NULLas an error-class highlight + hint is the seam with ADR-0027, which designs the walker diagnostics-severity model the flagging belongs in. ADR-0027 itself says its WARNING severity "has no triggers until ADR-0026 is implemented" — i.e. ADR-0026's type-mismatch /= NULLconditions are those triggers.
Why deferred, not done badly now: building the flagging
as a standalone ad-hoc mechanism (a bespoke HighlightClass:: Error pass + a bespoke hint) before ADR-0027's severity
model exists would almost certainly be reworked when that
model lands. The clean sequencing is: implement ADR-0027's
diagnostics-severity model, and make ADR-0026 §7's
type-mismatch + = NULL detection the first triggers feeding
it.
This is a sequencing judgement made while you were away.
If you'd rather have a quick standalone §7 flag now, say so;
otherwise the recommendation is to fold it into ADR-0027.
It's recorded in ADR-0026's "As-built notes", requirements.md
C5a, and CLAUDE.md's deferred list.
§4. What's next
- ADR-0027 — input-field validity indicator. Now
genuinely unblocked: ADR-0026 supplies the WARNING-severity
triggers (type mismatch,
= NULL). Implementing ADR-0027 should also land ADR-0026 step 5's flagging as its first triggers (see §3). - ADR-0028 — query plans (
explain). Also unblocked —show data … where(now real) is the filtered query whose plan flips between a full scan and an index search. - Either order after ADR-0027, per handoff-16.
Other open clusters are unchanged from handoff-16 §3
(snapshot/undo U1/U2; constraints C3 — CHECK can now
reuse ADR-0026's expression grammar; C4 m:n; C3a; C1
table rename; H1; SD1; TT5 CI; V4; I1; TU1).
Prioritisation is a user product decision — ask.
§5. Architectural delta (vs. handoff-16)
Grammar / walker
Node::Subgrammar(&'static Node)— new variant; static counterpart ofDynamicSubgrammar.WalkContext::subgrammar_depth: usize;walker::driver::MAX_SUBGRAMMAR_DEPTH = 64.CompletionProbegainedpending_hint_mode— completion now suppresses keyword candidates at a grammar-declaredProseOnlyslot (the WHERE operand, which also accepts a column ident, would otherwise surface the misleadingnull/true/falsetrio).src/dsl/grammar/expr.rs— the whole expression fragment +build_expr. Entry point:pub static expr::OR_EXPR.
Command AST
Expr/Predicate/Operand/CompareOp— new, recursive; indsl::command, re-exported fromdsl.RowFilter::Where(Expr)(wasWhere{column,value});RowFilter::eq(col, val)convenience constructor.Command::ShowDatagainedfilter: Option<Expr>,limit: Option<u64>.
db
Request::QueryData/Database::query_datagainedfilter+limit; signature is nowquery_data(table, filter, limit, source).compile_expr/compile_predicate/compile_operand/bind_where_literal/syntactic_bound—Expr→ SQL.do_query_databuildsWHERE/ORDER BY/LIMIT.
Catalog
- New key
parse.custom.expression_too_deep(depth cap).
A latent bug fixed in passing
completion::invalid_ident_at_cursor treated a digit-led
token (1) as an identifier — now that the WHERE operand
slot accepts a column ref and a literal, it mis-flagged
where id=1's 1 as an unknown column. It now skips
digit-led tokens. Regression test:
completion::tests::numeric_literal_in_where_is_not_flagged_as_invalid_column.
§6. How to take over
- Read this file, then handoff-16 for the design-run context that set ADR-0026 up.
- Read
CLAUDE.md— working-style rules. - Read
docs/adr/0026-complex-where-expressions.md— especially the new "As-built notes" section (§3 builder realization, the deviations, the step-5 deferral). - Read
docs/adr/0027-input-validity-indicator.md— the recommended next ADR; §3 of this handoff explains how ADR-0026 step 5 folds into it. - Run
cargo test— 1079 passing, 0 failing, 1 ignored. - Run
cargo clippy --all-targets -- -D warnings— clean. - Decide the §3 question (standalone §7 flag now vs. fold into ADR-0027), then start ADR-0027.
Note on the typing-surface matrix
tests/typing_surface/ is now 161 cells. After any
grammar/walker change: a failing cell with correct new
behaviour → update its snapshot
(INSTA_UPDATE=always cargo test --test typing_surface_matrix <family>); a failing cell with wrong behaviour → the cell
earned its keep. This session: 5 delete_with_where /
update_with_where snapshots were correctly updated (the
WHERE operand surface widened), and the new
where_expression family added 9 cells.