db: 3d fix — don't let shortid auto-fill mask INSERT arity mismatch
plan_shortid_autofill read exactly listed_columns.len() cells from the materialised row source. When the row source produced a different column count than the user's list, the extra columns were silently dropped (wider → wrong data, insert succeeded) or read out of range (narrower). Guard: if the materialised statement's column_count differs from the listed-column count, skip auto-fill and execute the verbatim statement so the engine reports the mismatch — matching the non-auto-fill path. A friendly pre-flight diagnostic remains sub-phase 3i. Tests: VALUES with too many values; INSERT…SELECT with a wider and a narrower projection — each rejected with nothing persisted.
This commit is contained in:
@@ -5832,6 +5832,15 @@ fn plan_shortid_autofill(
|
||||
// as concrete rows for the listed columns.
|
||||
let listed_count = listed_columns.len();
|
||||
let mut stmt = conn.prepare(row_source).map_err(DbError::from_rusqlite)?;
|
||||
// Arity guard: if the row source's column count disagrees with
|
||||
// the user's column list, do NOT auto-fill — reading
|
||||
// `listed_count` cells would silently drop extra columns (or
|
||||
// error opaquely on too few). Defer to the verbatim statement
|
||||
// so the engine reports the mismatch as it does on the
|
||||
// non-auto-fill path (a friendly pre-flight lands in 3i).
|
||||
if stmt.column_count() != listed_count {
|
||||
return Ok((sql.to_string(), Vec::new()));
|
||||
}
|
||||
let mut rows: Vec<Vec<rusqlite::types::Value>> = Vec::new();
|
||||
{
|
||||
let mut q = stmt.query([]).map_err(DbError::from_rusqlite)?;
|
||||
|
||||
@@ -661,3 +661,56 @@ fn compound_pk_with_shortid_member_autofills() {
|
||||
);
|
||||
assert_eq!(rows[0][1], "1", "{rows:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autofill_does_not_mask_arity_mismatch() {
|
||||
// A column-list / value-count mismatch must still be rejected
|
||||
// even when a shortid auto-fill would otherwise kick in — the
|
||||
// auto-fill path must not silently drop the extra value. (3i
|
||||
// adds a friendly pre-flight; until then we defer to the
|
||||
// engine rather than mask the error.)
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
|
||||
let outcome = run_sqlinsert(&db, &rt, "sqlinsert into t (label) values ('a', 'b')");
|
||||
assert!(
|
||||
outcome.is_err(),
|
||||
"arity mismatch must be rejected, not masked: {outcome:?}",
|
||||
);
|
||||
let rows = csv_rows(&project, "t");
|
||||
assert!(rows.is_empty(), "no row should land on a rejected insert: {rows:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autofill_insert_select_wider_projection_is_rejected() {
|
||||
// SELECT projects more columns than the list: the guard defers
|
||||
// to the engine instead of dropping the extra projection.
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "src", &[("a", Type::Text), ("b", Type::Text)], &["a"]);
|
||||
create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
|
||||
run_sqlinsert(&db, &rt, "sqlinsert into src (a, b) values ('p', 'q')").expect("seed");
|
||||
let outcome = run_sqlinsert(&db, &rt, "sqlinsert into t (label) select a, b from src");
|
||||
assert!(outcome.is_err(), "wider projection must be rejected: {outcome:?}");
|
||||
assert!(csv_rows(&project, "t").is_empty(), "nothing should land");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autofill_insert_select_narrower_projection_is_rejected() {
|
||||
// SELECT projects fewer columns than the list: the guard
|
||||
// prevents an out-of-range read and defers to the engine.
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "src", &[("a", Type::Text)], &["a"]);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::ShortId), ("x", Type::Text), ("y", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
run_sqlinsert(&db, &rt, "sqlinsert into src (a) values ('p')").expect("seed");
|
||||
let outcome = run_sqlinsert(&db, &rt, "sqlinsert into t (x, y) select a from src");
|
||||
assert!(outcome.is_err(), "narrower projection must be rejected: {outcome:?}");
|
||||
assert!(csv_rows(&project, "t").is_empty(), "nothing should land");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user