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//! ```ignore
20//! use fraiseql_core::compiler::ir::{AuthoringIR, IRType, IRField};
21//!
22//! let ir = AuthoringIR {
23//!     types: vec![
24//!         IRType {
25//!             name: "User".to_string(),
26//!             fields: vec![
27//!                 IRField {
28//!                     name: "id".to_string(),
29//!                     field_type: "Int!".to_string(),
30//!                     nullable: false,
31//!                 }
32//!             ],
33//!             sql_source: Some("v_user".to_string()),
34//!         }
35//!     ],
36//!     queries: vec![],
37//!     mutations: vec![],
38//!     subscriptions: vec![],
39//! };
40//! ```
41
42use std::collections::HashMap;
43
44use serde::{Deserialize, Serialize};
45
46use crate::validation::ValidationRule;
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 Python decorators).
87    /// Key: table name (e.g., "tf_sales")
88    /// Value: FactTableMetadata as JSON
89    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
90    pub fact_tables: HashMap<String, serde_json::Value>,
91}
92
93impl AuthoringIR {
94    /// Create empty IR.
95    #[must_use]
96    pub fn new() -> Self {
97        Self {
98            types:         Vec::new(),
99            enums:         Vec::new(),
100            interfaces:    Vec::new(),
101            unions:        Vec::new(),
102            input_types:   Vec::new(),
103            scalars:       Vec::new(),
104            queries:       Vec::new(),
105            mutations:     Vec::new(),
106            subscriptions: Vec::new(),
107            fact_tables:   HashMap::new(),
108        }
109    }
110}
111
112impl Default for AuthoringIR {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118/// IR Type definition.
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
120pub struct IRType {
121    /// Type name (e.g., "User").
122    pub name: String,
123
124    /// Field definitions.
125    pub fields: Vec<IRField>,
126
127    /// SQL source (table/view name).
128    pub sql_source: Option<String>,
129
130    /// Type description.
131    pub description: Option<String>,
132}
133
134/// IR Field definition.
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub struct IRField {
137    /// Field name.
138    pub name: String,
139
140    /// Field type (e.g., `"String!"`, `"Int"`, `"[User]"`).
141    pub field_type: String,
142
143    /// Is field nullable?
144    pub nullable: bool,
145
146    /// Field description.
147    pub description: Option<String>,
148
149    /// SQL column mapping.
150    pub sql_column: Option<String>,
151}
152
153/// IR Query definition.
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155pub struct IRQuery {
156    /// Query name (e.g., "users", "user").
157    pub name: String,
158
159    /// Return type name.
160    pub return_type: String,
161
162    /// Does this return a list?
163    pub returns_list: bool,
164
165    /// Is return value nullable?
166    pub nullable: bool,
167
168    /// Query arguments.
169    pub arguments: Vec<IRArgument>,
170
171    /// SQL source (table/view).
172    pub sql_source: Option<String>,
173
174    /// Query description.
175    pub description: Option<String>,
176
177    /// Auto-wired parameters (where, orderBy, limit, offset).
178    pub auto_params: AutoParams,
179}
180
181/// IR Mutation definition.
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct IRMutation {
184    /// Mutation name (e.g., "createUser", "updatePost").
185    pub name: String,
186
187    /// Return type name.
188    pub return_type: String,
189
190    /// Is return value nullable?
191    pub nullable: bool,
192
193    /// Mutation arguments.
194    pub arguments: Vec<IRArgument>,
195
196    /// Mutation description.
197    pub description: Option<String>,
198
199    /// SQL operation type.
200    pub operation: MutationOperation,
201}
202
203/// IR Subscription definition.
204#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
205pub struct IRSubscription {
206    /// Subscription name.
207    pub name: String,
208
209    /// Return type name.
210    pub return_type: String,
211
212    /// Subscription arguments.
213    pub arguments: Vec<IRArgument>,
214
215    /// Subscription description.
216    pub description: Option<String>,
217}
218
219/// IR Argument definition.
220#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
221pub struct IRArgument {
222    /// Argument name.
223    pub name: String,
224
225    /// Argument type.
226    pub arg_type: String,
227
228    /// Is argument nullable?
229    pub nullable: bool,
230
231    /// Default value (as JSON).
232    pub default_value: Option<serde_json::Value>,
233
234    /// Argument description.
235    pub description: Option<String>,
236}
237
238/// Auto-wired parameters configuration.
239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
240pub struct AutoParams {
241    /// Enable WHERE parameter?
242    #[serde(default)]
243    pub has_where: bool,
244
245    /// Enable orderBy parameter?
246    #[serde(default)]
247    pub has_order_by: bool,
248
249    /// Enable limit parameter?
250    #[serde(default)]
251    pub has_limit: bool,
252
253    /// Enable offset parameter?
254    #[serde(default)]
255    pub has_offset: bool,
256}
257
258/// Mutation operation type.
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
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, 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, 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, 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, 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 (as JSON).
352    pub default_value: Option<serde_json::Value>,
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 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}