1use crate::validator::Validator;
21
22#[derive(Debug, Clone)]
24pub struct Schema {
25 pub tables: Vec<TableDef>,
27}
28
29#[derive(Debug, Clone)]
31pub struct TableDef {
32 pub name: String,
34 pub columns: Vec<ColumnDef>,
36}
37
38#[derive(Debug, Clone)]
40pub struct ColumnDef {
41 pub name: String,
43 pub typ: String,
45 pub nullable: bool,
47 pub primary_key: bool,
49}
50
51impl Schema {
52 pub fn new() -> Self {
54 Self { tables: Vec::new() }
55 }
56
57 pub fn add_table(&mut self, table: TableDef) {
59 self.tables.push(table);
60 }
61
62 pub fn to_validator(&self) -> Validator {
64 let mut v = Validator::new();
65 for table in &self.tables {
66 let cols: Vec<&str> = table.columns.iter().map(|c| c.name.as_str()).collect();
67 v.add_table(&table.name, &cols);
68 }
69 v
70 }
71
72 pub fn from_qail_schema(input: &str) -> Result<Self, String> {
81 let mut schema = Schema::new();
82 let mut current_table: Option<TableDef> = None;
83
84 for line in input.lines() {
85 let line = line.trim();
86
87 if line.is_empty() || line.starts_with("--") {
89 continue;
90 }
91
92 if let Some(rest) = line.strip_prefix("table ") {
94 if let Some(t) = current_table.take() {
96 schema.tables.push(t);
97 }
98
99 let name = rest
101 .split('(')
102 .next()
103 .map(|s| s.trim())
104 .ok_or_else(|| format!("Invalid table line: {}", line))?;
105
106 current_table = Some(TableDef::new(name));
107 }
108 else if line == ")" {
110 if let Some(t) = current_table.take() {
111 schema.tables.push(t);
112 }
113 }
114 else if let Some(ref mut table) = current_table {
116 let line = line.trim_end_matches(',');
118
119 let parts: Vec<&str> = line.split_whitespace().collect();
120 if parts.len() >= 2 {
121 let col_name = parts[0];
122 let col_type = parts[1];
123 let not_null = parts.len() > 2
124 && parts.iter().any(|&p| p.eq_ignore_ascii_case("not"))
125 && parts.iter().any(|&p| p.eq_ignore_ascii_case("null"));
126
127 table.columns.push(ColumnDef {
128 name: col_name.to_string(),
129 typ: col_type.to_string(),
130 nullable: !not_null,
131 primary_key: false,
132 });
133 }
134 }
135 }
136
137 if let Some(t) = current_table {
139 schema.tables.push(t);
140 }
141
142 Ok(schema)
143 }
144
145 pub fn from_file(path: &std::path::Path) -> Result<Self, String> {
147 let content = crate::schema_source::read_qail_schema_source(path)?;
148 Self::from_qail_schema(&content)
149 }
150}
151
152impl Default for Schema {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158impl TableDef {
159 pub fn new(name: &str) -> Self {
161 Self {
162 name: name.to_string(),
163 columns: Vec::new(),
164 }
165 }
166
167 pub fn add_column(&mut self, col: ColumnDef) {
169 self.columns.push(col);
170 }
171
172 pub fn column(mut self, name: &str, typ: &str) -> Self {
174 self.columns.push(ColumnDef {
175 name: name.to_string(),
176 typ: typ.to_string(),
177 nullable: true,
178 primary_key: false,
179 });
180 self
181 }
182
183 pub fn pk(mut self, name: &str, typ: &str) -> Self {
185 self.columns.push(ColumnDef {
186 name: name.to_string(),
187 typ: typ.to_string(),
188 nullable: false,
189 primary_key: true,
190 });
191 self
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_schema_from_qail_schema() {
201 let qail = r#"
202table users (
203 id uuid not null,
204 email varchar not null
205)
206"#;
207
208 let schema = Schema::from_qail_schema(qail).unwrap();
209 assert_eq!(schema.tables.len(), 1);
210 assert_eq!(schema.tables[0].name, "users");
211 assert_eq!(schema.tables[0].columns.len(), 2);
212 }
213
214 #[test]
215 fn test_schema_to_validator() {
216 let schema = Schema {
217 tables: vec![
218 TableDef::new("users")
219 .pk("id", "uuid")
220 .column("email", "varchar"),
221 ],
222 };
223
224 let validator = schema.to_validator();
225 assert!(validator.validate_table("users").is_ok());
226 assert!(validator.validate_column("users", "id").is_ok());
227 assert!(validator.validate_column("users", "email").is_ok());
228 }
229
230 #[test]
231 fn test_table_builder() {
232 let table = TableDef::new("orders")
233 .pk("id", "uuid")
234 .column("total", "decimal")
235 .column("status", "varchar");
236
237 assert_eq!(table.columns.len(), 3);
238 assert!(table.columns[0].primary_key);
239 }
240
241 #[test]
246 fn test_build_schema_parses_ref_syntax() {
247 let schema_content = r#"
248table users {
249 id UUID primary_key
250 email TEXT
251}
252
253table posts {
254 id UUID primary_key
255 user_id UUID ref:users.id
256 title TEXT
257}
258"#;
259
260 let schema = crate::build::Schema::parse(schema_content).unwrap();
261
262 assert!(schema.has_table("users"));
264 assert!(schema.has_table("posts"));
265
266 let posts = schema.table("posts").unwrap();
268 assert_eq!(posts.foreign_keys.len(), 1);
269
270 let fk = &posts.foreign_keys[0];
271 assert_eq!(fk.column, "user_id");
272 assert_eq!(fk.ref_table, "users");
273 assert_eq!(fk.ref_column, "id");
274 }
275
276 #[test]
277 fn test_relation_registry_forward_lookup() {
278 let mut registry = RelationRegistry::new();
279 registry.register("posts", "user_id", "users", "id");
280
281 let result = registry.get("posts", "users");
283 assert!(result.is_some());
284 let (from_col, to_col) = result.unwrap();
285 assert_eq!(from_col, "user_id");
286 assert_eq!(to_col, "id");
287 }
288
289 #[test]
290 fn test_relation_registry_from_build_schema() {
291 let schema_content = r#"
292table users {
293 id UUID
294}
295
296table posts {
297 user_id UUID ref:users.id
298}
299
300table comments {
301 post_id UUID ref:posts.id
302 user_id UUID ref:users.id
303}
304"#;
305
306 let schema = crate::build::Schema::parse(schema_content).unwrap();
307 let registry = RelationRegistry::from_build_schema(&schema);
308
309 assert!(registry.get("posts", "users").is_some());
311
312 assert!(registry.get("comments", "posts").is_some());
314
315 assert!(registry.get("comments", "users").is_some());
317
318 let referencing = registry.referencing("users");
320 assert!(referencing.contains(&"posts"));
321 assert!(referencing.contains(&"comments"));
322 }
323
324 #[test]
325 fn test_join_on_produces_correct_ast() {
326 use crate::Qail;
327
328 {
330 let mut reg = super::RUNTIME_RELATIONS.write().unwrap();
331 reg.register("posts", "user_id", "users", "id");
332 }
333
334 let query = Qail::get("users").join_on("posts");
337
338 assert_eq!(query.joins.len(), 1);
339 let join = &query.joins[0];
340 assert_eq!(join.table, "posts");
341
342 let on = join.on.as_ref().expect("Should have ON conditions");
344 assert_eq!(on.len(), 1);
345
346 {
348 let mut reg = super::RUNTIME_RELATIONS.write().unwrap();
349 *reg = RelationRegistry::new();
350 }
351 }
352
353 #[test]
354 fn test_join_on_optional_returns_self_when_no_relation() {
355 use crate::Qail;
356
357 {
359 let mut reg = super::RUNTIME_RELATIONS.write().unwrap();
360 *reg = RelationRegistry::new();
361 }
362
363 let query = Qail::get("users").join_on_optional("nonexistent");
365 assert!(query.joins.is_empty());
366 }
367
368 #[test]
369 fn test_join_on_returns_self_when_no_relation() {
370 use crate::Qail;
371
372 {
373 let mut reg = super::RUNTIME_RELATIONS.write().unwrap();
374 *reg = RelationRegistry::new();
375 }
376
377 let query = Qail::get("users").join_on("nonexistent");
378 assert!(query.joins.is_empty());
379 }
380
381 #[test]
382 fn test_try_join_on_returns_error_when_no_relation() {
383 use crate::Qail;
384
385 {
386 let mut reg = super::RUNTIME_RELATIONS.write().unwrap();
387 *reg = RelationRegistry::new();
388 }
389
390 let err = Qail::get("users")
391 .try_join_on("nonexistent")
392 .expect_err("expected missing relation error");
393 assert!(err.contains("No relation found"));
394 }
395}
396
397use std::collections::HashMap;
398use std::sync::LazyLock;
399use std::sync::RwLock;
400
401#[derive(Debug, Default)]
403pub struct RelationRegistry {
404 forward: HashMap<(String, String), (String, String)>,
406 reverse: HashMap<String, Vec<String>>,
408}
409
410impl RelationRegistry {
411 pub fn new() -> Self {
413 Self::default()
414 }
415
416 pub fn register(&mut self, from_table: &str, from_col: &str, to_table: &str, to_col: &str) {
425 self.forward.insert(
426 (from_table.to_string(), to_table.to_string()),
427 (from_col.to_string(), to_col.to_string()),
428 );
429
430 self.reverse
431 .entry(to_table.to_string())
432 .or_default()
433 .push(from_table.to_string());
434 }
435
436 pub fn get(&self, from_table: &str, to_table: &str) -> Option<(&str, &str)> {
445 self.forward
446 .get(&(from_table.to_string(), to_table.to_string()))
447 .map(|(a, b)| (a.as_str(), b.as_str()))
448 }
449
450 pub fn referencing(&self, table: &str) -> Vec<&str> {
452 self.reverse
453 .get(table)
454 .map(|v| v.iter().map(|s| s.as_str()).collect())
455 .unwrap_or_default()
456 }
457
458 pub fn from_build_schema(schema: &crate::build::Schema) -> Self {
460 let mut registry = Self::new();
461
462 for table in schema.tables.values() {
463 for fk in &table.foreign_keys {
464 registry.register(&table.name, &fk.column, &fk.ref_table, &fk.ref_column);
465 }
466 }
467
468 registry
469 }
470}
471
472pub static RUNTIME_RELATIONS: LazyLock<RwLock<RelationRegistry>> =
474 LazyLock::new(|| RwLock::new(RelationRegistry::new()));
475
476pub fn load_schema_relations(path: &str) -> Result<usize, String> {
479 let schema = crate::build::Schema::parse_file(path)?;
480 let mut registry = RUNTIME_RELATIONS
481 .write()
482 .map_err(|e| format!("Lock error: {}", e))?;
483
484 let mut count = 0;
485 for table in schema.tables.values() {
486 for fk in &table.foreign_keys {
487 registry.register(&table.name, &fk.column, &fk.ref_table, &fk.ref_column);
488 count += 1;
489 }
490 }
491
492 Ok(count)
493}
494
495pub fn lookup_relation(from_table: &str, to_table: &str) -> Option<(String, String)> {
498 let registry = RUNTIME_RELATIONS.read().ok()?;
499 let (fc, tc) = registry.get(from_table, to_table)?;
500 Some((fc.to_string(), tc.to_string()))
501}