vespertide_core/schema/
constraint.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use crate::schema::{
5    ReferenceAction,
6    names::{ColumnName, TableName},
7};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
10#[serde(rename_all = "snake_case", tag = "type")]
11pub enum TableConstraint {
12    PrimaryKey {
13        #[serde(default)]
14        auto_increment: bool,
15        columns: Vec<ColumnName>,
16    },
17    Unique {
18        #[serde(skip_serializing_if = "Option::is_none")]
19        name: Option<String>,
20        columns: Vec<ColumnName>,
21    },
22    ForeignKey {
23        #[serde(skip_serializing_if = "Option::is_none")]
24        name: Option<String>,
25        columns: Vec<ColumnName>,
26        ref_table: TableName,
27        ref_columns: Vec<ColumnName>,
28        on_delete: Option<ReferenceAction>,
29        on_update: Option<ReferenceAction>,
30    },
31    Check {
32        name: String,
33        expr: String,
34    },
35    Index {
36        #[serde(skip_serializing_if = "Option::is_none")]
37        name: Option<String>,
38        columns: Vec<ColumnName>,
39    },
40}
41
42impl TableConstraint {
43    /// Returns the columns referenced by this constraint.
44    /// For Check constraints, returns an empty slice (expression-based, not column-based).
45    pub fn columns(&self) -> &[ColumnName] {
46        match self {
47            TableConstraint::PrimaryKey { columns, .. } => columns,
48            TableConstraint::Unique { columns, .. } => columns,
49            TableConstraint::ForeignKey { columns, .. } => columns,
50            TableConstraint::Index { columns, .. } => columns,
51            TableConstraint::Check { .. } => &[],
52        }
53    }
54
55    /// Apply a prefix to referenced table names in this constraint.
56    /// Only affects ForeignKey constraints (which reference other tables).
57    pub fn with_prefix(self, prefix: &str) -> Self {
58        if prefix.is_empty() {
59            return self;
60        }
61        match self {
62            TableConstraint::ForeignKey {
63                name,
64                columns,
65                ref_table,
66                ref_columns,
67                on_delete,
68                on_update,
69            } => TableConstraint::ForeignKey {
70                name,
71                columns,
72                ref_table: format!("{}{}", prefix, ref_table),
73                ref_columns,
74                on_delete,
75                on_update,
76            },
77            // Other constraints don't reference external tables
78            other => other,
79        }
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_columns_primary_key() {
89        let pk = TableConstraint::PrimaryKey {
90            auto_increment: false,
91            columns: vec!["id".into(), "tenant_id".into()],
92        };
93        assert_eq!(pk.columns().len(), 2);
94        assert_eq!(pk.columns()[0], "id");
95        assert_eq!(pk.columns()[1], "tenant_id");
96    }
97
98    #[test]
99    fn test_columns_unique() {
100        let unique = TableConstraint::Unique {
101            name: Some("uq_email".into()),
102            columns: vec!["email".into()],
103        };
104        assert_eq!(unique.columns().len(), 1);
105        assert_eq!(unique.columns()[0], "email");
106    }
107
108    #[test]
109    fn test_columns_foreign_key() {
110        let fk = TableConstraint::ForeignKey {
111            name: Some("fk_user".into()),
112            columns: vec!["user_id".into()],
113            ref_table: "users".into(),
114            ref_columns: vec!["id".into()],
115            on_delete: None,
116            on_update: None,
117        };
118        assert_eq!(fk.columns().len(), 1);
119        assert_eq!(fk.columns()[0], "user_id");
120    }
121
122    #[test]
123    fn test_columns_index() {
124        let idx = TableConstraint::Index {
125            name: Some("ix_created_at".into()),
126            columns: vec!["created_at".into()],
127        };
128        assert_eq!(idx.columns().len(), 1);
129        assert_eq!(idx.columns()[0], "created_at");
130    }
131
132    #[test]
133    fn test_columns_check_returns_empty() {
134        let check = TableConstraint::Check {
135            name: "check_positive".into(),
136            expr: "amount > 0".into(),
137        };
138        assert!(check.columns().is_empty());
139    }
140
141    #[test]
142    fn test_with_prefix_foreign_key() {
143        let fk = TableConstraint::ForeignKey {
144            name: Some("fk_user".into()),
145            columns: vec!["user_id".into()],
146            ref_table: "users".into(),
147            ref_columns: vec!["id".into()],
148            on_delete: None,
149            on_update: None,
150        };
151        let prefixed = fk.with_prefix("myapp_");
152        if let TableConstraint::ForeignKey { ref_table, .. } = prefixed {
153            assert_eq!(ref_table.as_str(), "myapp_users");
154        } else {
155            panic!("Expected ForeignKey");
156        }
157    }
158
159    #[test]
160    fn test_with_prefix_non_fk_unchanged() {
161        let pk = TableConstraint::PrimaryKey {
162            auto_increment: false,
163            columns: vec!["id".into()],
164        };
165        let prefixed = pk.clone().with_prefix("myapp_");
166        assert_eq!(pk, prefixed);
167
168        let unique = TableConstraint::Unique {
169            name: Some("uq_email".into()),
170            columns: vec!["email".into()],
171        };
172        let prefixed = unique.clone().with_prefix("myapp_");
173        assert_eq!(unique, prefixed);
174
175        let idx = TableConstraint::Index {
176            name: Some("ix_created_at".into()),
177            columns: vec!["created_at".into()],
178        };
179        let prefixed = idx.clone().with_prefix("myapp_");
180        assert_eq!(idx, prefixed);
181
182        let check = TableConstraint::Check {
183            name: "check_positive".into(),
184            expr: "amount > 0".into(),
185        };
186        let prefixed = check.clone().with_prefix("myapp_");
187        assert_eq!(check, prefixed);
188    }
189
190    #[test]
191    fn test_with_prefix_empty_prefix() {
192        let fk = TableConstraint::ForeignKey {
193            name: Some("fk_user".into()),
194            columns: vec!["user_id".into()],
195            ref_table: "users".into(),
196            ref_columns: vec!["id".into()],
197            on_delete: None,
198            on_update: None,
199        };
200        let prefixed = fk.clone().with_prefix("");
201        assert_eq!(fk, prefixed);
202    }
203}