use serde::{Deserialize, Serialize};
use crate::schema::{
ReferenceAction,
check_violation_strategy::CheckViolationStrategy,
fk_orphan_strategy::ForeignKeyOrphanStrategy,
names::{ColumnName, TableName},
pk_addition_strategy::PrimaryKeyAdditionStrategy,
unique_strategy::{KeepPolicy, UniqueConstraintStrategy},
};
fn is_default_unique_strategy(s: &UniqueConstraintStrategy) -> bool {
matches!(
s,
UniqueConstraintStrategy::DeleteDuplicates {
keep: KeepPolicy::First
}
)
}
#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "serde `skip_serializing_if` callbacks must have signature `fn(&T) -> bool`"
)]
fn is_default_fk_orphan_strategy(s: &ForeignKeyOrphanStrategy) -> bool {
matches!(s, ForeignKeyOrphanStrategy::NullifyOrphans)
}
fn is_default_check_violation_strategy(s: &CheckViolationStrategy) -> bool {
matches!(s, CheckViolationStrategy::DeleteViolatingRows)
}
fn is_default_pk_addition_strategy(s: &PrimaryKeyAdditionStrategy) -> bool {
matches!(
s,
PrimaryKeyAdditionStrategy::DeleteDuplicates {
keep: KeepPolicy::First
}
)
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case", tag = "type")]
#[non_exhaustive]
pub enum TableConstraint {
PrimaryKey {
#[serde(default)]
auto_increment: bool,
columns: Vec<ColumnName>,
#[serde(default, skip_serializing_if = "is_default_pk_addition_strategy")]
strategy: PrimaryKeyAdditionStrategy,
},
Unique {
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
columns: Vec<ColumnName>,
#[serde(default, skip_serializing_if = "is_default_unique_strategy")]
strategy: UniqueConstraintStrategy,
},
ForeignKey {
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
columns: Vec<ColumnName>,
ref_table: TableName,
ref_columns: Vec<ColumnName>,
on_delete: Option<ReferenceAction>,
on_update: Option<ReferenceAction>,
#[serde(default, skip_serializing_if = "is_default_fk_orphan_strategy")]
orphan_strategy: ForeignKeyOrphanStrategy,
},
Check {
name: String,
expr: String,
#[serde(default, skip_serializing_if = "is_default_check_violation_strategy")]
strategy: CheckViolationStrategy,
},
Index {
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
columns: Vec<ColumnName>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ConstraintKind {
PrimaryKey,
ForeignKey,
Unique,
Check,
Index,
}
impl TableConstraint {
#[must_use]
pub fn kind(&self) -> ConstraintKind {
match self {
TableConstraint::PrimaryKey { .. } => ConstraintKind::PrimaryKey,
TableConstraint::ForeignKey { .. } => ConstraintKind::ForeignKey,
TableConstraint::Unique { .. } => ConstraintKind::Unique,
TableConstraint::Check { .. } => ConstraintKind::Check,
TableConstraint::Index { .. } => ConstraintKind::Index,
}
}
pub fn columns(&self) -> &[ColumnName] {
match self {
TableConstraint::PrimaryKey { columns, .. }
| TableConstraint::Unique { columns, .. }
| TableConstraint::ForeignKey { columns, .. }
| TableConstraint::Index { columns, .. } => columns,
TableConstraint::Check { .. } => &[],
}
}
pub fn with_prefix(self, prefix: &str) -> Self {
if prefix.is_empty() {
return self;
}
match self {
TableConstraint::ForeignKey {
name,
columns,
ref_table,
ref_columns,
on_delete,
on_update,
orphan_strategy,
} => TableConstraint::ForeignKey {
name,
columns,
ref_table: format!("{prefix}{ref_table}").into(),
ref_columns,
on_delete,
on_update,
orphan_strategy,
},
other => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_columns_primary_key() {
let pk = TableConstraint::PrimaryKey {
auto_increment: false,
columns: vec!["id".into(), "tenant_id".into()],
strategy: PrimaryKeyAdditionStrategy::default(),
};
assert_eq!(pk.columns().len(), 2);
assert_eq!(pk.columns()[0], "id");
assert_eq!(pk.columns()[1], "tenant_id");
}
#[test]
fn test_columns_unique() {
let unique = TableConstraint::Unique {
name: Some("uq_email".into()),
columns: vec!["email".into()],
strategy: crate::schema::UniqueConstraintStrategy::DeleteDuplicates {
keep: crate::schema::KeepPolicy::First,
},
};
assert_eq!(unique.columns().len(), 1);
assert_eq!(unique.columns()[0], "email");
}
#[test]
fn test_columns_foreign_key() {
let fk = TableConstraint::ForeignKey {
name: Some("fk_user".into()),
columns: vec!["user_id".into()],
ref_table: "users".into(),
ref_columns: vec!["id".into()],
on_delete: None,
on_update: None,
orphan_strategy: crate::ForeignKeyOrphanStrategy::default(),
};
assert_eq!(fk.columns().len(), 1);
assert_eq!(fk.columns()[0], "user_id");
}
#[test]
fn test_columns_index() {
let idx = TableConstraint::Index {
name: Some("ix_created_at".into()),
columns: vec!["created_at".into()],
};
assert_eq!(idx.columns().len(), 1);
assert_eq!(idx.columns()[0], "created_at");
}
#[test]
fn test_columns_check_returns_empty() {
let check = TableConstraint::Check {
name: "check_positive".into(),
expr: "amount > 0".into(),
strategy: crate::CheckViolationStrategy::default(),
};
assert!(check.columns().is_empty());
}
#[test]
fn test_kind() {
let constraints = [
(
TableConstraint::PrimaryKey {
auto_increment: false,
columns: vec!["id".into()],
strategy: PrimaryKeyAdditionStrategy::default(),
},
ConstraintKind::PrimaryKey,
),
(
TableConstraint::ForeignKey {
name: None,
columns: vec!["user_id".into()],
ref_table: "user".into(),
ref_columns: vec!["id".into()],
on_delete: None,
on_update: None,
orphan_strategy: crate::ForeignKeyOrphanStrategy::default(),
},
ConstraintKind::ForeignKey,
),
(
TableConstraint::Unique {
name: None,
columns: vec!["email".into()],
strategy: crate::schema::UniqueConstraintStrategy::DeleteDuplicates {
keep: crate::schema::KeepPolicy::First,
},
},
ConstraintKind::Unique,
),
(
TableConstraint::Check {
name: "check_positive".into(),
expr: "amount > 0".into(),
strategy: crate::CheckViolationStrategy::default(),
},
ConstraintKind::Check,
),
(
TableConstraint::Index {
name: None,
columns: vec!["email".into()],
},
ConstraintKind::Index,
),
];
for (constraint, expected) in constraints {
assert_eq!(constraint.kind(), expected);
}
}
#[test]
fn test_with_prefix_foreign_key() {
let fk = TableConstraint::ForeignKey {
name: Some("fk_user".into()),
columns: vec!["user_id".into()],
ref_table: "users".into(),
ref_columns: vec!["id".into()],
on_delete: None,
on_update: None,
orphan_strategy: crate::ForeignKeyOrphanStrategy::default(),
};
let prefixed = fk.with_prefix("myapp_");
if let TableConstraint::ForeignKey { ref_table, .. } = prefixed {
assert_eq!(ref_table.as_str(), "myapp_users");
} else {
panic!("Expected ForeignKey");
}
}
#[test]
fn test_with_prefix_non_fk_unchanged() {
let pk = TableConstraint::PrimaryKey {
auto_increment: false,
columns: vec!["id".into()],
strategy: PrimaryKeyAdditionStrategy::default(),
};
let prefixed = pk.clone().with_prefix("myapp_");
assert_eq!(pk, prefixed);
let unique = TableConstraint::Unique {
name: Some("uq_email".into()),
columns: vec!["email".into()],
strategy: crate::schema::UniqueConstraintStrategy::DeleteDuplicates {
keep: crate::schema::KeepPolicy::First,
},
};
let prefixed = unique.clone().with_prefix("myapp_");
assert_eq!(unique, prefixed);
let idx = TableConstraint::Index {
name: Some("ix_created_at".into()),
columns: vec!["created_at".into()],
};
let prefixed = idx.clone().with_prefix("myapp_");
assert_eq!(idx, prefixed);
let check = TableConstraint::Check {
name: "check_positive".into(),
expr: "amount > 0".into(),
strategy: crate::CheckViolationStrategy::default(),
};
let prefixed = check.clone().with_prefix("myapp_");
assert_eq!(check, prefixed);
}
#[test]
fn test_with_prefix_empty_prefix() {
let fk = TableConstraint::ForeignKey {
name: Some("fk_user".into()),
columns: vec!["user_id".into()],
ref_table: "users".into(),
ref_columns: vec!["id".into()],
on_delete: None,
on_update: None,
orphan_strategy: crate::ForeignKeyOrphanStrategy::default(),
};
let prefixed = fk.clone().with_prefix("");
assert_eq!(fk, prefixed);
}
}