Skip to main content

nodedb_crdt/
constraint.rs

1//! SQL constraint definitions for CRDT collections.
2//!
3//! Constraints are checked at commit time against the leader's state.
4//! They define invariants that must hold globally, even though individual
5//! agents operate optimistically without them.
6
7use serde::{Deserialize, Serialize};
8
9/// The kind of SQL constraint to enforce.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub enum ConstraintKind {
12    /// No two rows may have the same value for this key.
13    /// Analogous to SQL `UNIQUE(column)`.
14    Unique,
15
16    /// The value must reference an existing key in another collection.
17    /// Analogous to SQL `FOREIGN KEY(column) REFERENCES other(key)`.
18    ForeignKey {
19        /// The referenced collection name.
20        ref_collection: String,
21        /// The referenced key field.
22        ref_key: String,
23    },
24
25    /// The value must not be null/empty.
26    /// Analogous to SQL `NOT NULL`.
27    NotNull,
28
29    /// Custom predicate — evaluated as a boolean expression on the row.
30    /// Analogous to SQL `CHECK(expression)`.
31    Check {
32        /// Human-readable description of the check.
33        description: String,
34    },
35}
36
37/// A constraint bound to a specific collection and field.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Constraint {
40    /// Unique name for this constraint (e.g., "users_email_unique").
41    pub name: String,
42    /// The collection (table) this constraint applies to.
43    pub collection: String,
44    /// The field (column) this constraint applies to.
45    pub field: String,
46    /// The kind of constraint.
47    pub kind: ConstraintKind,
48}
49
50/// A set of constraints for a schema.
51#[derive(Debug, Clone, Default)]
52pub struct ConstraintSet {
53    constraints: Vec<Constraint>,
54}
55
56impl ConstraintSet {
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    /// Add a constraint.
62    pub fn add(&mut self, constraint: Constraint) {
63        self.constraints.push(constraint);
64    }
65
66    /// Add a UNIQUE constraint.
67    pub fn add_unique(&mut self, name: &str, collection: &str, field: &str) {
68        self.add(Constraint {
69            name: name.to_string(),
70            collection: collection.to_string(),
71            field: field.to_string(),
72            kind: ConstraintKind::Unique,
73        });
74    }
75
76    /// Add a FOREIGN KEY constraint.
77    pub fn add_foreign_key(
78        &mut self,
79        name: &str,
80        collection: &str,
81        field: &str,
82        ref_collection: &str,
83        ref_key: &str,
84    ) {
85        self.add(Constraint {
86            name: name.to_string(),
87            collection: collection.to_string(),
88            field: field.to_string(),
89            kind: ConstraintKind::ForeignKey {
90                ref_collection: ref_collection.to_string(),
91                ref_key: ref_key.to_string(),
92            },
93        });
94    }
95
96    /// Add a NOT NULL constraint.
97    pub fn add_not_null(&mut self, name: &str, collection: &str, field: &str) {
98        self.add(Constraint {
99            name: name.to_string(),
100            collection: collection.to_string(),
101            field: field.to_string(),
102            kind: ConstraintKind::NotNull,
103        });
104    }
105
106    /// Get all constraints for a given collection.
107    pub fn for_collection(&self, collection: &str) -> Vec<&Constraint> {
108        self.constraints
109            .iter()
110            .filter(|c| c.collection == collection)
111            .collect()
112    }
113
114    /// Get all constraints.
115    pub fn all(&self) -> &[Constraint] {
116        &self.constraints
117    }
118
119    /// Number of constraints.
120    pub fn len(&self) -> usize {
121        self.constraints.len()
122    }
123
124    pub fn is_empty(&self) -> bool {
125        self.constraints.is_empty()
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn constraint_set_operations() {
135        let mut cs = ConstraintSet::new();
136        cs.add_unique("users_email_unique", "users", "email");
137        cs.add_not_null("users_name_nn", "users", "name");
138        cs.add_foreign_key("posts_author_fk", "posts", "author_id", "users", "id");
139
140        assert_eq!(cs.len(), 3);
141        assert_eq!(cs.for_collection("users").len(), 2);
142        assert_eq!(cs.for_collection("posts").len(), 1);
143        assert_eq!(cs.for_collection("missing").len(), 0);
144    }
145}