Wires the stratified WHERE-expression fragment into the three
filter commands and compiles the resulting Expr to SQL.
Grammar (data.rs): the `update` / `delete` `where` clause is
now the expression fragment (`Subgrammar(&expr::OR_EXPR)`) in
place of the single `col = val` slot; `show data` gains an
optional `where <expr>` and an optional `limit <n>` (a
non-negative integer, validated at parse time). The
expression's right-hand operands are a schema-aware
`DynamicSubgrammar` so the hint panel still narrows to the
left column's type (ADR-0026 §8) — but the inner grammar is
permissive: a type-mismatched literal still parses (§7).
AST: `RowFilter::Where{column,value}` -> `RowFilter::Where(Expr)`;
`ShowData` gains `filter: Option<Expr>` and `limit: Option<u64>`.
A `RowFilter::eq` convenience constructor keeps simple-equality
call sites and tests readable.
SQL (db.rs): `compile_expr` lowers an `Expr` to a
parameterised WHERE — every literal a `?` placeholder,
identifiers `quote_ident`-quoted, `<>` for inequality. A
literal compared against a column binds through that column's
type where compatible and falls back to its syntactic shape on
a mismatch (§7 — permissive). `show data ... limit n` emits
`LIMIT ?` with an implicit primary-key `ORDER BY`, so it is a
stable "first n by primary key".
completion.rs: `invalid_ident_at_cursor` no longer mis-flags a
digit-led literal (`1`) as an unknown column now that the
WHERE operand slot also accepts a column reference; a
`ProseOnly` slot suppresses keyword candidates even when the
expected set also carries a column ident.
11 db integration tests cover AND / OR / NOT, BETWEEN, IN,
LIKE, filtered `show data`, and limit ordering; walker and
expr unit tests cover the parse surface. Type-mismatch /
`= NULL` diagnostic flagging (§7 highlight + hint) is the
remaining ADR-0026 piece.
`create table … with pk` parsed column types as `name:type`,
while `add column` uses `name(type)`. Unify on the parens
form so column-type syntax is consistent across the DSL:
create table T with pk id(serial), name(text)
Only `COL_SPEC` changes (`:` → `( … )`); `build_create_table`
reads columns by role, so it is unaffected. The `:` that
separates table from column in `add column` / `drop column`
is unchanged. Sweeps the test suite, the typing-surface
matrix (two `after_colon` cells renamed to `after_paren`,
4 snapshots regenerated), the friendly catalog's usage
templates, ADR-0009's example, and requirements.md.
1039 passing / 0 failing / 1 ignored; clippy clean.
run_replay parsed each line with the schemaless parse_command, so
Phase D typed-slot rejections (wrong-count value lists, wrong-type
column values) fired only at bind time during replay — inconsistent
with the interactive path (handoff-12 §2.1).
run_replay now re-snapshots the schema per line (the schema mutates
as replayed create-table / add-column commands run) and parses with
parse_command_with_schema. Extracted build_schema_cache, shared with
the interactive refresh_schema_cache.
Added a replay integration test asserting a typed-slot violation is
caught at parse time (through the replay.error_parse wrapper).
Implements the U4 replay command per handoff §A3:
replay <path>
Reads <path> and dispatches each non-blank, non-`#`-comment
line through the same DSL pipeline as interactive input.
Aborts at the first per-line failure (parse or runtime),
reporting the line number; previously dispatched commands
stay applied (no rollback) — matches the "I'm replaying my
history" mental model where partial replay is a recoverable
state.
Architecture choices and why:
- **Parsed by the DSL parser** (Command::Replay), not as an
app-level command alongside `import` / `export`. The
handoff's implementation sketch was explicit and the
parsed-AST shape gives us a clean test surface for the
path-lexing rules. A new `path_literal` parser terminal
accepts either a single-quoted string (escape rules
mirror `string_literal` — `''` for a literal quote) or a
bare run of non-whitespace, with explicit refusal of `'`,
`(`, `)`, `;` in bare form. Empty paths fail at parse
time so file-system-layer errors aren't shadowed by
silly inputs.
- **Routed away from the worker thread.** Command::Replay
is intercepted in `App::dispatch_dsl` and emitted as
`Action::Replay` rather than `Action::ExecuteDsl`. Two
reasons: (1) the worker has no filesystem context, and
(2) the replay invocation must NOT land in
`history.log` — otherwise `replay history.log` would
re-trigger itself recursively. Only the individual
sub-commands write to history.log via the normal
per-command persistence path.
- **Inner loop separated from spawn.** `runtime::spawn_replay`
is a thin tokio::spawn wrapper around `runtime::run_replay`,
which is `pub` and returns a Vec<AppEvent>. The inner
function is what tests exercise, sidestepping mpsc plumbing.
- **Relative paths resolve under the project root** so
`replay history.log` works without ceremony from inside
any project. Absolute paths pass through unchanged.
- **Nested `replay` is refused.** Allowing `replay foo` from
inside a replay file invites infinite-loop footguns and
opens design questions (transitive composition, ordering)
we'd rather not answer right now. Refusal is explicit.
New plumbing:
- `Command::Replay { path }` AST variant + verb/target_table.
- `Action::Replay { path }` runtime action.
- `AppEvent::ReplayCompleted { path, count }` and
`AppEvent::ReplayFailed { path, line_number, command, error }`.
- `runtime::run_replay` (public) and `runtime::spawn_replay`.
- App handlers render success as
`[ok] replay <path> — N command(s) run` and failures as
`replay <path> failed at line N: <error>` with a
` > <command>` echo line for line context. Line 0 is the
"file open failed" signal — header reads
`replay <path> failed: <error>` and the echo line is
suppressed.
- In-app `help` lists the new command with a continuation
describing comment/blank handling and the relative-path
rule.
Tests (+20):
- 7 parser tests covering bare/quoted/escaped paths,
case-insensitive keyword, and refusal cases (no path,
empty quoted path).
- 9 integration tests in `tests/replay_command.rs`:
- happy 3-line replay → 3 commands run, state mutated;
- blank lines + `#` comments skipped;
- empty file + only-comments file → count 0;
- missing file → ReplayFailed line_number 0;
- parse failure mid-replay → reports correct line +
leaves earlier commands applied + does NOT run later
lines;
- runtime failure mid-replay (refers to nonexistent
table) → reports correct line;
- nested replay refused;
- history.log contains per-command entries but NOT the
`replay …` invocation itself.
- 4 App-level tests: Action::Replay dispatch (not
ExecuteDsl); ReplayCompleted rendering; ReplayFailed
rendering with and without line-number context.
541 -> 561 passing, clippy clean with nursery lints,
release build successful.
A future ADR on the parser-as-source-of-truth direction
(handoff §"Pending §3") would bring richer error reporting
for replay parse failures (currently uses the same
single-line wording as interactive parse failures, which is
adequate but not great when a script has many lines around
the failing one).