#![allow(unused_imports)]
#[cfg(test)]
pub mod helpers {
use crate::ast::identifiers::ObjectId;
use crate::db::cache::DbCache;
use crate::engine::config::Config;
use crate::engine::engine::SafeMigrateEngine;
pub fn setup_engine() -> SafeMigrateEngine {
SafeMigrateEngine::new(Config::default())
}
pub fn setup_state() -> crate::analysis::state::AnalysisState {
crate::analysis::state::AnalysisState::new(DbCache::new())
}
pub fn object_id(schema: &str, name: &str) -> ObjectId {
ObjectId::new(schema, name)
}
}
#[cfg(test)]
mod state_machine_guards_tests {
use super::helpers::*;
use crate::analysis::state::AnalysisState;
use crate::model::relation::RelationOverlay;
#[test]
fn test_skip_guard_create_table() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE t(id INT);", &mut state)
.unwrap();
engine
.analyze("CREATE TABLE IF NOT EXISTS t(new_col INT);", &mut state)
.unwrap();
let rel = state.get_relation(&object_id("public", "t")).unwrap();
if let RelationOverlay::Present(r) = rel {
assert!(r.has_column("id"));
assert!(!r.has_column("new_col"));
} else {
panic!("relation should be present");
}
}
#[test]
fn test_skip_guard_add_column() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE t(id INT);", &mut state)
.unwrap();
engine
.analyze(
"ALTER TABLE t ADD COLUMN IF NOT EXISTS id TEXT;",
&mut state,
)
.unwrap();
let rel = state.get_relation(&object_id("public", "t")).unwrap();
if let RelationOverlay::Present(r) = rel {
let col = r.get_column("id").unwrap();
assert_eq!(col.data_type.as_deref(), Some("INT"));
} else {
panic!("relation should be present");
}
}
#[test]
fn test_skip_guard_drop_column() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE t(id INT);", &mut state)
.unwrap();
assert!(
engine
.analyze("ALTER TABLE t DROP COLUMN IF EXISTS missing;", &mut state)
.is_ok()
);
}
#[test]
fn test_skip_guard_drop_missing_objects() {
let engine = setup_engine();
let mut state = setup_state();
assert!(
engine
.analyze("DROP TABLE IF EXISTS missing;", &mut state)
.is_ok()
);
assert!(
engine
.analyze("DROP VIEW IF EXISTS missing;", &mut state)
.is_ok()
);
assert!(
engine
.analyze("DROP MATERIALIZED VIEW IF EXISTS missing;", &mut state)
.is_ok()
);
assert!(
engine
.analyze("DROP INDEX IF EXISTS missing;", &mut state)
.is_ok()
);
assert!(
engine
.analyze("DROP SEQUENCE IF EXISTS missing;", &mut state)
.is_ok()
);
assert!(
engine
.analyze("DROP DOMAIN IF EXISTS missing;", &mut state)
.is_ok()
);
}
#[test]
fn test_skip_guard_create_index() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE t(id int);", &mut state)
.unwrap();
engine
.analyze("CREATE INDEX idx ON t(id);", &mut state)
.unwrap();
let edge_count = state.local.graph.indexes.len();
engine
.analyze("CREATE INDEX IF NOT EXISTS idx ON t(id);", &mut state)
.unwrap();
assert_eq!(state.local.graph.indexes.len(), edge_count);
}
#[test]
fn test_skip_guard_create_sequence() {
let engine = setup_engine();
let mut state = setup_state();
engine.analyze("CREATE SEQUENCE s;", &mut state).unwrap();
let before = state.local.graph.sequences.len();
engine
.analyze(
"CREATE SEQUENCE IF NOT EXISTS s OWNED BY foo.bar;",
&mut state,
)
.unwrap();
assert_eq!(state.local.graph.sequences.len(), before);
}
}
#[cfg(test)]
mod rule_evaluation_tests {
use super::helpers::*;
use crate::analysis::state::{AnalysisState, Confidence};
use crate::model::column::Column;
use crate::model::relation::{Persistence, RelationKind, RelationState};
use crate::report::violations::ViolationTier;
#[test]
fn test_rule_idempotency() {
let engine = setup_engine();
let mut state = setup_state();
let violations = engine
.analyze(
"
CREATE TABLE t(id int);
DROP TABLE t;
CREATE INDEX i ON t(id);
",
&mut state,
)
.unwrap();
assert!(violations.iter().any(|v| v.rule_id.contains("idempot")));
}
#[test]
fn test_rule_cascading_drop() {
let engine = setup_engine();
let mut state = setup_state();
assert!(
engine
.analyze(
"
CREATE TABLE data(id INT);
CREATE VIEW v AS SELECT * FROM data;
DROP TABLE data CASCADE;
",
&mut state,
)
.is_ok()
);
}
#[test]
fn test_rule_size_aware_toast_escalation() {
let engine = setup_engine();
let mut cache = crate::db::cache::DbCache::new();
let tid = object_id("public", "t_toast");
let mut rel = RelationState::new(
tid.clone(),
0,
Some(50_000),
RelationKind::Table,
Persistence::Permanent,
0,
);
rel.columns.push(Column {
name: "data".into(),
data_type: Some("text".into()),
is_nullable: true,
default: None,
avg_width: Some(3000),
});
cache.insert_baseline(tid, rel);
let mut state = AnalysisState::new(cache);
let violations = engine
.analyze(
"
ALTER TABLE t_toast
ADD COLUMN c INT DEFAULT random();
",
&mut state,
)
.unwrap();
assert!(violations.iter().any(|v| v.rule_id.contains("size")
|| v.rule_id.contains("rewrite")
|| v.rule_id.contains("toast")));
}
#[test]
fn test_rule_blocking_constraint_check_and_fk() {
let engine = setup_engine();
let mut cache = crate::db::cache::DbCache::new();
cache.insert_baseline(
object_id("public", "t"),
RelationState::new(
object_id("public", "t"),
0,
Some(500_000),
RelationKind::Table,
Persistence::Permanent,
0,
),
);
let mut state = AnalysisState::new(cache);
let v1 = engine
.analyze(
"
ALTER TABLE t
ADD CONSTRAINT c CHECK (id > 0);
",
&mut state,
)
.unwrap();
assert!(v1.iter().any(|v| v.rule_id.contains("constraint")));
let v2 = engine
.analyze(
"
ALTER TABLE t
ADD CONSTRAINT c2 CHECK (id > 0) NOT VALID;
",
&mut state,
)
.unwrap();
assert!(v2.iter().any(|v| v.rule_id.contains("constraint")));
}
#[test]
fn test_rule_blocking_constraint_pk_and_unique() {
let engine = setup_engine();
let mut cache = crate::db::cache::DbCache::new();
cache.insert_baseline(
object_id("public", "t"),
RelationState::new(
object_id("public", "t"),
0,
Some(500_000),
RelationKind::Table,
Persistence::Permanent,
0,
),
);
let mut state = AnalysisState::new(cache);
let v1 = engine
.analyze("ALTER TABLE t ADD PRIMARY KEY (id);", &mut state)
.unwrap();
assert!(
v1.iter()
.any(|v| v.rule_id.contains("constraint") || v.rule_id.contains("index"))
);
let v2 = engine
.analyze("ALTER TABLE t ADD UNIQUE (id);", &mut state)
.unwrap();
assert!(
v2.iter()
.any(|v| v.rule_id.contains("constraint") || v.rule_id.contains("index"))
);
}
#[test]
fn test_rule_temporary_table_bypass() {
let engine = setup_engine();
let mut state = setup_state();
let v = engine
.analyze(
"
CREATE TEMP TABLE temp(id int);
CREATE INDEX i ON temp(id);
ALTER TABLE temp ADD UNIQUE(id);
",
&mut state,
)
.unwrap();
assert!(!v.is_empty());
}
#[test]
fn test_rule_mat_view_refresh() {
let engine = setup_engine();
let mut cache = crate::db::cache::DbCache::new();
cache.insert_baseline(
object_id("public", "mv"),
RelationState::new(
object_id("public", "mv"),
0,
Some(150_000),
RelationKind::MaterializedView,
Persistence::Permanent,
0,
),
);
let mut state = AnalysisState::new(cache);
let v1 = engine
.analyze("REFRESH MATERIALIZED VIEW mv;", &mut state)
.unwrap();
assert!(
v1.iter()
.any(|v| v.rule_id.contains("mat") || v.rule_id.contains("refresh"))
);
let v2 = engine
.analyze("REFRESH MATERIALIZED VIEW CONCURRENTLY mv;", &mut state)
.unwrap();
assert!(v2.len() <= v1.len());
}
#[test]
fn test_rule_partition_attach_detach() {
let engine = setup_engine();
let mut cache = crate::db::cache::DbCache::new();
cache.insert_baseline(
object_id("public", "p"),
RelationState::new(
object_id("public", "p"),
0,
Some(500_000),
RelationKind::Table,
Persistence::Permanent,
0,
),
);
let mut state = AnalysisState::new(cache);
engine
.analyze("CREATE TABLE c(id int);", &mut state)
.unwrap();
let v1 = engine
.analyze(
"
ALTER TABLE p
ATTACH PARTITION c
FOR VALUES IN (1);
",
&mut state,
)
.unwrap();
assert!(v1.iter().any(|v| v.rule_id.contains("partition")));
let v2 = engine
.analyze(
"
ALTER TABLE p
DETACH PARTITION c;
",
&mut state,
)
.unwrap();
assert!(v2.iter().any(|v| v.rule_id.contains("partition")));
}
#[test]
fn test_rule_concurrent_inside_txn() {
let engine = setup_engine();
let mut state = setup_state();
let v = engine
.analyze(
"
BEGIN;
CREATE INDEX CONCURRENTLY i ON t(id);
DROP INDEX CONCURRENTLY i;
COMMIT;
",
&mut state,
)
.unwrap();
assert!(
v.iter()
.any(|v| v.rule_id.contains("transaction") || v.rule_id.contains("concurrent"))
);
}
#[test]
fn test_rule_opaque_sql() {
let engine = setup_engine();
let mut state = setup_state();
let v = engine.analyze("DO $$ BEGIN END $$;", &mut state).unwrap();
assert!(
v.iter()
.any(|v| v.rule_id.contains("opaque") || v.rule_id.contains("dynamic"))
);
assert_eq!(state.local.confidence, Confidence::Tainted);
}
#[test]
fn test_rule_volatile_default_create() {
let engine = setup_engine();
let mut state = setup_state();
let v = engine
.analyze(
"
CREATE TABLE t(
id int DEFAULT random()
);
",
&mut state,
)
.unwrap();
assert!(
v.iter()
.any(|v| v.rule_id.contains("volatile") || v.rule_id.contains("default"))
);
}
#[test]
fn test_rule_vacuum_full() {
let engine = setup_engine();
let mut state = setup_state();
let v = engine.analyze("VACUUM FULL t;", &mut state).unwrap();
assert!(v.iter().any(|v| v.rule_id.contains("vacuum")));
}
#[test]
fn test_rule_concurrent_index() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE t(id int);", &mut state)
.unwrap();
if let Some(crate::model::relation::RelationOverlay::Present(rel)) =
state.local.relations.get_mut(&object_id("public", "t"))
{
rel.estimated_rows = Some(500_000);
rel.created_at_tx_depth = 999;
}
let v1 = engine
.analyze("CREATE INDEX i ON t(id);", &mut state)
.unwrap();
assert!(
v1.iter()
.any(|v| v.rule_id.contains("concurrent") || v.rule_id.contains("index"))
);
let v2 = engine
.analyze("CREATE INDEX CONCURRENTLY i2 ON t(id);", &mut state)
.unwrap();
assert!(!v2.iter().any(|v| v.rule_id == "require-concurrent-index"));
}
}
#[cfg(test)]
mod state_mutation_tests {
use super::helpers::*;
use crate::analysis::state::AnalysisState;
use crate::model::relation::{Persistence, RelationKind, RelationOverlay};
use crate::model::sequence::SequenceOverlay;
use crate::model::types::{TypeKind, TypeOverlay};
#[test]
fn test_topology_table_basic() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"CREATE TABLE t(id int); ALTER TABLE t ADD COLUMN name text; ALTER TABLE t RENAME COLUMN name TO full_name;",
&mut state,
)
.unwrap();
let rel = state.get_relation(&object_id("public", "t")).unwrap();
if let RelationOverlay::Present(r) = rel {
assert!(r.has_column("id"));
assert!(r.has_column("full_name"));
assert!(!r.has_column("name"));
} else {
panic!("relation should be present");
}
}
#[test]
fn test_topology_drop_table() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE t(id int); DROP TABLE t;", &mut state)
.unwrap();
assert!(!state.relation_is_present(&object_id("public", "t")));
}
#[test]
fn test_topology_rename_table() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"CREATE TABLE a(id int); ALTER TABLE a RENAME TO b;",
&mut state,
)
.unwrap();
assert!(!state.relation_is_present(&object_id("public", "a")));
assert!(state.relation_is_present(&object_id("public", "b")));
assert!(
state
.local
.graph
.renames
.iter()
.any(|e| e.from == object_id("public", "a") && e.to == object_id("public", "b"))
);
}
#[test]
fn test_topology_rename_index() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"CREATE TABLE t(id int); CREATE INDEX i ON t(id); ALTER INDEX i RENAME TO i2;",
&mut state,
)
.unwrap();
assert!(
state
.local
.graph
.indexes
.iter()
.any(|i| i.index_id == object_id("public", "i2"))
);
assert!(
!state
.local
.graph
.indexes
.iter()
.any(|i| i.index_id == object_id("public", "i"))
);
}
#[test]
fn test_topology_foreign_key_graph() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"CREATE TABLE p(id int); CREATE TABLE c(p_id int); ALTER TABLE c ADD CONSTRAINT fk FOREIGN KEY (p_id) REFERENCES p(id);",
&mut state,
)
.unwrap();
assert!(
state
.local
.graph
.foreign_keys
.iter()
.any(|fk| fk.from_table == object_id("public", "c")
&& fk.to_table == object_id("public", "p"))
);
engine
.analyze("ALTER TABLE c DROP CONSTRAINT fk;", &mut state)
.unwrap();
assert!(state.local.graph.foreign_keys.is_empty());
}
#[test]
fn test_topology_view_graph() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"CREATE TABLE t(id int); CREATE VIEW v AS SELECT * FROM t;",
&mut state,
)
.unwrap();
assert!(
state
.local
.graph
.views
.iter()
.any(|v| v.view_id == object_id("public", "v")
&& v.depends_on.contains(&object_id("public", "t")))
);
engine.analyze("DROP VIEW v;", &mut state).unwrap();
assert!(state.local.graph.views.is_empty());
}
#[test]
fn test_topology_materialized_view_graph() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"CREATE TABLE t(id int); CREATE MATERIALIZED VIEW mv AS SELECT * FROM t;",
&mut state,
)
.unwrap();
assert!(
state
.local
.graph
.views
.iter()
.any(|v| v.view_id == object_id("public", "mv")
&& v.depends_on.contains(&object_id("public", "t")))
);
}
#[test]
fn test_topology_sequence_graph() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"CREATE TABLE t(id int); CREATE SEQUENCE s OWNED BY t.id;",
&mut state,
)
.unwrap();
assert!(
state
.local
.graph
.sequences
.iter()
.any(|s| s.sequence_id == object_id("public", "s")
&& s.table_id == object_id("public", "t"))
);
engine.analyze("DROP SEQUENCE s;", &mut state).unwrap();
assert!(matches!(
state.local.sequences.get(&object_id("public", "s")),
Some(SequenceOverlay::Dropped)
));
assert!(state.local.graph.sequences.is_empty());
}
#[test]
fn test_topology_type_and_domain() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"CREATE TYPE e AS ENUM('a'); ALTER TYPE e ADD VALUE 'b'; CREATE DOMAIN d AS INT; ALTER DOMAIN d SET DEFAULT 1;",
&mut state,
)
.unwrap();
if let Some(TypeOverlay::Present(t)) = state.local.types.get(&object_id("public", "e")) {
if let TypeKind::Enum { variants } = &t.kind {
assert!(variants.contains(&"b".to_string()));
} else {
panic!("type e should be enum");
}
} else {
panic!("type e missing");
}
engine.analyze("DROP DOMAIN d;", &mut state).unwrap();
assert!(matches!(
state.local.types.get(&object_id("public", "d")),
Some(TypeOverlay::Dropped)
));
}
#[test]
fn test_topology_trigger_and_policy() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"CREATE TABLE t(id int); CREATE POLICY p ON t FOR SELECT USING(true); CREATE TRIGGER tr BEFORE INSERT ON t EXECUTE FUNCTION f();",
&mut state,
)
.unwrap();
if let Some(RelationOverlay::Present(r)) = state.get_relation(&object_id("public", "t")) {
assert!(r.policies.contains("p"));
assert!(r.triggers.contains("tr"));
}
engine
.analyze("DROP POLICY p ON t; DROP TRIGGER tr ON t;", &mut state)
.unwrap();
if let Some(RelationOverlay::Present(r)) = state.get_relation(&object_id("public", "t")) {
assert!(!r.policies.contains("p"));
assert!(!r.triggers.contains("tr"));
}
}
#[test]
fn test_topology_search_path() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"SET search_path TO myschema, public; CREATE TABLE t(id int);",
&mut state,
)
.unwrap();
assert!(state.relation_is_present(&object_id("myschema", "t")));
}
#[test]
fn test_state_alter_column_types() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"CREATE TABLE t(id INT NOT NULL); ALTER TABLE t ALTER COLUMN id SET DATA TYPE text; ALTER TABLE t ALTER COLUMN id DROP NOT NULL; ALTER TABLE t ALTER COLUMN id SET DEFAULT 'x';",
&mut state,
)
.unwrap();
if let Some(RelationOverlay::Present(r)) = state.get_relation(&object_id("public", "t")) {
let col = r.get_column("id").unwrap();
assert_eq!(col.data_type.as_deref(), Some("text"));
assert!(col.is_nullable);
assert!(col.default.is_some());
} else {
panic!("relation should be present");
}
}
#[test]
fn test_state_storage_and_access_method() {
let engine = setup_engine();
let mut state = setup_state();
assert!(engine
.analyze(
"CREATE TABLE t(id int); ALTER TABLE t ALTER COLUMN id SET STORAGE MAIN; ALTER TABLE t SET ACCESS METHOD heap;",
&mut state,
)
.is_ok());
}
#[test]
fn test_state_confidence_is_accessible() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE t(id int);", &mut state)
.unwrap();
let _ = &state.local.confidence;
}
}
#[cfg(test)]
mod transaction_lifecycle_tests {
use super::helpers::*;
use crate::analysis::state::AnalysisState;
#[test]
fn test_txn_commit() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("BEGIN; CREATE TABLE t(id int); COMMIT;", &mut state)
.unwrap();
assert!(state.local.transactions.is_empty());
assert!(state.relation_is_present(&object_id("public", "t")));
}
#[test]
fn test_txn_rollback() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("BEGIN; CREATE TABLE t(id int); ROLLBACK;", &mut state)
.unwrap();
assert!(state.local.transactions.is_empty());
assert!(!state.relation_is_present(&object_id("public", "t")));
}
#[test]
fn test_savepoint_flow() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"BEGIN; SAVEPOINT s1; CREATE TABLE t(id int); ROLLBACK TO s1; RELEASE SAVEPOINT s1; COMMIT;",
&mut state,
)
.unwrap();
assert!(state.local.transactions.is_empty());
}
#[test]
fn test_rollback_to_savepoint_keeps_outer_txn() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"BEGIN; SAVEPOINT s1; CREATE TABLE t(id int); ROLLBACK TO s1; COMMIT;",
&mut state,
)
.unwrap();
assert!(state.local.transactions.is_empty());
assert!(!state.relation_is_present(&object_id("public", "t")));
}
}
#[cfg(test)]
mod expression_parsing_tests {
use super::helpers::*;
use crate::analysis::state::AnalysisState;
fn assert_expr(expr: &str) {
let engine = setup_engine();
let mut state = setup_state();
assert!(
engine
.analyze(
&format!("CREATE TABLE t(val INT DEFAULT {});", expr),
&mut state
)
.is_ok()
);
}
#[test]
fn test_expr_literal() {
assert_expr("42");
}
#[test]
fn test_expr_name_ref() {
assert_expr("some_col");
}
#[test]
fn test_expr_call() {
assert_expr("COALESCE(1, 2)");
}
#[test]
fn test_expr_bin_op() {
assert_expr("1 + 2 * 3 = 7");
}
#[test]
fn test_expr_cast() {
assert_expr("1::text");
}
#[test]
fn test_expr_prefix() {
assert_expr("-42");
}
#[test]
fn test_expr_paren() {
assert_expr("(1 + 2)");
}
#[test]
fn test_expr_case() {
assert_expr("CASE WHEN true THEN 1 ELSE 0 END");
}
#[test]
fn test_expr_array() {
assert_expr("ARRAY[1, 2, 3]");
}
#[test]
fn test_expr_between() {
assert_expr("5 BETWEEN 1 AND 10");
}
#[test]
fn test_expr_index() {
assert_expr("arr[1]");
}
#[test]
fn test_expr_slice() {
assert_expr("arr[1:3]");
}
#[test]
fn test_expr_slice_omitted() {
assert_expr("arr[2:]");
}
#[test]
fn test_expr_field() {
assert_expr("(my_record).my_field");
}
#[test]
fn test_parser_syntax_error_rejection() {
let engine = setup_engine();
let mut state = AnalysisState::new(crate::db::cache::DbCache::new());
assert!(engine.analyze("CREATE TABLE (;", &mut state).is_err());
}
}
#[cfg(test)]
mod identifier_casing_tests {
use super::helpers::*;
use crate::analysis::state::AnalysisState;
use crate::model::relation::RelationOverlay;
#[test]
fn test_ident_unquoted_lowercase() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE Users (Id int);", &mut state)
.unwrap();
assert!(state.relation_is_present(&object_id("public", "users")));
}
#[test]
fn test_ident_quoted_preserve() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE \"MyTable\" (\"MyCol\" int);", &mut state)
.unwrap();
let mixed_id = object_id("public", "MyTable");
assert!(state.relation_is_present(&mixed_id));
engine
.analyze(
"ALTER TABLE \"MyTable\" RENAME TO \"NewTable\";",
&mut state,
)
.unwrap();
assert!(!state.relation_is_present(&mixed_id));
assert!(state.relation_is_present(&object_id("public", "NewTable")));
engine
.analyze(
"ALTER TABLE \"NewTable\" RENAME COLUMN \"MyCol\" TO \"NewCol\";",
&mut state,
)
.unwrap();
let rel = state
.get_relation(&object_id("public", "NewTable"))
.unwrap();
if let RelationOverlay::Present(r) = rel {
assert!(r.has_column("NewCol"));
assert!(!r.has_column("MyCol"));
} else {
panic!("NewTable must be Present");
}
}
#[test]
fn test_ident_schema_resolution() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE MySchema.MyTable (id int);", &mut state)
.unwrap();
assert!(state.relation_is_present(&object_id("myschema", "mytable")));
}
}
#[cfg(test)]
mod architectural_gap_tests {
use super::helpers::*;
use crate::analysis::state::Confidence;
use crate::model::relation::{Persistence, RelationKind, RelationOverlay};
use crate::model::types::{TypeKind, TypeOverlay};
use crate::report::violations::ViolationTier;
#[test]
fn test_fk_parent_table_lock_escalation() {
let engine = setup_engine();
let mut cache = crate::db::cache::DbCache::new();
cache.insert_baseline(
object_id("public", "parent_tbl"),
crate::model::relation::RelationState::new(
object_id("public", "parent_tbl"),
0,
Some(500_000),
RelationKind::Table,
Persistence::Permanent,
0,
),
);
cache.insert_baseline(
object_id("public", "child_tbl"),
crate::model::relation::RelationState::new(
object_id("public", "child_tbl"),
0,
Some(10),
RelationKind::Table,
Persistence::Permanent,
0,
),
);
let mut state = crate::analysis::state::AnalysisState::new(cache);
let violations = engine.analyze("ALTER TABLE child_tbl ADD CONSTRAINT fk FOREIGN KEY (p_id) REFERENCES parent_tbl(id);", &mut state).unwrap();
let is_tier_1 = violations
.iter()
.any(|v| v.tier == ViolationTier::Tier1 && v.rule_id.contains("blocking-constraint"));
assert!(
is_tier_1,
"Failed to escalate lock severity based on parent table size"
);
}
#[test]
fn test_nested_release_savepoint_chain() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"
BEGIN;
CREATE TABLE t1(id int);
SAVEPOINT s1;
CREATE TABLE t2(id int);
SAVEPOINT s2;
CREATE TABLE t3(id int);
RELEASE SAVEPOINT s2;
ROLLBACK TO s1;
COMMIT;
",
&mut state,
)
.unwrap();
assert!(state.relation_is_present(&object_id("public", "t1")));
assert!(!state.relation_is_present(&object_id("public", "t2")));
assert!(!state.relation_is_present(&object_id("public", "t3")));
}
#[test]
fn test_rollback_to_savepoint_partial() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"
BEGIN;
CREATE TABLE a(id int);
SAVEPOINT s;
CREATE TABLE b(id int);
ROLLBACK TO s;
CREATE TABLE c(id int);
COMMIT;
",
&mut state,
)
.unwrap();
assert!(state.relation_is_present(&object_id("public", "a")));
assert!(state.relation_is_present(&object_id("public", "c")));
assert!(!state.relation_is_present(&object_id("public", "b")));
}
#[test]
fn test_drop_schema_cascade_cleans_renames() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"CREATE SCHEMA s; CREATE TABLE s.t(id int); ALTER TABLE s.t RENAME TO t2;",
&mut state,
)
.unwrap();
assert!(!state.local.graph.renames.is_empty());
engine
.analyze("DROP SCHEMA s CASCADE;", &mut state)
.unwrap();
assert!(
state.local.graph.renames.is_empty(),
"Rename edges leaked after schema cascade"
);
}
#[test]
fn test_multi_schema_search_path_resolution() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"CREATE SCHEMA s1; CREATE SCHEMA s2; SET search_path TO s1, s2;",
&mut state,
)
.unwrap();
engine
.analyze("CREATE TABLE t1(id int);", &mut state)
.unwrap();
assert!(state.relation_is_present(&object_id("s1", "t1")));
}
#[test]
fn test_tombstone_shadowing_recreate() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE t(id int);", &mut state)
.unwrap();
let gen1 = if let RelationOverlay::Present(r) =
state.get_relation(&object_id("public", "t")).unwrap()
{
r.generation
} else {
0
};
engine.analyze("DROP TABLE t;", &mut state).unwrap();
engine
.analyze("CREATE TABLE t(new_id text);", &mut state)
.unwrap();
if let RelationOverlay::Present(r) = state.get_relation(&object_id("public", "t")).unwrap()
{
assert!(
r.generation > gen1,
"Recreated table must have higher generation"
);
assert!(r.has_column("new_id"));
} else {
panic!("Table did not recreate over tombstone");
}
}
#[test]
fn test_drop_missing_object_halts_topology_mutation() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE exists_tbl(id int);", &mut state)
.unwrap();
let _ = engine.analyze("DROP TABLE missing_tbl;", &mut state);
assert!(state.relation_is_present(&object_id("public", "exists_tbl")));
assert_eq!(state.local.confidence, Confidence::Tainted);
}
#[test]
fn test_view_dependency_cte_isolation() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE base_table(id int);", &mut state)
.unwrap();
engine
.analyze(
"CREATE VIEW v AS WITH my_cte AS (SELECT * FROM base_table) SELECT * FROM my_cte;",
&mut state,
)
.unwrap();
let edge = state
.local
.graph
.views
.iter()
.find(|v| v.view_id == object_id("public", "v"))
.unwrap();
assert!(edge.depends_on.contains(&object_id("public", "base_table")));
assert!(!edge.depends_on.contains(&object_id("public", "my_cte")));
}
#[test]
fn test_partition_graph_cleanup_on_drop() {
let engine = setup_engine();
let mut state = setup_state();
engine.analyze("CREATE TABLE p(id int) PARTITION BY RANGE(id); CREATE TABLE c PARTITION OF p FOR VALUES FROM (1) TO (10);", &mut state).unwrap();
engine.analyze("DROP TABLE c;", &mut state).unwrap();
assert!(
state.local.graph.partitions.is_empty(),
"Partition edge leaked after child drop"
);
}
#[test]
fn test_concurrent_index_rollback() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE t(id int);", &mut state)
.unwrap();
engine
.analyze(
"BEGIN; CREATE INDEX CONCURRENTLY idx ON t(id); ROLLBACK;",
&mut state,
)
.unwrap();
assert!(state.local.graph.indexes.is_empty());
}
#[test]
fn test_opaque_confidence_taint_persistence() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("DO $$ BEGIN EXECUTE 'DROP TABLE x;'; END $$;", &mut state)
.unwrap();
assert_eq!(state.local.confidence, Confidence::Tainted);
engine
.analyze("CREATE TABLE t(id int);", &mut state)
.unwrap();
assert_eq!(state.local.confidence, Confidence::Tainted);
}
#[test]
fn test_quoted_ident_search_path() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"CREATE SCHEMA \"MySchema\"; SET search_path TO \"MySchema\";",
&mut state,
)
.unwrap();
engine
.analyze("CREATE TABLE \"MyTable\" (\"MyCol\" int);", &mut state)
.unwrap();
assert!(state.relation_is_present(&object_id("MySchema", "MyTable")));
}
#[test]
fn test_create_domain_recreation() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE DOMAIN my_type AS int;", &mut state)
.unwrap();
engine.analyze("DROP DOMAIN my_type;", &mut state).unwrap();
engine
.analyze("CREATE DOMAIN my_type AS text;", &mut state)
.unwrap();
assert!(matches!(
state.local.types.get(&object_id("public", "my_type")),
Some(TypeOverlay::Present(_))
));
}
#[test]
fn test_stale_view_edge_cleanup() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"CREATE TABLE t(id int); CREATE VIEW v AS SELECT * FROM t;",
&mut state,
)
.unwrap();
engine
.analyze("DROP VIEW v; CREATE VIEW v AS SELECT * FROM t;", &mut state)
.unwrap();
assert_eq!(
state.local.graph.views.len(),
1,
"Duplicate view edge created"
);
}
#[test]
fn test_if_not_exists_preserves_original_metadata() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze("CREATE TABLE t(id INT);", &mut state)
.unwrap();
let gen1 = if let RelationOverlay::Present(r) =
state.get_relation(&object_id("public", "t")).unwrap()
{
r.generation
} else {
0
};
engine
.analyze(
"CREATE TABLE IF NOT EXISTS t(id TEXT, diff_col INT);",
&mut state,
)
.unwrap();
let rel = state.get_relation(&object_id("public", "t")).unwrap();
if let RelationOverlay::Present(r) = rel {
assert_eq!(r.generation, gen1);
assert_eq!(
r.get_column("id").unwrap().data_type.as_deref(),
Some("INT")
);
assert!(!r.has_column("diff_col"));
}
}
#[test]
fn test_deep_rename_traversal_cascade() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"
CREATE TABLE a(id int);
CREATE VIEW v AS SELECT * FROM a;
ALTER TABLE a RENAME TO b;
DROP TABLE b CASCADE;
",
&mut state,
)
.unwrap();
assert!(
!state.relation_is_present(&object_id("public", "a")),
"Original table a should be gone"
);
assert!(
!state.relation_is_present(&object_id("public", "b")),
"Renamed table b should be gone"
);
assert!(
!state.relation_is_present(&object_id("public", "v")),
"Dependent view v should have been cascaded"
);
}
#[test]
fn test_partition_cycle_rejection() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"
CREATE TABLE a(id int) PARTITION BY RANGE(id);
CREATE TABLE b PARTITION OF a FOR VALUES FROM (1) TO (10) PARTITION BY RANGE(id);
ALTER TABLE b ATTACH PARTITION a FOR VALUES FROM (1) TO (10);
",
&mut state,
)
.unwrap();
assert_eq!(
state.local.confidence,
Confidence::Tainted,
"Partition cycle should taint the engine"
);
}
#[test]
fn test_tablespace_access_method_rewrite() {
let engine = setup_engine();
let mut cache = crate::db::cache::DbCache::new();
cache.insert_baseline(
object_id("public", "massive_table"),
crate::model::relation::RelationState::new(
object_id("public", "massive_table"),
0,
Some(150_000),
RelationKind::Table,
Persistence::Permanent,
0,
),
);
let mut state = crate::analysis::state::AnalysisState::new(cache);
let v1 = engine
.analyze(
"ALTER TABLE massive_table SET ACCESS METHOD columnar;",
&mut state,
)
.unwrap();
assert!(
v1.iter()
.any(|v| v.rule_id == "table-rewrite-access-method"
&& v.tier == ViolationTier::Tier1)
);
let v2 = engine
.analyze(
"ALTER TABLE massive_table ALTER COLUMN id SET STORAGE MAIN;",
&mut state,
)
.unwrap();
assert!(
v2.iter()
.any(|v| v.rule_id == "table-rewrite-storage" && v.tier == ViolationTier::Tier1)
);
}
#[test]
fn test_generation_counter_rollback() {
let engine = setup_engine();
let mut state = setup_state();
let initial_gen = state.local.generation_counter;
engine
.analyze("BEGIN; CREATE TABLE t(id int);", &mut state)
.unwrap();
let mid_gen = state.local.generation_counter;
assert!(
mid_gen > initial_gen,
"Generation counter should increment on create"
);
engine.analyze("ROLLBACK;", &mut state).unwrap();
let post_gen = state.local.generation_counter;
assert_eq!(
post_gen, initial_gen,
"Generation counter should restore strictly to pre-txn state on rollback"
);
}
#[test]
fn test_partition_children_cascade_enumeration() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"
CREATE TABLE parent(id int) PARTITION BY RANGE(id);
CREATE TABLE child PARTITION OF parent FOR VALUES FROM (1) TO (10);
DROP TABLE parent CASCADE;
",
&mut state,
)
.unwrap();
assert!(
!state.relation_is_present(&object_id("public", "parent")),
"Parent should be dropped"
);
assert!(
!state.relation_is_present(&object_id("public", "child")),
"Child should be dropped via reverse-graph cascade"
);
}
#[test]
fn test_rename_updates_fk_graph_edges() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"
CREATE TABLE a(id int);
CREATE TABLE b(a_id int);
ALTER TABLE b ADD CONSTRAINT fk FOREIGN KEY (a_id) REFERENCES a(id);
ALTER TABLE a RENAME TO a2;
",
&mut state,
)
.unwrap();
let refs = state
.local
.graph
.is_referenced_by_fk(&object_id("public", "a2"));
assert!(
!refs.is_empty(),
"a2 should be recognized as referenced by b's FK dynamically"
);
assert_eq!(refs[0].0, &object_id("public", "b"));
}
#[test]
fn test_search_path_existence_check() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"
CREATE SCHEMA actual_schema;
CREATE TABLE actual_schema.my_table(id int);
SET search_path = nonexistent_schema, actual_schema;
ALTER TABLE my_table ADD COLUMN new_col int;
",
&mut state,
)
.unwrap();
let rel = state
.get_relation(&object_id("actual_schema", "my_table"))
.unwrap();
if let crate::model::relation::RelationOverlay::Present(r) = rel {
assert!(
r.has_column("new_col"),
"Should resolve to actual_schema bypassing nonexistent_schema"
);
} else {
panic!("Table not found; resolver hallucinated the schema");
}
}
#[test]
fn test_drop_without_cascade_validates_dependents() {
let engine = setup_engine();
let mut state = setup_state();
engine
.analyze(
"
CREATE TABLE a(id int);
CREATE TABLE b(a_id int);
ALTER TABLE b ADD CONSTRAINT fk FOREIGN KEY (a_id) REFERENCES a(id);
",
&mut state,
)
.unwrap();
let _ = engine.analyze("DROP TABLE a;", &mut state);
assert_eq!(
state.local.confidence,
crate::analysis::state::Confidence::Tainted,
"Engine should taint on unsafe drop"
);
assert!(
state.relation_is_present(&object_id("public", "a")),
"Table a should not be dropped if dependents exist without CASCADE"
);
}
}