feat: ADR-0035 4c — DROP TABLE [IF EXISTS]
Add advanced-mode SQL `DROP TABLE [IF EXISTS] <name>` -> SqlDropTable, executing through the existing do_drop_table (cascade / inbound- relationship refusal / metadata cleanup) — full parity with the simple `drop table`. The only new behaviour is `IF EXISTS` as a no-op-with-note: a new DropOutcome::Skipped mirroring CreateOutcome::Skipped (journalled, no snapshot), rendered via a new ddl.drop_skipped_absent note + DslDropSkipped event. - Grammar: SQL_DROP_TABLE node (entry `drop`, shape `table [if exists] <name> [;]`), registered Advanced. SQL-first dispatch: `drop table T` -> SqlDropTable in advanced; `drop column`/`relationship`/`index`/ `constraint` fall back to the simple `drop` node (and still execute). - Worker: Request::SqlDropTable + db.sql_drop_table; the if-exists-and- absent arm journals + replies Skipped without a snapshot, else snapshot_then(do_drop_table) -> Dropped. - Completion: advanced `drop ` now surfaces the SQL `table` (the shared-entry-word behaviour from `create`); test split into simple (full DSL list) + advanced (SQL surface). Known shared-entry-word completion unevenness (advanced `drop ` offers only `table`; partial `drop rel` returns an empty list) deferred to 4i (merge candidate sets for shared entry words) along with a flagged user request to visually distinguish simple- vs advanced-mode completions in the hint UI — tracked in ADR §13 4i (d)/(e), the 4c plan, and the completion test. The DSL drops still parse + execute via fallback. 10 new tests (parse/builder + Tier-3: drop existing + one-undo-step + restore, IF EXISTS skip + journal, plain-absent error, inbound refusal). Docs: ADR-0035 Status/§13, README, requirements.md Q1. Tests: 1805 passing, 0 failing, 1 ignored. Clippy clean.
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
//! 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_column: "pid".to_string(),
|
||||
parent_table: "parent".to_string(),
|
||||
parent_column: Some("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");
|
||||
}
|
||||
@@ -218,6 +218,7 @@ fn command_kind_label(cmd: &rdbms_playground::dsl::Command) -> String {
|
||||
CreateTable { .. } => "CreateTable".into(),
|
||||
SqlCreateTable { .. } => "SqlCreateTable".into(),
|
||||
DropTable { .. } => "DropTable".into(),
|
||||
SqlDropTable { .. } => "SqlDropTable".into(),
|
||||
AddColumn { .. } => "AddColumn".into(),
|
||||
DropColumn { .. } => "DropColumn".into(),
|
||||
RenameColumn { .. } => "RenameColumn".into(),
|
||||
|
||||
Reference in New Issue
Block a user