1use quote::ToTokens;
7
8use crate::error::Error;
9use crate::model::{
10 ColumnDef, DefaultValue, EnumDef, ForeignKey, OnDelete, RelationDef, Schema, SqlType, TableDef,
11};
12
13enum ParsedField {
16 Column(Box<ColumnDef>),
17 Relation(RelationDef),
18}
19
20pub fn parse_sources(sources: &[(String, String)]) -> Result<Schema, Error> {
25 parse_sources_with_external(sources, &[])
26}
27
28pub fn parse_sources_with_external(
33 sources: &[(String, String)],
34 external: &[TableDef],
35) -> Result<Schema, Error> {
36 let mut files = Vec::new();
37 for (name, code) in sources {
38 let file = syn::parse_file(code).map_err(|e| Error::new(format!("{name}: {e}")))?;
39 files.push((name, file));
40 }
41
42 let mut enums: Vec<EnumDef> = Vec::new();
43 for (name, file) in &files {
44 for item in &file.items {
45 if let syn::Item::Enum(e) = item
46 && has_derive(&e.attrs, "DbEnum")
47 {
48 let def = enum_from_item(e).map_err(|e| Error::new(format!("{name}: {e}")))?;
49 if enums.iter().any(|x| x.rust_name == def.rust_name) {
50 return Err(Error::new(format!(
51 "{name}: duplicate DbEnum {}",
52 def.rust_name
53 )));
54 }
55 enums.push(def);
56 }
57 }
58 }
59 enums.sort_by(|a, b| a.rust_name.cmp(&b.rust_name));
60
61 let mut tables: Vec<TableDef> = Vec::new();
62 for (name, file) in &files {
63 for item in &file.items {
64 if let syn::Item::Struct(s) = item
65 && (has_derive(&s.attrs, "Table") || has_model_attr(&s.attrs))
66 {
67 let table = table_from_struct(s, Some(&enums))
68 .map_err(|e| Error::new(format!("{name}: {e}")))?;
69 if tables.iter().any(|t| t.name == table.name) {
70 return Err(Error::new(format!(
71 "{name}: duplicate table {}",
72 table.name
73 )));
74 }
75 if external.iter().any(|t| t.name == table.name) {
76 return Err(Error::new(format!(
77 "{name}: table {} is already defined by a module — rename the \
78 struct or set #[orm(table = \"...\")]",
79 table.name
80 )));
81 }
82 tables.push(table);
83 }
84 }
85 }
86 tables.sort_by(|a, b| a.name.cmp(&b.name));
87
88 let by_struct: Vec<(String, String)> = tables
93 .iter()
94 .chain(external)
95 .map(|t| (t.struct_name.clone(), t.name.clone()))
96 .collect();
97 let table_names: Vec<String> = tables
98 .iter()
99 .chain(external)
100 .map(|t| t.name.clone())
101 .collect();
102 for table in &mut tables {
103 for col in &mut table.columns {
104 if let Some(fk) = &mut col.references {
105 if let Some((_, tn)) = by_struct.iter().find(|(s, _)| *s == fk.table) {
106 fk.table = tn.clone();
107 } else if !table_names.contains(&fk.table) {
108 return Err(Error::new(format!(
109 "{}.{}: references unknown table/struct `{}`",
110 table.struct_name, col.name, fk.table
111 )));
112 }
113 }
114 }
115 }
116
117 for table in &mut tables {
121 let fk_targets: Vec<(String, String)> = table
122 .columns
123 .iter()
124 .filter_map(|c| {
125 c.references
126 .as_ref()
127 .map(|fk| (c.name.clone(), fk.table.clone()))
128 })
129 .collect();
130 for rel in &mut table.relations {
131 if let Some((_, target)) = fk_targets
132 .iter()
133 .find(|(name, _)| *name == rel.local_column)
134 {
135 rel.target_table = target.clone();
136 }
137 }
138 }
139
140 Ok(Schema { tables, enums })
141}
142
143pub fn enum_from_item(item: &syn::ItemEnum) -> Result<EnumDef, Error> {
146 let rust_name = item.ident.to_string();
147 let mut values = Vec::new();
148 for v in &item.variants {
149 if !matches!(v.fields, syn::Fields::Unit) {
150 return Err(Error::new(format!(
151 "{rust_name}::{}: DbEnum variants must be unit variants",
152 v.ident
153 )));
154 }
155 values.push(to_snake_case(&v.ident.to_string()));
156 }
157 if values.is_empty() {
158 return Err(Error::new(format!(
159 "{rust_name}: DbEnum needs at least one variant"
160 )));
161 }
162 Ok(EnumDef { rust_name, values })
163}
164
165pub fn table_from_struct(
172 item: &syn::ItemStruct,
173 enums: Option<&[EnumDef]>,
174) -> Result<TableDef, Error> {
175 let struct_name = item.ident.to_string();
176
177 let mut table_name: Option<String> = None;
178 let mut composite_uniques: Vec<Vec<String>> = Vec::new();
179 let mut composite_indexes: Vec<Vec<String>> = Vec::new();
180 for attr in item.attrs.iter().filter(|a| a.path().is_ident("orm")) {
181 attr.parse_nested_meta(|meta| {
182 if meta.path.is_ident("table") {
183 let lit: syn::LitStr = meta.value()?.parse()?;
184 table_name = Some(lit.value());
185 Ok(())
186 } else if meta.path.is_ident("unique") {
187 composite_uniques.push(parse_struct_column_list(&meta, "unique")?);
188 Ok(())
189 } else if meta.path.is_ident("index") {
190 composite_indexes.push(parse_struct_column_list(&meta, "index")?);
191 Ok(())
192 } else {
193 Err(meta.error(
194 "unknown #[orm(...)] key on a struct; expected `table = \"...\"`, \
195 `unique(col, ...)` or `index(col, ...)`",
196 ))
197 }
198 })
199 .map_err(|e| Error::new(format!("{struct_name}: {e}")))?;
200 }
201 let name = table_name.unwrap_or_else(|| pluralize(&to_snake_case(&struct_name)));
202
203 let syn::Fields::Named(fields) = &item.fields else {
204 return Err(Error::new(format!(
205 "{struct_name}: a Table struct needs named fields"
206 )));
207 };
208
209 let mut columns = Vec::new();
210 let mut relations = Vec::new();
211 for field in &fields.named {
212 match field_from_field(&struct_name, field, enums)? {
213 ParsedField::Column(c) => columns.push(*c),
214 ParsedField::Relation(r) => relations.push(r),
215 }
216 }
217
218 for rel in &mut relations {
221 let Some(col) = columns.iter().find(|c| c.name == rel.local_column) else {
222 return Err(Error::new(format!(
223 "{struct_name}.{}: relation column `{}` is not a field on this struct",
224 rel.field, rel.local_column
225 )));
226 };
227 if col.references.is_none() {
228 return Err(Error::new(format!(
229 "{struct_name}.{}: relation column `{}` has no #[orm(references(...))] — a relation \
230 must join through a foreign key",
231 rel.field, rel.local_column
232 )));
233 }
234 rel.nullable = col.nullable;
235 }
236
237 for cols in composite_uniques.iter().chain(composite_indexes.iter()) {
240 for col in cols {
241 if columns.iter().all(|c| &c.name != col) {
242 return Err(Error::new(format!(
243 "{struct_name}: unique(...)/index(...) references unknown column `{col}`"
244 )));
245 }
246 }
247 }
248
249 let table = TableDef {
250 name,
251 struct_name: struct_name.clone(),
252 columns,
253 relations,
254 composite_uniques,
255 composite_indexes,
256 };
257 if table.primary_key().is_empty() {
258 return Err(Error::new(format!(
259 "{struct_name}: no primary key — add an `id: i64` field or mark fields with #[orm(primary_key)]"
260 )));
261 }
262 Ok(table)
263}
264
265fn parse_struct_column_list(
268 meta: &syn::meta::ParseNestedMeta,
269 key: &str,
270) -> syn::Result<Vec<String>> {
271 let mut cols = Vec::new();
272 meta.parse_nested_meta(|m| {
273 let Some(ident) = m.path.get_ident() else {
274 return Err(m.error("expected a column name"));
275 };
276 cols.push(ident.to_string());
277 Ok(())
278 })?;
279 if cols.is_empty() {
280 return Err(meta.error(format!(
281 "{key}(...) needs at least one column, e.g. {key}(a, b)"
282 )));
283 }
284 Ok(cols)
285}
286
287fn field_from_field(
290 struct_name: &str,
291 field: &syn::Field,
292 enums: Option<&[EnumDef]>,
293) -> Result<ParsedField, Error> {
294 if let Some(rel) = relation_from_field(struct_name, field)? {
295 return Ok(ParsedField::Relation(rel));
296 }
297 Ok(ParsedField::Column(Box::new(column_from_field(
298 struct_name,
299 field,
300 enums,
301 )?)))
302}
303
304fn relation_from_field(
316 struct_name: &str,
317 field: &syn::Field,
318) -> Result<Option<RelationDef>, Error> {
319 let field_name = field.ident.as_ref().expect("named field").to_string();
320 let ctx = format!("{struct_name}.{field_name}");
321
322 let mut local_column: Option<String> = None;
323 let mut other_key = false;
324 for attr in field.attrs.iter().filter(|a| a.path().is_ident("orm")) {
325 let metas = attr
326 .parse_args_with(
327 syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
328 )
329 .map_err(|e| Error::new(format!("{ctx}: {e}")))?;
330 for meta in &metas {
331 if meta.path().is_ident("relation") {
332 let syn::Meta::NameValue(nv) = meta else {
333 return Err(Error::new(format!(
334 "{ctx}: expected `relation = fk_column`"
335 )));
336 };
337 let syn::Expr::Path(p) = &nv.value else {
338 return Err(Error::new(format!(
339 "{ctx}: relation value must be a column name"
340 )));
341 };
342 let Some(ident) = p.path.get_ident() else {
343 return Err(Error::new(format!(
344 "{ctx}: relation value must be a column name"
345 )));
346 };
347 local_column = Some(ident.to_string());
348 } else {
349 other_key = true;
350 }
351 }
352 }
353 let Some(local_column) = local_column else {
354 return Ok(None);
355 };
356 if other_key {
357 return Err(Error::new(format!(
358 "{ctx}: a #[orm(relation = ...)] field takes no other orm keys"
359 )));
360 }
361
362 let (nullable, inner) = unwrap_option(&field.ty);
363 if !nullable {
364 return Err(Error::new(format!(
365 "{ctx}: a relation field must be `Option<Target>` (it is `None` until loaded)"
366 )));
367 }
368 let Some(target_struct) = last_segment_ident(inner) else {
369 return Err(Error::new(format!(
370 "{ctx}: relation target must be a struct type"
371 )));
372 };
373
374 Ok(Some(RelationDef {
375 field: field_name,
376 target_struct,
377 target_table: String::new(),
379 local_column,
380 nullable: false,
382 }))
383}
384
385fn column_from_field(
386 struct_name: &str,
387 field: &syn::Field,
388 enums: Option<&[EnumDef]>,
389) -> Result<ColumnDef, Error> {
390 let name = field.ident.as_ref().expect("named field").to_string();
391 let ctx = format!("{struct_name}.{name}");
392
393 let mut unique = false;
394 let mut json = false;
395 let mut text = false;
396 let mut index = false;
397 let mut explicit_pk = false;
398 let mut default: Option<DefaultValue> = None;
399 let mut references: Option<ForeignKey> = None;
400 let mut renamed_from: Option<String> = None;
401
402 for attr in field.attrs.iter().filter(|a| a.path().is_ident("orm")) {
403 attr.parse_nested_meta(|meta| {
404 if meta.path.is_ident("unique") {
405 unique = true;
406 } else if meta.path.is_ident("json") {
407 json = true;
408 } else if meta.path.is_ident("text") {
409 text = true;
410 } else if meta.path.is_ident("index") {
411 index = true;
412 } else if meta.path.is_ident("primary_key") {
413 explicit_pk = true;
414 } else if meta.path.is_ident("renamed_from") {
415 let lit: syn::LitStr = meta.value()?.parse()?;
416 renamed_from = Some(lit.value());
417 } else if meta.path.is_ident("default") {
418 let expr: syn::Expr = meta.value()?.parse()?;
419 default = Some(parse_default(&expr).map_err(|m| meta.error(m))?);
420 } else if meta.path.is_ident("references") {
421 let mut target: Option<String> = None;
422 let mut on_delete: Option<OnDelete> = None;
423 meta.parse_nested_meta(|m| {
424 if m.path.is_ident("on_delete") {
425 let ident: syn::Ident = m.value()?.parse()?;
426 on_delete = Some(match ident.to_string().as_str() {
427 "cascade" => OnDelete::Cascade,
428 "set_null" => OnDelete::SetNull,
429 "restrict" => OnDelete::Restrict,
430 other => {
431 return Err(m.error(format!(
432 "unknown on_delete `{other}`; expected cascade, set_null or restrict"
433 )));
434 }
435 });
436 } else if let Some(ident) = m.path.get_ident() {
437 target = Some(ident.to_string());
438 } else {
439 return Err(m.error("expected a struct name, e.g. references(Event)"));
440 }
441 Ok(())
442 })?;
443 let Some(target) = target else {
444 return Err(meta.error("references(...) needs a target, e.g. references(Event)"));
445 };
446 references = Some(ForeignKey { table: target, column: "id".into(), on_delete });
447 } else {
448 return Err(meta.error(
449 "unknown #[orm(...)] key; expected unique, json, text, index, primary_key, default, references or renamed_from",
450 ));
451 }
452 Ok(())
453 })
454 .map_err(|e| Error::new(format!("{ctx}: {e}")))?;
455 }
456
457 let (nullable, inner) = unwrap_option(&field.ty);
458 let rust_type = inner.to_token_stream().to_string().replace(' ', "");
459
460 let (ty, is_enum, check_in) = if json {
461 (SqlType::Text, false, None)
462 } else if matches!(rust_type.as_str(), "u64" | "usize" | "isize") {
463 return Err(Error::new(format!(
467 "{ctx}: `{rust_type}` cannot be stored in SQLite (INTEGER is i64) — use i64"
468 )));
469 } else if text {
470 if native_sql_type(inner).is_some() {
474 return Err(Error::new(format!(
475 "{ctx}: #[orm(text)] is for non-native types (this one maps natively already)"
476 )));
477 }
478 (SqlType::Text, true, None)
479 } else if let Some(t) = native_sql_type(inner) {
480 (t, false, None)
481 } else if let Some(enums) = enums {
482 let type_name = last_segment_ident(inner);
483 match enums
484 .iter()
485 .find(|e| Some(e.rust_name.as_str()) == type_name.as_deref())
486 {
487 Some(e) => (SqlType::Text, true, Some(e.values.clone())),
488 None => {
489 return Err(Error::new(format!(
490 "{ctx}: unsupported type `{rust_type}` — use a native type, derive DbEnum on it, or mark the field #[orm(json)]"
491 )));
492 }
493 }
494 } else {
495 (SqlType::Text, true, None)
497 };
498
499 let mut primary_key = explicit_pk;
500 if name == "id" && ty == SqlType::Integer && !nullable {
501 primary_key = true;
502 }
503 if primary_key && nullable {
504 return Err(Error::new(format!("{ctx}: a primary key cannot be Option")));
505 }
506
507 if let Some(d) = &default {
508 validate_default(&ctx, ty, d, check_in.as_deref())?;
509 }
510 if index && (unique || primary_key) {
511 return Err(Error::new(format!(
512 "{ctx}: #[orm(index)] is redundant — unique/primary key columns are already indexed"
513 )));
514 }
515
516 Ok(ColumnDef {
517 name,
518 rust_type,
519 ty,
520 nullable,
521 primary_key,
522 unique,
523 json,
524 is_enum,
525 index,
526 default,
527 references,
528 check_in,
529 renamed_from,
530 })
531}
532
533fn parse_default(expr: &syn::Expr) -> Result<DefaultValue, String> {
534 match expr {
535 syn::Expr::Path(p) if p.path.is_ident("now") => Ok(DefaultValue::Now),
536 syn::Expr::Lit(l) => match &l.lit {
537 syn::Lit::Int(i) => Ok(DefaultValue::Int(
538 i.base10_parse().map_err(|e| e.to_string())?,
539 )),
540 syn::Lit::Float(f) => Ok(DefaultValue::Float(
541 f.base10_parse().map_err(|e| e.to_string())?,
542 )),
543 syn::Lit::Str(s) => Ok(DefaultValue::Text(s.value())),
544 syn::Lit::Bool(b) => Ok(DefaultValue::Bool(b.value)),
545 _ => Err("unsupported default literal".into()),
546 },
547 syn::Expr::Unary(u) if matches!(u.op, syn::UnOp::Neg(_)) => match parse_default(&u.expr)? {
548 DefaultValue::Int(i) => Ok(DefaultValue::Int(-i)),
549 DefaultValue::Float(f) => Ok(DefaultValue::Float(-f)),
550 _ => Err("cannot negate this default".into()),
551 },
552 _ => Err("expected a literal or `now`, e.g. default = 0 or default = now".into()),
553 }
554}
555
556fn validate_default(
557 ctx: &str,
558 ty: SqlType,
559 d: &DefaultValue,
560 check_in: Option<&[String]>,
561) -> Result<(), Error> {
562 let ok = matches!(
563 (ty, d),
564 (SqlType::Integer, DefaultValue::Int(_))
565 | (SqlType::Real, DefaultValue::Float(_))
566 | (SqlType::Real, DefaultValue::Int(_))
567 | (SqlType::Text, DefaultValue::Text(_))
568 | (SqlType::Boolean, DefaultValue::Bool(_))
569 | (SqlType::Timestamp, DefaultValue::Now)
570 );
571 if !ok {
572 return Err(Error::new(format!(
573 "{ctx}: default {d:?} does not fit column type {ty:?}"
574 )));
575 }
576 if let (Some(values), DefaultValue::Text(s)) = (check_in, d)
577 && !values.iter().any(|v| v == s)
578 {
579 return Err(Error::new(format!(
580 "{ctx}: default '{s}' is not one of the enum values {values:?}"
581 )));
582 }
583 Ok(())
584}
585
586pub fn native_sql_type(ty: &syn::Type) -> Option<SqlType> {
589 let seg = last_segment(ty)?;
590 Some(match seg.ident.to_string().as_str() {
591 "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" => SqlType::Integer,
594 "f32" | "f64" => SqlType::Real,
595 "bool" => SqlType::Boolean,
596 "String" => SqlType::Text,
597 "Uuid" => SqlType::Text,
598 "NaiveDateTime" | "DateTime" => SqlType::Timestamp,
599 "NaiveDate" | "NaiveTime" => SqlType::Text,
600 "Vec" => {
601 if let syn::PathArguments::AngleBracketed(args) = &seg.arguments
602 && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
603 && last_segment(inner).is_some_and(|s| s.ident == "u8")
604 {
605 SqlType::Blob
606 } else {
607 return None;
608 }
609 }
610 _ => return None,
611 })
612}
613
614fn unwrap_option(ty: &syn::Type) -> (bool, &syn::Type) {
615 if let Some(seg) = last_segment(ty)
616 && seg.ident == "Option"
617 && let syn::PathArguments::AngleBracketed(args) = &seg.arguments
618 && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
619 {
620 return (true, inner);
621 }
622 (false, ty)
623}
624
625fn last_segment(ty: &syn::Type) -> Option<&syn::PathSegment> {
626 if let syn::Type::Path(p) = ty {
627 p.path.segments.last()
628 } else {
629 None
630 }
631}
632
633fn last_segment_ident(ty: &syn::Type) -> Option<String> {
634 last_segment(ty).map(|s| s.ident.to_string())
635}
636
637pub fn has_model_attr(attrs: &[syn::Attribute]) -> bool {
643 attrs.iter().any(|a| {
644 a.path()
645 .segments
646 .last()
647 .is_some_and(|s| s.ident == "model")
648 })
649}
650
651pub fn has_derive(attrs: &[syn::Attribute], name: &str) -> bool {
654 attrs
655 .iter()
656 .filter(|a| a.path().is_ident("derive"))
657 .any(|a| {
658 let mut found = false;
659 let _ = a.parse_nested_meta(|meta| {
660 if meta.path.segments.last().is_some_and(|s| s.ident == name) {
661 found = true;
662 }
663 Ok(())
664 });
665 found
666 })
667}
668
669pub fn to_snake_case(s: &str) -> String {
670 let mut out = String::new();
671 for (i, ch) in s.chars().enumerate() {
672 if ch.is_uppercase() {
673 if i != 0 {
674 out.push('_');
675 }
676 out.extend(ch.to_lowercase());
677 } else {
678 out.push(ch);
679 }
680 }
681 out
682}
683
684pub fn pluralize(s: &str) -> String {
688 if let Some(stem) = s.strip_suffix('y')
689 && stem.chars().last().is_some_and(|c| !"aeiou".contains(c))
690 {
691 return format!("{stem}ies");
692 }
693 if s.ends_with('s')
694 || s.ends_with('x')
695 || s.ends_with('z')
696 || s.ends_with("ch")
697 || s.ends_with("sh")
698 {
699 return format!("{s}es");
700 }
701 format!("{s}s")
702}