Files
rdbms-playground/tests/sql_insert.rs
T
claude@clouddev1 18d34d0d36 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.
2026-05-22 12:30:57 +00:00

717 lines
26 KiB
Rust

//! Sub-phase 3b integration tests for the advanced-mode SQL
//! `INSERT` surface (ADR-0033 §1).
//!
//! Covers:
//! - Worker round-trip: a validated `INSERT` runs against the
//! database, returns the affected-row count, and re-persists the
//! target table's CSV (ADR-0030 §11).
//! - Single-row, multi-row, explicit-column-list, and full-arity
//! (no column list) forms.
//! - `history.log` records the literal submitted line.
//! - A failing INSERT (PK conflict) rolls back and does NOT
//! re-persist the CSV.
//! - Parse path: `parse_command` (advanced mode) lowers the dev
//! `sqlinsert` scaffold to `Command::SqlInsert`, reconstructing
//! valid `insert …` SQL and extracting the target table.
//! - `__rdbms_*` target tables are rejected at the grammar layer.
//!
//! The dev `sqlinsert` entry word keeps the SQL INSERT path
//! isolated from the DSL `insert` word until sub-phase 3j; the
//! worker-level tests call `db.run_sql_insert` directly with the
//! real reconstructed SQL.
use rdbms_playground::db::{Database, DbError, InsertResult};
use rdbms_playground::dsl::{ColumnSpec, Command, Type, parse_command};
use rdbms_playground::persistence::Persistence;
use rdbms_playground::project;
fn rt() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("tokio rt")
}
fn open_project_db() -> (project::Project, Database, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("create tempdir");
let project =
project::open_or_create(None, Some(dir.path())).expect("open or create project");
let persistence = Persistence::new(project.path().to_path_buf());
let db = Database::open_with_persistence(project.db_path(), persistence)
.expect("open db with persistence");
(project, db, dir)
}
fn read_csv(project: &project::Project, table: &str) -> Option<String> {
std::fs::read_to_string(project.path().join("data").join(format!("{table}.csv"))).ok()
}
/// Create a two-column table `T(a int pk, b text)` — no
/// auto-generated columns, so 3b's explicit-values requirement is
/// satisfied by every INSERT below.
fn create_t(db: &Database, rt: &tokio::runtime::Runtime) {
rt.block_on(db.create_table(
"T".to_string(),
vec![
ColumnSpec::new("a", Type::Int),
ColumnSpec::new("b", Type::Text),
],
vec!["a".to_string()],
None,
))
.expect("create table T");
}
#[test]
fn single_row_insert_persists_and_counts() {
let (project, db, _dir) = open_project_db();
let rt = rt();
create_t(&db, &rt);
let result = rt
.block_on(db.run_sql_insert(
"insert into T (a, b) values (1, 'Ada')".to_string(),
Some("insert into T (a, b) values (1, 'Ada')".to_string()),
"T".to_string(),
Vec::new(),
String::new(),
))
.expect("insert runs");
assert_eq!(result.rows_affected, 1, "one row inserted");
let csv = read_csv(&project, "T").expect("T.csv written after insert");
assert!(csv.contains("Ada"), "CSV reflects the inserted row: {csv:?}");
}
#[test]
fn multi_row_insert_persists_both_rows() {
let (project, db, _dir) = open_project_db();
let rt = rt();
create_t(&db, &rt);
let result = rt
.block_on(db.run_sql_insert(
"insert into T (a, b) values (1, 'first'), (2, 'second')".to_string(),
None,
"T".to_string(),
Vec::new(),
String::new(),
))
.expect("multi-row insert runs");
assert_eq!(result.rows_affected, 2, "two rows inserted");
let csv = read_csv(&project, "T").expect("T.csv written");
assert!(
csv.contains("first") && csv.contains("second"),
"CSV reflects both inserted rows: {csv:?}",
);
}
#[test]
fn no_column_list_full_arity_insert_persists() {
let (project, db, _dir) = open_project_db();
let rt = rt();
create_t(&db, &rt);
let result = rt
.block_on(db.run_sql_insert(
"insert into T values (7, 'full-arity')".to_string(),
None,
"T".to_string(),
Vec::new(),
String::new(),
))
.expect("full-arity insert runs");
assert_eq!(result.rows_affected, 1);
let csv = read_csv(&project, "T").expect("T.csv written");
assert!(csv.contains("full-arity"), "CSV reflects the row: {csv:?}");
}
#[test]
fn insert_appends_literal_line_to_history() {
let (project, db, _dir) = open_project_db();
let rt = rt();
create_t(&db, &rt);
// ADR-0030 §11: the literal submitted line lands in history.log.
let source = "insert into T (a, b) values (1, 'logged')";
rt.block_on(db.run_sql_insert(
"insert into T (a, b) values (1, 'logged')".to_string(),
Some(source.to_string()),
"T".to_string(),
Vec::new(),
String::new(),
))
.expect("insert runs");
let body = std::fs::read_to_string(project.path().join("history.log"))
.expect("history.log present after an INSERT");
assert!(
body.contains(source),
"history.log records the literal INSERT line: {body:?}",
);
}
#[test]
fn failed_insert_rolls_back_and_does_not_repersist() {
let (project, db, _dir) = open_project_db();
let rt = rt();
create_t(&db, &rt);
// First insert succeeds and persists.
rt.block_on(db.run_sql_insert(
"insert into T (a, b) values (1, 'kept')".to_string(),
None,
"T".to_string(),
Vec::new(),
String::new(),
))
.expect("first insert runs");
// Second insert violates the primary key — it must fail and
// leave persistence untouched (the transaction rolls back
// before finalize_persistence).
let outcome = rt.block_on(db.run_sql_insert(
"insert into T (a, b) values (1, 'discarded')".to_string(),
None,
"T".to_string(),
Vec::new(),
String::new(),
));
assert!(outcome.is_err(), "duplicate PK must fail: {outcome:?}");
let csv = read_csv(&project, "T").expect("T.csv still present");
assert!(
csv.contains("kept") && !csv.contains("discarded"),
"failed insert must not be persisted: {csv:?}",
);
}
#[test]
fn failed_multi_row_insert_is_atomic() {
// ADR-0033 §3b DA gate: a multi-row INSERT whose later tuple
// violates a constraint fails as one statement — no partial
// rows land, and the CSV is not rewritten.
let (project, db, _dir) = open_project_db();
let rt = rt();
create_t(&db, &rt);
rt.block_on(db.run_sql_insert(
"insert into T (a, b) values (1, 'existing')".to_string(),
None,
"T".to_string(),
Vec::new(),
String::new(),
))
.expect("seed row");
// Row (2,…) is new but (1,…) collides on the PK — the whole
// statement must fail with neither tuple applied.
let outcome = rt.block_on(db.run_sql_insert(
"insert into T (a, b) values (2, 'fresh'), (1, 'collides')".to_string(),
None,
"T".to_string(),
Vec::new(),
String::new(),
));
assert!(outcome.is_err(), "multi-row PK conflict must fail: {outcome:?}");
let csv = read_csv(&project, "T").expect("T.csv still present");
assert!(
csv.contains("existing") && !csv.contains("fresh") && !csv.contains("collides"),
"no tuple from the failed multi-row insert may land: {csv:?}",
);
}
#[test]
fn parse_path_lowers_sqlinsert_scaffold_to_command() {
// Advanced-mode parse of the dev scaffold reconstructs valid
// `insert …` SQL and extracts the target table.
let command = parse_command("sqlinsert into Orders (id, total) values (1, 99.5)")
.expect("sqlinsert parses in advanced mode");
match command {
Command::SqlInsert { sql, target_table, .. } => {
assert_eq!(sql, "insert into Orders (id, total) values (1, 99.5)");
assert_eq!(target_table, "Orders");
}
other => panic!("expected Command::SqlInsert, got {other:?}"),
}
}
#[test]
fn parse_path_rejects_internal_target_table() {
let result = parse_command("sqlinsert into __rdbms_playground_columns values (1)");
assert!(
result.is_err(),
"an internal `__rdbms_*` target must be rejected: {result:?}",
);
}
// =================================================================
// Sub-phase 3c — INSERT … SELECT
// =================================================================
/// Create a two-column table `name(a int pk, b text)`.
fn create_named(db: &Database, rt: &tokio::runtime::Runtime, name: &str) {
rt.block_on(db.create_table(
name.to_string(),
vec![
ColumnSpec::new("a", Type::Int),
ColumnSpec::new("b", Type::Text),
],
vec!["a".to_string()],
None,
))
.unwrap_or_else(|e| panic!("create table {name}: {e:?}"));
}
#[test]
fn parse_path_lowers_insert_select_to_command() {
let command = parse_command("sqlinsert into archive select * from source")
.expect("INSERT … SELECT parses in advanced mode");
match command {
Command::SqlInsert { sql, target_table, .. } => {
assert_eq!(sql, "insert into archive select * from source");
assert_eq!(target_table, "archive");
}
other => panic!("expected Command::SqlInsert, got {other:?}"),
}
}
#[test]
fn parse_path_lowers_with_prefixed_insert_select() {
// R4: a WITH-prefixed SELECT row source lowers verbatim.
let command = parse_command(
"sqlinsert into archive with t as (select * from orders) select * from t",
)
.expect("WITH-prefixed INSERT … SELECT parses");
match command {
Command::SqlInsert { sql, target_table, .. } => {
assert_eq!(
sql,
"insert into archive with t as (select * from orders) select * from t",
);
assert_eq!(target_table, "archive");
}
other => panic!("expected Command::SqlInsert, got {other:?}"),
}
}
#[test]
fn insert_select_copies_rows_and_persists() {
let (project, db, _dir) = open_project_db();
let rt = rt();
create_named(&db, &rt, "source");
create_named(&db, &rt, "archive");
rt.block_on(db.run_sql_insert(
"insert into source (a, b) values (1, 'one'), (2, 'two')".to_string(),
None,
"source".to_string(),
Vec::new(),
String::new(),
))
.expect("seed source");
let result = rt
.block_on(db.run_sql_insert(
"insert into archive select * from source".to_string(),
Some("insert into archive select * from source".to_string()),
"archive".to_string(),
Vec::new(),
String::new(),
))
.expect("INSERT … SELECT runs");
assert_eq!(result.rows_affected, 2, "both source rows copied");
let csv = read_csv(&project, "archive").expect("archive.csv written");
assert!(
csv.contains("one") && csv.contains("two"),
"archive CSV reflects the copied rows: {csv:?}",
);
}
#[test]
fn insert_select_with_column_list_and_projection_persists() {
let (project, db, _dir) = open_project_db();
let rt = rt();
create_named(&db, &rt, "source");
create_named(&db, &rt, "target");
rt.block_on(db.run_sql_insert(
"insert into source (a, b) values (5, 'five')".to_string(),
None,
"source".to_string(),
Vec::new(),
String::new(),
))
.expect("seed source");
let result = rt
.block_on(db.run_sql_insert(
"insert into target (a, b) select a, b from source".to_string(),
None,
"target".to_string(),
Vec::new(),
String::new(),
))
.expect("column-list + projection INSERT … SELECT runs");
assert_eq!(result.rows_affected, 1);
let csv = read_csv(&project, "target").expect("target.csv written");
assert!(csv.contains("five"), "target CSV reflects the row: {csv:?}");
}
#[test]
fn with_prefixed_insert_select_runs_and_persists() {
// R4 end-to-end: the CTE row source executes and lands rows.
let (project, db, _dir) = open_project_db();
let rt = rt();
create_named(&db, &rt, "orders");
create_named(&db, &rt, "archive");
rt.block_on(db.run_sql_insert(
"insert into orders (a, b) values (1, 'a'), (2, 'b')".to_string(),
None,
"orders".to_string(),
Vec::new(),
String::new(),
))
.expect("seed orders");
let result = rt
.block_on(db.run_sql_insert(
"insert into archive with t as (select * from orders) select * from t".to_string(),
None,
"archive".to_string(),
Vec::new(),
String::new(),
))
.expect("WITH-prefixed INSERT … SELECT runs");
assert_eq!(result.rows_affected, 2);
let csv = read_csv(&project, "archive").expect("archive.csv written");
assert!(
csv.contains('a') && csv.contains('b'),
"archive CSV reflects the CTE-sourced rows: {csv:?}",
);
}
#[test]
fn insert_select_from_self_runs_as_plain_insert() {
// DA gate: INSERT … SELECT where the source is the target
// executes as a plain insert (InsertResult — no cascade
// summary; cascade output is a DELETE-only concept, 3f).
let (project, db, _dir) = open_project_db();
let rt = rt();
create_named(&db, &rt, "T");
rt.block_on(db.run_sql_insert(
"insert into T (a, b) values (1, 'x'), (2, 'y')".to_string(),
None,
"T".to_string(),
Vec::new(),
String::new(),
))
.expect("seed");
let result = rt
.block_on(db.run_sql_insert(
"insert into T select a + 10, b from T".to_string(),
None,
"T".to_string(),
Vec::new(),
String::new(),
))
.expect("self-sourced INSERT … SELECT runs");
assert_eq!(result.rows_affected, 2, "two rows copied with shifted PK");
let csv = read_csv(&project, "T").expect("T.csv written");
assert!(
csv.contains("11") && csv.contains("12"),
"the shifted-PK copies landed: {csv:?}",
);
}
// =================================================================
// Sub-phase 3d — shortid auto-fill (worker)
// =================================================================
/// Full-stack: parse the dev `sqlinsert …` scaffold (so
/// `listed_columns` / `row_source` are extracted exactly as the
/// app does) and run it through the worker.
fn run_sqlinsert(
db: &Database,
rt: &tokio::runtime::Runtime,
input: &str,
) -> Result<InsertResult, DbError> {
match parse_command(input).expect("parse sqlinsert") {
Command::SqlInsert {
sql,
target_table,
listed_columns,
row_source,
} => rt.block_on(db.run_sql_insert(
sql,
Some(input.to_string()),
target_table,
listed_columns,
row_source,
)),
other => panic!("expected Command::SqlInsert, got {other:?}"),
}
}
fn create_cols(
db: &Database,
rt: &tokio::runtime::Runtime,
name: &str,
cols: &[(&str, Type)],
pk: &[&str],
) {
rt.block_on(db.create_table(
name.to_string(),
cols.iter().map(|(n, t)| ColumnSpec::new(*n, *t)).collect(),
pk.iter().map(|s| (*s).to_string()).collect(),
None,
))
.unwrap_or_else(|e| panic!("create table {name}: {e:?}"));
}
/// Data rows of a table's CSV (header skipped), each split on `,`.
/// The test data uses comma/quote-free values, so a plain split is
/// sufficient.
fn csv_rows(project: &project::Project, table: &str) -> Vec<Vec<String>> {
read_csv(project, table)
.unwrap_or_default()
.lines()
.skip(1)
.filter(|l| !l.is_empty())
.map(|l| l.split(',').map(str::to_string).collect())
.collect()
}
#[test]
fn values_autofills_omitted_shortid_pk() {
let (project, db, _dir) = open_project_db();
let rt = rt();
create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
let result = run_sqlinsert(&db, &rt, "sqlinsert into t (label) values ('x')")
.expect("auto-fill insert runs");
assert_eq!(result.rows_affected, 1);
let rows = csv_rows(&project, "t");
assert_eq!(rows.len(), 1, "one row: {rows:?}");
assert!(!rows[0][0].is_empty(), "id auto-filled: {rows:?}");
assert_eq!(rows[0][1], "x", "label preserved: {rows:?}");
}
#[test]
fn values_multirow_autofills_distinct_shortids() {
let (project, db, _dir) = open_project_db();
let rt = rt();
create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
let result = run_sqlinsert(
&db,
&rt,
"sqlinsert into t (label) values ('a'), ('b'), ('c')",
)
.expect("multi-row auto-fill runs");
assert_eq!(result.rows_affected, 3);
let rows = csv_rows(&project, "t");
let ids: std::collections::HashSet<&String> = rows.iter().map(|r| &r[0]).collect();
assert_eq!(ids.len(), 3, "three DISTINCT non-empty shortids: {rows:?}");
assert!(rows.iter().all(|r| !r[0].is_empty()), "no empty id: {rows:?}");
}
#[test]
fn explicit_shortid_value_is_respected() {
let (project, db, _dir) = open_project_db();
let rt = rt();
create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
// The user provided `id` explicitly — it must be honoured
// verbatim (the override WARNING is sub-phase 3i).
run_sqlinsert(
&db,
&rt,
"sqlinsert into t (id, label) values ('hardcoded', 'x')",
)
.expect("explicit-id insert runs");
let rows = csv_rows(&project, "t");
assert_eq!(rows[0][0], "hardcoded", "explicit id preserved: {rows:?}");
}
#[test]
fn insert_select_autofills_distinct_shortids() {
let (project, db, _dir) = open_project_db();
let rt = rt();
create_cols(&db, &rt, "source", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
create_cols(&db, &rt, "target", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
run_sqlinsert(&db, &rt, "sqlinsert into source (label) values ('a'), ('b')")
.expect("seed source");
let result = run_sqlinsert(
&db,
&rt,
"sqlinsert into target (label) select label from source",
)
.expect("INSERT … SELECT auto-fill runs");
assert_eq!(result.rows_affected, 2);
let rows = csv_rows(&project, "target");
let ids: std::collections::HashSet<&String> = rows.iter().map(|r| &r[0]).collect();
assert_eq!(ids.len(), 2, "two DISTINCT fresh shortids: {rows:?}");
assert!(rows.iter().all(|r| !r[0].is_empty()), "no empty id: {rows:?}");
}
#[test]
fn combined_serial_and_shortid_autofill() {
let (project, db, _dir) = open_project_db();
let rt = rt();
// id: serial PK (engine rowid), code: shortid (worker), name.
create_cols(
&db,
&rt,
"t",
&[("id", Type::Serial), ("code", Type::ShortId), ("name", Type::Text)],
&["id"],
);
run_sqlinsert(&db, &rt, "sqlinsert into t (name) values ('x')")
.expect("combined auto-fill runs");
let rows = csv_rows(&project, "t");
assert_eq!(rows.len(), 1, "{rows:?}");
assert_eq!(rows[0][0], "1", "serial PK engine-filled: {rows:?}");
assert!(!rows[0][1].is_empty(), "shortid worker-filled: {rows:?}");
assert_eq!(rows[0][2], "x", "name preserved: {rows:?}");
}
#[test]
fn autofill_logs_original_source_not_rewritten_sql() {
// ADR-0030 §11: even though the worker rewrites the executed
// statement to bind synthesised shortids, history.log records
// the user's original line verbatim.
let (project, db, _dir) = open_project_db();
let rt = rt();
create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
let input = "sqlinsert into t (label) values ('x')";
run_sqlinsert(&db, &rt, input).expect("auto-fill insert runs");
let body = std::fs::read_to_string(project.path().join("history.log"))
.expect("history.log present");
assert!(body.contains(input), "original line logged: {body:?}");
// The rewritten parameterised INSERT must not leak into history.
assert!(
!body.contains("INSERT INTO") && !body.contains("?1"),
"rewritten SQL must not be logged: {body:?}",
);
}
#[test]
fn shortid_autofill_respects_mixed_case_column_name() {
// ADR-0009 / 3d DA gate: identifiers are case-preserving. The
// omitted-shortid detection must match the case-preserved
// schema name `MyId`, not a lowercased form.
let (project, db, _dir) = open_project_db();
let rt = rt();
create_cols(&db, &rt, "t", &[("MyId", Type::ShortId), ("label", Type::Text)], &["MyId"]);
run_sqlinsert(&db, &rt, "sqlinsert into t (label) values ('x')")
.expect("mixed-case shortid auto-fill runs");
let rows = csv_rows(&project, "t");
assert_eq!(rows.len(), 1, "{rows:?}");
assert!(!rows[0][0].is_empty(), "MyId auto-filled: {rows:?}");
}
#[test]
fn two_shortids_pk_and_nonpk_both_autofill_distinctly() {
// Two shortid columns (one PK, one not), both omitted: each
// gets its own distinct-per-row batch.
let (project, db, _dir) = open_project_db();
let rt = rt();
create_cols(
&db,
&rt,
"t",
&[("id", Type::ShortId), ("code", Type::ShortId), ("label", Type::Text)],
&["id"],
);
let result = run_sqlinsert(&db, &rt, "sqlinsert into t (label) values ('x'), ('y')")
.expect("two-shortid auto-fill runs");
assert_eq!(result.rows_affected, 2);
let rows = csv_rows(&project, "t");
let ids: std::collections::HashSet<&String> = rows.iter().map(|x| &x[0]).collect();
let codes: std::collections::HashSet<&String> = rows.iter().map(|x| &x[1]).collect();
assert_eq!(ids.len(), 2, "distinct ids: {rows:?}");
assert_eq!(codes.len(), 2, "distinct codes: {rows:?}");
assert!(
rows.iter().all(|x| !x[0].is_empty() && !x[1].is_empty()),
"both shortid columns filled on every row: {rows:?}",
);
}
#[test]
fn two_shortids_one_provided_one_autofilled() {
// The user provides one shortid and omits the other; the
// provided value is honoured and only the omitted one fills.
let (project, db, _dir) = open_project_db();
let rt = rt();
create_cols(
&db,
&rt,
"t",
&[("id", Type::ShortId), ("code", Type::ShortId), ("label", Type::Text)],
&["id"],
);
run_sqlinsert(&db, &rt, "sqlinsert into t (id, label) values ('myid', 'x')")
.expect("partial-shortid insert runs");
let rows = csv_rows(&project, "t");
assert_eq!(rows[0][0], "myid", "provided id preserved: {rows:?}");
assert!(!rows[0][1].is_empty(), "omitted code auto-filled: {rows:?}");
}
#[test]
fn compound_pk_with_shortid_member_autofills() {
// A shortid that is part of a compound PK still auto-fills when
// omitted (membership in the PK is irrelevant to the fill).
let (project, db, _dir) = open_project_db();
let rt = rt();
create_cols(
&db,
&rt,
"t",
&[("id", Type::ShortId), ("region", Type::Int), ("label", Type::Text)],
&["id", "region"],
);
run_sqlinsert(&db, &rt, "sqlinsert into t (region, label) values (1, 'x')")
.expect("compound-pk insert runs");
let rows = csv_rows(&project, "t");
assert!(
!rows[0][0].is_empty(),
"shortid PK member auto-filled: {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");
}