//! 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::completion::{SchemaCache, TableColumn}; use rdbms_playground::db::{Database, DbError, InsertResult}; use rdbms_playground::dsl::{ColumnSpec, Command, Type, Value, parse_command}; use rdbms_playground::event::AppEvent; use rdbms_playground::input_render::{ AmbientHint, InputState, ambient_hint_in_mode, classify_input_with_schema_in_mode, }; use rdbms_playground::mode::Mode; use rdbms_playground::persistence::Persistence; use rdbms_playground::project; use rdbms_playground::runtime::run_replay; 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 { 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(), false, )) .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(), false, )) .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(), false, )) .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(), false, )) .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(), false, )) .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(), false, )); 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(), false, )) .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(), false, )); 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("insert into Orders (id, total) values (1, 99.5)") .expect("insert 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("insert 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("insert 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( "insert 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(), false, )) .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(), false, )) .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(), false, )) .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(), false, )) .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(), false, )) .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(), false, )) .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(), false, )) .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(), false, )) .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 { match parse_command(input).expect("parse insert") { Command::SqlInsert { sql, target_table, listed_columns, row_source, returning, .. } => rt.block_on(db.run_sql_insert( sql, Some(input.to_string()), target_table, listed_columns, row_source, returning, )), 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> { 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, "insert 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, "insert 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, "insert 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, "insert into source (label) values ('a'), ('b')") .expect("seed source"); let result = run_sqlinsert( &db, &rt, "insert 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, "insert 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 = "insert 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, "insert 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, "insert 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, "insert 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, "insert 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, "insert 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 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 // 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, "insert into src (a, b) values ('p', 'q')").expect("seed"); let outcome = run_sqlinsert(&db, &rt, "insert 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, "insert into src (a) values ('p')").expect("seed"); let outcome = run_sqlinsert(&db, &rt, "insert 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"); } // ================================================================= // Sub-phase 3g — RETURNING (ADR-0033 §5) // ================================================================= #[test] fn insert_returning_star_returns_inserted_row() { let (_project, db, _dir) = open_project_db(); let rt = rt(); create_cols(&db, &rt, "t", &[("id", Type::Int), ("b", Type::Text)], &["id"]); let result = run_sqlinsert(&db, &rt, "insert into t (id, b) values (1, 'Ada') returning *") .expect("INSERT … RETURNING * runs"); assert_eq!(result.rows_affected, 1, "one row inserted"); assert_eq!(result.data.rows.len(), 1, "RETURNING yielded the inserted row"); assert_eq!(result.data.columns, vec!["id".to_string(), "b".to_string()]); assert_eq!(result.data.rows[0][1], Some("Ada".to_string())); } #[test] fn insert_multirow_returning_id_yields_distinct_rows() { let (_project, db, _dir) = open_project_db(); let rt = rt(); create_cols(&db, &rt, "t", &[("id", Type::Int), ("b", Type::Text)], &["id"]); let result = run_sqlinsert( &db, &rt, "insert into t (id, b) values (1, 'a'), (2, 'b'), (3, 'c') returning id", ) .expect("multi-row INSERT … RETURNING id runs"); assert_eq!(result.rows_affected, 3, "three rows inserted"); assert_eq!(result.data.columns, vec!["id".to_string()]); let ids: std::collections::BTreeSet<_> = result.data.rows.iter().map(|r| r[0].clone()).collect(); assert_eq!(ids.len(), 3, "three distinct ids returned: {:?}", result.data.rows); } #[test] fn insert_returning_autofills_shortid_and_returns_it() { // The auto-fill × RETURNING interaction (3d × 3g): the worker // rewrites the INSERT to add the generated shortid, and the // rewrite must PRESERVE the RETURNING tail so the generated id // surfaces in the returned row. 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, "insert into t (label) values ('x') returning *") .expect("auto-fill INSERT … RETURNING * runs"); assert_eq!(result.rows_affected, 1, "one row inserted (RETURNING-counted)"); assert_eq!(result.data.rows.len(), 1, "RETURNING yielded the row"); // `id` is the auto-filled shortid column; it must be non-empty in // the returned row (proving the rewrite kept RETURNING). let id_idx = result.data.columns.iter().position(|c| c == "id").expect("id column"); let id_val = result.data.rows[0][id_idx].clone(); assert!(id_val.is_some_and(|s| !s.is_empty()), "generated shortid surfaced via RETURNING"); } #[test] fn insert_returning_recovers_bare_column_type() { // 3g type recovery: a bare-column RETURNING ref recovers its // playground type via the column-origin path (a `bool` column // renders as the word, not 0/1). let (_project, db, _dir) = open_project_db(); let rt = rt(); create_cols(&db, &rt, "t", &[("id", Type::Int), ("active", Type::Bool)], &["id"]); let result = run_sqlinsert(&db, &rt, "insert into t (id, active) values (1, true) returning active") .expect("INSERT … RETURNING active runs"); assert_eq!(result.data.column_types, vec![Some(Type::Bool)], "bool type recovered"); assert_eq!(result.data.rows[0][0], Some("true".to_string()), "rendered as the bool word"); } #[test] fn insert_returning_computed_expression_is_typeless() { // 3g: a computed RETURNING projection has no base-table origin, // so its recovered type is None (renders with neutral alignment). let (_project, db, _dir) = open_project_db(); let rt = rt(); create_cols(&db, &rt, "t", &[("id", Type::Int), ("n", Type::Int)], &["id"]); let result = run_sqlinsert(&db, &rt, "insert into t (id, n) values (1, 5) returning n + 1") .expect("INSERT … RETURNING runs"); assert_eq!(result.data.column_types, vec![None], "computed projection is typeless"); assert_eq!(result.data.rows[0][0], Some("6".to_string()), "engine evaluated n + 1"); } #[test] fn insert_returning_recovers_multiple_bare_column_types() { // 3g type recovery spans the playground vocabulary. RETURNING // reuses the SELECT column-origin path (`resolve_select_column_ // types`), exhaustively type-tested on the SELECT side; this // pins a representative spread reached via the RETURNING tail. let (_project, db, _dir) = open_project_db(); let rt = rt(); create_cols( &db, &rt, "t", &[ ("id", Type::Int), ("txt", Type::Text), ("amount", Type::Decimal), ("ratio", Type::Real), ("flag", Type::Bool), ], &["id"], ); let result = run_sqlinsert( &db, &rt, "insert into t (id, txt, amount, ratio, flag) values (1, 'a', 9.50, 1.5, true) returning id, txt, amount, ratio, flag", ) .expect("INSERT … RETURNING runs"); assert_eq!( result.data.column_types, vec![ Some(Type::Int), Some(Type::Text), Some(Type::Decimal), Some(Type::Real), Some(Type::Bool), ], "each bare-column RETURNING ref recovered its playground type", ); } #[test] fn multirow_autofill_returning_yields_distinct_generated_ids() { // DA gate (3d × 3g): multi-row INSERT with an omitted shortid PK // AND RETURNING — the auto-fill rewrite produces N tuples with N // distinct generated ids, and RETURNING * must surface all N // rows each carrying its own generated id. 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, "insert into t (label) values ('a'), ('b'), ('c') returning *", ) .expect("multi-row auto-fill INSERT … RETURNING * runs"); assert_eq!(result.rows_affected, 3, "three rows inserted"); assert_eq!(result.data.rows.len(), 3, "three rows returned"); let id_idx = result.data.columns.iter().position(|c| c == "id").expect("id column"); let ids: std::collections::BTreeSet<_> = result.data.rows.iter().map(|r| r[id_idx].clone()).collect(); assert_eq!(ids.len(), 3, "three DISTINCT generated ids via RETURNING: {:?}", result.data.rows); assert!(ids.iter().all(|v| v.as_ref().is_some_and(|s| !s.is_empty())), "all ids non-empty"); } #[test] fn insert_select_returning_executes_and_returns_rows() { // DA gate: the grammar accepts INSERT … SELECT … RETURNING; this // pins that it also EXECUTES through run_returning (the SELECT row // source feeds the insert, and RETURNING yields the inserted rows). let (_project, db, _dir) = open_project_db(); let rt = rt(); create_cols(&db, &rt, "src", &[("id", Type::Int), ("b", Type::Text)], &["id"]); create_cols(&db, &rt, "dst", &[("id", Type::Int), ("b", Type::Text)], &["id"]); run_sqlinsert(&db, &rt, "insert into src (id, b) values (1, 'x'), (2, 'y')").expect("seed src"); let result = run_sqlinsert(&db, &rt, "insert into dst select * from src returning id, b") .expect("INSERT … SELECT … RETURNING runs"); assert_eq!(result.rows_affected, 2, "two rows copied"); assert_eq!(result.data.rows.len(), 2, "RETURNING yielded both inserted rows"); let bs: std::collections::BTreeSet<_> = result.data.rows.iter().map(|r| r[1].clone()).collect(); assert!(bs.contains(&Some("x".to_string())) && bs.contains(&Some("y".to_string()))); } // ================================================================= // Sub-phase 3h — UPSERT (ON CONFLICT … DO NOTHING / DO UPDATE) // ================================================================= #[test] fn conflict_target_columns_excluded_from_listed_columns() { // DA gate: the ON CONFLICT (col, …) target uses a DISTINCT role // from the inserted column list, so build_sql_insert's // listed_columns (which drives shortid auto-fill) must NOT pick // up the conflict-target columns. If it did, an omitted shortid // would look "listed" and auto-fill would wrongly skip. match parse_command("insert into t (name) values ('x') on conflict (id) do nothing") .expect("parse upsert") { Command::SqlInsert { listed_columns, .. } => { assert_eq!( listed_columns, vec!["name".to_string()], "only the inserted column list, not the conflict target", ); } other => panic!("expected SqlInsert, got {other:?}"), } } #[test] fn autofill_upsert_real_conflict_preserves_clause_and_excluded() { // DA gate (stronger than autofill_preserves_on_conflict_clause, // which can't tell a preserved clause from a dropped one because // the generated id never conflicts). Here the table has a shortid // PK (auto-filled) AND a UNIQUE `code`. The second insert reuses // code 'A', so it hits a REAL conflict: with the ON CONFLICT // clause preserved through the auto-fill rewrite it DO-UPDATEs via // excluded; if the rewrite had dropped the clause it would raise a // UNIQUE violation instead (the `.expect` would panic). let (project, db, _dir) = open_project_db(); let rt = rt(); rt.block_on(db.create_table( "t".to_string(), vec![ ColumnSpec::new("id", Type::ShortId), ColumnSpec { unique: true, ..ColumnSpec::new("code", Type::Text) }, ColumnSpec::new("label", Type::Text), ], vec!["id".to_string()], None, )) .expect("create table with shortid pk + unique code"); run_sqlinsert(&db, &rt, "insert into t (code, label) values ('A', 'first')").expect("seed"); let result = run_sqlinsert( &db, &rt, "insert into t (code, label) values ('A', 'second') on conflict (code) do update set label = excluded.label", ) .expect("auto-filled UPSERT with a real conflict (clause preserved)"); assert_eq!(result.rows_affected, 1, "the conflicting row was updated, not inserted"); let rows = csv_rows(&project, "t"); assert_eq!(rows.len(), 1, "still one row (DO UPDATE, not a second insert)"); assert!(rows[0].iter().any(|c| c == "second"), "label updated via excluded: {rows:?}"); assert!(!rows[0].iter().any(|c| c == "first"), "old label replaced: {rows:?}"); } #[test] fn on_conflict_do_nothing_keeps_existing_row() { let (project, db, _dir) = open_project_db(); let rt = rt(); create_cols(&db, &rt, "t", &[("id", Type::Int), ("name", Type::Text)], &["id"]); run_sqlinsert(&db, &rt, "insert into t (id, name) values (1, 'orig')").expect("seed"); let result = run_sqlinsert( &db, &rt, "insert into t (id, name) values (1, 'new') on conflict (id) do nothing", ) .expect("ON CONFLICT DO NOTHING runs"); assert_eq!(result.rows_affected, 0, "conflicting row left untouched"); let rows = csv_rows(&project, "t"); assert_eq!(rows.len(), 1, "still one row"); assert!(rows[0].iter().any(|c| c == "orig"), "original value kept: {rows:?}"); } #[test] fn on_conflict_do_update_applies_excluded() { let (project, db, _dir) = open_project_db(); let rt = rt(); create_cols(&db, &rt, "t", &[("id", Type::Int), ("name", Type::Text)], &["id"]); run_sqlinsert(&db, &rt, "insert into t (id, name) values (1, 'orig')").expect("seed"); let result = run_sqlinsert( &db, &rt, "insert into t (id, name) values (1, 'new') on conflict (id) do update set name = excluded.name", ) .expect("ON CONFLICT DO UPDATE runs"); assert_eq!(result.rows_affected, 1, "the conflicting row was updated"); let rows = csv_rows(&project, "t"); assert_eq!(rows.len(), 1, "still one row (updated, not inserted)"); assert!(rows[0].iter().any(|c| c == "new"), "row updated to excluded.name: {rows:?}"); } #[test] fn on_conflict_do_nothing_without_target() { let (_project, db, _dir) = open_project_db(); let rt = rt(); create_cols(&db, &rt, "t", &[("id", Type::Int), ("name", Type::Text)], &["id"]); run_sqlinsert(&db, &rt, "insert into t (id, name) values (1, 'orig')").expect("seed"); let result = run_sqlinsert( &db, &rt, "insert into t (id, name) values (1, 'x') on conflict do nothing", ) .expect("ON CONFLICT (no target) DO NOTHING runs"); assert_eq!(result.rows_affected, 0, "any-conflict do-nothing absorbed the duplicate"); } #[test] fn autofill_preserves_on_conflict_clause() { // DA gate / landmine: an INSERT with an omitted shortid PK AND an // ON CONFLICT tail. The auto-fill rewrite reconstructs INSERT … // VALUES …; row_source must stop before ON CONFLICT (so the // materialisation prepare doesn't choke) and the rewrite must // re-append the clause (so it isn't silently dropped). The fresh // generated id won't conflict, so the row inserts — the point is // the rewrite doesn't prepare-fail and the clause survives. 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, "insert into t (label) values ('x') on conflict (id) do nothing", ) .expect("auto-fill INSERT with ON CONFLICT runs (clause preserved)"); assert_eq!(result.rows_affected, 1, "row inserted with a generated id"); let rows = csv_rows(&project, "t"); assert_eq!(rows.len(), 1, "one row landed"); } #[test] fn sql_dml_validates_literal_values_like_the_dsl() { // ADR-0036 Phase 1: advanced-mode SQL `INSERT` now validates each // literal value against its column type before the (still verbatim) // insert runs, sharing the DSL's per-type validators. `2025/01/15` is // a malformed date (slashes, not dashes) — the DSL rejects it at bind // time, and advanced-mode SQL now refuses it too (it used to splice the // literal into text and let a STRICT TEXT column accept anything). let (project, db, _d) = open_project_db(); let r = rt(); r.block_on(db.create_table( "T".to_string(), vec![ ColumnSpec::new("id", Type::Int), ColumnSpec::new("d", Type::Date), ], vec!["id".to_string()], Some("create table T with pk id(int)".to_string()), )) .expect("create T(id int pk, d date)"); // DSL path — validates the `date` and rejects the malformed value. let dsl = r.block_on(db.insert( "T".to_string(), Some(vec!["id".to_string(), "d".to_string()]), vec![Value::Number("1".to_string()), Value::Text("2025/01/15".to_string())], Some("insert".to_string()), )); assert!( dsl.is_err(), "the DSL insert path validates `date` and rejects 2025/01/15; got {dsl:?}" ); // SQL path (advanced mode, full pipeline) — now REJECTS it too. std::fs::write( project.path().join("ins.commands"), "insert into T (id, d) values (2, '2025/01/15')\n", ) .expect("write script"); let events = r.block_on(run_replay(&db, project.path(), "ins.commands")); assert!( matches!(events.last(), Some(AppEvent::ReplayFailed { .. })), "advanced-mode SQL validates the `date` literal and refuses \ 2025/01/15 (ADR-0036 Phase 1); events: {events:?}" ); // A valid date still inserts (the bound/verbatim path is unaffected). std::fs::write( project.path().join("ok.commands"), "insert into T (id, d) values (3, '2025-01-15')\n", ) .expect("write script"); let ok = r.block_on(run_replay(&db, project.path(), "ok.commands")); assert!( matches!(ok.last(), Some(AppEvent::ReplayCompleted { count, .. }) if *count == 1), "a well-formed date still inserts; events: {ok:?}" ); } #[test] fn sql_insert_expression_value_is_not_validated_and_runs() { // An expression position (not a bare literal) is left to the engine — // ADR-0036 Phase 1 has nothing static to validate, so `1 + 2` into an // int column computes 3 and inserts; it must not be mis-classified as // a literal or rejected. let (project, db, _d) = open_project_db(); let r = rt(); r.block_on(db.create_table( "T".to_string(), vec![ ColumnSpec::new("id", Type::Int), ColumnSpec::new("n", Type::Int), ], vec!["id".to_string()], Some("create table T with pk id(int)".to_string()), )) .expect("create T"); std::fs::write( project.path().join("e.commands"), "insert into T (id, n) values (1, 1 + 2)\n", ) .expect("write script"); let events = r.block_on(run_replay(&db, project.path(), "e.commands")); assert!( matches!(events.last(), Some(AppEvent::ReplayCompleted { count, .. }) if *count == 1), "the expression value executes (engine computes it); events: {events:?}" ); } #[test] fn sql_insert_multi_row_validates_each_literal() { // Validation applies to every literal row; a malformed `date` in the // second tuple is caught (ADR-0036 Phase 1 — execution is verbatim, so // multi-row comes for free). let (project, db, _d) = open_project_db(); let r = rt(); r.block_on(db.create_table( "T".to_string(), vec![ ColumnSpec::new("id", Type::Int), ColumnSpec::new("d", Type::Date), ], vec!["id".to_string()], Some("create table T with pk id(int)".to_string()), )) .expect("create T"); std::fs::write( project.path().join("m.commands"), "insert into T (id, d) values (1, '2025-01-15'), (2, '2025/02/20')\n", ) .expect("write script"); let events = r.block_on(run_replay(&db, project.path(), "m.commands")); assert!( matches!(events.last(), Some(AppEvent::ReplayFailed { .. })), "the malformed date in the second row is caught; events: {events:?}" ); } #[test] fn sql_insert_natural_order_validates_against_schema_columns() { // With no explicit column list, positions map to the schema's columns // in definition order (engine semantics) — validation must use that // mapping, not an explicit list (ADR-0036 Phase 1). let (project, db, _d) = open_project_db(); let r = rt(); r.block_on(db.create_table( "T".to_string(), vec![ ColumnSpec::new("id", Type::Int), ColumnSpec::new("d", Type::Date), ], vec!["id".to_string()], Some("create table T with pk id(int)".to_string()), )) .expect("create T"); std::fs::write( project.path().join("nat.commands"), "insert into T values (1, '2025/02/20')\n", ) .expect("write script"); let events = r.block_on(run_replay(&db, project.path(), "nat.commands")); assert!( matches!(events.last(), Some(AppEvent::ReplayFailed { .. })), "natural-order insert validates the date against column `d`; events: {events:?}" ); } // ================================================================= // ADR-0036 Phase 3a — live typed-slot hint for the UPSERT // `ON CONFLICT … DO UPDATE SET col = ` value position. // ================================================================= #[test] fn advanced_upsert_do_update_set_offers_typed_slot_hint() { // ADR-0036 Phase 3a: the `DO UPDATE SET col = ` value position // shares the SQL UPDATE `SET` treatment, so it drives the same // column-typed slot hint (boundary-aware lookahead → typed slot). let mut cache = SchemaCache::default(); let cols = vec![ TableColumn { name: "id".to_string(), user_type: Type::Int, not_null: false, has_default: false }, TableColumn { name: "Name".to_string(), user_type: Type::Text, not_null: false, has_default: false }, ]; cache.tables.push("Customers".to_string()); cache.columns.push("id".to_string()); cache.columns.push("Name".to_string()); cache.table_columns.insert("Customers".to_string(), cols); let input = "insert into Customers (id, Name) values (1, 'x') on conflict (id) do update set Name="; let hint = ambient_hint_in_mode(input, input.len(), None, &cache, Mode::Advanced); let Some(AmbientHint::Prose(prose)) = hint else { panic!("expected a Prose hint at the UPSERT SET value slot, got {hint:?}"); }; assert!(prose.contains("Name"), "hint names the column `Name`: {prose:?}"); assert!( prose.contains("quoted string"), "text-column hint says `quoted string`: {prose:?}" ); } // ================================================================= // ADR-0036 Phase 3b — live typed-slot hints + highlighting for the // INSERT `VALUES (…)` positions (per-position column mapping via the // `Node::SetColumn` primitive; boundary-aware lookahead per position). // ================================================================= /// Build a `SchemaCache` for the advanced-mode typing-surface tests. fn vschema(tables: &[(&str, &[(&str, Type)])]) -> SchemaCache { let mut cache = SchemaCache::default(); for (table, cols) in tables { let table_cols: Vec = cols .iter() .map(|(n, t)| TableColumn { name: (*n).to_string(), user_type: *t, not_null: false, has_default: false, }) .collect(); cache.tables.push((*table).to_string()); for c in &table_cols { if !cache.columns.contains(&c.name) { cache.columns.push(c.name.clone()); } } cache.table_columns.insert((*table).to_string(), table_cols); } cache } fn prose_at(input: &str, schema: &SchemaCache) -> String { let hint = ambient_hint_in_mode(input, input.len(), None, schema, Mode::Advanced); match hint { Some(AmbientHint::Prose(p)) => p, other => panic!("expected a Prose hint for {input:?}, got {other:?}"), } } #[test] fn advanced_insert_form_a_value_offers_typed_slot_hint() { // Form A (explicit column list): the value position maps to the // user-listed column, so the hint is that column's typed prose. let schema = vschema(&[("Things", &[("k", Type::Int), ("note", Type::Text)])]); let prose = prose_at("insert into Things (note) values (", &schema); assert!(prose.contains("note"), "names listed column `note`: {prose:?}"); assert!(prose.contains("quoted string"), "text-column prose: {prose:?}"); } #[test] fn advanced_insert_form_b_value_maps_first_column() { // Form B (no column list): positions map to ALL columns in // declaration order, so the first position is the first column. let schema = vschema(&[("Things", &[("k", Type::Int), ("note", Type::Text)])]); let prose = prose_at("insert into Things values (", &schema); assert!(prose.contains("k"), "names first column `k`: {prose:?}"); assert!(prose.contains("integer"), "int-column prose: {prose:?}"); } #[test] fn advanced_insert_second_position_hints_second_column() { // Per-position mapping advances: after the first value + comma, the // hint is the SECOND column's typed prose. let schema = vschema(&[("Things", &[("k", Type::Int), ("note", Type::Text)])]); let prose = prose_at("insert into Things (k, note) values (5, ", &schema); assert!(prose.contains("note"), "second position names `note`: {prose:?}"); assert!(prose.contains("quoted string"), "text-column prose: {prose:?}"); } #[test] fn advanced_insert_value_int_mismatch_is_caught_live() { let schema = vschema(&[("Things", &[("k", Type::Int), ("note", Type::Text)])]); let bad = classify_input_with_schema_in_mode( "insert into Things (k) values (3.14)", &schema, Mode::Advanced, ); assert!(!matches!(bad, InputState::Valid), "decimal into int rejected live: {bad:?}"); let ok = classify_input_with_schema_in_mode( "insert into Things (k) values (5)", &schema, Mode::Advanced, ); assert!(matches!(ok, InputState::Valid), "valid int literal parses: {ok:?}"); } #[test] fn advanced_insert_string_into_int_is_caught_live() { // The Option-A win over the structural fallback: a wrong-KIND lone // literal (a string into an int column) is rejected WHILE TYPING, // not only at execution. let schema = vschema(&[("Things", &[("k", Type::Int), ("note", Type::Text)])]); let bad = classify_input_with_schema_in_mode( "insert into Things (k) values ('text')", &schema, Mode::Advanced, ); assert!(!matches!(bad, InputState::Valid), "string into int rejected live: {bad:?}"); } #[test] fn advanced_insert_multi_row_typed_and_mismatch_caught() { let schema = vschema(&[("Things", &[("k", Type::Int), ("note", Type::Text)])]); let ok = classify_input_with_schema_in_mode( "insert into Things (k, note) values (1, 'a'), (2, 'b')", &schema, Mode::Advanced, ); assert!(matches!(ok, InputState::Valid), "well-formed multi-row parses: {ok:?}"); let bad = classify_input_with_schema_in_mode( "insert into Things (k, note) values (1, 'a'), (3.14, 'b')", &schema, Mode::Advanced, ); assert!( !matches!(bad, InputState::Valid), "a mismatch in the second row is caught: {bad:?}" ); } #[test] fn advanced_insert_form_b_maps_all_columns_including_serial() { // SQL Form B supplies a value for EVERY column (no auto-fill), so // the position count = all columns, and a serial column's position // takes an int literal (unlike the DSL, which omits auto-gen cols). let schema = vschema(&[( "Customers", &[("id", Type::Serial), ("Name", Type::Text), ("Email", Type::Text)], )]); let state = classify_input_with_schema_in_mode( "insert into Customers values (1, 'Bob', 'b@c')", &schema, Mode::Advanced, ); assert!(matches!(state, InputState::Valid), "Form B maps all 3 columns: {state:?}"); } #[test] fn advanced_insert_value_expressions_still_parse_via_sql_expr() { // Regression guard: a non-lone-literal value position (arithmetic, // literal-prefixed, function call, signed number) falls through to // sql_expr unchanged — the typed slot must not steal it. let schema = vschema(&[("Things", &[("k", Type::Int), ("note", Type::Text)])]); for input in [ "insert into Things (k) values (1 + 2)", "insert into Things (k, note) values (5, upper(note))", "insert into Things (k) values (-5)", "insert into Things (k) values ((select 1))", ] { let state = classify_input_with_schema_in_mode(input, &schema, Mode::Advanced); assert!(matches!(state, InputState::Valid), "{input:?} must parse: {state:?}"); } }