//! Sub-phase 4e Tier-3 end-to-end tests for advanced-mode SQL //! `ALTER TABLE` add/drop/rename column (ADR-0035 §4e). //! //! These drive the **full advanced-mode pipeline** via `run_replay`: a //! literal `alter table …` line is parsed in Advanced mode, routed to //! `Command::SqlAlterTable`, decomposed by the runtime to the existing //! column executor, and persisted. They prove the decomposition for all //! three actions and the **raw-text DEFAULT/CHECK ADD COLUMN** path (the //! 4e executor extension). The drop/rename refusals (PK / FK / index / //! table-CHECK) live in the shared executors and are covered by //! `tests/column_op_guards.rs` — the SQL surface reaches the same code. use rdbms_playground::db::Database; use rdbms_playground::dsl::Value; use rdbms_playground::event::AppEvent; 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::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 db = Database::open_with_persistence( project.db_path(), Persistence::new(project.path().to_path_buf()), ) .expect("db"); (project, db, dir) } fn column_names(db: &Database, r: &tokio::runtime::Runtime) -> Vec { r.block_on(db.describe_table("T".to_string(), None)) .expect("describe") .columns .into_iter() .map(|c| c.name) .collect() } #[test] fn e2e_alter_table_add_rename_drop_and_raw_default_check() { let (project, db, _d) = open(); let r = rt(); // A script exercising all three actions through the full pipeline. // `v` is added (simple) so there is a non-PK column to rename/drop; // a row is inserted before the ADD so the DEFAULT backfill is // exercised by the rebuild. std::fs::write( project.path().join("alter.commands"), "create table T with pk id(int)\n\ add column T: v (text)\n\ insert into T (id, v) values (1, 'a')\n\ alter table T add column qty int default 0 check (qty >= 0)\n\ alter table T rename column v to label\n\ alter table T add column note text\n\ alter table T drop column note\n", ) .expect("write script"); let events = r.block_on(run_replay(&db, project.path(), "alter.commands")); match events.last().expect("at least one event") { AppEvent::ReplayCompleted { count, .. } => { assert_eq!(*count, 7, "all seven lines replayed; events: {events:?}"); } other => panic!("expected ReplayCompleted, got {other:?} (events: {events:?})"), } // Final schema: id, label (renamed from v), qty; `note` added then // dropped. let cols = column_names(&db, &r); assert_eq!(cols, vec!["id".to_string(), "label".to_string(), "qty".to_string()]); // The DEFAULT backfilled the pre-existing row to qty = 0. let rows = r .block_on(db.query_data("T".to_string(), None, None, None)) .expect("query") .rows; assert_eq!(rows.len(), 1); // qty is the third column; the rebuild backfilled the default. assert_eq!(rows[0][2].as_deref(), Some("0"), "DEFAULT 0 backfilled the existing row"); // The CHECK (qty >= 0) is enforced: a negative qty is refused. assert!( r.block_on(db.insert( "T".to_string(), Some(vec!["id".to_string(), "qty".to_string()]), vec![Value::Number("2".to_string()), Value::Number("-1".to_string())], Some("insert".to_string()), )) .is_err(), "the raw-text CHECK (qty >= 0) added via ALTER is enforced" ); // A non-negative qty is accepted. r.block_on(db.insert( "T".to_string(), Some(vec!["id".to_string(), "qty".to_string()]), vec![Value::Number("3".to_string()), Value::Number("7".to_string())], Some("insert".to_string()), )) .expect("qty = 7 satisfies the CHECK"); } #[test] fn e2e_alter_add_column_survives_rebuild() { // The column added via SQL ALTER (with a raw CHECK) round-trips // through the text artifacts and survives a rebuild. let (project, db, _d) = open(); let r = rt(); std::fs::write( project.path().join("alter.commands"), "create table T with pk id(int)\n\ alter table T add column qty int check (qty >= 0)\n", ) .expect("write script"); r.block_on(run_replay(&db, project.path(), "alter.commands")); assert!(column_names(&db, &r).contains(&"qty".to_string())); r.block_on(db.rebuild_from_text(project.path().to_path_buf(), Some("rebuild".to_string()))) .expect("rebuild"); // The CHECK survives the rebuild — a negative qty is still refused. assert!(column_names(&db, &r).contains(&"qty".to_string())); assert!( r.block_on(db.insert( "T".to_string(), Some(vec!["id".to_string(), "qty".to_string()]), vec![Value::Number("1".to_string()), Value::Number("-5".to_string())], Some("insert".to_string()), )) .is_err(), "the ALTER-added CHECK is intact after rebuild" ); }