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.
|
// as concrete rows for the listed columns.
|
||||||
let listed_count = listed_columns.len();
|
let listed_count = listed_columns.len();
|
||||||
let mut stmt = conn.prepare(row_source).map_err(DbError::from_rusqlite)?;
|
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 rows: Vec<Vec<rusqlite::types::Value>> = Vec::new();
|
||||||
{
|
{
|
||||||
let mut q = stmt.query([]).map_err(DbError::from_rusqlite)?;
|
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:?}");
|
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