1use serde::{Deserialize, Serialize};
2
3use crate::schema::{
4 ReferenceAction,
5 check_violation_strategy::CheckViolationStrategy,
6 fk_orphan_strategy::ForeignKeyOrphanStrategy,
7 names::{ColumnName, TableName},
8 pk_addition_strategy::PrimaryKeyAdditionStrategy,
9 unique_strategy::{KeepPolicy, UniqueConstraintStrategy},
10};
11
12fn is_default_unique_strategy(s: &UniqueConstraintStrategy) -> bool {
16 matches!(
17 s,
18 UniqueConstraintStrategy::DeleteDuplicates {
19 keep: KeepPolicy::First
20 }
21 )
22}
23
24#[expect(
28 clippy::trivially_copy_pass_by_ref,
29 reason = "serde `skip_serializing_if` callbacks must have signature `fn(&T) -> bool`"
30)]
31fn is_default_fk_orphan_strategy(s: &ForeignKeyOrphanStrategy) -> bool {
32 matches!(s, ForeignKeyOrphanStrategy::NullifyOrphans)
33}
34
35fn is_default_check_violation_strategy(s: &CheckViolationStrategy) -> bool {
39 matches!(s, CheckViolationStrategy::DeleteViolatingRows)
40}
41
42fn is_default_pk_addition_strategy(s: &PrimaryKeyAdditionStrategy) -> bool {
46 matches!(
47 s,
48 PrimaryKeyAdditionStrategy::DeleteDuplicates {
49 keep: KeepPolicy::First
50 }
51 )
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
65#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
66#[serde(rename_all = "snake_case", tag = "type")]
67#[non_exhaustive]
68pub enum TableConstraint {
69 PrimaryKey {
83 #[serde(default)]
84 auto_increment: bool,
85 columns: Vec<ColumnName>,
86 #[serde(default, skip_serializing_if = "is_default_pk_addition_strategy")]
87 strategy: PrimaryKeyAdditionStrategy,
88 },
89 Unique {
104 #[serde(skip_serializing_if = "Option::is_none")]
105 name: Option<String>,
106 columns: Vec<ColumnName>,
107 #[serde(default, skip_serializing_if = "is_default_unique_strategy")]
108 strategy: UniqueConstraintStrategy,
109 },
110 ForeignKey {
123 #[serde(skip_serializing_if = "Option::is_none")]
124 name: Option<String>,
125 columns: Vec<ColumnName>,
126 ref_table: TableName,
127 ref_columns: Vec<ColumnName>,
128 on_delete: Option<ReferenceAction>,
129 on_update: Option<ReferenceAction>,
130 #[serde(default, skip_serializing_if = "is_default_fk_orphan_strategy")]
131 orphan_strategy: ForeignKeyOrphanStrategy,
132 },
133 Check {
150 name: String,
151 expr: String,
152 #[serde(default, skip_serializing_if = "is_default_check_violation_strategy")]
153 strategy: CheckViolationStrategy,
154 },
155 Index {
157 #[serde(skip_serializing_if = "Option::is_none")]
158 name: Option<String>,
159 columns: Vec<ColumnName>,
160 },
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
172#[serde(rename_all = "snake_case")]
173#[non_exhaustive]
174pub enum ConstraintKind {
175 PrimaryKey,
177 ForeignKey,
179 Unique,
181 Check,
183 Index,
185}
186
187impl TableConstraint {
188 #[must_use]
190 pub fn kind(&self) -> ConstraintKind {
191 match self {
192 TableConstraint::PrimaryKey { .. } => ConstraintKind::PrimaryKey,
193 TableConstraint::ForeignKey { .. } => ConstraintKind::ForeignKey,
194 TableConstraint::Unique { .. } => ConstraintKind::Unique,
195 TableConstraint::Check { .. } => ConstraintKind::Check,
196 TableConstraint::Index { .. } => ConstraintKind::Index,
197 }
198 }
199
200 pub fn columns(&self) -> &[ColumnName] {
203 match self {
204 TableConstraint::PrimaryKey { columns, .. }
205 | TableConstraint::Unique { columns, .. }
206 | TableConstraint::ForeignKey { columns, .. }
207 | TableConstraint::Index { columns, .. } => columns,
208 TableConstraint::Check { .. } => &[],
209 }
210 }
211
212 pub fn with_prefix(self, prefix: &str) -> Self {
215 if prefix.is_empty() {
216 return self;
217 }
218 match self {
219 TableConstraint::ForeignKey {
220 name,
221 columns,
222 ref_table,
223 ref_columns,
224 on_delete,
225 on_update,
226 orphan_strategy,
227 } => TableConstraint::ForeignKey {
228 name,
229 columns,
230 ref_table: format!("{prefix}{ref_table}").into(),
231 ref_columns,
232 on_delete,
233 on_update,
234 orphan_strategy,
235 },
236 other => other,
238 }
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn test_columns_primary_key() {
248 let pk = TableConstraint::PrimaryKey {
249 auto_increment: false,
250 columns: vec!["id".into(), "tenant_id".into()],
251 strategy: PrimaryKeyAdditionStrategy::default(),
252 };
253 assert_eq!(pk.columns().len(), 2);
254 assert_eq!(pk.columns()[0], "id");
255 assert_eq!(pk.columns()[1], "tenant_id");
256 }
257
258 #[test]
259 fn test_columns_unique() {
260 let unique = TableConstraint::Unique {
261 name: Some("uq_email".into()),
262 columns: vec!["email".into()],
263 strategy: crate::schema::UniqueConstraintStrategy::DeleteDuplicates {
264 keep: crate::schema::KeepPolicy::First,
265 },
266 };
267 assert_eq!(unique.columns().len(), 1);
268 assert_eq!(unique.columns()[0], "email");
269 }
270
271 #[test]
272 fn test_columns_foreign_key() {
273 let fk = TableConstraint::ForeignKey {
274 name: Some("fk_user".into()),
275 columns: vec!["user_id".into()],
276 ref_table: "users".into(),
277 ref_columns: vec!["id".into()],
278 on_delete: None,
279 on_update: None,
280 orphan_strategy: crate::ForeignKeyOrphanStrategy::default(),
281 };
282 assert_eq!(fk.columns().len(), 1);
283 assert_eq!(fk.columns()[0], "user_id");
284 }
285
286 #[test]
287 fn test_columns_index() {
288 let idx = TableConstraint::Index {
289 name: Some("ix_created_at".into()),
290 columns: vec!["created_at".into()],
291 };
292 assert_eq!(idx.columns().len(), 1);
293 assert_eq!(idx.columns()[0], "created_at");
294 }
295
296 #[test]
297 fn test_columns_check_returns_empty() {
298 let check = TableConstraint::Check {
299 name: "check_positive".into(),
300 expr: "amount > 0".into(),
301 strategy: crate::CheckViolationStrategy::default(),
302 };
303 assert!(check.columns().is_empty());
304 }
305
306 #[test]
307 fn test_kind() {
308 let constraints = [
309 (
310 TableConstraint::PrimaryKey {
311 auto_increment: false,
312 columns: vec!["id".into()],
313 strategy: PrimaryKeyAdditionStrategy::default(),
314 },
315 ConstraintKind::PrimaryKey,
316 ),
317 (
318 TableConstraint::ForeignKey {
319 name: None,
320 columns: vec!["user_id".into()],
321 ref_table: "user".into(),
322 ref_columns: vec!["id".into()],
323 on_delete: None,
324 on_update: None,
325 orphan_strategy: crate::ForeignKeyOrphanStrategy::default(),
326 },
327 ConstraintKind::ForeignKey,
328 ),
329 (
330 TableConstraint::Unique {
331 name: None,
332 columns: vec!["email".into()],
333 strategy: crate::schema::UniqueConstraintStrategy::DeleteDuplicates {
334 keep: crate::schema::KeepPolicy::First,
335 },
336 },
337 ConstraintKind::Unique,
338 ),
339 (
340 TableConstraint::Check {
341 name: "check_positive".into(),
342 expr: "amount > 0".into(),
343 strategy: crate::CheckViolationStrategy::default(),
344 },
345 ConstraintKind::Check,
346 ),
347 (
348 TableConstraint::Index {
349 name: None,
350 columns: vec!["email".into()],
351 },
352 ConstraintKind::Index,
353 ),
354 ];
355
356 for (constraint, expected) in constraints {
357 assert_eq!(constraint.kind(), expected);
358 }
359 }
360
361 #[test]
362 fn test_with_prefix_foreign_key() {
363 let fk = TableConstraint::ForeignKey {
364 name: Some("fk_user".into()),
365 columns: vec!["user_id".into()],
366 ref_table: "users".into(),
367 ref_columns: vec!["id".into()],
368 on_delete: None,
369 on_update: None,
370 orphan_strategy: crate::ForeignKeyOrphanStrategy::default(),
371 };
372 let prefixed = fk.with_prefix("myapp_");
373 if let TableConstraint::ForeignKey { ref_table, .. } = prefixed {
374 assert_eq!(ref_table.as_str(), "myapp_users");
375 } else {
376 panic!("Expected ForeignKey");
377 }
378 }
379
380 #[test]
381 fn test_with_prefix_non_fk_unchanged() {
382 let pk = TableConstraint::PrimaryKey {
383 auto_increment: false,
384 columns: vec!["id".into()],
385 strategy: PrimaryKeyAdditionStrategy::default(),
386 };
387 let prefixed = pk.clone().with_prefix("myapp_");
388 assert_eq!(pk, prefixed);
389
390 let unique = TableConstraint::Unique {
391 name: Some("uq_email".into()),
392 columns: vec!["email".into()],
393 strategy: crate::schema::UniqueConstraintStrategy::DeleteDuplicates {
394 keep: crate::schema::KeepPolicy::First,
395 },
396 };
397 let prefixed = unique.clone().with_prefix("myapp_");
398 assert_eq!(unique, prefixed);
399
400 let idx = TableConstraint::Index {
401 name: Some("ix_created_at".into()),
402 columns: vec!["created_at".into()],
403 };
404 let prefixed = idx.clone().with_prefix("myapp_");
405 assert_eq!(idx, prefixed);
406
407 let check = TableConstraint::Check {
408 name: "check_positive".into(),
409 expr: "amount > 0".into(),
410 strategy: crate::CheckViolationStrategy::default(),
411 };
412 let prefixed = check.clone().with_prefix("myapp_");
413 assert_eq!(check, prefixed);
414 }
415
416 #[test]
417 fn test_with_prefix_empty_prefix() {
418 let fk = TableConstraint::ForeignKey {
419 name: Some("fk_user".into()),
420 columns: vec!["user_id".into()],
421 ref_table: "users".into(),
422 ref_columns: vec!["id".into()],
423 on_delete: None,
424 on_update: None,
425 orphan_strategy: crate::ForeignKeyOrphanStrategy::default(),
426 };
427 let prefixed = fk.clone().with_prefix("");
428 assert_eq!(fk, prefixed);
429 }
430}