mockforge_vbr/
constraints.rs

1//! Constraint enforcement
2//!
3//! This module handles constraint validation and enforcement for foreign keys,
4//! unique constraints, and check constraints.
5
6use crate::{Error, Result};
7use serde_json::Value;
8use std::collections::HashMap;
9
10/// Constraint validator
11pub struct ConstraintValidator;
12
13impl ConstraintValidator {
14    /// Validate foreign key constraint
15    pub async fn validate_foreign_key(
16        &self,
17        database: &dyn crate::database::VirtualDatabase,
18        table_name: &str,
19        field: &str,
20        value: &Value,
21        target_table: &str,
22        target_field: &str,
23    ) -> Result<()> {
24        // Check if the referenced record exists
25        let query = format!("SELECT COUNT(*) FROM {} WHERE {} = ?", target_table, target_field);
26
27        let params = vec![value.clone()];
28        let results = database.query(&query, &params).await?;
29
30        if results.is_empty() || results[0].get("COUNT(*)") == Some(&Value::Number(0.into())) {
31            return Err(Error::generic(format!(
32                "Foreign key constraint violation: {} = {:?} does not exist in {}.{}",
33                field, value, target_table, target_field
34            )));
35        }
36
37        Ok(())
38    }
39
40    /// Validate unique constraint
41    pub async fn validate_unique(
42        &self,
43        database: &dyn crate::database::VirtualDatabase,
44        table_name: &str,
45        fields: &[String],
46        values: &HashMap<String, Value>,
47        exclude_id: Option<&Value>,
48    ) -> Result<()> {
49        let mut conditions = Vec::new();
50        let mut params = Vec::new();
51
52        for field in fields {
53            if let Some(value) = values.get(field) {
54                conditions.push(format!("{} = ?", field));
55                params.push(value.clone());
56            }
57        }
58
59        if conditions.is_empty() {
60            return Ok(()); // No fields to check
61        }
62
63        let mut query =
64            format!("SELECT COUNT(*) FROM {} WHERE {}", table_name, conditions.join(" AND "));
65
66        if let Some(id) = exclude_id {
67            query.push_str(" AND id != ?");
68            params.push(id.clone());
69        }
70
71        let results = database.query(&query, &params).await?;
72
73        if !results.is_empty() {
74            if let Some(count) = results[0].get("COUNT(*)") {
75                if count.as_u64().unwrap_or(0) > 0 {
76                    return Err(Error::generic(format!(
77                        "Unique constraint violation: combination of {:?} already exists",
78                        fields
79                    )));
80                }
81            }
82        }
83
84        Ok(())
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::database::{InMemoryDatabase, VirtualDatabase};
92
93    async fn setup_test_db() -> InMemoryDatabase {
94        let mut db = InMemoryDatabase::new().await.unwrap();
95        db.initialize().await.unwrap();
96
97        // Create a test users table
98        db.create_table("CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)")
99            .await
100            .unwrap();
101
102        // Create a test orders table with foreign key reference
103        db.create_table(
104            "CREATE TABLE IF NOT EXISTS orders (id TEXT PRIMARY KEY, user_id TEXT, total REAL)",
105        )
106        .await
107        .unwrap();
108
109        db
110    }
111
112    #[tokio::test]
113    async fn test_validate_foreign_key_success() {
114        let db = setup_test_db().await;
115
116        // Insert a user
117        db.execute(
118            "INSERT INTO users (id, name) VALUES (?, ?)",
119            &[
120                Value::String("user-1".to_string()),
121                Value::String("John".to_string()),
122            ],
123        )
124        .await
125        .unwrap();
126
127        let validator = ConstraintValidator;
128        let result = validator
129            .validate_foreign_key(
130                &db,
131                "orders",
132                "user_id",
133                &Value::String("user-1".to_string()),
134                "users",
135                "id",
136            )
137            .await;
138
139        assert!(result.is_ok());
140    }
141
142    #[tokio::test]
143    async fn test_validate_foreign_key_failure() {
144        let db = setup_test_db().await;
145
146        let validator = ConstraintValidator;
147        let result = validator
148            .validate_foreign_key(
149                &db,
150                "orders",
151                "user_id",
152                &Value::String("nonexistent-user".to_string()),
153                "users",
154                "id",
155            )
156            .await;
157
158        assert!(result.is_err());
159        let err = result.unwrap_err();
160        assert!(err.to_string().contains("Foreign key constraint violation"));
161    }
162
163    #[tokio::test]
164    async fn test_validate_unique_no_duplicates() {
165        let db = setup_test_db().await;
166
167        // Insert a user
168        db.execute(
169            "INSERT INTO users (id, name) VALUES (?, ?)",
170            &[
171                Value::String("user-1".to_string()),
172                Value::String("John".to_string()),
173            ],
174        )
175        .await
176        .unwrap();
177
178        let validator = ConstraintValidator;
179        let mut values = HashMap::new();
180        values.insert("name".to_string(), Value::String("Jane".to_string()));
181
182        let result = validator
183            .validate_unique(&db, "users", &["name".to_string()], &values, None)
184            .await;
185
186        assert!(result.is_ok());
187    }
188
189    #[tokio::test]
190    async fn test_validate_unique_with_duplicate() {
191        let db = setup_test_db().await;
192
193        // Insert a user
194        db.execute(
195            "INSERT INTO users (id, name) VALUES (?, ?)",
196            &[
197                Value::String("user-1".to_string()),
198                Value::String("John".to_string()),
199            ],
200        )
201        .await
202        .unwrap();
203
204        let validator = ConstraintValidator;
205        let mut values = HashMap::new();
206        values.insert("name".to_string(), Value::String("John".to_string()));
207
208        let result = validator
209            .validate_unique(&db, "users", &["name".to_string()], &values, None)
210            .await;
211
212        assert!(result.is_err());
213        let err = result.unwrap_err();
214        assert!(err.to_string().contains("Unique constraint violation"));
215    }
216
217    #[tokio::test]
218    async fn test_validate_unique_with_exclude_id() {
219        let db = setup_test_db().await;
220
221        // Insert a user
222        db.execute(
223            "INSERT INTO users (id, name) VALUES (?, ?)",
224            &[
225                Value::String("user-1".to_string()),
226                Value::String("John".to_string()),
227            ],
228        )
229        .await
230        .unwrap();
231
232        let validator = ConstraintValidator;
233        let mut values = HashMap::new();
234        values.insert("name".to_string(), Value::String("John".to_string()));
235
236        // Should pass when excluding the same record
237        let result = validator
238            .validate_unique(
239                &db,
240                "users",
241                &["name".to_string()],
242                &values,
243                Some(&Value::String("user-1".to_string())),
244            )
245            .await;
246
247        assert!(result.is_ok());
248    }
249
250    #[tokio::test]
251    async fn test_validate_unique_empty_fields() {
252        let db = setup_test_db().await;
253
254        let validator = ConstraintValidator;
255        let values = HashMap::new();
256
257        let result = validator.validate_unique(&db, "users", &[], &values, None).await;
258
259        assert!(result.is_ok());
260    }
261
262    #[tokio::test]
263    async fn test_validate_unique_missing_value() {
264        let db = setup_test_db().await;
265
266        let validator = ConstraintValidator;
267        let values = HashMap::new(); // No values provided
268
269        let result = validator
270            .validate_unique(&db, "users", &["name".to_string()], &values, None)
271            .await;
272
273        // Should pass because no values to check
274        assert!(result.is_ok());
275    }
276
277    #[tokio::test]
278    async fn test_validate_unique_multiple_fields() {
279        let db = setup_test_db().await;
280
281        // Create a table with composite unique constraint
282        db.create_table(
283            "CREATE TABLE IF NOT EXISTS products (id TEXT PRIMARY KEY, category TEXT, sku TEXT)",
284        )
285        .await
286        .unwrap();
287
288        // Insert a product
289        db.execute(
290            "INSERT INTO products (id, category, sku) VALUES (?, ?, ?)",
291            &[
292                Value::String("prod-1".to_string()),
293                Value::String("electronics".to_string()),
294                Value::String("SKU-001".to_string()),
295            ],
296        )
297        .await
298        .unwrap();
299
300        let validator = ConstraintValidator;
301        let mut values = HashMap::new();
302        values.insert("category".to_string(), Value::String("electronics".to_string()));
303        values.insert("sku".to_string(), Value::String("SKU-002".to_string())); // Different SKU
304
305        let result = validator
306            .validate_unique(
307                &db,
308                "products",
309                &["category".to_string(), "sku".to_string()],
310                &values,
311                None,
312            )
313            .await;
314
315        assert!(result.is_ok());
316    }
317}