Skip to main content

mockforge_vbr/
seeding.rs

1//! Data seeding functionality
2//!
3//! This module provides functionality to seed the VBR database with initial data
4//! from JSON/YAML files or programmatically via API.
5
6use crate::entities::{Entity, EntityRegistry};
7use crate::{Error, Result};
8use serde_json::Value;
9use std::collections::HashMap;
10use std::path::Path;
11
12/// Seed data structure
13///
14/// Represents seed data organized by entity name
15pub type SeedData = HashMap<String, Vec<HashMap<String, Value>>>;
16
17/// Seed a single entity with records
18///
19/// # Arguments
20/// * `database` - The virtual database instance
21/// * `registry` - The entity registry
22/// * `entity_name` - Name of the entity to seed
23/// * `records` - Records to insert
24pub async fn seed_entity(
25    database: &dyn crate::database::VirtualDatabase,
26    registry: &EntityRegistry,
27    entity_name: &str,
28    records: &[HashMap<String, Value>],
29) -> Result<usize> {
30    let entity = registry
31        .get(entity_name)
32        .ok_or_else(|| Error::generic(format!("Entity '{}' not found", entity_name)))?;
33
34    let table_name = entity.table_name();
35    let mut inserted_count = 0;
36
37    for record in records {
38        // Validate foreign keys before insertion
39        validate_foreign_keys(registry, entity, record)?;
40
41        // Build INSERT query
42        let fields: Vec<String> = record.keys().cloned().collect();
43        let placeholders: Vec<String> = (0..fields.len()).map(|_| "?".to_string()).collect();
44
45        let query = format!(
46            "INSERT INTO {} ({}) VALUES ({})",
47            table_name,
48            fields.join(", "),
49            placeholders.join(", ")
50        );
51
52        // Prepare values in the same order as fields
53        let values: Vec<Value> =
54            fields.iter().map(|f| record.get(f).cloned().unwrap_or(Value::Null)).collect();
55
56        database.execute(&query, &values).await?;
57        inserted_count += 1;
58    }
59
60    Ok(inserted_count)
61}
62
63/// Seed multiple entities with dependency ordering
64///
65/// This function automatically orders entities based on foreign key dependencies
66/// to ensure parent entities are seeded before child entities.
67///
68/// # Arguments
69/// * `database` - The virtual database instance
70/// * `registry` - The entity registry
71/// * `seed_data` - Seed data organized by entity name
72pub async fn seed_all(
73    database: &dyn crate::database::VirtualDatabase,
74    registry: &EntityRegistry,
75    seed_data: &SeedData,
76) -> Result<HashMap<String, usize>> {
77    // Build dependency graph
78    let order = topological_sort(registry, seed_data)?;
79
80    let mut results = HashMap::new();
81
82    // Seed entities in dependency order
83    for entity_name in order {
84        if let Some(records) = seed_data.get(&entity_name) {
85            let count = seed_entity(database, registry, &entity_name, records).await?;
86            results.insert(entity_name.clone(), count);
87        }
88    }
89
90    Ok(results)
91}
92
93/// Load seed data from a JSON file
94pub async fn load_seed_file_json<P: AsRef<Path>>(path: P) -> Result<SeedData> {
95    let content = tokio::fs::read_to_string(path.as_ref())
96        .await
97        .map_err(|e| Error::generic(format!("Failed to read seed file: {}", e)))?;
98
99    let json: Value = serde_json::from_str(&content)
100        .map_err(|e| Error::generic(format!("Failed to parse JSON: {}", e)))?;
101
102    parse_seed_data(json)
103}
104
105/// Load seed data from a YAML file
106pub async fn load_seed_file_yaml<P: AsRef<Path>>(path: P) -> Result<SeedData> {
107    let content = tokio::fs::read_to_string(path.as_ref())
108        .await
109        .map_err(|e| Error::generic(format!("Failed to read seed file: {}", e)))?;
110
111    let yaml: Value = serde_yaml::from_str(&content)
112        .map_err(|e| Error::generic(format!("Failed to parse YAML: {}", e)))?;
113
114    parse_seed_data(yaml)
115}
116
117/// Load seed data from a file (auto-detect format)
118pub async fn load_seed_file<P: AsRef<Path>>(path: P) -> Result<SeedData> {
119    let path_ref = path.as_ref();
120    let ext = path_ref.extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase();
121
122    match ext.as_str() {
123        "json" => load_seed_file_json(path_ref).await,
124        "yaml" | "yml" => load_seed_file_yaml(path_ref).await,
125        _ => {
126            // Try JSON first, then YAML
127            match load_seed_file_json(path_ref).await {
128                Ok(data) => Ok(data),
129                Err(_) => load_seed_file_yaml(path_ref).await,
130            }
131        }
132    }
133}
134
135/// Parse seed data from JSON/YAML Value
136fn parse_seed_data(value: Value) -> Result<SeedData> {
137    let obj = value
138        .as_object()
139        .ok_or_else(|| Error::generic("Seed data must be an object".to_string()))?;
140
141    let mut seed_data = HashMap::new();
142
143    for (entity_name, records_value) in obj {
144        let records = records_value
145            .as_array()
146            .ok_or_else(|| {
147                Error::generic(format!("Entity '{}' seed data must be an array", entity_name))
148            })?
149            .iter()
150            .map(|v| {
151                v.as_object()
152                    .ok_or_else(|| {
153                        Error::generic(format!(
154                            "Record in entity '{}' must be an object",
155                            entity_name
156                        ))
157                    })
158                    .map(|obj| {
159                        obj.iter()
160                            .map(|(k, v)| (k.clone(), v.clone()))
161                            .collect::<HashMap<String, Value>>()
162                    })
163            })
164            .collect::<Result<Vec<_>>>()?;
165
166        seed_data.insert(entity_name.clone(), records);
167    }
168
169    Ok(seed_data)
170}
171
172/// Validate foreign keys in a record
173fn validate_foreign_keys(
174    registry: &EntityRegistry,
175    entity: &Entity,
176    record: &HashMap<String, Value>,
177) -> Result<()> {
178    for fk in &entity.schema.foreign_keys {
179        if let Some(fk_value) = record.get(&fk.field) {
180            // Check if the referenced entity exists
181            let target_entity = registry.get(&fk.target_entity).ok_or_else(|| {
182                Error::generic(format!(
183                    "Target entity '{}' not found for foreign key '{}'",
184                    fk.target_entity, fk.field
185                ))
186            })?;
187
188            let _target_table = target_entity.table_name();
189
190            // For now, we'll validate during insertion (database will enforce)
191            // This is a placeholder for more sophisticated validation
192            if fk_value.is_null()
193                && !entity
194                    .schema
195                    .base
196                    .fields
197                    .iter()
198                    .find(|f| f.name == fk.field)
199                    .map(|f| !f.required)
200                    .unwrap_or(false)
201            {
202                return Err(Error::generic(format!("Foreign key '{}' cannot be null", fk.field)));
203            }
204        }
205    }
206
207    Ok(())
208}
209
210/// Perform topological sort of entities based on foreign key dependencies
211///
212/// Returns entities in an order where parent entities come before child entities.
213fn topological_sort(registry: &EntityRegistry, seed_data: &SeedData) -> Result<Vec<String>> {
214    // Build dependency graph
215    let mut graph: HashMap<String, Vec<String>> = HashMap::new();
216    let mut in_degree: HashMap<String, usize> = HashMap::new();
217
218    // Initialize all entities that will be seeded
219    for entity_name in seed_data.keys() {
220        graph.insert(entity_name.clone(), Vec::new());
221        in_degree.insert(entity_name.clone(), 0);
222    }
223
224    // Build edges based on foreign keys
225    for entity_name in seed_data.keys() {
226        if let Some(entity) = registry.get(entity_name) {
227            for fk in &entity.schema.foreign_keys {
228                if seed_data.contains_key(&fk.target_entity) {
229                    // Add edge from target_entity to entity_name
230                    graph.entry(fk.target_entity.clone()).or_default().push(entity_name.clone());
231                    *in_degree.entry(entity_name.clone()).or_insert(0) += 1;
232                }
233            }
234        }
235    }
236
237    // Kahn's algorithm for topological sort
238    let mut queue: Vec<String> = in_degree
239        .iter()
240        .filter(|(_, &degree)| degree == 0)
241        .map(|(name, _)| name.clone())
242        .collect();
243
244    let mut result = Vec::new();
245
246    while let Some(node) = queue.pop() {
247        result.push(node.clone());
248
249        if let Some(neighbors) = graph.get(&node) {
250            for neighbor in neighbors {
251                let degree = in_degree.get_mut(neighbor).unwrap();
252                *degree -= 1;
253                if *degree == 0 {
254                    queue.push(neighbor.clone());
255                }
256            }
257        }
258    }
259
260    // Check for cycles
261    if result.len() != seed_data.len() {
262        return Err(Error::generic(
263            "Circular dependency detected in foreign key relationships".to_string(),
264        ));
265    }
266
267    Ok(result)
268}
269
270/// Clear all data from an entity
271pub async fn clear_entity(
272    database: &dyn crate::database::VirtualDatabase,
273    registry: &EntityRegistry,
274    entity_name: &str,
275) -> Result<()> {
276    let entity = registry
277        .get(entity_name)
278        .ok_or_else(|| Error::generic(format!("Entity '{}' not found", entity_name)))?;
279
280    let table_name = entity.table_name();
281    let query = format!("DELETE FROM {}", table_name);
282
283    database.execute(&query, &[]).await?;
284
285    Ok(())
286}
287
288/// Clear all data from all entities
289pub async fn clear_all(
290    database: &dyn crate::database::VirtualDatabase,
291    registry: &EntityRegistry,
292) -> Result<()> {
293    // Get entities in reverse dependency order (children first)
294    let entities: Vec<String> = registry.list();
295
296    // Simple approach: delete from all tables
297    // In a more sophisticated implementation, we'd respect foreign key constraints
298    for entity_name in entities {
299        if let Err(e) = clear_entity(database, registry, &entity_name).await {
300            // Log error but continue
301            tracing::warn!("Failed to clear entity '{}': {}", entity_name, e);
302        }
303    }
304
305    Ok(())
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn test_parse_seed_data() {
314        let json = serde_json::json!({
315            "users": [
316                {"id": "user1", "name": "Alice"},
317                {"id": "user2", "name": "Bob"}
318            ],
319            "orders": [
320                {"id": "order1", "user_id": "user1", "total": 100.0}
321            ]
322        });
323
324        let seed_data = parse_seed_data(json).unwrap();
325        assert_eq!(seed_data.len(), 2);
326        assert_eq!(seed_data.get("users").unwrap().len(), 2);
327        assert_eq!(seed_data.get("orders").unwrap().len(), 1);
328    }
329
330    #[test]
331    fn test_topological_sort() {
332        // This would require setting up a full registry, so we'll test it in integration tests
333    }
334}