use super::ddl::{
CheckConstraint, Column, ForeignKey, Index, PrimaryKey, SqliteEntity, Table, UniqueConstraint,
View,
};
use crate::collection::EntityCollection;
use crate::traits::EntityKind;
use std::borrow::Cow;
use std::collections::HashMap;
impl EntityCollection<Table> {
#[must_use]
pub fn one(&self, name: &str) -> Option<&Table> {
self.entities.iter().find(|t| t.name == name)
}
pub fn delete(&mut self, name: &str) -> Option<Table> {
if let Some(pos) = self.entities.iter().position(|t| t.name == name) {
Some(self.entities.remove(pos))
} else {
None
}
}
}
impl EntityCollection<Column> {
#[must_use]
pub fn one(&self, table: &str, name: &str) -> Option<&Column> {
self.entities
.iter()
.find(|c| c.table == table && c.name == name)
}
#[must_use]
pub fn for_table(&self, table: &str) -> Vec<&Column> {
self.entities.iter().filter(|c| c.table == table).collect()
}
pub fn delete(&mut self, table: &str, name: &str) -> Option<Column> {
if let Some(pos) = self
.entities
.iter()
.position(|c| c.table == table && c.name == name)
{
Some(self.entities.remove(pos))
} else {
None
}
}
}
impl EntityCollection<Index> {
#[must_use]
pub fn one(&self, name: &str) -> Option<&Index> {
self.entities.iter().find(|i| i.name == name)
}
#[must_use]
pub fn for_table(&self, table: &str) -> Vec<&Index> {
self.entities.iter().filter(|i| i.table == table).collect()
}
}
impl EntityCollection<ForeignKey> {
#[must_use]
pub fn one(&self, name: &str) -> Option<&ForeignKey> {
self.entities.iter().find(|f| f.name == name)
}
#[must_use]
pub fn for_table(&self, table: &str) -> Vec<&ForeignKey> {
self.entities.iter().filter(|f| f.table == table).collect()
}
}
impl EntityCollection<PrimaryKey> {
#[must_use]
pub fn for_table(&self, table: &str) -> Option<&PrimaryKey> {
self.entities.iter().find(|p| p.table == table)
}
}
impl EntityCollection<UniqueConstraint> {
#[must_use]
pub fn one(&self, name: &str) -> Option<&UniqueConstraint> {
self.entities.iter().find(|u| u.name == name)
}
#[must_use]
pub fn for_table(&self, table: &str) -> Vec<&UniqueConstraint> {
self.entities.iter().filter(|u| u.table == table).collect()
}
}
impl EntityCollection<CheckConstraint> {
#[must_use]
pub fn one(&self, name: &str) -> Option<&CheckConstraint> {
self.entities.iter().find(|c| c.name == name)
}
#[must_use]
pub fn for_table(&self, table: &str) -> Vec<&CheckConstraint> {
self.entities.iter().filter(|c| c.table == table).collect()
}
}
impl EntityCollection<View> {
#[must_use]
pub fn one(&self, name: &str) -> Option<&View> {
self.entities.iter().find(|v| v.name == name)
}
}
#[derive(Debug, Clone, Default)]
pub struct SQLiteDDL {
pub tables: EntityCollection<Table>,
pub columns: EntityCollection<Column>,
pub indexes: EntityCollection<Index>,
pub fks: EntityCollection<ForeignKey>,
pub pks: EntityCollection<PrimaryKey>,
pub uniques: EntityCollection<UniqueConstraint>,
pub checks: EntityCollection<CheckConstraint>,
pub views: EntityCollection<View>,
}
impl SQLiteDDL {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_entities(entities: Vec<SqliteEntity>) -> Self {
let mut ddl = Self::new();
for entity in entities {
ddl.push_entity(entity);
}
ddl
}
pub fn push_entity(&mut self, entity: SqliteEntity) {
match entity {
SqliteEntity::Table(t) => self.tables.push(t),
SqliteEntity::Column(c) => self.columns.push(c),
SqliteEntity::Index(i) => self.indexes.push(i),
SqliteEntity::ForeignKey(f) => self.fks.push(f),
SqliteEntity::PrimaryKey(p) => self.pks.push(p),
SqliteEntity::UniqueConstraint(u) => self.uniques.push(u),
SqliteEntity::CheckConstraint(c) => self.checks.push(c),
SqliteEntity::View(v) => self.views.push(v),
};
}
#[must_use]
pub fn to_entities(&self) -> Vec<SqliteEntity> {
let mut entities = Vec::new();
for t in self.tables.list() {
entities.push(SqliteEntity::Table(t.clone()));
}
for c in self.columns.list() {
entities.push(SqliteEntity::Column(c.clone()));
}
for i in self.indexes.list() {
entities.push(SqliteEntity::Index(i.clone()));
}
for f in self.fks.list() {
entities.push(SqliteEntity::ForeignKey(f.clone()));
}
for p in self.pks.list() {
entities.push(SqliteEntity::PrimaryKey(p.clone()));
}
for u in self.uniques.list() {
entities.push(SqliteEntity::UniqueConstraint(u.clone()));
}
for c in self.checks.list() {
entities.push(SqliteEntity::CheckConstraint(c.clone()));
}
for v in self.views.list() {
entities.push(SqliteEntity::View(v.clone()));
}
entities
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.tables.is_empty()
&& self.columns.is_empty()
&& self.indexes.is_empty()
&& self.fks.is_empty()
&& self.pks.is_empty()
&& self.uniques.is_empty()
&& self.checks.is_empty()
&& self.views.is_empty()
}
#[must_use]
pub fn table_entities<'a>(&'a self, table_name: &str) -> TableEntities<'a> {
TableEntities {
columns: self.columns.for_table(table_name),
indexes: self.indexes.for_table(table_name),
fks: self.fks.for_table(table_name),
pk: self.pks.for_table(table_name),
uniques: self.uniques.for_table(table_name),
checks: self.checks.for_table(table_name),
}
}
}
pub struct TableEntities<'a> {
pub columns: Vec<&'a Column>,
pub indexes: Vec<&'a Index>,
pub fks: Vec<&'a ForeignKey>,
pub pk: Option<&'a PrimaryKey>,
pub uniques: Vec<&'a UniqueConstraint>,
pub checks: Vec<&'a CheckConstraint>,
}
pub use crate::traits::DiffType;
#[derive(Debug, Clone)]
pub struct EntityDiff {
pub diff_type: DiffType,
pub kind: EntityKind,
pub table: Option<String>,
pub name: String,
pub changes: HashMap<String, (String, String)>,
pub left: Option<SqliteEntity>,
pub right: Option<SqliteEntity>,
}
#[must_use]
pub fn diff_ddl(left: &SQLiteDDL, right: &SQLiteDDL) -> Vec<EntityDiff> {
let mut diffs = Vec::new();
diff_entity_type(
left.tables.list(),
right.tables.list(),
|t| t.name.to_string(),
|t| SqliteEntity::Table(t.clone()),
None,
EntityKind::Table,
&mut diffs,
);
diff_entity_type_with(
left.columns.list(),
right.columns.list(),
|c| format!("{}:{}", c.table, c.name),
|c| SqliteEntity::Column(c.clone()),
Some(&|c: &Column| c.table.to_string()),
EntityKind::Column,
columns_equivalent,
&mut diffs,
);
diff_entity_type(
left.indexes.list(),
right.indexes.list(),
|i| i.name.to_string(),
|i| SqliteEntity::Index(i.clone()),
Some(&|i: &Index| i.table.to_string()),
EntityKind::Index,
&mut diffs,
);
diff_entity_type_with(
left.fks.list(),
right.fks.list(),
|f| f.name.to_string(),
|f| SqliteEntity::ForeignKey(f.clone()),
Some(&|f: &ForeignKey| f.table.to_string()),
EntityKind::ForeignKey,
foreign_keys_equivalent,
&mut diffs,
);
diff_entity_type(
left.pks.list(),
right.pks.list(),
|p| p.table.to_string(),
|p| SqliteEntity::PrimaryKey(p.clone()),
Some(&|p: &PrimaryKey| p.table.to_string()),
EntityKind::PrimaryKey,
&mut diffs,
);
diff_entity_type(
left.uniques.list(),
right.uniques.list(),
|u| u.name.to_string(),
|u| SqliteEntity::UniqueConstraint(u.clone()),
Some(&|u: &UniqueConstraint| u.table.to_string()),
EntityKind::UniqueConstraint,
&mut diffs,
);
diff_entity_type(
left.checks.list(),
right.checks.list(),
|c| c.name.to_string(),
|c| SqliteEntity::CheckConstraint(c.clone()),
Some(&|c: &CheckConstraint| c.table.to_string()),
EntityKind::CheckConstraint,
&mut diffs,
);
diff_entity_type(
left.views.list(),
right.views.list(),
|v| v.name.to_string(),
|v| SqliteEntity::View(v.clone()),
None,
EntityKind::View,
&mut diffs,
);
diffs
}
fn diff_entity_type<T: Clone + PartialEq>(
left: &[T],
right: &[T],
key_fn: impl Fn(&T) -> String,
to_entity: impl Fn(&T) -> SqliteEntity,
table_fn: Option<&dyn Fn(&T) -> String>,
kind: EntityKind,
diffs: &mut Vec<EntityDiff>,
) {
diff_entity_type_with(
left,
right,
key_fn,
to_entity,
table_fn,
kind,
PartialEq::eq,
diffs,
);
}
#[allow(clippy::too_many_arguments)]
fn diff_entity_type_with<T: Clone>(
left: &[T],
right: &[T],
key_fn: impl Fn(&T) -> String,
to_entity: impl Fn(&T) -> SqliteEntity,
table_fn: Option<&dyn Fn(&T) -> String>,
kind: EntityKind,
equivalent: impl Fn(&T, &T) -> bool,
diffs: &mut Vec<EntityDiff>,
) {
let left_map: HashMap<String, &T> = left.iter().map(|e| (key_fn(e), e)).collect();
let right_map: HashMap<String, &T> = right.iter().map(|e| (key_fn(e), e)).collect();
for left_entity in left {
let key = key_fn(left_entity);
if !right_map.contains_key(&key) {
diffs.push(EntityDiff {
diff_type: DiffType::Drop,
kind,
table: table_fn.map(|f| f(left_entity)),
name: key,
changes: HashMap::new(),
left: Some(to_entity(left_entity)),
right: None,
});
}
}
for right_entity in right {
let key = key_fn(right_entity);
if !left_map.contains_key(&key) {
diffs.push(EntityDiff {
diff_type: DiffType::Create,
kind,
table: table_fn.map(|f| f(right_entity)),
name: key,
changes: HashMap::new(),
left: None,
right: Some(to_entity(right_entity)),
});
}
}
for left_entity in left {
let key = key_fn(left_entity);
if let Some(right_entity) = right_map.get(&key)
&& !equivalent(left_entity, right_entity)
{
diffs.push(EntityDiff {
diff_type: DiffType::Alter,
kind,
table: table_fn.map(|f| f(right_entity)),
name: key,
changes: HashMap::new(), left: Some(to_entity(left_entity)),
right: Some(to_entity(right_entity)),
});
}
}
}
fn columns_equivalent(left: &Column, right: &Column) -> bool {
let mut left = left.clone();
let mut right = right.clone();
left.sql_type = Cow::Owned(left.sql_type.to_ascii_lowercase());
right.sql_type = Cow::Owned(right.sql_type.to_ascii_lowercase());
left == right
}
fn normalize_fk_action(action: &Option<Cow<'static, str>>) -> Option<Cow<'static, str>> {
match action.as_deref() {
None => None,
Some(action) if action.eq_ignore_ascii_case("NO ACTION") => None,
Some(action) => Some(Cow::Owned(action.to_ascii_uppercase())),
}
}
fn foreign_keys_equivalent(left: &ForeignKey, right: &ForeignKey) -> bool {
let mut left = left.clone();
let mut right = right.clone();
left.on_delete = normalize_fk_action(&left.on_delete);
left.on_update = normalize_fk_action(&left.on_update);
right.on_delete = normalize_fk_action(&right.on_delete);
right.on_update = normalize_fk_action(&right.on_update);
left == right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ddl_collection_push() {
let mut ddl = SQLiteDDL::new();
ddl.tables.push(Table::new("users"));
ddl.columns.push(Column::new("users", "id", "integer"));
ddl.columns.push(Column::new("users", "name", "text"));
assert_eq!(ddl.tables.len(), 1);
assert_eq!(ddl.columns.len(), 2);
assert_eq!(ddl.columns.for_table("users").len(), 2);
}
#[test]
fn test_ddl_to_entities() {
let mut ddl = SQLiteDDL::new();
ddl.tables.push(Table::new("users"));
ddl.columns
.push(Column::new("users", "id", "integer").not_null());
let entities = ddl.to_entities();
assert_eq!(entities.len(), 2);
}
#[test]
fn test_diff_create() {
let left = SQLiteDDL::new();
let mut right = SQLiteDDL::new();
right.tables.push(Table::new("users"));
let diffs = diff_ddl(&left, &right);
assert_eq!(diffs.len(), 1);
assert_eq!(diffs[0].diff_type, DiffType::Create);
assert_eq!(diffs[0].kind, EntityKind::Table);
}
#[test]
fn test_diff_drop() {
let mut left = SQLiteDDL::new();
left.tables.push(Table::new("users"));
let right = SQLiteDDL::new();
let diffs = diff_ddl(&left, &right);
assert_eq!(diffs.len(), 1);
assert_eq!(diffs[0].diff_type, DiffType::Drop);
}
#[test]
fn introspected_types_and_no_action_fks_match_macro_snapshots() {
let mut introspected = SQLiteDDL::new();
introspected.tables.push(Table::new("child"));
introspected.tables.push(Table::new("parent"));
introspected
.columns
.push(Column::new("child", "id", "integer").not_null());
introspected
.columns
.push(Column::new("child", "parent_id", "integer").not_null());
introspected.fks.push(
ForeignKey::from_strings(
"child".to_string(),
"child_parent_id_fk".to_string(),
vec!["parent_id".to_string()],
"parent".to_string(),
vec!["id".to_string()],
)
.on_delete("NO ACTION")
.on_update("no action"),
);
let mut macro_snapshot = SQLiteDDL::new();
macro_snapshot.tables.push(Table::new("child"));
macro_snapshot.tables.push(Table::new("parent"));
macro_snapshot
.columns
.push(Column::new("child", "id", "INTEGER").not_null());
macro_snapshot
.columns
.push(Column::new("child", "parent_id", "INTEGER").not_null());
macro_snapshot.fks.push(ForeignKey::from_strings(
"child".to_string(),
"child_parent_id_fk".to_string(),
vec!["parent_id".to_string()],
"parent".to_string(),
vec!["id".to_string()],
));
let diffs = diff_ddl(&introspected, ¯o_snapshot);
assert!(diffs.is_empty(), "unexpected diffs: {diffs:#?}");
}
}