1use crate::validator::Validator;
24use serde::{Deserialize, Serialize};
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Schema {
28 pub tables: Vec<TableDef>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct TableDef {
33 pub name: String,
34 pub columns: Vec<ColumnDef>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ColumnDef {
39 pub name: String,
40 #[serde(rename = "type", alias = "typ")]
41 pub typ: String,
42 #[serde(default)]
43 pub nullable: bool,
44 #[serde(default)]
45 pub primary_key: bool,
46}
47
48impl Schema {
49 pub fn new() -> Self {
51 Self { tables: Vec::new() }
52 }
53
54 pub fn add_table(&mut self, table: TableDef) {
56 self.tables.push(table);
57 }
58
59 pub fn to_validator(&self) -> Validator {
61 let mut v = Validator::new();
62 for table in &self.tables {
63 let cols: Vec<&str> = table.columns.iter().map(|c| c.name.as_str()).collect();
64 v.add_table(&table.name, &cols);
65 }
66 v
67 }
68
69 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
71 serde_json::from_str(json)
72 }
73
74 pub fn from_qail_schema(input: &str) -> Result<Self, String> {
83 let mut schema = Schema::new();
84 let mut current_table: Option<TableDef> = None;
85
86 for line in input.lines() {
87 let line = line.trim();
88
89 if line.is_empty() || line.starts_with("--") {
91 continue;
92 }
93
94 if let Some(rest) = line.strip_prefix("table ") {
96 if let Some(t) = current_table.take() {
98 schema.tables.push(t);
99 }
100
101 let name = rest
103 .split('(')
104 .next()
105 .map(|s| s.trim())
106 .ok_or_else(|| format!("Invalid table line: {}", line))?;
107
108 current_table = Some(TableDef::new(name));
109 }
110 else if line == ")" {
112 if let Some(t) = current_table.take() {
113 schema.tables.push(t);
114 }
115 }
116 else if let Some(ref mut table) = current_table {
118 let line = line.trim_end_matches(',');
120
121 let parts: Vec<&str> = line.split_whitespace().collect();
122 if parts.len() >= 2 {
123 let col_name = parts[0];
124 let col_type = parts[1];
125 let not_null = parts.len() > 2
126 && parts.iter().any(|&p| p.eq_ignore_ascii_case("not"))
127 && parts.iter().any(|&p| p.eq_ignore_ascii_case("null"));
128
129 table.columns.push(ColumnDef {
130 name: col_name.to_string(),
131 typ: col_type.to_string(),
132 nullable: !not_null,
133 primary_key: false,
134 });
135 }
136 }
137 }
138
139 if let Some(t) = current_table {
141 schema.tables.push(t);
142 }
143
144 Ok(schema)
145 }
146
147 pub fn from_file(path: &std::path::Path) -> Result<Self, String> {
149 let content = std::fs::read_to_string(path)
150 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
151
152 if path.extension().map(|e| e == "json").unwrap_or(false) {
154 Self::from_json(&content).map_err(|e| e.to_string())
155 } else {
156 Self::from_qail_schema(&content)
157 }
158 }
159}
160
161impl Default for Schema {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167impl TableDef {
168 pub fn new(name: &str) -> Self {
170 Self {
171 name: name.to_string(),
172 columns: Vec::new(),
173 }
174 }
175
176 pub fn add_column(&mut self, col: ColumnDef) {
178 self.columns.push(col);
179 }
180
181 pub fn column(mut self, name: &str, typ: &str) -> Self {
183 self.columns.push(ColumnDef {
184 name: name.to_string(),
185 typ: typ.to_string(),
186 nullable: true,
187 primary_key: false,
188 });
189 self
190 }
191
192 pub fn pk(mut self, name: &str, typ: &str) -> Self {
194 self.columns.push(ColumnDef {
195 name: name.to_string(),
196 typ: typ.to_string(),
197 nullable: false,
198 primary_key: true,
199 });
200 self
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn test_schema_from_json() {
210 let json = r#"{
211 "tables": [{
212 "name": "users",
213 "columns": [
214 { "name": "id", "type": "uuid", "nullable": false, "primary_key": true },
215 { "name": "email", "type": "varchar", "nullable": false }
216 ]
217 }]
218 }"#;
219
220 let schema = Schema::from_json(json).unwrap();
221 assert_eq!(schema.tables.len(), 1);
222 assert_eq!(schema.tables[0].name, "users");
223 assert_eq!(schema.tables[0].columns.len(), 2);
224 }
225
226 #[test]
227 fn test_schema_to_validator() {
228 let schema = Schema {
229 tables: vec![
230 TableDef::new("users")
231 .pk("id", "uuid")
232 .column("email", "varchar"),
233 ],
234 };
235
236 let validator = schema.to_validator();
237 assert!(validator.validate_table("users").is_ok());
238 assert!(validator.validate_column("users", "id").is_ok());
239 assert!(validator.validate_column("users", "email").is_ok());
240 }
241
242 #[test]
243 fn test_table_builder() {
244 let table = TableDef::new("orders")
245 .pk("id", "uuid")
246 .column("total", "decimal")
247 .column("status", "varchar");
248
249 assert_eq!(table.columns.len(), 3);
250 assert!(table.columns[0].primary_key);
251 }
252
253 #[test]
258 fn test_build_schema_parses_ref_syntax() {
259 let schema_content = r#"
260table users {
261 id UUID primary_key
262 email TEXT
263}
264
265table posts {
266 id UUID primary_key
267 user_id UUID ref:users.id
268 title TEXT
269}
270"#;
271
272 let schema = crate::build::Schema::parse(schema_content).unwrap();
273
274 assert!(schema.has_table("users"));
276 assert!(schema.has_table("posts"));
277
278 let posts = schema.table("posts").unwrap();
280 assert_eq!(posts.foreign_keys.len(), 1);
281
282 let fk = &posts.foreign_keys[0];
283 assert_eq!(fk.column, "user_id");
284 assert_eq!(fk.ref_table, "users");
285 assert_eq!(fk.ref_column, "id");
286 }
287
288 #[test]
289 fn test_relation_registry_forward_lookup() {
290 let mut registry = RelationRegistry::new();
291 registry.register("posts", "user_id", "users", "id");
292
293 let result = registry.get("posts", "users");
295 assert!(result.is_some());
296 let (from_col, to_col) = result.unwrap();
297 assert_eq!(from_col, "user_id");
298 assert_eq!(to_col, "id");
299 }
300
301 #[test]
302 fn test_relation_registry_from_build_schema() {
303 let schema_content = r#"
304table users {
305 id UUID
306}
307
308table posts {
309 user_id UUID ref:users.id
310}
311
312table comments {
313 post_id UUID ref:posts.id
314 user_id UUID ref:users.id
315}
316"#;
317
318 let schema = crate::build::Schema::parse(schema_content).unwrap();
319 let registry = RelationRegistry::from_build_schema(&schema);
320
321 assert!(registry.get("posts", "users").is_some());
323
324 assert!(registry.get("comments", "posts").is_some());
326
327 assert!(registry.get("comments", "users").is_some());
329
330 let referencing = registry.referencing("users");
332 assert!(referencing.contains(&"posts"));
333 assert!(referencing.contains(&"comments"));
334 }
335
336 #[test]
337 fn test_join_on_produces_correct_ast() {
338 use crate::Qail;
339
340 {
342 let mut reg = super::RUNTIME_RELATIONS.write().unwrap();
343 reg.register("posts", "user_id", "users", "id");
344 }
345
346 let query = Qail::get("users").join_on("posts");
349
350 assert_eq!(query.joins.len(), 1);
351 let join = &query.joins[0];
352 assert_eq!(join.table, "posts");
353
354 let on = join.on.as_ref().expect("Should have ON conditions");
356 assert_eq!(on.len(), 1);
357
358 {
360 let mut reg = super::RUNTIME_RELATIONS.write().unwrap();
361 *reg = RelationRegistry::new();
362 }
363 }
364
365 #[test]
366 fn test_join_on_optional_returns_self_when_no_relation() {
367 use crate::Qail;
368
369 {
371 let mut reg = super::RUNTIME_RELATIONS.write().unwrap();
372 *reg = RelationRegistry::new();
373 }
374
375 let query = Qail::get("users").join_on_optional("nonexistent");
377 assert!(query.joins.is_empty());
378 }
379}
380
381use std::collections::HashMap;
382use std::sync::RwLock;
383use std::sync::LazyLock;
384
385#[derive(Debug, Default)]
386pub struct RelationRegistry {
387 forward: HashMap<(String, String), (String, String)>,
389 reverse: HashMap<String, Vec<String>>,
391}
392
393impl RelationRegistry {
394 pub fn new() -> Self {
396 Self::default()
397 }
398
399 pub fn register(&mut self, from_table: &str, from_col: &str, to_table: &str, to_col: &str) {
401 self.forward.insert(
402 (from_table.to_string(), to_table.to_string()),
403 (from_col.to_string(), to_col.to_string()),
404 );
405
406 self.reverse
407 .entry(to_table.to_string())
408 .or_default()
409 .push(from_table.to_string());
410 }
411
412 pub fn get(&self, from_table: &str, to_table: &str) -> Option<(&str, &str)> {
415 self.forward
416 .get(&(from_table.to_string(), to_table.to_string()))
417 .map(|(a, b)| (a.as_str(), b.as_str()))
418 }
419
420 pub fn referencing(&self, table: &str) -> Vec<&str> {
422 self.reverse
423 .get(table)
424 .map(|v| v.iter().map(|s| s.as_str()).collect())
425 .unwrap_or_default()
426 }
427
428 pub fn from_build_schema(schema: &crate::build::Schema) -> Self {
430 let mut registry = Self::new();
431
432 for table in schema.tables.values() {
433 for fk in &table.foreign_keys {
434 registry.register(
435 &table.name,
436 &fk.column,
437 &fk.ref_table,
438 &fk.ref_column,
439 );
440 }
441 }
442
443 registry
444 }
445}
446
447pub static RUNTIME_RELATIONS: LazyLock<RwLock<RelationRegistry>> =
449 LazyLock::new(|| RwLock::new(RelationRegistry::new()));
450
451pub fn load_schema_relations(path: &str) -> Result<usize, String> {
454 let schema = crate::build::Schema::parse_file(path)?;
455 let mut registry = RUNTIME_RELATIONS.write().map_err(|e| format!("Lock error: {}", e))?;
456
457 let mut count = 0;
458 for table in schema.tables.values() {
459 for fk in &table.foreign_keys {
460 registry.register(&table.name, &fk.column, &fk.ref_table, &fk.ref_column);
461 count += 1;
462 }
463 }
464
465 Ok(count)
466}
467
468pub fn lookup_relation(from_table: &str, to_table: &str) -> Option<(String, String)> {
471 let registry = RUNTIME_RELATIONS.read().ok()?;
472 let (fc, tc) = registry.get(from_table, to_table)?;
473 Some((fc.to_string(), tc.to_string()))
474}