Skip to main content

forgedb_parser/
ast.rs

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