use crate::core::{inventory, FieldType, ModelEntry, ModelSchema, Relation};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct SchemaSnapshot {
pub tables: Vec<TableSnapshot>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub m2m_tables: Vec<M2MTableSnapshot>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub indexes: Vec<IndexSnapshot>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checks: Vec<CheckSnapshot>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CheckSnapshot {
pub name: String,
pub table: String,
pub expr: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IndexSnapshot {
pub name: String,
pub table: String,
pub columns: Vec<String>,
pub unique: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct M2MTableSnapshot {
pub through: String,
pub src_table: String,
pub src_col: String,
pub dst_table: String,
pub dst_col: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TableSnapshot {
pub name: String,
pub model: String,
pub fields: Vec<FieldSnapshot>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FieldSnapshot {
pub name: String,
pub column: String,
pub ty: String,
pub nullable: bool,
pub primary_key: bool,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub max_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub min: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub max: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub default: Option<String>,
#[serde(skip_serializing_if = "is_false", default)]
pub auto: bool,
#[serde(skip_serializing_if = "is_false", default)]
pub unique: bool,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub fk: Option<RelationSnapshot>,
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_false(v: &bool) -> bool {
!*v
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RelationSnapshot {
pub kind: String,
pub to: String,
pub on: String,
}
impl SchemaSnapshot {
#[must_use]
pub fn from_registry() -> Self {
let entries: Vec<&ModelEntry> = inventory::iter::<ModelEntry>.into_iter().collect();
let mut tables: Vec<TableSnapshot> =
entries.iter().map(|e| TableSnapshot::from_schema(e.schema)).collect();
tables.sort_by(|a, b| a.name.cmp(&b.name));
let m2m_tables = collect_m2m_tables(entries.iter().map(|e| e.schema));
let indexes = collect_indexes(entries.iter().map(|e| e.schema));
let checks = collect_checks(entries.iter().map(|e| e.schema));
Self { tables, m2m_tables, indexes, checks }
}
#[must_use]
pub fn from_registry_for_app(app: &str) -> Self {
let entries: Vec<&ModelEntry> = inventory::iter::<ModelEntry>
.into_iter()
.filter(|e| e.resolved_app_label() == Some(app))
.collect();
let mut tables: Vec<TableSnapshot> =
entries.iter().map(|e| TableSnapshot::from_schema(e.schema)).collect();
tables.sort_by(|a, b| a.name.cmp(&b.name));
let m2m_tables = collect_m2m_tables(entries.iter().map(|e| e.schema));
let indexes = collect_indexes(entries.iter().map(|e| e.schema));
let checks = collect_checks(entries.iter().map(|e| e.schema));
Self { tables, m2m_tables, indexes, checks }
}
#[must_use]
pub fn from_models(models: &[&ModelSchema]) -> Self {
let mut tables: Vec<TableSnapshot> =
models.iter().map(|s| TableSnapshot::from_schema(s)).collect();
tables.sort_by(|a, b| a.name.cmp(&b.name));
let m2m_tables = collect_m2m_tables(models.iter().copied());
let indexes = collect_indexes(models.iter().copied());
let checks = collect_checks(models.iter().copied());
Self { tables, m2m_tables, indexes, checks }
}
#[must_use]
pub fn m2m_table(&self, through: &str) -> Option<&M2MTableSnapshot> {
self.m2m_tables.iter().find(|t| t.through == through)
}
#[must_use]
pub fn index(&self, name: &str) -> Option<&IndexSnapshot> {
self.indexes.iter().find(|i| i.name == name)
}
#[must_use]
pub fn check(&self, name: &str) -> Option<&CheckSnapshot> {
self.checks.iter().find(|c| c.name == name)
}
#[must_use]
pub fn table(&self, name: &str) -> Option<&TableSnapshot> {
self.tables.iter().find(|t| t.name == name)
}
}
impl TableSnapshot {
#[must_use]
pub fn from_schema(s: &ModelSchema) -> Self {
let mut fields: Vec<FieldSnapshot> =
s.scalar_fields().map(FieldSnapshot::from_schema).collect();
fields.sort_by(|a, b| a.column.cmp(&b.column));
Self {
name: s.table.to_owned(),
model: s.name.to_owned(),
fields,
}
}
#[must_use]
pub fn field(&self, column: &str) -> Option<&FieldSnapshot> {
self.fields.iter().find(|f| f.column == column)
}
}
impl FieldSnapshot {
fn from_schema(f: &crate::core::FieldSchema) -> Self {
let fk = f.relation.and_then(|r| match r {
Relation::Fk { to, on } => Some(RelationSnapshot {
kind: "fk".into(),
to: to.to_owned(),
on: on.to_owned(),
}),
Relation::O2O { to, on } => Some(RelationSnapshot {
kind: "o2o".into(),
to: to.to_owned(),
on: on.to_owned(),
}),
});
Self {
name: f.name.to_owned(),
column: f.column.to_owned(),
ty: field_type_name(f.ty).to_owned(),
nullable: f.nullable,
primary_key: f.primary_key,
max_length: f.max_length,
min: f.min,
max: f.max,
default: f.default.map(str::to_owned),
auto: f.auto,
unique: f.unique,
fk,
}
}
}
fn field_type_name(ty: FieldType) -> &'static str {
match ty {
FieldType::I32 => "i32",
FieldType::I64 => "i64",
FieldType::F32 => "f32",
FieldType::F64 => "f64",
FieldType::Bool => "bool",
FieldType::String => "string",
FieldType::DateTime => "datetime",
FieldType::Date => "date",
FieldType::Uuid => "uuid",
FieldType::Json => "json",
}
}
fn collect_checks<'a>(schemas: impl Iterator<Item = &'a ModelSchema>) -> Vec<CheckSnapshot> {
let mut seen = std::collections::HashSet::new();
let mut out: Vec<CheckSnapshot> = Vec::new();
for schema in schemas {
for c in schema.check_constraints {
if seen.insert(c.name) {
out.push(CheckSnapshot {
name: c.name.to_owned(),
table: schema.table.to_owned(),
expr: c.expr.to_owned(),
});
}
}
}
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
fn collect_indexes<'a>(schemas: impl Iterator<Item = &'a ModelSchema>) -> Vec<IndexSnapshot> {
let mut seen = std::collections::HashSet::new();
let mut out: Vec<IndexSnapshot> = Vec::new();
for schema in schemas {
for idx in schema.indexes {
if seen.insert(idx.name) {
out.push(IndexSnapshot {
name: idx.name.to_owned(),
table: schema.table.to_owned(),
columns: idx.columns.iter().map(|&c| c.to_owned()).collect(),
unique: idx.unique,
});
}
}
}
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
fn collect_m2m_tables<'a>(
schemas: impl Iterator<Item = &'a ModelSchema>,
) -> Vec<M2MTableSnapshot> {
let mut seen = std::collections::HashSet::new();
let mut out: Vec<M2MTableSnapshot> = Vec::new();
for schema in schemas {
for rel in schema.m2m {
if seen.insert(rel.through) {
out.push(M2MTableSnapshot {
through: rel.through.to_owned(),
src_table: schema.table.to_owned(),
src_col: rel.src_col.to_owned(),
dst_table: rel.to.to_owned(),
dst_col: rel.dst_col.to_owned(),
});
}
}
}
out.sort_by(|a, b| a.through.cmp(&b.through));
out
}