//! 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; 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 { 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(), )) .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(), )) .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(), )) .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(), )) .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(), )) .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(), )); 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(), )) .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(), )); 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(), )) .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(), )) .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(), )) .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(), )) .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(), )) .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(), )) .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(), )) .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(), )) .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:?}", ); }