fix: X4 — advanced-mode SQL INSERT auto-fills omitted non-PK serial (MAX+1)

A Form-A advanced-mode INSERT that omitted a non-PK serial column left it
silently NULL (the column is INTEGER UNIQUE, not NOT NULL, so SQLite
permits it), while simple-mode do_insert auto-fills it with MAX+1. That
violated ADR-0018 §1's "auto-generated on every path" contract and was the
unprincipled serial-vs-shortid asymmetry the ADR set out to remove
(advanced mode already auto-fills shortid).

Fix (decision: advanced mode matches simple mode): the advanced-mode
auto-fill reconstruction — renamed plan_shortid_autofill →
plan_autogen_autofill — now also fills an omitted non-PK serial with
MAX(col)+1 … MAX+n per row (single- and multi-row), reading MAX once under
the worker's single-writer serialisation. PK serial stays on the rowid
alias; Form B (no column list) still supplies every column. Honours
ADR-0018 §1/§5; no ADR amendment needed (the contract already said "every
path"). requirements.md X4 marked resolved.

Tests: 1949 passing (+1), 0 failed, 0 skipped, 1 ignored; clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-27 11:18:57 +00:00
parent 4928a4c4fd
commit 42306d33e3
4 changed files with 121 additions and 28 deletions
+31
View File
@@ -707,6 +707,37 @@ fn autofill_does_not_mask_arity_mismatch() {
assert!(rows.is_empty(), "no row should land on a rejected insert: {rows:?}");
}
#[test]
fn sql_insert_autofills_omitted_nonpk_serial() {
// ADR-0018 §1/§5 + X4: advanced-mode SQL INSERT auto-fills an omitted
// non-PK `serial` column with MAX+1 (per row), exactly as simple-mode
// `do_insert` does — honouring the "auto-generated on every path"
// contract. (Was: silently inserting NULL.) Mirrors the existing
// shortid auto-fill, which already runs on this path.
let (project, db, _dir) = open_project_db();
let rt = rt();
create_cols(&db, &rt, "t", &[("id", Type::Int), ("seq", Type::Serial)], &["id"]);
// Single row, omitting the non-PK serial `seq`.
run_sqlinsert(&db, &rt, "insert into t (id) values (10)").expect("single-row insert runs");
// Multi-row, omitting `seq` — each row gets a distinct, increasing
// serial continuing from the current MAX.
run_sqlinsert(&db, &rt, "insert into t (id) values (20), (30)")
.expect("multi-row insert runs");
let rows = csv_rows(&project, "t");
// No NULL serials, and the sequence is 1, 2, 3 across the three rows.
assert_eq!(
rows,
vec![
vec!["10".to_string(), "1".to_string()],
vec!["20".to_string(), "2".to_string()],
vec!["30".to_string(), "3".to_string()],
],
"omitted non-PK serial auto-filled MAX+1 per row (no NULLs): {rows:?}",
);
}
#[test]
fn autofill_insert_select_wider_projection_is_rejected() {
// SELECT projects more columns than the list: the guard defers