//! Sub-phase 4c integration tests for advanced-mode SQL //! `DROP TABLE [IF EXISTS]` (ADR-0035 §4). //! //! `SqlDropTable` executes through the same `do_drop_table` machinery //! as the simple `drop table` (cascade / inbound-relationship refusal / //! metadata cleanup); the only new behaviour is `IF EXISTS` as a //! no-op-with-note (`DropOutcome::Skipped`). These drive the worker //! directly; parsing (text → `Command::SqlDropTable`) is covered by the //! `sql_drop_table_tests` in `src/dsl/grammar/ddl.rs`. use rdbms_playground::db::{Database, DropOutcome}; use rdbms_playground::dsl::{ColumnSpec, SqlForeignKey, Type, Value}; 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(undo: bool) -> (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_and_undo(project.db_path(), persistence, undo) .expect("open db with persistence"); (project, db, dir) } /// Create a simple `T (id int primary key, body text)`. fn make_t(db: &Database, r: &tokio::runtime::Runtime) { r.block_on(db.sql_create_table( "T".to_string(), vec![ColumnSpec::new("id", Type::Int), ColumnSpec::new("body", Type::Text)], vec!["id".to_string()], vec![], vec![], vec![], false, Some("create table T (id int primary key, body text)".to_string()), )) .expect("create T"); } #[test] fn drop_table_removes_an_existing_table() { let (_p, db, _d) = open(false); let r = rt(); make_t(&db, &r); let out = r .block_on(db.sql_drop_table("T".to_string(), false, Some("drop table T".to_string()))) .expect("drop"); assert!(matches!(out, DropOutcome::Dropped)); assert!(!r.block_on(db.list_tables()).unwrap().contains(&"T".to_string())); } #[test] fn if_exists_on_an_absent_table_is_a_noop_and_journalled() { let (p, db, _d) = open(false); let r = rt(); let line = "drop table if exists Ghost"; let out = r .block_on(db.sql_drop_table("Ghost".to_string(), true, Some(line.to_string()))) .expect("IF EXISTS on an absent table succeeds as a no-op"); assert!(matches!(out, DropOutcome::Skipped)); // The no-op is still journalled (ADR-0034), like the create-skip. let log = std::fs::read_to_string(p.path().join("history.log")).expect("read history.log"); assert!(log.contains(line), "the skipped drop should be journalled; log:\n{log}"); } #[test] fn plain_drop_of_an_absent_table_errors() { let (_p, db, _d) = open(false); let r = rt(); let res = r.block_on(db.sql_drop_table("Ghost".to_string(), false, Some("drop table Ghost".to_string()))); assert!(res.is_err(), "plain DROP TABLE on an absent table errors (no IF EXISTS)"); } #[test] fn dropping_a_referenced_parent_is_refused() { // Parity with `do_drop_table`: a table with inbound relationships // can't be dropped (ADR-0013), via the SQL path too. let (_p, db, _d) = open(false); let r = rt(); r.block_on(db.sql_create_table( "parent".to_string(), vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("label", Type::Text)], vec!["id".to_string()], vec![], vec![], vec![], false, Some("create table parent (id serial primary key, label text)".to_string()), )) .expect("create parent"); r.block_on(db.sql_create_table( "child".to_string(), vec![ColumnSpec::new("id", Type::Serial), ColumnSpec::new("pid", Type::Int)], vec!["id".to_string()], vec![], vec![], vec![SqlForeignKey { name: None, child_columns: vec!["pid".to_string()], parent_table: "parent".to_string(), parent_columns: Some(vec!["id".to_string()]), on_delete: rdbms_playground::dsl::ReferentialAction::NoAction, on_update: rdbms_playground::dsl::ReferentialAction::NoAction, }], false, Some("create table child (id serial primary key, pid int references parent(id))".to_string()), )) .expect("create child with FK"); // The parent is referenced — refused (even with IF EXISTS, since the // table *does* exist; the refusal is about the relationship). assert!( r.block_on(db.sql_drop_table("parent".to_string(), false, Some("drop table parent".to_string()))) .is_err(), "a referenced parent can't be dropped" ); // Dropping the child first succeeds, then the parent. r.block_on(db.sql_drop_table("child".to_string(), false, Some("drop table child".to_string()))) .expect("drop child"); r.block_on(db.sql_drop_table("parent".to_string(), false, Some("drop table parent".to_string()))) .expect("now the parent drops"); } #[test] fn drop_table_is_one_undo_step_and_restores_data() { let (_p, db, _d) = open(true); // undo enabled let r = rt(); make_t(&db, &r); r.block_on(db.insert( "T".to_string(), Some(vec!["id".to_string(), "body".to_string()]), vec![Value::Number("1".to_string()), Value::Text("hi".to_string())], Some("insert".to_string()), )) .expect("row"); r.block_on(db.sql_drop_table("T".to_string(), false, Some("drop table T".to_string()))) .expect("drop"); assert!(!r.block_on(db.list_tables()).unwrap().contains(&"T".to_string())); // One undo brings the table — and its row — back. assert!(r.block_on(db.undo()).expect("undo").is_some(), "the drop was one undo step"); assert!(r.block_on(db.list_tables()).unwrap().contains(&"T".to_string())); let data = r .block_on(db.query_data("T".to_string(), None, None, None)) .expect("query"); assert_eq!(data.rows.len(), 1, "the dropped row was restored by undo"); }