fix: INSERT Form B value-count UX (ADR-0033 Amendment 5)

Three layered fixes for advanced/simple-mode positional INSERT
value-count mismatches (e.g. `insert into T values (...)` with the
wrong number of values for T's column count), plus ADR-0033
Amendment 5 recording the gate refinement.

Walker diagnostic (dml_insert_arity_diagnostics): the function's own
doc-comment recorded the no-column-list (Form B) case as deferred.
This commit closes that gap. Form B mismatches now emit a new
diagnostic.insert_arity_mismatch_form_b ERROR per offending tuple,
keyed off the target table's column count from the schema cache. The
[ERR] validity indicator (ADR-0027) lights up at typing time for the
reported scenario, no longer needing a submit.

Cross-mode pointer gate (advanced_alternative_note): refactored from
a hand-rolled Form B count check to a single input_verdict_in_mode(
input, schema, Mode::Advanced) call. The pointer fires only when the
verdict is None — the ADR-0027 sense of "valid". Any future static
check added to the verdict pipeline participates automatically; no
per-feature maintenance.

Teaching notes for the value-count cases users can hit before the
indicator turns red:
  - simple-mode submit: insert.form_b_extra_values_note covers under-,
    in-window, and over-supply against the Form B contract; suppressed
    when the cross-mode pointer fires (to avoid parallel advice).
  - advanced-mode dispatch pre-flight:
    insert.form_b_positional_count_mismatch_note catches a submitted
    mismatch with a teaching message before the engine produces its
    raw NOT-NULL / type error.

The advanced_mode.also_valid_sql pointer wording was reworked to
"trying to write SQL? switch with `mode advanced`, or prefix `:` to
run once". One insta snapshot regenerated.

ADR-0033 Amendment 5 records the gate change: Amendment 3's "would
parse in advanced mode" now reads as "valid in advanced mode" in the
explicit verdict-is-None sense, with the precise definition spelled
out so future readers can't drift back to the syntactic-only reading.
ADR-0000 index entry updated; docs/requirements.md H1a citation added
listing the three new pedagogical strings.

Tests added (8): four walker arity tests (under-supply, over-supply,
match, unknown-table); two app-level teaching-note tests for the
sibling cases (under-supply, over-supply beyond total); one pointer-
gate unit test pinning the bug-case suppression; one gate-precedence
test ensuring only one advice line per error. Existing
simple_mode_submit_of_sql_construct_appends_advanced_pointer updated
to use a known schema (the new validity gate requires it).

Full suite: 2031 passed, 0 failed, 0 unexpected skips. Clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-28 16:38:33 +00:00
parent 9468324d56
commit c12ed1da9a
9 changed files with 898 additions and 48 deletions
+94
View File
@@ -1553,6 +1553,100 @@ The existing `delete … --all-rows` fall-back is unaffected. Folded into
the ADR-0038 echo effort (the fix that makes `update … --all-rows`
echoable).
## Amendment 5 — `also_valid_sql` fires on *validity*, not just *parse* (2026-05-28)
Amendment 3 introduced the `advanced_mode.also_valid_sql` pointer and
described its gate as *"the same line would parse in advanced mode."*
Issue #1 surfaced that this is too weak: an advanced-mode positional
`INSERT INTO T VALUES (…)` with a value count that doesn't match the
target table's column count **parses** (the SQL grammar accepts any
positional value count) but then fails at the engine. The pointer
fired, telling the user to switch modes, and the same line failed
again — the suggestion was misleading. Recorded with explicit user
approval (2026-05-28).
### Why "parse" was the wrong bar
"Would parse" is a purely syntactic check: the structural grammar
accepts the input. It says nothing about whether subsequent static
checks (type slots, INSERT arity, predicate warnings, schema-existence
diagnostics, …) would also pass. The cross-mode pointer's job is to
direct the user to the mode that **actually** runs the line; a
syntactic-only gate over-promises in every case where a non-grammar
static check would still reject. The Form B positional arity case is
just the first one observed.
### "Valid" — the explicit definition used by this amendment
The pointer fires only when the line is **valid** in advanced mode,
where **valid** is the ADR-0027 sense — operationally, the value
returned by `crate::dsl::walker::input_verdict_in_mode(input, schema,
Mode::Advanced)` is `None`:
- The structural parse succeeds (`WalkOutcome::Match`), **and**
- No diagnostic of `Severity::Warning` or `Severity::Error` is
produced by any pass that contributes to `input_verdict_in_mode`
(schema-existence, SQL-predicate warnings, compound-arity errors,
INSERT-arity errors — Form A *and* Form B — and any further
diagnostic added to the pipeline in the future).
This is the same condition the in-app `[ERR]` / `[WRN]` validity
indicator uses (ADR-0027 §2). Tying the pointer to it gives one
canonical "is this line OK in this mode?" answer that every surface
shares — by construction, the indicator and the pointer can never
disagree about a given line.
### Decision
- `advanced_alternative_note` (`src/input_render.rs`) reads
`input_verdict_in_mode(…, Mode::Advanced)`. The pointer fires only
for verdict `None`.
- Any static diagnostic that participates in the verdict automatically
participates in the gate. No bespoke pre-flight check lives in the
pointer helper.
- The Amendment 3 description (*"would parse in advanced mode"*)
should be read as a synonym for **valid** in this amendment's
sense, not as a literal syntactic-only test.
### Diagnostic added to close the issue #1 gap
ADR-0033 §8.1's existing `dml_insert_arity_diagnostics` checked only
Form A (column list present). Its own doc-comment recorded the Form B
case as deferred. This amendment closes that gap: the same function
now also checks the Form B case (no column list) by comparing each
VALUES tuple's arity against the target table's full column count from
the schema cache, emitting a new diagnostic key
`diagnostic.insert_arity_mismatch_form_b`. The `[ERR]` validity
indicator now lights up at typing time for the user-reported scenario,
and the cross-mode pointer gate sees the verdict and stays silent.
### Tests
The amendment's behavioural promise is locked down by:
- `dsl::walker::tests::insert_form_b_arity_mismatch_under_supply_fires` —
the user's reported 4-col / 3-value case produces an Error diagnostic.
- `dsl::walker::tests::insert_form_b_arity_mismatch_over_supply_fires` —
symmetric case, 5 values for a 4-col table.
- `dsl::walker::tests::insert_form_b_arity_match_is_silent` — happy path
(every column supplied, autos included) stays silent.
- `dsl::walker::tests::insert_form_b_arity_unknown_table_is_silent` —
defensive: an unknown table defers to the schema-existence pass.
- `input_render::tests::ambient_hint_omits_advanced_pointer_when_form_b_value_count_wouldnt_match` —
the pointer is suppressed for the bug case via the validity verdict.
- `app::tests::simple_mode_submit_of_sql_construct_appends_advanced_pointer` —
multi-row VALUES (a true SQL-only construct) still fires the pointer
when the table exists in the schema.
### Behaviour preserved
The pointer still fires for every input that is genuinely SQL-only
syntax in simple mode and is valid as advanced SQL — `delete …
returning *`, multi-row VALUES against a known table, `INSERT … ON
CONFLICT …` over a known target, and so on. The narrowing is
**exactly** the set of inputs where switching modes wouldn't help, no
more.
## See also
- ADR-0005 — the ten-type vocabulary INSERT works with.