feat: bring simple-mode insert arity diagnostics to parity with advanced

A wrong-count simple-mode insert now shows the friendly per-column arity
message at typing time (instead of a bare "expected `,`/`)`") and is
blocked from dispatch at submit — unifying simple and advanced mode onto
the one ADR-0027 model (structural parse + ERROR diagnostic), where they
had diverged.

Grammar: a simple-mode-only arity gate (dsl_insert_value_list) routes a
wrong-count DSL insert tuple to the type-blind fallback so it matches
structurally and the per-tuple arity diagnostic fires. The gate is gated
to simple mode, so advanced behaviour is unchanged. count_tuple_values
and the target-column selection (insert_target_columns) are now shared
by both grammars.

Diagnostic: dml_insert_arity_diagnostics is mode-aware — advanced Form B
expects all columns; simple Form B/C expects the user-fillable columns
(serial/shortid auto-fill). It counts the DSL Form A role and scans the
keyword-less Form C tuple. New catalog keys name the fillable/auto split
and the all-auto-table case.

Submit: a wrong-count DSL insert now parses Ok + carries the ERROR
diagnostic, so a unified Ok-arm pre-flight (dsl_insert_count_mismatch_notes)
blocks dispatch and teaches; the previous Err-arm note retires.
advanced_alternative_note's gate now reads the validity verdict so it
still fires for the parse-Ok-with-error shape.

Docs: ADR-0036 Amendment 2 (+ README index) and requirements.md H1a.
This commit is contained in:
claude@clouddev1
2026-05-29 20:45:21 +00:00
parent 7cccf4eabb
commit 10e5197c19
16 changed files with 812 additions and 240 deletions
+77 -13
View File
@@ -1361,6 +1361,30 @@ impl App {
source: input.to_string(),
}];
}
// Issue #17: simple-mode (DSL) counterpart. A wrong-count
// DSL insert now parses `Ok` (so the typing-time arity
// diagnostic can fire), so dispatch is gated here — the
// same teaching the old parse-error path showed, now with
// the insert reliably blocked from reaching the worker.
if let Some(notes) = crate::input_render::dsl_insert_count_mismatch_notes(
input,
&cmd,
&self.schema_cache,
) {
self.push_output(OutputLine {
text: crate::t!("dsl.running", input = input),
kind: OutputKind::Echo,
mode_at_submission: mode,
styled_runs: None,
});
for note in notes {
self.note_error(note);
}
self.note_error(render_usage_block(input));
return vec![Action::JournalFailure {
source: input.to_string(),
}];
}
self.push_output(OutputLine {
text: crate::t!("dsl.running", input = input),
kind: OutputKind::Echo,
@@ -1426,19 +1450,13 @@ impl App {
{
self.note_error(note);
}
// Issue #1 sub-task 2: append a teaching note when the
// Form B `insert into <T> values (…)` line failed
// because the user supplied more values than the
// non-auto-generated columns expect. The parse error
// shows the literal "expected `)`"; the note explains
// *why* fewer values are expected and shows the
// column-list override path.
if mode == Mode::Simple
&& let Some(note) =
crate::input_render::form_b_extra_values_note(input, &self.schema_cache)
{
self.note_error(note);
}
// Issue #1 sub-task 2's Form B teaching note used to be
// appended here, because a wrong-count Form B insert
// failed to parse and landed in this Err arm. As of issue
// #17 such tuples parse `Ok` (so the typing-time arity
// diagnostic fires) and the teaching + dispatch block now
// live in the Ok arm's `dsl_insert_count_mismatch_notes`
// pre-flight — a single model shared with advanced mode.
// ADR-0021 §2: append the usage block (if a
// known command-entry keyword was consumed) or
// the available-commands fallback (§5).
@@ -3129,6 +3147,52 @@ mod tests {
);
}
#[test]
fn simple_mode_submit_of_form_b_count_mismatch_does_not_dispatch() {
// Issue #17 EXECUTION SAFETY. Once simple-mode wrong-count Form B
// tuples parse `Ok` (so the typing-time arity diagnostic can
// fire), the submit path must still NOT dispatch the insert — a
// wrong-count insert would otherwise reach the worker and fail
// (or, worse, write the wrong row). The unified Ok-arm pre-flight
// must block dispatch exactly as the old parse-error path did.
let mut app = App::new();
install_customers_schema_two_serials(&mut app);
type_str(&mut app, "insert into Customers values ('Oli', 52, 3)");
let actions = submit(&mut app);
assert!(
!actions.iter().any(|a| matches!(a, Action::ExecuteDsl { .. })),
"simple-mode Form B count mismatch must NOT dispatch; got: {actions:?}",
);
}
#[test]
fn simple_mode_submit_of_form_b_undersupply_does_not_dispatch() {
// Companion to the above for under-supply.
let mut app = App::new();
install_customers_schema_two_serials(&mut app);
type_str(&mut app, "insert into Customers values ('Oli')");
let actions = submit(&mut app);
assert!(
!actions.iter().any(|a| matches!(a, Action::ExecuteDsl { .. })),
"simple-mode Form B under-supply must NOT dispatch; got: {actions:?}",
);
}
#[test]
fn simple_mode_submit_of_form_a_count_mismatch_does_not_dispatch() {
// Form A (explicit column list) wrong count must also not
// dispatch — it previously parse-errored; the unified pre-flight
// must keep it blocked.
let mut app = App::new();
install_customers_schema_two_serials(&mut app);
type_str(&mut app, "insert into Customers (Name, Age) values ('Oli')");
let actions = submit(&mut app);
assert!(
!actions.iter().any(|a| matches!(a, Action::ExecuteDsl { .. })),
"simple-mode Form A count mismatch must NOT dispatch; got: {actions:?}",
);
}
#[test]
fn simple_mode_submit_of_sql_construct_appends_advanced_pointer() {
// ADR-0033 Amendment 3 (+ Amendment 5): submitting a line in