use std::collections::{BTreeMap, BTreeSet};
use crate::identifier::{Identifier, QualifiedName};
use crate::ir::catalog::Catalog;
use crate::ir::grant::GrantTarget;
use crate::ir::sequence::Sequence;
use super::change::Change;
use super::changeset::{ChangeSet, RevokeWithOwnerObservation, UnmanagedGrantObservation};
use super::destructiveness::Destructiveness;
use super::grants::diff_grants;
use super::owner_op::{AlterObjectOwner, OwnerObjectKind};
use super::sequence_op::{SequenceOp, SequenceOpEntry};
#[allow(clippy::too_many_lines)]
pub fn diff_sequences(
target: &Catalog,
source: &Catalog,
out: &mut ChangeSet,
managed_roles: &BTreeSet<Identifier>,
) {
let target_map: BTreeMap<&QualifiedName, &Sequence> = target
.sequences
.iter()
.filter(|s| s.owned_by.is_none())
.map(|s| (&s.qname, s))
.collect();
let source_map: BTreeMap<&QualifiedName, &Sequence> = source
.sequences
.iter()
.filter(|s| s.owned_by.is_none())
.map(|s| (&s.qname, s))
.collect();
for (qname, source_seq) in &source_map {
if !target_map.contains_key(qname) {
out.push(
Change::CreateSequence((*source_seq).clone()),
Destructiveness::Safe,
);
}
}
for (qname, target_seq) in &target_map {
match source_map.get(qname) {
None => {
out.push(
Change::DropSequence((*qname).clone()),
Destructiveness::RequiresApproval {
reason: format!("drops sequence {qname} (current value lost)"),
},
);
}
Some(source_seq) => {
if target_seq.start != source_seq.start {
out.push(
Change::DropSequence((*qname).clone()),
Destructiveness::RequiresApproval {
reason: format!(
"drops sequence {qname} to change START (current value lost)"
),
},
);
out.push(
Change::CreateSequence((*source_seq).clone()),
Destructiveness::Safe,
);
continue;
}
let ops = diff_sequence_fields(target_seq, source_seq);
if !ops.is_empty() {
out.push(
Change::AlterSequence {
qname: (*qname).clone(),
ops,
},
Destructiveness::Safe,
);
}
if let Some(source_owner) = &source_seq.owner
&& target_seq.owner.as_ref() != Some(source_owner)
{
let from = target_seq.owner.clone().unwrap_or_else(|| {
Identifier::from_unquoted("__unknown_owner__").expect("literal is valid")
});
out.push(
Change::AlterObjectOwner(AlterObjectOwner {
kind: OwnerObjectKind::Sequence,
qname: (*qname).clone(),
signature: String::new(),
from,
to: source_owner.clone(),
}),
Destructiveness::Safe,
);
}
{
let object_label = format!("sequence {qname}");
let (to_add, to_revoke, unmanaged) =
diff_grants(&target_seq.grants, &source_seq.grants, managed_roles);
for g in to_add {
out.push(
Change::GrantObjectPrivilege {
qname: (*qname).clone(),
kind: OwnerObjectKind::Sequence,
signature: String::new(),
grant: g,
},
Destructiveness::Safe,
);
}
for g in to_revoke {
if let Some(source_owner) = &source_seq.owner {
out.revokes_with_owner.push(RevokeWithOwnerObservation {
object_label: object_label.clone(),
privilege_label: g.privilege.sql_keyword().into(),
grantee: g.grantee.clone(),
owner: source_owner.clone(),
});
}
out.push(
Change::RevokeObjectPrivilege {
qname: (*qname).clone(),
kind: OwnerObjectKind::Sequence,
signature: String::new(),
grant: g,
},
Destructiveness::Safe,
);
}
for g in unmanaged {
if let GrantTarget::Role(role_name) = &g.grantee {
out.unmanaged_grants.push(UnmanagedGrantObservation {
object_label: object_label.clone(),
privilege_label: g.privilege.sql_keyword().into(),
role_name: role_name.clone(),
});
}
}
}
}
}
}
}
fn diff_sequence_fields(target: &Sequence, source: &Sequence) -> Vec<SequenceOpEntry> {
let mut ops = Vec::new();
if target.increment != source.increment {
ops.push(SequenceOpEntry {
op: SequenceOp::SetIncrement(source.increment),
destructiveness: Destructiveness::Safe,
});
}
if target.min_value != source.min_value {
ops.push(SequenceOpEntry {
op: SequenceOp::SetMinValue(source.min_value),
destructiveness: Destructiveness::Safe,
});
}
if target.max_value != source.max_value {
ops.push(SequenceOpEntry {
op: SequenceOp::SetMaxValue(source.max_value),
destructiveness: Destructiveness::Safe,
});
}
if target.cache != source.cache {
ops.push(SequenceOpEntry {
op: SequenceOp::SetCache(source.cache),
destructiveness: Destructiveness::Safe,
});
}
if target.cycle != source.cycle {
ops.push(SequenceOpEntry {
op: SequenceOp::SetCycle(source.cycle),
destructiveness: Destructiveness::Safe,
});
}
if target.data_type != source.data_type {
ops.push(SequenceOpEntry {
op: SequenceOp::SetDataType(source.data_type.clone()),
destructiveness: Destructiveness::RequiresApproval {
reason: format!(
"changing data type may overflow current value of sequence {}",
target.qname
),
},
});
}
if target.owned_by != source.owned_by {
ops.push(SequenceOpEntry {
op: SequenceOp::SetOwnedBy(source.owned_by.clone()),
destructiveness: Destructiveness::Safe,
});
}
ops
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
use crate::identifier::Identifier;
use crate::ir::column_type::ColumnType;
use crate::ir::sequence::SequenceOwner;
fn id(s: &str) -> Identifier {
Identifier::from_unquoted(s).unwrap()
}
fn qn(name: &str) -> QualifiedName {
QualifiedName::new(id("app"), id(name))
}
fn seq(name: &str) -> Sequence {
Sequence {
qname: qn(name),
data_type: ColumnType::BigInt,
start: 1,
increment: 1,
min_value: None,
max_value: None,
cache: 1,
cycle: false,
owned_by: None,
comment: None,
owner: None,
grants: vec![],
}
}
#[test]
fn add_sequence_is_safe() {
let target = Catalog::empty();
let mut source = Catalog::empty();
source.sequences.push(seq("seq1"));
let mut cs = ChangeSet::new();
diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
assert_eq!(cs.len(), 1);
assert!(matches!(cs.entries[0].change, Change::CreateSequence(_)));
assert_eq!(cs.entries[0].destructiveness, Destructiveness::Safe);
}
#[test]
fn drop_sequence_requires_approval() {
let mut target = Catalog::empty();
target.sequences.push(seq("seq1"));
let source = Catalog::empty();
let mut cs = ChangeSet::new();
diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
assert_eq!(cs.len(), 1);
let entry = &cs.entries[0];
assert!(matches!(entry.change, Change::DropSequence(_)));
assert!(entry.destructiveness.requires_approval());
}
#[test]
fn increment_change_emits_alter_safe() {
let mut target = Catalog::empty();
target.sequences.push(seq("seq1"));
let mut source = Catalog::empty();
source.sequences.push(Sequence {
increment: 2,
..seq("seq1")
});
let mut cs = ChangeSet::new();
diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
assert_eq!(cs.len(), 1);
let entry = &cs.entries[0];
match &entry.change {
Change::AlterSequence { qname, ops } => {
assert_eq!(qname, &qn("seq1"));
assert_eq!(ops.len(), 1);
assert!(matches!(ops[0].op, SequenceOp::SetIncrement(2)));
assert_eq!(ops[0].destructiveness, Destructiveness::Safe);
}
other => panic!("got {other:?}"),
}
}
#[test]
fn data_type_change_requires_approval_inside_alter() {
let mut target = Catalog::empty();
target.sequences.push(seq("seq1"));
let mut source = Catalog::empty();
source.sequences.push(Sequence {
data_type: ColumnType::Integer,
..seq("seq1")
});
let mut cs = ChangeSet::new();
diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
let entry = &cs.entries[0];
match &entry.change {
Change::AlterSequence { ops, .. } => {
assert_eq!(ops.len(), 1);
assert!(ops[0].destructiveness.requires_approval());
assert!(!ops[0].destructiveness.data_loss_risk());
}
other => panic!("got {other:?}"),
}
}
#[test]
fn cycle_min_max_cache_each_emit_safe_ops() {
let mut target = Catalog::empty();
target.sequences.push(seq("seq1"));
let mut source = Catalog::empty();
source.sequences.push(Sequence {
cycle: true,
min_value: Some(0),
max_value: Some(1_000_000),
cache: 32,
..seq("seq1")
});
let mut cs = ChangeSet::new();
diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
match &cs.entries[0].change {
Change::AlterSequence { ops, .. } => {
assert_eq!(ops.len(), 4);
assert!(
ops.iter()
.all(|o| o.destructiveness == Destructiveness::Safe)
);
}
other => panic!("got {other:?}"),
}
}
#[test]
fn start_change_emits_drop_then_create() {
let mut target = Catalog::empty();
target.sequences.push(seq("seq1"));
let mut source = Catalog::empty();
source.sequences.push(Sequence {
start: 100,
..seq("seq1")
});
let mut cs = ChangeSet::new();
diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
assert_eq!(cs.len(), 2);
assert!(matches!(cs.entries[0].change, Change::DropSequence(_)));
assert!(matches!(cs.entries[1].change, Change::CreateSequence(_)));
}
#[test]
fn owned_sequences_are_skipped() {
let owned = Sequence {
owned_by: Some(SequenceOwner {
table: qn("users"),
column: id("id"),
}),
..seq("seq1")
};
let mut target = Catalog::empty();
target.sequences.push(owned);
let source = Catalog::empty(); let mut cs = ChangeSet::new();
diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
assert!(
cs.is_empty(),
"owned sequences are driven by their owner column"
);
}
#[test]
fn equal_sequences_emit_nothing() {
let mut target = Catalog::empty();
target.sequences.push(seq("seq1"));
let mut source = Catalog::empty();
source.sequences.push(seq("seq1"));
let mut cs = ChangeSet::new();
diff_sequences(&target, &source, &mut cs, &BTreeSet::new());
assert!(cs.is_empty());
}
}