use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const SNAPSHOT_FORMAT_VERSION: &str = "1";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AppliedSchema {
pub djogi_version: String,
pub enums: BTreeMap<String, EnumSchema>,
pub format_version: String,
pub generated_at: String,
pub indexes: Vec<IndexSchema>,
pub models: BTreeMap<String, TableSchema>,
pub registered_apps: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TableSchema {
pub app: Option<String>,
pub columns: Vec<ColumnSchema>,
#[serde(default)]
pub exclusion_constraints: Vec<ExclusionConstraintSchema>,
pub fts: Option<FtsSchema>,
pub is_through: bool,
pub moved_from_app: Option<String>,
pub partition: Option<PartitionSchema>,
pub primary_key: PrimaryKeySchema,
pub rationale: Option<String>,
pub renamed_from: Option<String>,
pub rls_enabled: bool,
#[serde(default)]
pub storage_params: Option<String>,
pub table: String,
#[serde(default)]
pub table_comment: Option<String>,
#[serde(default)]
pub tablespace: Option<String>,
pub tenant_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ColumnSchema {
pub check: Option<String>,
#[serde(default)]
pub comment: Option<String>,
pub default_sql: Option<String>,
pub foreign_key: Option<ForeignKeySchema>,
#[serde(default)]
pub generated: Option<GeneratedColumnSchema>,
#[serde(default)]
pub identity: Option<IdentityKindSchema>,
pub index_type: Option<IndexTypeSchema>,
pub indexed: bool,
pub max_length: Option<u32>,
pub name: String,
pub nullable: bool,
pub on_delete: Option<OnDeleteSchema>,
pub outbox_exclude: bool,
pub rationale: Option<String>,
pub relation_kind: Option<RelationKindSchema>,
pub renamed_from: Option<String>,
pub sequence_within: Option<String>,
pub sql_type: String,
pub unique: bool,
#[serde(default, skip)]
pub type_change_using: Option<String>,
}
impl PartialEq for ColumnSchema {
fn eq(&self, other: &Self) -> bool {
let ColumnSchema {
check: self_check,
comment: self_comment,
default_sql: self_default_sql,
foreign_key: self_foreign_key,
generated: self_generated,
identity: self_identity,
index_type: self_index_type,
indexed: self_indexed,
max_length: self_max_length,
name: self_name,
nullable: self_nullable,
on_delete: self_on_delete,
outbox_exclude: self_outbox_exclude,
rationale: self_rationale,
relation_kind: self_relation_kind,
renamed_from: self_renamed_from,
sequence_within: self_sequence_within,
sql_type: self_sql_type,
unique: self_unique,
type_change_using: _,
} = self;
let ColumnSchema {
check: other_check,
comment: other_comment,
default_sql: other_default_sql,
foreign_key: other_foreign_key,
generated: other_generated,
identity: other_identity,
index_type: other_index_type,
indexed: other_indexed,
max_length: other_max_length,
name: other_name,
nullable: other_nullable,
on_delete: other_on_delete,
outbox_exclude: other_outbox_exclude,
rationale: other_rationale,
relation_kind: other_relation_kind,
renamed_from: other_renamed_from,
sequence_within: other_sequence_within,
sql_type: other_sql_type,
unique: other_unique,
type_change_using: _,
} = other;
self_check == other_check
&& self_comment == other_comment
&& self_default_sql == other_default_sql
&& self_foreign_key == other_foreign_key
&& self_generated == other_generated
&& self_identity == other_identity
&& self_index_type == other_index_type
&& self_indexed == other_indexed
&& self_max_length == other_max_length
&& self_name == other_name
&& self_nullable == other_nullable
&& self_on_delete == other_on_delete
&& self_outbox_exclude == other_outbox_exclude
&& self_rationale == other_rationale
&& self_relation_kind == other_relation_kind
&& self_renamed_from == other_renamed_from
&& self_sequence_within == other_sequence_within
&& self_sql_type == other_sql_type
&& self_unique == other_unique
}
}
impl Eq for ColumnSchema {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ForeignKeySchema {
#[serde(default)]
pub deferrable: bool,
#[serde(default)]
pub initially_deferred: bool,
pub on_delete: OnDeleteSchema,
pub ref_column: String,
pub ref_table: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OnDeleteSchema {
Restrict,
Cascade,
SetNull,
SetDefault,
NoAction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RelationKindSchema {
ForeignKey,
OneToOne,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IdentityKindSchema {
ByDefault,
Always,
}
impl IdentityKindSchema {
pub fn sql_clause(self) -> &'static str {
match self {
Self::ByDefault => "GENERATED BY DEFAULT AS IDENTITY",
Self::Always => "GENERATED ALWAYS AS IDENTITY",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GeneratedColumnSchema {
pub expression: String,
pub stored: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExclusionElementSchema {
pub expr: String,
pub with_operator: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExclusionConstraintSchema {
#[serde(default)]
pub deferrable: bool,
pub elements: Vec<ExclusionElementSchema>,
#[serde(default)]
pub extension_dependency: Option<String>,
#[serde(default)]
pub initially_deferred: bool,
pub name: String,
pub using: String,
pub where_clause: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NamedNotNullConstraintSchema {
pub name: String,
pub column: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TemporalPrimaryKeyConstraintSchema {
pub name: String,
pub columns: Vec<String>,
pub without_overlaps_column: String,
#[serde(default)]
pub include: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PeriodForeignKeyConstraintSchema {
pub name: String,
pub columns: Vec<String>,
pub period_column: String,
pub ref_table: String,
pub ref_columns: Vec<String>,
pub ref_period_column: String,
#[serde(default)]
pub deferrable: bool,
#[serde(default)]
pub initially_deferred: bool,
#[serde(default = "default_true")]
pub enforced: bool,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IndexTypeSchema {
BTree,
Gin,
Gist,
Hash,
Spgist,
Brin,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IndexColumnSchema {
pub name: String,
pub nulls: IndexNullsOrderSchema,
pub opclass: Option<String>,
pub order: IndexOrderSchema,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IndexOrderSchema {
Asc,
Desc,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IndexNullsOrderSchema {
Default,
First,
Last,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum IndexTargetSchema {
Columns(Vec<IndexColumnSchema>),
Expression(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IndexKindSchema {
NonUnique,
UniqueConstraint,
UniqueIndex,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IndexSchema {
pub extension_dependency: Option<String>,
pub include: Vec<String>,
pub index_type: IndexTypeSchema,
pub kind: IndexKindSchema,
pub name: String,
pub nulls_not_distinct: bool,
pub predicate: Option<String>,
pub requires_out_of_transaction: bool,
pub table: String,
pub target: IndexTargetSchema,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PartitionSchema {
Range { column: String },
Hash { column: String, partitions: u16 },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FtsSchema {
pub column: String,
pub dictionary: String,
pub source: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PrimaryKeySchema {
pub columns: Vec<String>,
pub kind: PkKindSchema,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PkKindSchema {
HeerId,
HeerIdRecencyBiased,
RanjId,
RanjIdRecencyBiased,
Serial,
Composite,
None,
Custom(CustomPkKindSchema),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CustomPkKindSchema {
pub default_sql: String,
pub sql_type: String,
pub type_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EnumSchema {
pub name: String,
pub variants: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum OnlineSafetyClassification {
OnlineSafe,
FastLockDestructiveGuarded,
ExpandContract,
OfflineOnly,
}
#[cfg(test)]
mod column_schema_type_change_using_tests {
use super::ColumnSchema;
fn base_column(name: &str) -> ColumnSchema {
ColumnSchema {
check: None,
comment: None,
default_sql: None,
foreign_key: None,
generated: None,
identity: None,
index_type: None,
indexed: false,
max_length: None,
name: name.to_string(),
nullable: false,
on_delete: None,
outbox_exclude: false,
rationale: None,
relation_kind: None,
renamed_from: None,
sequence_within: None,
sql_type: "TEXT".to_string(),
unique: false,
type_change_using: None,
}
}
#[test]
fn type_change_using_is_excluded_from_partial_eq() {
let snapshot_loaded = base_column("kind");
let projected_from_descriptor = ColumnSchema {
type_change_using: Some("kind::uuid".to_string()),
..base_column("kind")
};
assert_eq!(snapshot_loaded, projected_from_descriptor);
assert_ne!(
snapshot_loaded.type_change_using,
projected_from_descriptor.type_change_using
);
}
#[test]
fn structural_difference_still_triggers_partial_eq_inequality() {
let before = base_column("kind");
let after = ColumnSchema {
sql_type: "UUID".to_string(),
type_change_using: Some("kind::uuid".to_string()),
..base_column("kind")
};
assert_ne!(
before, after,
"sql_type difference must trip PartialEq even when type_change_using is set"
);
}
#[test]
fn type_change_using_is_dropped_on_serialize() {
let with_using = ColumnSchema {
type_change_using: Some("kind::uuid".to_string()),
..base_column("kind")
};
let json = serde_json::to_string(&with_using).expect("serialize");
assert!(
!json.contains("type_change_using"),
"serialized JSON must not contain the transient slot: {json}"
);
let round_tripped: ColumnSchema = serde_json::from_str(&json).expect("deserialize");
assert!(
round_tripped.type_change_using.is_none(),
"deserialize must yield None for the skipped slot: {round_tripped:?}"
);
}
#[test]
fn type_change_using_deserialize_defaults_when_absent() {
let json = serde_json::to_string(&base_column("kind")).expect("serialize");
let loaded: ColumnSchema = serde_json::from_str(&json).expect("deserialize");
assert!(loaded.type_change_using.is_none());
}
}
#[cfg(test)]
mod online_safety_classification_tests {
use super::OnlineSafetyClassification;
#[test]
fn online_safety_classification_has_four_distinct_variants() {
let all = [
OnlineSafetyClassification::OnlineSafe,
OnlineSafetyClassification::FastLockDestructiveGuarded,
OnlineSafetyClassification::ExpandContract,
OnlineSafetyClassification::OfflineOnly,
];
for (i, lhs) in all.iter().enumerate() {
for (j, rhs) in all.iter().enumerate() {
if i == j {
assert_eq!(lhs, rhs);
} else {
assert_ne!(lhs, rhs, "variants at {i} and {j} must differ");
}
}
}
}
#[test]
fn only_expand_contract_routes_to_live_plan() {
let routes_to_live_plan = |c: OnlineSafetyClassification| -> bool {
matches!(c, OnlineSafetyClassification::ExpandContract)
};
assert!(routes_to_live_plan(
OnlineSafetyClassification::ExpandContract
));
assert!(!routes_to_live_plan(OnlineSafetyClassification::OnlineSafe));
assert!(!routes_to_live_plan(
OnlineSafetyClassification::FastLockDestructiveGuarded
));
assert!(!routes_to_live_plan(
OnlineSafetyClassification::OfflineOnly
));
}
}