use std::collections::{BTreeMap, BTreeSet};
use crate::identifier::Identifier;
use crate::ir::catalog::Catalog;
use crate::ir::schema::Schema;
use super::change::{Change, CollationChange};
use super::changeset::ChangeSet;
use super::destructiveness::Destructiveness;
use super::owner_grants::{ColumnGrantMode, diff_owner_and_grants};
use super::owner_op::CatalogObjectRef;
#[allow(clippy::too_many_lines)] pub fn diff_schemas(
target: &Catalog,
source: &Catalog,
out: &mut ChangeSet,
managed_roles: &BTreeSet<Identifier>,
) {
let target_map: BTreeMap<&Identifier, &Schema> =
target.schemas.iter().map(|s| (&s.name, s)).collect();
let source_map: BTreeMap<&Identifier, &Schema> =
source.schemas.iter().map(|s| (&s.name, s)).collect();
for (name, source_schema) in &source_map {
if !target_map.contains_key(name) {
out.push(
Change::CreateSchema((*source_schema).clone()),
Destructiveness::Safe,
);
let empty_target = Schema {
name: source_schema.name.clone(),
comment: None,
owner: None,
grants: vec![],
};
emit_schema_attribute_changes(&empty_target, source_schema, managed_roles, out);
}
}
for (name, target_schema) in &target_map {
match source_map.get(name) {
None => {
out.push(
Change::DropSchema((*name).clone()),
Destructiveness::RequiresApproval {
reason: format!("drops schema {name}"),
},
);
for coll in target
.collations
.iter()
.filter(|c| &c.qname.schema == *name)
{
out.push(
Change::Collation(CollationChange::Drop {
qname: coll.qname.clone(),
}),
Destructiveness::RequiresApproval {
reason: format!(
"drops collation {} (schema {} is being dropped)",
coll.qname, name,
),
},
);
}
}
Some(source_schema) => {
if target_schema.comment != source_schema.comment {
out.push(
Change::AlterSchema {
name: (*name).clone(),
comment: source_schema.comment.clone(),
},
Destructiveness::Safe,
);
}
emit_schema_attribute_changes(target_schema, source_schema, managed_roles, out);
}
}
}
}
fn emit_schema_attribute_changes(
target_schema: &Schema,
source_schema: &Schema,
managed_roles: &BTreeSet<Identifier>,
out: &mut ChangeSet,
) {
diff_owner_and_grants(
&CatalogObjectRef::Schema(source_schema.name.clone()),
target_schema.owner.as_ref(),
source_schema.owner.as_ref(),
&target_schema.grants,
&source_schema.grants,
managed_roles,
ColumnGrantMode::ObjectOnly,
out,
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::grant::{Grant, GrantTarget, Privilege};
use std::collections::BTreeSet;
fn id(s: &str) -> Identifier {
Identifier::from_unquoted(s).unwrap()
}
fn sch(name: &str, comment: Option<&str>) -> Schema {
Schema {
name: id(name),
comment: comment.map(str::to_string),
owner: None,
grants: vec![],
}
}
#[test]
fn new_schema_with_owner_and_grant_emits_all_three_changes() {
let target = Catalog::empty();
let mut source = Catalog::empty();
source.schemas.push(Schema {
name: id("app"),
comment: None,
owner: Some(id("app_owner")),
grants: vec![Grant {
grantee: GrantTarget::Role(id("app_user")),
privilege: Privilege::Usage,
with_grant_option: false,
columns: None,
}],
});
let mut cs = ChangeSet::new();
diff_schemas(&target, &source, &mut cs, &BTreeSet::from([id("app_user")]));
let changes: Vec<_> = cs.iter().map(|e| &e.change).collect();
assert!(
changes.iter().any(|c| matches!(c, Change::CreateSchema(_))),
"expected CreateSchema in {changes:?}"
);
assert!(
changes
.iter()
.any(|c| matches!(c, Change::AlterObjectOwner(_))),
"expected AlterObjectOwner in {changes:?}"
);
assert!(
changes
.iter()
.any(|c| matches!(c, Change::GrantObjectPrivilege { .. })),
"expected GrantObjectPrivilege in {changes:?}"
);
assert_eq!(changes.len(), 3, "unexpected extra changes: {changes:?}");
}
#[test]
fn add_schema_emits_create_safe() {
let target = Catalog::empty();
let mut source = Catalog::empty();
source.schemas.push(sch("app", None));
let mut cs = ChangeSet::new();
diff_schemas(&target, &source, &mut cs, &BTreeSet::new());
assert_eq!(cs.len(), 1);
let entry = cs.iter().next().unwrap();
assert!(matches!(entry.change, Change::CreateSchema(_)));
assert_eq!(entry.destructiveness, Destructiveness::Safe);
}
#[test]
fn drop_schema_emits_drop_requires_approval() {
let mut target = Catalog::empty();
target.schemas.push(sch("legacy", None));
let source = Catalog::empty();
let mut cs = ChangeSet::new();
diff_schemas(&target, &source, &mut cs, &BTreeSet::new());
assert_eq!(cs.len(), 1);
let entry = cs.iter().next().unwrap();
assert!(matches!(entry.change, Change::DropSchema(_)));
assert!(entry.destructiveness.requires_approval());
assert!(!entry.destructiveness.data_loss_risk());
}
#[test]
fn comment_change_emits_alter_safe() {
let mut target = Catalog::empty();
target.schemas.push(sch("app", Some("v1")));
let mut source = Catalog::empty();
source.schemas.push(sch("app", Some("v2")));
let mut cs = ChangeSet::new();
diff_schemas(&target, &source, &mut cs, &BTreeSet::new());
assert_eq!(cs.len(), 1);
let entry = &cs.entries[0];
match &entry.change {
Change::AlterSchema { name, comment } => {
assert_eq!(name, &id("app"));
assert_eq!(comment.as_deref(), Some("v2"));
}
other => panic!("expected AlterSchema, got {other:?}"),
}
assert_eq!(entry.destructiveness, Destructiveness::Safe);
}
#[test]
fn comment_clear_emits_alter_with_none() {
let mut target = Catalog::empty();
target.schemas.push(sch("app", Some("v1")));
let mut source = Catalog::empty();
source.schemas.push(sch("app", None));
let mut cs = ChangeSet::new();
diff_schemas(&target, &source, &mut cs, &BTreeSet::new());
let entry = &cs.entries[0];
match &entry.change {
Change::AlterSchema { comment, .. } => assert!(comment.is_none()),
other => panic!("expected AlterSchema, got {other:?}"),
}
}
#[test]
fn equal_schemas_emit_nothing() {
let mut target = Catalog::empty();
target.schemas.push(sch("app", Some("v1")));
let mut source = Catalog::empty();
source.schemas.push(sch("app", Some("v1")));
let mut cs = ChangeSet::new();
diff_schemas(&target, &source, &mut cs, &BTreeSet::new());
assert!(cs.is_empty());
}
#[test]
fn drop_schema_cascades_collation_drops() {
use crate::diff::CollationChange;
use crate::identifier::QualifiedName;
use crate::ir::collation::{Collation, CollationProvider};
fn qn(schema: &str, name: &str) -> QualifiedName {
QualifiedName::new(id(schema), id(name))
}
fn coll(schema: &str, name: &str) -> Collation {
Collation {
qname: qn(schema, name),
provider: CollationProvider::Libc,
lc_collate: "C".into(),
lc_ctype: "C".into(),
deterministic: true,
version: None,
owner: None,
comment: None,
}
}
let mut target = Catalog::empty();
target.schemas.push(sch("audit", None));
target.collations.push(coll("audit", "coll_a"));
target.collations.push(coll("audit", "coll_b"));
let source = Catalog::empty();
let mut cs = ChangeSet::new();
diff_schemas(&target, &source, &mut cs, &BTreeSet::new());
let changes: Vec<_> = cs.iter().map(|e| &e.change).collect();
assert!(
changes
.iter()
.any(|c| matches!(c, Change::DropSchema(n) if n == &id("audit"))),
"expected DropSchema(audit) in {changes:?}",
);
let drop_coll_qnames: Vec<&QualifiedName> = changes
.iter()
.filter_map(|c| {
if let Change::Collation(CollationChange::Drop { qname }) = c {
Some(qname)
} else {
None
}
})
.collect();
assert_eq!(
drop_coll_qnames.len(),
2,
"expected 2 CollationChange::Drop, got {drop_coll_qnames:?} (all changes: {changes:?})",
);
assert!(
drop_coll_qnames.contains(&&qn("audit", "coll_a")),
"expected drop of audit.coll_a",
);
assert!(
drop_coll_qnames.contains(&&qn("audit", "coll_b")),
"expected drop of audit.coll_b",
);
for entry in cs.iter() {
if let Change::Collation(CollationChange::Drop { .. }) = &entry.change {
assert!(
entry.destructiveness.requires_approval(),
"DropCollation must require approval",
);
}
}
}
}