1use crate::validator::Validator;
24use serde::{Deserialize, Serialize};
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Schema {
29 pub tables: Vec<TableDef>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct TableDef {
36 pub name: String,
38 pub columns: Vec<ColumnDef>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ColumnDef {
45 pub name: String,
47 #[serde(rename = "type", alias = "typ")]
49 pub typ: String,
50 #[serde(default)]
52 pub nullable: bool,
53 #[serde(default)]
55 pub primary_key: bool,
56}
57
58impl Schema {
59 pub fn new() -> Self {
61 Self { tables: Vec::new() }
62 }
63
64 pub fn add_table(&mut self, table: TableDef) {
66 self.tables.push(table);
67 }
68
69 pub fn to_validator(&self) -> Validator {
71 let mut v = Validator::new();
72 for table in &self.tables {
73 let cols: Vec<&str> = table.columns.iter().map(|c| c.name.as_str()).collect();
74 v.add_table(&table.name, &cols);
75 }
76 v
77 }
78
79 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
81 serde_json::from_str(json)
82 }
83
84 pub fn from_qail_schema(input: &str) -> Result<Self, String> {
93 let mut schema = Schema::new();
94 let mut current_table: Option<TableDef> = None;
95
96 for line in input.lines() {
97 let line = line.trim();
98
99 if line.is_empty() || line.starts_with("--") {
101 continue;
102 }
103
104 if let Some(rest) = line.strip_prefix("table ") {
106 if let Some(t) = current_table.take() {
108 schema.tables.push(t);
109 }
110
111 let name = rest
113 .split('(')
114 .next()
115 .map(|s| s.trim())
116 .ok_or_else(|| format!("Invalid table line: {}", line))?;
117
118 current_table = Some(TableDef::new(name));
119 }
120 else if line == ")" {
122 if let Some(t) = current_table.take() {
123 schema.tables.push(t);
124 }
125 }
126 else if let Some(ref mut table) = current_table {
128 let line = line.trim_end_matches(',');
130
131 let parts: Vec<&str> = line.split_whitespace().collect();
132 if parts.len() >= 2 {
133 let col_name = parts[0];
134 let col_type = parts[1];
135 let not_null = parts.len() > 2
136 && parts.iter().any(|&p| p.eq_ignore_ascii_case("not"))
137 && parts.iter().any(|&p| p.eq_ignore_ascii_case("null"));
138
139 table.columns.push(ColumnDef {
140 name: col_name.to_string(),
141 typ: col_type.to_string(),
142 nullable: !not_null,
143 primary_key: false,
144 });
145 }
146 }
147 }
148
149 if let Some(t) = current_table {
151 schema.tables.push(t);
152 }
153
154 Ok(schema)
155 }
156
157 pub fn from_file(path: &std::path::Path) -> Result<Self, String> {
159 if path.extension().map(|e| e == "json").unwrap_or(false) {
161 let content = std::fs::read_to_string(path)
162 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
163 Self::from_json(&content).map_err(|e| e.to_string())
164 } else {
165 let content = crate::schema_source::read_qail_schema_source(path)?;
166 Self::from_qail_schema(&content)
167 }
168 }
169}
170
171impl Default for Schema {
172 fn default() -> Self {
173 Self::new()
174 }
175}
176
177impl TableDef {
178 pub fn new(name: &str) -> Self {
180 Self {
181 name: name.to_string(),
182 columns: Vec::new(),
183 }
184 }
185
186 pub fn add_column(&mut self, col: ColumnDef) {
188 self.columns.push(col);
189 }
190
191 pub fn column(mut self, name: &str, typ: &str) -> Self {
193 self.columns.push(ColumnDef {
194 name: name.to_string(),
195 typ: typ.to_string(),
196 nullable: true,
197 primary_key: false,
198 });
199 self
200 }
201
202 pub fn pk(mut self, name: &str, typ: &str) -> Self {
204 self.columns.push(ColumnDef {
205 name: name.to_string(),
206 typ: typ.to_string(),
207 nullable: false,
208 primary_key: true,
209 });
210 self
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_schema_from_json() {
220 let json = r#"{
221 "tables": [{
222 "name": "users",
223 "columns": [
224 { "name": "id", "type": "uuid", "nullable": false, "primary_key": true },
225 { "name": "email", "type": "varchar", "nullable": false }
226 ]
227 }]
228 }"#;
229
230 let schema = Schema::from_json(json).unwrap();
231 assert_eq!(schema.tables.len(), 1);
232 assert_eq!(schema.tables[0].name, "users");
233 assert_eq!(schema.tables[0].columns.len(), 2);
234 }
235
236 #[test]
237 fn test_schema_to_validator() {
238 let schema = Schema {
239 tables: vec![
240 TableDef::new("users")
241 .pk("id", "uuid")
242 .column("email", "varchar"),
243 ],
244 };
245
246 let validator = schema.to_validator();
247 assert!(validator.validate_table("users").is_ok());
248 assert!(validator.validate_column("users", "id").is_ok());
249 assert!(validator.validate_column("users", "email").is_ok());
250 }
251
252 #[test]
253 fn test_table_builder() {
254 let table = TableDef::new("orders")
255 .pk("id", "uuid")
256 .column("total", "decimal")
257 .column("status", "varchar");
258
259 assert_eq!(table.columns.len(), 3);
260 assert!(table.columns[0].primary_key);
261 }
262
263 #[test]
268 fn test_build_schema_parses_ref_syntax() {
269 let schema_content = r#"
270table users {
271 id UUID primary_key
272 email TEXT
273}
274
275table posts {
276 id UUID primary_key
277 user_id UUID ref:users.id
278 title TEXT
279}
280"#;
281
282 let schema = crate::build::Schema::parse(schema_content).unwrap();
283
284 assert!(schema.has_table("users"));
286 assert!(schema.has_table("posts"));
287
288 let posts = schema.table("posts").unwrap();
290 assert_eq!(posts.foreign_keys.len(), 1);
291
292 let fk = &posts.foreign_keys[0];
293 assert_eq!(fk.column, "user_id");
294 assert_eq!(fk.ref_table, "users");
295 assert_eq!(fk.ref_column, "id");
296 }
297
298 #[test]
299 fn test_relation_registry_forward_lookup() {
300 let mut registry = RelationRegistry::new();
301 registry.register("posts", "user_id", "users", "id");
302
303 let result = registry.get("posts", "users");
305 assert!(result.is_some());
306 let (from_col, to_col) = result.unwrap();
307 assert_eq!(from_col, "user_id");
308 assert_eq!(to_col, "id");
309 }
310
311 #[test]
312 fn test_relation_registry_from_build_schema() {
313 let schema_content = r#"
314table users {
315 id UUID
316}
317
318table posts {
319 user_id UUID ref:users.id
320}
321
322table comments {
323 post_id UUID ref:posts.id
324 user_id UUID ref:users.id
325}
326"#;
327
328 let schema = crate::build::Schema::parse(schema_content).unwrap();
329 let registry = RelationRegistry::from_build_schema(&schema);
330
331 assert!(registry.get("posts", "users").is_some());
333
334 assert!(registry.get("comments", "posts").is_some());
336
337 assert!(registry.get("comments", "users").is_some());
339
340 let referencing = registry.referencing("users");
342 assert!(referencing.contains(&"posts"));
343 assert!(referencing.contains(&"comments"));
344 }
345
346 #[test]
347 fn test_join_on_produces_correct_ast() {
348 use crate::Qail;
349
350 {
352 let mut reg = super::RUNTIME_RELATIONS.write().unwrap();
353 reg.register("posts", "user_id", "users", "id");
354 }
355
356 let query = Qail::get("users").join_on("posts");
359
360 assert_eq!(query.joins.len(), 1);
361 let join = &query.joins[0];
362 assert_eq!(join.table, "posts");
363
364 let on = join.on.as_ref().expect("Should have ON conditions");
366 assert_eq!(on.len(), 1);
367
368 {
370 let mut reg = super::RUNTIME_RELATIONS.write().unwrap();
371 *reg = RelationRegistry::new();
372 }
373 }
374
375 #[test]
376 fn test_join_on_optional_returns_self_when_no_relation() {
377 use crate::Qail;
378
379 {
381 let mut reg = super::RUNTIME_RELATIONS.write().unwrap();
382 *reg = RelationRegistry::new();
383 }
384
385 let query = Qail::get("users").join_on_optional("nonexistent");
387 assert!(query.joins.is_empty());
388 }
389
390 #[test]
391 fn test_join_on_returns_self_when_no_relation() {
392 use crate::Qail;
393
394 {
395 let mut reg = super::RUNTIME_RELATIONS.write().unwrap();
396 *reg = RelationRegistry::new();
397 }
398
399 let query = Qail::get("users").join_on("nonexistent");
400 assert!(query.joins.is_empty());
401 }
402
403 #[test]
404 fn test_try_join_on_returns_error_when_no_relation() {
405 use crate::Qail;
406
407 {
408 let mut reg = super::RUNTIME_RELATIONS.write().unwrap();
409 *reg = RelationRegistry::new();
410 }
411
412 let err = Qail::get("users")
413 .try_join_on("nonexistent")
414 .expect_err("expected missing relation error");
415 assert!(err.contains("No relation found"));
416 }
417}
418
419use std::collections::HashMap;
420use std::sync::LazyLock;
421use std::sync::RwLock;
422
423#[derive(Debug, Default)]
425pub struct RelationRegistry {
426 forward: HashMap<(String, String), (String, String)>,
428 reverse: HashMap<String, Vec<String>>,
430}
431
432impl RelationRegistry {
433 pub fn new() -> Self {
435 Self::default()
436 }
437
438 pub fn register(&mut self, from_table: &str, from_col: &str, to_table: &str, to_col: &str) {
447 self.forward.insert(
448 (from_table.to_string(), to_table.to_string()),
449 (from_col.to_string(), to_col.to_string()),
450 );
451
452 self.reverse
453 .entry(to_table.to_string())
454 .or_default()
455 .push(from_table.to_string());
456 }
457
458 pub fn get(&self, from_table: &str, to_table: &str) -> Option<(&str, &str)> {
467 self.forward
468 .get(&(from_table.to_string(), to_table.to_string()))
469 .map(|(a, b)| (a.as_str(), b.as_str()))
470 }
471
472 pub fn referencing(&self, table: &str) -> Vec<&str> {
474 self.reverse
475 .get(table)
476 .map(|v| v.iter().map(|s| s.as_str()).collect())
477 .unwrap_or_default()
478 }
479
480 pub fn from_build_schema(schema: &crate::build::Schema) -> Self {
482 let mut registry = Self::new();
483
484 for table in schema.tables.values() {
485 for fk in &table.foreign_keys {
486 registry.register(&table.name, &fk.column, &fk.ref_table, &fk.ref_column);
487 }
488 }
489
490 registry
491 }
492}
493
494pub static RUNTIME_RELATIONS: LazyLock<RwLock<RelationRegistry>> =
496 LazyLock::new(|| RwLock::new(RelationRegistry::new()));
497
498pub fn load_schema_relations(path: &str) -> Result<usize, String> {
501 let schema = crate::build::Schema::parse_file(path)?;
502 let mut registry = RUNTIME_RELATIONS
503 .write()
504 .map_err(|e| format!("Lock error: {}", e))?;
505
506 let mut count = 0;
507 for table in schema.tables.values() {
508 for fk in &table.foreign_keys {
509 registry.register(&table.name, &fk.column, &fk.ref_table, &fk.ref_column);
510 count += 1;
511 }
512 }
513
514 Ok(count)
515}
516
517pub fn lookup_relation(from_table: &str, to_table: &str) -> Option<(String, String)> {
520 let registry = RUNTIME_RELATIONS.read().ok()?;
521 let (fc, tc) = registry.get(from_table, to_table)?;
522 Some((fc.to_string(), tc.to_string()))
523}