Skip to main content

forgedb_parser/
ast.rs

1/// Abstract Syntax Tree representation
2
3/// Constraint parameter value
4#[derive(Debug, Clone, PartialEq)]
5pub enum ConstraintParam {
6    Number(i64),
7    String(String),
8}
9
10/// Constraint directive (e.g., @min(10), @email)
11#[derive(Debug, Clone, PartialEq)]
12pub struct Constraint {
13    pub name: String,
14    pub params: Vec<ConstraintParam>,
15}
16
17impl Constraint {
18    pub fn new(name: String) -> Self {
19        Constraint {
20            name,
21            params: Vec::new(),
22        }
23    }
24
25    pub fn with_param(mut self, param: ConstraintParam) -> Self {
26        self.params.push(param);
27        self
28    }
29}
30
31#[derive(Debug, Clone, PartialEq)]
32pub enum IndexType {
33    Hash,  // Default for exact matches, unordered types
34    BTree, // For range queries on ordered types
35}
36
37#[derive(Debug, Clone, PartialEq)]
38pub struct CompositeIndex {
39    pub fields: Vec<String>,
40}
41
42#[derive(Debug, Clone, PartialEq)]
43pub enum FieldType {
44    U32,
45    U64,
46    I32,
47    I64,
48    F64,
49    Bool,
50    String,
51    Uuid,
52    Timestamp,
53    // Fixed-size types (Sprint 8)
54    Char(usize),                       // Fixed-size character array: char(N)
55    FixedArray(Box<FieldType>, usize), // Fixed array: [type; count]
56    StructType(String),                // Reference to a struct by name
57    OptionalStructType(String),        // Optional struct reference
58    /// Nullable wrapper for primitive/scalar types (e.g. `age: ?i32` or `age: i32?`)
59    Nullable(Box<FieldType>),
60    // Relations
61    Relation(RelationType),
62    // Component references (Sprint 17)
63    Component(ComponentReference),
64}
65
66/// Component protocol (Sprint 17)
67#[derive(Debug, Clone, PartialEq)]
68pub enum ComponentProtocol {
69    Tsx,  // tsx://
70    Jsx,  // jsx://
71    Api,  // api://
72}
73
74/// Relation inclusion for components (Sprint 17)
75#[derive(Debug, Clone, PartialEq)]
76pub enum RelationInclusion {
77    None,
78    All,
79    Specific(Vec<String>),
80}
81
82/// Component reference (Sprint 17)
83#[derive(Debug, Clone, PartialEq)]
84pub struct ComponentReference {
85    pub protocol: ComponentProtocol,
86    pub path: String,
87    pub relations: RelationInclusion,
88}
89
90#[derive(Debug, Clone, PartialEq)]
91pub enum RelationType {
92    /// One-to-many: [Post] means this model has many Posts
93    OneToMany(String),
94    /// Required reference: *User means this model must reference a User
95    RequiredReference(String),
96    /// Optional reference: ?User means this model optionally references a User
97    OptionalReference(String),
98    /// Many-to-many: detected from bidirectional OneToMany fields
99    ManyToMany(String),
100}
101
102#[derive(Debug, Clone, PartialEq)]
103pub struct Field {
104    pub name: String,
105    pub field_type: FieldType,
106    pub auto_generate: bool,          // + symbol
107    pub unique: bool,                 // & symbol
108    pub indexed: bool,                // ^ symbol
109    pub constraints: Vec<Constraint>, // @ directives
110    pub index_type: IndexType,        // Hash or BTree
111    pub is_computed: bool,            // @computed directive
112    pub fulltext_indexed: bool,       // @fulltext directive (Sprint 18)
113    pub is_materialized: bool,        // @materialized directive (Sprint 19)
114}
115
116/// Represents a struct definition (Sprint 8)
117#[derive(Debug, Clone, PartialEq)]
118pub struct Struct {
119    pub name: String,
120    pub fields: Vec<Field>,
121}
122
123#[derive(Debug, Clone, PartialEq)]
124pub struct Model {
125    pub name: String,
126    pub fields: Vec<Field>,
127    pub composite_indexes: Vec<CompositeIndex>,
128    pub soft_delete: bool, // @soft_delete directive at model level (Sprint 19)
129}
130
131#[derive(Debug, Clone, PartialEq)]
132pub struct Schema {
133    pub structs: Vec<Struct>, // Struct definitions (Sprint 8)
134    pub models: Vec<Model>,
135}
136
137impl Schema {
138    /// Find a model by name
139    pub fn find_model(&self, name: &str) -> Option<&Model> {
140        self.models.iter().find(|m| m.name == name)
141    }
142
143    /// Find a struct by name (Sprint 8)
144    pub fn find_struct(&self, name: &str) -> Option<&Struct> {
145        self.structs.iter().find(|s| s.name == name)
146    }
147
148    /// Validate all relations in the schema
149    pub fn validate_relations(&self) -> Result<(), String> {
150        for model in &self.models {
151            for field in &model.fields {
152                if let FieldType::Relation(rel_type) = &field.field_type {
153                    let target = rel_type.target_model();
154                    // Check if target model exists
155                    if self.find_model(target).is_none() {
156                        return Err(format!(
157                            "Model '{}' field '{}' references undefined model '{}'",
158                            model.name, field.name, target
159                        ));
160                    }
161                }
162            }
163        }
164        Ok(())
165    }
166
167    /// Validate struct references (Sprint 8)
168    pub fn validate_struct_references(&self) -> Result<(), String> {
169        // Validate structs don't contain variable-length types
170        for struct_def in &self.structs {
171            for field in &struct_def.fields {
172                if !field.field_type.is_fixed_size() {
173                    return Err(format!(
174                        "Struct '{}' field '{}' contains variable-length type. Structs can only contain fixed-size types.",
175                        struct_def.name, field.name
176                    ));
177                }
178            }
179        }
180
181        // Validate struct references in models
182        for model in &self.models {
183            for field in &model.fields {
184                if let Some(struct_name) = field.field_type.struct_name() {
185                    if self.find_struct(struct_name).is_none() {
186                        return Err(format!(
187                            "Model '{}' field '{}' references undefined struct '{}'",
188                            model.name, field.name, struct_name
189                        ));
190                    }
191                }
192            }
193        }
194        Ok(())
195    }
196
197    /// Detect one-to-many relationships by finding matching reference and collection fields
198    pub fn detect_relations(&self) -> Vec<RelationPair> {
199        let mut relations = Vec::new();
200
201        for model in &self.models {
202            for field in &model.fields {
203                if let FieldType::Relation(RelationType::OneToMany(target_model)) =
204                    &field.field_type
205                {
206                    // Found a one-to-many field, look for corresponding FK in target model
207                    if let Some(target) = self.find_model(target_model) {
208                        // Find a reference back to this model
209                        for target_field in &target.fields {
210                            if let FieldType::Relation(rel) = &target_field.field_type {
211                                if rel.is_reference() && rel.target_model() == model.name {
212                                    relations.push(RelationPair {
213                                        parent_model: model.name.clone(),
214                                        parent_field: field.name.clone(),
215                                        child_model: target.name.clone(),
216                                        child_field: target_field.name.clone(),
217                                        is_required: matches!(
218                                            rel,
219                                            RelationType::RequiredReference(_)
220                                        ),
221                                    });
222                                }
223                            }
224                        }
225                    }
226                }
227            }
228        }
229
230        relations
231    }
232
233    /// Detect many-to-many relationships by finding bidirectional OneToMany fields
234    /// Returns pairs where Model A has [ModelB] and Model B has [ModelA]
235    /// Excludes relationships that are already handled by FK references (1:N)
236    pub fn detect_many_to_many_relations(&self) -> Vec<ManyToManyRelation> {
237        let mut m2m_relations = Vec::new();
238        let mut processed_pairs = std::collections::HashSet::new();
239
240        // First, identify all 1:N relationships (OneToMany with matching FK)
241        // We need to track specific FIELD pairs, not just model pairs
242        let one_to_many_field_pairs: std::collections::HashSet<(String, String, String, String)> =
243            self.detect_relations()
244                .iter()
245                .map(|rel| {
246                    // For a 1:N relation, the parent's OneToMany field and child's FK field form a pair
247                    // We store both orderings to match against
248                    vec![
249                        (
250                            rel.parent_model.clone(),
251                            rel.parent_field.clone(),
252                            rel.child_model.clone(),
253                            rel.child_field.clone(),
254                        ),
255                        (
256                            rel.child_model.clone(),
257                            rel.child_field.clone(),
258                            rel.parent_model.clone(),
259                            rel.parent_field.clone(),
260                        ),
261                    ]
262                })
263                .flatten()
264                .collect();
265
266        for model in &self.models {
267            for field in &model.fields {
268                if let FieldType::Relation(RelationType::OneToMany(target_model)) =
269                    &field.field_type
270                {
271                    // Check if target model has a OneToMany field pointing back to this model
272                    if let Some(target) = self.find_model(target_model) {
273                        for target_field in &target.fields {
274                            if let FieldType::Relation(RelationType::OneToMany(back_ref)) =
275                                &target_field.field_type
276                            {
277                                if back_ref == &model.name {
278                                    // Check if this specific FIELD pair is NOT a 1:N relationship (no FK)
279                                    // by checking if this field pair is in the one_to_many_field_pairs set
280                                    let is_one_to_many = one_to_many_field_pairs.contains(&(
281                                        model.name.clone(),
282                                        field.name.clone(),
283                                        target_model.clone(),
284                                        target_field.name.clone(),
285                                    ));
286
287                                    if !is_one_to_many {
288                                        // Found a true M:N relationship
289                                        // Create a consistent ordering to avoid duplicates
290                                        let mut models_fields = vec![
291                                            (model.name.as_str(), field.name.as_str()),
292                                            (target.name.as_str(), target_field.name.as_str()),
293                                        ];
294                                        models_fields.sort();
295
296                                        let pair_key = format!(
297                                            "{}:{}:{}:{}",
298                                            models_fields[0].0,
299                                            models_fields[0].1,
300                                            models_fields[1].0,
301                                            models_fields[1].1
302                                        );
303
304                                        if !processed_pairs.contains(&pair_key) {
305                                            processed_pairs.insert(pair_key);
306
307                                            // Always store with consistent ordering
308                                            let (model1, field1, model2, field2) =
309                                                if model.name < target.name {
310                                                    (
311                                                        model.name.clone(),
312                                                        field.name.clone(),
313                                                        target.name.clone(),
314                                                        target_field.name.clone(),
315                                                    )
316                                                } else {
317                                                    (
318                                                        target.name.clone(),
319                                                        target_field.name.clone(),
320                                                        model.name.clone(),
321                                                        field.name.clone(),
322                                                    )
323                                                };
324
325                                            m2m_relations.push(ManyToManyRelation {
326                                                model1,
327                                                field1,
328                                                model2,
329                                                field2,
330                                            });
331                                        }
332                                    }
333                                }
334                            }
335                        }
336                    }
337                }
338            }
339        }
340
341        m2m_relations
342    }
343}
344
345#[derive(Debug, Clone, PartialEq)]
346pub struct RelationPair {
347    pub parent_model: String,
348    pub parent_field: String,
349    pub child_model: String,
350    pub child_field: String,
351    pub is_required: bool,
352}
353
354/// Represents a many-to-many relationship between two models
355#[derive(Debug, Clone, PartialEq)]
356pub struct ManyToManyRelation {
357    pub model1: String,
358    pub field1: String,
359    pub model2: String,
360    pub field2: String,
361}
362
363impl FieldType {
364    pub fn to_rust_type(&self) -> String {
365        match self {
366            FieldType::U32 => "u32".to_string(),
367            FieldType::U64 => "u64".to_string(),
368            FieldType::I32 => "i32".to_string(),
369            FieldType::I64 => "i64".to_string(),
370            FieldType::F64 => "f64".to_string(),
371            FieldType::Bool => "bool".to_string(),
372            FieldType::String => "String".to_string(),
373            FieldType::Uuid => "uuid::Uuid".to_string(),
374            FieldType::Timestamp => "i64".to_string(),
375            FieldType::Char(size) => format!("[u8; {}]", size),
376            FieldType::FixedArray(inner_type, count) => {
377                format!("[{}; {}]", inner_type.to_rust_type(), count)
378            }
379            FieldType::StructType(name) => name.clone(),
380            FieldType::OptionalStructType(name) => format!("Option<{}>", name),
381            FieldType::Nullable(inner) => format!("Option<{}>", inner.to_rust_type()),
382            FieldType::Relation(rel) => match rel {
383                RelationType::RequiredReference(model) => {
384                    format!("uuid::Uuid /* FK to {} */", model)
385                }
386                RelationType::OptionalReference(model) => {
387                    format!("Option<uuid::Uuid> /* FK to {} */", model)
388                }
389                RelationType::OneToMany(_) => "/* virtual field - no storage */".to_string(),
390                RelationType::ManyToMany(_) => {
391                    "/* virtual field - stored in junction table */".to_string()
392                }
393            },
394            FieldType::Component(_) => "/* component reference - no storage */".to_string(),
395        }
396    }
397
398    pub fn is_auto_incrementable(&self) -> bool {
399        matches!(self, FieldType::U32 | FieldType::U64)
400    }
401
402    pub fn is_auto_generatable(&self) -> bool {
403        matches!(
404            self,
405            FieldType::U32 | FieldType::U64 | FieldType::Uuid | FieldType::Timestamp
406        )
407    }
408
409    pub fn is_relation(&self) -> bool {
410        matches!(self, FieldType::Relation(_))
411    }
412
413    /// Check if this type is fixed-size (Sprint 8)
414    pub fn is_fixed_size(&self) -> bool {
415        match self {
416            FieldType::U32
417            | FieldType::U64
418            | FieldType::I32
419            | FieldType::I64
420            | FieldType::F64
421            | FieldType::Bool
422            | FieldType::Uuid
423            | FieldType::Timestamp
424            | FieldType::Char(_) => true,
425            FieldType::FixedArray(inner, _) => inner.is_fixed_size(),
426            FieldType::StructType(_) => true, // Structs must be fixed-size
427            FieldType::OptionalStructType(_) => true, // Optional struct still fixed-size (uses discriminant)
428            FieldType::Nullable(inner) => inner.is_fixed_size(),
429            FieldType::String => false,
430            FieldType::Relation(_) => false, // Relations are virtual or variable
431            FieldType::Component(_) => false, // Components are virtual
432        }
433    }
434
435    /// Get struct name if this is a struct type (Sprint 8)
436    pub fn struct_name(&self) -> Option<&str> {
437        match self {
438            FieldType::StructType(name) => Some(name),
439            FieldType::OptionalStructType(name) => Some(name),
440            _ => None,
441        }
442    }
443
444    /// Get the size in bytes for fixed-size types (Sprint 8)
445    pub fn size_in_bytes(&self, schema: &Schema) -> usize {
446        match self {
447            FieldType::U32 | FieldType::I32 => 4,
448            FieldType::U64 | FieldType::I64 | FieldType::F64 | FieldType::Timestamp => 8,
449            FieldType::Bool => 1,
450            FieldType::Uuid => 16,
451            FieldType::Char(size) => *size,
452            FieldType::FixedArray(inner, count) => inner.size_in_bytes(schema) * count,
453            FieldType::StructType(name) => {
454                if let Some(struct_def) = schema.find_struct(name) {
455                    Struct::calculate_size(struct_def, schema)
456                } else {
457                    0
458                }
459            }
460            FieldType::OptionalStructType(name) => {
461                // Option adds 1 byte discriminant + size of struct
462                1 + if let Some(struct_def) = schema.find_struct(name) {
463                    Struct::calculate_size(struct_def, schema)
464                } else {
465                    0
466                }
467            }
468            // Nullable wraps the inner type in Option; use the inner size (discriminant handled by Rust)
469            FieldType::Nullable(inner) => inner.size_in_bytes(schema),
470            _ => 0, // Variable-size or virtual
471        }
472    }
473
474    /// Get alignment requirement for this type (Sprint 8)
475    pub fn alignment(&self, schema: &Schema) -> usize {
476        match self {
477            FieldType::U32 | FieldType::I32 => 4,
478            FieldType::U64 | FieldType::I64 | FieldType::F64 | FieldType::Timestamp => 8,
479            FieldType::Bool => 1,
480            FieldType::Uuid => 16, // UUID is typically 16-byte aligned
481            FieldType::Char(_) => 1,
482            FieldType::FixedArray(inner, _) => inner.alignment(schema),
483            FieldType::StructType(name) => {
484                if let Some(struct_def) = schema.find_struct(name) {
485                    Struct::calculate_alignment(struct_def, schema)
486                } else {
487                    1
488                }
489            }
490            FieldType::OptionalStructType(name) => {
491                if let Some(struct_def) = schema.find_struct(name) {
492                    Struct::calculate_alignment(struct_def, schema)
493                } else {
494                    1
495                }
496            }
497            FieldType::Nullable(inner) => inner.alignment(schema),
498            _ => 1,
499        }
500    }
501
502    /// Determine if this type supports range queries (ordered)
503    pub fn supports_range_queries(&self) -> bool {
504        matches!(
505            self,
506            FieldType::U32
507                | FieldType::U64
508                | FieldType::I32
509                | FieldType::I64
510                | FieldType::F64
511                | FieldType::Timestamp
512        )
513    }
514
515    /// Get the default index type for this field type
516    pub fn default_index_type(&self) -> IndexType {
517        if self.supports_range_queries() {
518            IndexType::BTree
519        } else {
520            IndexType::Hash
521        }
522    }
523}
524
525impl Struct {
526    /// Calculate the total size of a struct with proper padding (Sprint 8)
527    pub fn calculate_size(struct_def: &Struct, schema: &Schema) -> usize {
528        let mut size = 0;
529        let mut max_alignment = 1;
530
531        for field in &struct_def.fields {
532            let field_size = field.field_type.size_in_bytes(schema);
533            let field_align = field.field_type.alignment(schema);
534
535            max_alignment = max_alignment.max(field_align);
536
537            // Add padding before field if needed
538            if size % field_align != 0 {
539                size += field_align - (size % field_align);
540            }
541
542            size += field_size;
543        }
544
545        // Add final padding to make struct size a multiple of its alignment
546        if size % max_alignment != 0 {
547            size += max_alignment - (size % max_alignment);
548        }
549
550        size
551    }
552
553    /// Calculate the alignment requirement for a struct (Sprint 8)
554    pub fn calculate_alignment(struct_def: &Struct, schema: &Schema) -> usize {
555        struct_def
556            .fields
557            .iter()
558            .map(|f| f.field_type.alignment(schema))
559            .max()
560            .unwrap_or(1)
561    }
562}
563
564impl Field {
565    /// Check if field has a specific constraint
566    pub fn has_constraint(&self, name: &str) -> bool {
567        self.constraints.iter().any(|c| c.name == name)
568    }
569
570    /// Get a constraint by name
571    pub fn get_constraint(&self, name: &str) -> Option<&Constraint> {
572        self.constraints.iter().find(|c| c.name == name)
573    }
574
575    /// Check if field is nullable (optional references, optional structs, and nullable primitives)
576    pub fn is_nullable(&self) -> bool {
577        matches!(
578            &self.field_type,
579            FieldType::Relation(RelationType::OptionalReference(_))
580                | FieldType::OptionalStructType(_)
581                | FieldType::Nullable(_)
582        )
583    }
584}
585
586impl RelationType {
587    pub fn target_model(&self) -> &str {
588        match self {
589            RelationType::OneToMany(model) => model,
590            RelationType::RequiredReference(model) => model,
591            RelationType::OptionalReference(model) => model,
592            RelationType::ManyToMany(model) => model,
593        }
594    }
595
596    pub fn is_one_to_many(&self) -> bool {
597        matches!(self, RelationType::OneToMany(_))
598    }
599
600    pub fn is_reference(&self) -> bool {
601        matches!(
602            self,
603            RelationType::RequiredReference(_) | RelationType::OptionalReference(_)
604        )
605    }
606
607    pub fn is_many_to_many(&self) -> bool {
608        matches!(self, RelationType::ManyToMany(_))
609    }
610}