Skip to main content

fraiseql_core/compiler/
ir.rs

1//! Intermediate Representation (IR) for schema compilation.
2//!
3//! The IR is the internal representation of a GraphQL schema during compilation.
4//! It's created from authoring-time JSON and transformed into runtime-optimized
5//! `CompiledSchema`.
6//!
7//! # IR Structure
8//!
9//! ```text
10//! AuthoringIR
11//! ├─ types: Vec<IRType>
12//! ├─ queries: Vec<IRQuery>
13//! ├─ mutations: Vec<IRMutation>
14//! └─ subscriptions: Vec<IRSubscription>
15//! ```
16//!
17//! # Example
18//!
19//! ```rust
20//! use fraiseql_core::compiler::ir::{AuthoringIR, IRType, IRField};
21//!
22//! let mut ir = AuthoringIR::new();
23//! ir.types.push(IRType {
24//!     name: "User".to_string(),
25//!     fields: vec![
26//!         IRField {
27//!             name: "id".to_string(),
28//!             field_type: "Int!".to_string(),
29//!             nullable: false,
30//!             description: None,
31//!             sql_column: None,
32//!         }
33//!     ],
34//!     sql_source: Some("v_user".to_string()),
35//!     description: None,
36//! });
37//! assert_eq!(ir.types.len(), 1);
38//! ```
39
40use std::collections::HashMap;
41
42use serde::{Deserialize, Serialize};
43
44use crate::{
45    compiler::fact_table::FactTableMetadata, schema::GraphQLValue, validation::ValidationRule,
46};
47
48/// Authoring Intermediate Representation.
49///
50/// This is the parsed representation of a GraphQL schema before
51/// SQL template generation and optimization.
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub struct AuthoringIR {
54    /// Type definitions.
55    pub types: Vec<IRType>,
56
57    /// Enum definitions.
58    #[serde(default)]
59    pub enums: Vec<IREnum>,
60
61    /// Interface definitions.
62    #[serde(default)]
63    pub interfaces: Vec<IRInterface>,
64
65    /// Union definitions.
66    #[serde(default)]
67    pub unions: Vec<IRUnion>,
68
69    /// Input type definitions.
70    #[serde(default)]
71    pub input_types: Vec<IRInputType>,
72
73    /// Custom scalar type definitions.
74    #[serde(default)]
75    pub scalars: Vec<IRScalar>,
76
77    /// Query definitions.
78    pub queries: Vec<IRQuery>,
79
80    /// Mutation definitions.
81    pub mutations: Vec<IRMutation>,
82
83    /// Subscription definitions.
84    pub subscriptions: Vec<IRSubscription>,
85
86    /// Fact table metadata (from authoring-language decorators).
87    /// Key: table name (e.g., "`tf_sales`")
88    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
89    pub fact_tables: HashMap<String, FactTableMetadata>,
90}
91
92impl AuthoringIR {
93    /// Create empty IR.
94    #[must_use]
95    pub fn new() -> Self {
96        Self {
97            types:         Vec::new(),
98            enums:         Vec::new(),
99            interfaces:    Vec::new(),
100            unions:        Vec::new(),
101            input_types:   Vec::new(),
102            scalars:       Vec::new(),
103            queries:       Vec::new(),
104            mutations:     Vec::new(),
105            subscriptions: Vec::new(),
106            fact_tables:   HashMap::new(),
107        }
108    }
109}
110
111impl Default for AuthoringIR {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117/// IR Type definition.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub struct IRType {
120    /// Type name (e.g., "User").
121    pub name: String,
122
123    /// Field definitions.
124    pub fields: Vec<IRField>,
125
126    /// SQL source (table/view name).
127    pub sql_source: Option<String>,
128
129    /// Type description.
130    pub description: Option<String>,
131}
132
133/// IR Field definition.
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub struct IRField {
136    /// Field name.
137    pub name: String,
138
139    /// Field type (e.g., `"String!"`, `"Int"`, `"[User]"`).
140    pub field_type: String,
141
142    /// Is field nullable?
143    pub nullable: bool,
144
145    /// Field description.
146    pub description: Option<String>,
147
148    /// SQL column mapping.
149    pub sql_column: Option<String>,
150}
151
152/// IR Query definition.
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
154pub struct IRQuery {
155    /// Query name (e.g., "users", "user").
156    pub name: String,
157
158    /// Return type name.
159    pub return_type: String,
160
161    /// Does this return a list?
162    pub returns_list: bool,
163
164    /// Is return value nullable?
165    pub nullable: bool,
166
167    /// Query arguments.
168    pub arguments: Vec<IRArgument>,
169
170    /// SQL source (table/view).
171    pub sql_source: Option<String>,
172
173    /// Query description.
174    pub description: Option<String>,
175
176    /// Auto-wired parameters (where, orderBy, limit, offset).
177    pub auto_params: AutoParams,
178}
179
180/// IR Mutation definition.
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
182pub struct IRMutation {
183    /// Mutation name (e.g., "createUser", "updatePost").
184    pub name: String,
185
186    /// Return type name.
187    pub return_type: String,
188
189    /// Is return value nullable?
190    pub nullable: bool,
191
192    /// Mutation arguments.
193    pub arguments: Vec<IRArgument>,
194
195    /// Mutation description.
196    pub description: Option<String>,
197
198    /// SQL operation type.
199    pub operation: MutationOperation,
200}
201
202/// IR Subscription definition.
203#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
204pub struct IRSubscription {
205    /// Subscription name.
206    pub name: String,
207
208    /// Return type name.
209    pub return_type: String,
210
211    /// Subscription arguments.
212    pub arguments: Vec<IRArgument>,
213
214    /// Subscription description.
215    pub description: Option<String>,
216}
217
218/// IR Argument definition.
219#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
220pub struct IRArgument {
221    /// Argument name.
222    pub name: String,
223
224    /// Argument type.
225    pub arg_type: String,
226
227    /// Is argument nullable?
228    pub nullable: bool,
229
230    /// Default value.
231    pub default_value: Option<GraphQLValue>,
232
233    /// Argument description.
234    pub description: Option<String>,
235}
236
237/// Auto-wired parameters configuration.
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
239pub struct AutoParams {
240    /// Enable WHERE parameter?
241    #[serde(default)]
242    pub has_where: bool,
243
244    /// Enable orderBy parameter?
245    #[serde(default)]
246    pub has_order_by: bool,
247
248    /// Enable limit parameter?
249    #[serde(default)]
250    pub has_limit: bool,
251
252    /// Enable offset parameter?
253    #[serde(default)]
254    pub has_offset: bool,
255}
256
257/// Mutation operation type.
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
259#[non_exhaustive]
260pub enum MutationOperation {
261    /// INSERT operation.
262    Create,
263
264    /// UPDATE operation.
265    Update,
266
267    /// DELETE operation.
268    Delete,
269
270    /// Custom SQL operation.
271    Custom,
272}
273
274/// IR Enum definition.
275#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276pub struct IREnum {
277    /// Enum name (e.g., "Status").
278    pub name: String,
279
280    /// Enum values.
281    pub values: Vec<IREnumValue>,
282
283    /// Enum description.
284    pub description: Option<String>,
285}
286
287/// IR Enum value definition.
288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
289pub struct IREnumValue {
290    /// Value name (e.g., "ACTIVE").
291    pub name: String,
292
293    /// Value description.
294    pub description: Option<String>,
295
296    /// Deprecation reason (if deprecated).
297    pub deprecation_reason: Option<String>,
298}
299
300/// IR Interface definition.
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
302pub struct IRInterface {
303    /// Interface name (e.g., "Node").
304    pub name: String,
305
306    /// Interface fields.
307    pub fields: Vec<IRField>,
308
309    /// Interface description.
310    pub description: Option<String>,
311}
312
313/// IR Union definition.
314#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
315pub struct IRUnion {
316    /// Union name (e.g., "`SearchResult`").
317    pub name: String,
318
319    /// Types that are part of this union.
320    pub types: Vec<String>,
321
322    /// Union description.
323    pub description: Option<String>,
324}
325
326/// IR Input type definition.
327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
328pub struct IRInputType {
329    /// Input type name (e.g., "`CreateUserInput`").
330    pub name: String,
331
332    /// Input fields.
333    pub fields: Vec<IRInputField>,
334
335    /// Input type description.
336    pub description: Option<String>,
337}
338
339/// IR Input field definition.
340#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
341pub struct IRInputField {
342    /// Field name.
343    pub name: String,
344
345    /// Field type (e.g., "String!", "Int").
346    pub field_type: String,
347
348    /// Is field nullable?
349    pub nullable: bool,
350
351    /// Default value.
352    pub default_value: Option<GraphQLValue>,
353
354    /// Field description.
355    pub description: Option<String>,
356}
357
358/// IR Scalar type definition.
359///
360/// Represents a custom scalar type with optional validation rules.
361/// Custom scalars allow developers to define domain-specific scalar types
362/// with validation rules beyond the builtin GraphQL scalars.
363#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
364pub struct IRScalar {
365    /// Scalar name (e.g., "Email", "ISBN", "IBAN").
366    pub name: String,
367
368    /// Scalar description.
369    pub description: Option<String>,
370
371    /// URL specification (RFC or standard that defines this scalar type).
372    /// Per GraphQL spec §3.5.1 (`specified_by_url`).
373    pub specified_by_url: Option<String>,
374
375    /// Validation rules for this scalar.
376    #[serde(default)]
377    pub validation_rules: Vec<ValidationRule>,
378
379    /// Base type for type aliases (e.g., "String" for Email alias).
380    pub base_type: Option<String>,
381}
382
383impl IRScalar {
384    /// Create a new scalar definition with minimal required fields.
385    #[must_use]
386    pub const fn new(name: String) -> Self {
387        Self {
388            name,
389            description: None,
390            specified_by_url: None,
391            validation_rules: Vec::new(),
392            base_type: None,
393        }
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn test_authoring_ir_new() {
403        let ir = AuthoringIR::new();
404        assert!(ir.types.is_empty());
405        assert!(ir.queries.is_empty());
406        assert!(ir.mutations.is_empty());
407        assert!(ir.subscriptions.is_empty());
408    }
409
410    #[test]
411    fn test_authoring_ir_with_scalars() {
412        let mut ir = AuthoringIR::new();
413
414        // Add custom scalar
415        ir.scalars.push(IRScalar::new("Email".to_string()));
416        ir.scalars.push(IRScalar::new("ISBN".to_string()));
417
418        assert_eq!(ir.scalars.len(), 2);
419        assert_eq!(ir.scalars[0].name, "Email");
420        assert_eq!(ir.scalars[1].name, "ISBN");
421    }
422
423    #[test]
424    fn test_ir_type() {
425        let ir_type = IRType {
426            name:        "User".to_string(),
427            fields:      vec![IRField {
428                name:        "id".to_string(),
429                field_type:  "Int!".to_string(),
430                nullable:    false,
431                description: None,
432                sql_column:  Some("id".to_string()),
433            }],
434            sql_source:  Some("v_user".to_string()),
435            description: Some("User type".to_string()),
436        };
437
438        assert_eq!(ir_type.name, "User");
439        assert_eq!(ir_type.fields.len(), 1);
440        assert_eq!(ir_type.sql_source, Some("v_user".to_string()));
441    }
442
443    #[test]
444    fn test_ir_query() {
445        let query = IRQuery {
446            name:         "users".to_string(),
447            return_type:  "User".to_string(),
448            returns_list: true,
449            nullable:     false,
450            arguments:    vec![],
451            sql_source:   Some("v_user".to_string()),
452            description:  None,
453            auto_params:  AutoParams {
454                has_where: true,
455                has_limit: true,
456                ..Default::default()
457            },
458        };
459
460        assert_eq!(query.name, "users");
461        assert!(query.returns_list);
462        assert!(query.auto_params.has_where);
463        assert!(query.auto_params.has_limit);
464    }
465
466    #[test]
467    fn test_ir_mutation() {
468        let mutation = IRMutation {
469            name:        "createUser".to_string(),
470            return_type: "User".to_string(),
471            nullable:    false,
472            arguments:   vec![IRArgument {
473                name:          "input".to_string(),
474                arg_type:      "CreateUserInput!".to_string(),
475                nullable:      false,
476                default_value: None,
477                description:   None,
478            }],
479            description: None,
480            operation:   MutationOperation::Create,
481        };
482
483        assert_eq!(mutation.name, "createUser");
484        assert_eq!(mutation.operation, MutationOperation::Create);
485        assert_eq!(mutation.arguments.len(), 1);
486    }
487
488    #[test]
489    fn test_auto_params_default() {
490        let params = AutoParams::default();
491        assert!(!params.has_where);
492        assert!(!params.has_order_by);
493        assert!(!params.has_limit);
494        assert!(!params.has_offset);
495    }
496
497    #[test]
498    fn test_mutation_operations() {
499        assert_eq!(MutationOperation::Create, MutationOperation::Create);
500        assert_ne!(MutationOperation::Create, MutationOperation::Update);
501    }
502
503    #[test]
504    fn test_ir_scalar_new() {
505        let scalar = IRScalar::new("Email".to_string());
506
507        assert_eq!(scalar.name, "Email");
508        assert_eq!(scalar.description, None);
509        assert_eq!(scalar.specified_by_url, None);
510        assert_eq!(scalar.validation_rules.len(), 0);
511        assert_eq!(scalar.base_type, None);
512    }
513
514    #[test]
515    fn test_ir_scalar_with_all_fields() {
516        let scalar = IRScalar {
517            name:             "Email".to_string(),
518            description:      Some("Valid email address".to_string()),
519            specified_by_url: Some("https://html.spec.whatwg.org/".to_string()),
520            validation_rules: vec![ValidationRule::Pattern {
521                pattern: r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$".to_string(),
522                message: Some("Invalid email format".to_string()),
523            }],
524            base_type:        Some("String".to_string()),
525        };
526
527        assert_eq!(scalar.name, "Email");
528        assert_eq!(scalar.description, Some("Valid email address".to_string()));
529        assert_eq!(scalar.specified_by_url, Some("https://html.spec.whatwg.org/".to_string()));
530        assert_eq!(scalar.validation_rules.len(), 1);
531        assert_eq!(scalar.base_type, Some("String".to_string()));
532    }
533
534    #[test]
535    fn test_ir_scalar_serialization() {
536        let scalar = IRScalar {
537            name:             "ISBN".to_string(),
538            description:      Some("International Standard Book Number".to_string()),
539            specified_by_url: Some("https://www.isbn-international.org/".to_string()),
540            validation_rules: vec![],
541            base_type:        None,
542        };
543
544        // Serialize to JSON
545        let json = serde_json::to_value(&scalar).expect("Should serialize");
546
547        // Verify structure
548        assert_eq!(json["name"], "ISBN");
549        assert_eq!(json["description"], "International Standard Book Number");
550        assert_eq!(json["specified_by_url"], "https://www.isbn-international.org/");
551        assert_eq!(json["validation_rules"], serde_json::json!([]));
552    }
553
554    #[test]
555    fn test_ir_scalar_deserialization() {
556        let json = serde_json::json!({
557            "name": "PhoneNumber",
558            "description": "Valid phone number",
559            "specified_by_url": null,
560            "validation_rules": [],
561            "base_type": "String"
562        });
563
564        let scalar: IRScalar = serde_json::from_value(json).expect("Should deserialize");
565
566        assert_eq!(scalar.name, "PhoneNumber");
567        assert_eq!(scalar.description, Some("Valid phone number".to_string()));
568        assert_eq!(scalar.specified_by_url, None);
569        assert_eq!(scalar.validation_rules.len(), 0);
570        assert_eq!(scalar.base_type, Some("String".to_string()));
571    }
572
573    #[test]
574    fn test_ir_scalar_equality() {
575        let scalar1 = IRScalar {
576            name:             "UUID".to_string(),
577            description:      Some("Universal Unique Identifier".to_string()),
578            specified_by_url: None,
579            validation_rules: vec![],
580            base_type:        None,
581        };
582
583        let scalar2 = IRScalar {
584            name:             "UUID".to_string(),
585            description:      Some("Universal Unique Identifier".to_string()),
586            specified_by_url: None,
587            validation_rules: vec![],
588            base_type:        None,
589        };
590
591        assert_eq!(scalar1, scalar2);
592    }
593}