Skip to main content

fraiseql_core/compiler/
mod.rs

1//! Schema compiler for FraiseQL v2.
2//!
3//! # Overview
4//!
5//! The compiler transforms GraphQL schema definitions (from Python/TypeScript decorators)
6//! into optimized, executable `CompiledSchema` with pre-generated SQL templates.
7//!
8//! ## Compilation Pipeline
9//!
10//! ```text
11//! JSON Schema (from decorators)
12//!         ↓
13//!    Parser (parse.rs)
14//!         ↓ [Syntax validation]
15//!
16//! Authoring IR (ir.rs)
17//!         ↓
18//!   Validator (validator.rs)
19//!         ↓ [Type checking, name validation]
20//!
21//! Lowering (lowering.rs)
22//!         ↓ [Build optimized IR for code generation]
23//!
24//!   SQL Templates
25//!         ↓ [Database-specific artifact]
26//!
27//!    Codegen (codegen.rs)
28//!         ↓ [Generate runtime schema metadata]
29//!
30//! CompiledSchema JSON
31//!         ↓
32//! Ready for Runtime Execution
33//! ```
34//!
35//! ## Design Principles
36//!
37//! ### 1. Separation of Concerns
38//!
39//! Schema definition (what queries exist?) is kept separate from execution
40//! artifacts (how to execute them?). This allows:
41//! - Different SQL generation strategies (optimize for OLTP vs OLAP)
42//! - Database-specific optimizations without changing schema
43//! - Reuse of schemas across backends
44//! - Testing schema independently from SQL generation
45//!
46//! ### 2. Staged Compilation
47//!
48//! Each phase has a specific responsibility:
49//! - **Parsing**: Convert JSON → AST, syntax validation
50//! - **Validation**: Type checking, semantic validation, circular reference detection
51//! - **Lowering**: Optimize IR, prepare for code generation
52//! - **Codegen**: Generate runtime metadata and schema introspection data
53//!
54//! This separation makes the compiler maintainable, testable, and allows reuse of
55//! phases for different purposes.
56//!
57//! ### 3. Immutable Intermediate State
58//!
59//! Each phase produces immutable data structures (AuthoringIR, CompiledSchema, etc.)
60//! This ensures:
61//! - Reproducible builds (same input = same output)
62//! - Thread-safe processing
63//! - Clear data flow and dependencies
64//! - Easy debugging and verification
65//!
66//! # Phases
67//!
68//! 1. **Parse** (`parser.rs`): JSON schema → Authoring IR
69//!    - Syntax validation
70//!    - AST construction
71//!
72//! 2. **Validate** (`validator.rs`): Type checking and semantic validation
73//!    - Field type binding
74//!    - Circular reference detection
75//!    - Auth rule validation
76//!
77//! 3. **Lower** (`lowering.rs`): IR optimization for execution
78//!    - Fact table extraction
79//!    - Query optimization
80//!    - Template preparation
81//!
82//! 4. **Codegen** (`codegen.rs`): Generate CompiledSchema
83//!    - Runtime metadata
84//!    - Schema introspection data
85//!    - Field mappings
86//!
87//! # Example
88//!
89//! ```rust,no_run
90//! use fraiseql_core::compiler::Compiler;
91//!
92//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
93//! // Create compiler
94//! let compiler = Compiler::new();
95//!
96//! // Compile schema from JSON
97//! let schema_json = r#"{
98//!     "types": [...],
99//!     "queries": [...]
100//! }"#;
101//!
102//! let compiled = compiler.compile(schema_json)?;
103//!
104//! // Output CompiledSchema JSON
105//! let output = compiled.to_json()?;
106//! # Ok(())
107//! # }
108//! ```
109
110pub mod aggregate_types;
111pub mod aggregation;
112mod codegen;
113pub mod compilation_cache;
114pub mod enum_validator;
115pub mod fact_table;
116pub mod ir;
117mod lowering;
118pub mod parser;
119pub mod validator;
120pub mod window_functions;
121
122pub use aggregate_types::{AggregateType, AggregateTypeGenerator, GroupByInput, HavingInput};
123pub use aggregation::{AggregationPlan, AggregationPlanner, AggregationRequest};
124pub use codegen::CodeGenerator;
125pub use compilation_cache::{CompilationCache, CompilationCacheConfig, CompilationCacheMetrics};
126pub use enum_validator::EnumValidator;
127pub use ir::{
128    AuthoringIR, AutoParams, IRArgument, IRField, IRMutation, IRQuery, IRSubscription, IRType,
129    MutationOperation,
130};
131pub use lowering::{DatabaseTarget, SqlTemplateGenerator};
132pub use parser::SchemaParser;
133pub use validator::{SchemaValidator, ValidationError};
134pub use window_functions::{WindowExecutionPlan, WindowFunction, WindowFunctionPlanner};
135
136use crate::{error::Result, schema::CompiledSchema};
137
138/// Compiler configuration.
139#[derive(Debug, Clone)]
140pub struct CompilerConfig {
141    /// Target database for SQL generation.
142    pub database_target: DatabaseTarget,
143
144    /// Enable SQL template optimization.
145    pub optimize_sql: bool,
146
147    /// Strict mode: Fail on warnings.
148    pub strict_mode: bool,
149
150    /// Enable debug output.
151    pub debug: bool,
152
153    /// Database URL for fact table introspection (optional).
154    /// If provided, compiler will auto-detect fact tables and generate aggregate types.
155    pub database_url: Option<String>,
156}
157
158impl Default for CompilerConfig {
159    fn default() -> Self {
160        Self {
161            database_target: DatabaseTarget::PostgreSQL,
162            optimize_sql:    true,
163            strict_mode:     false,
164            debug:           false,
165            database_url:    None,
166        }
167    }
168}
169
170/// Schema compiler.
171///
172/// Transforms authoring-time schema definitions into runtime-optimized
173/// `CompiledSchema` with pre-generated SQL templates.
174///
175/// # Example
176///
177/// ```rust,no_run
178/// use fraiseql_core::compiler::{Compiler, CompilerConfig, DatabaseTarget};
179///
180/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
181/// let config = CompilerConfig {
182///     database_target: DatabaseTarget::PostgreSQL,
183///     optimize_sql: true,
184///     ..Default::default()
185/// };
186///
187/// let compiler = Compiler::with_config(config);
188/// let compiled = compiler.compile(r#"{"types": [], "queries": []}"#)?;
189/// # Ok(())
190/// # }
191/// ```
192pub struct Compiler {
193    config:    CompilerConfig,
194    parser:    SchemaParser,
195    validator: SchemaValidator,
196    lowering:  SqlTemplateGenerator,
197    codegen:   CodeGenerator,
198}
199
200impl Compiler {
201    /// Create new compiler with default configuration.
202    #[must_use]
203    pub fn new() -> Self {
204        Self::with_config(CompilerConfig::default())
205    }
206
207    /// Create new compiler with custom configuration.
208    #[must_use]
209    pub fn with_config(config: CompilerConfig) -> Self {
210        Self {
211            parser: SchemaParser::new(),
212            validator: SchemaValidator::new(),
213            lowering: SqlTemplateGenerator::new(config.database_target),
214            codegen: CodeGenerator::new(config.optimize_sql),
215            config,
216        }
217    }
218
219    /// Compile schema from JSON.
220    ///
221    /// # Arguments
222    ///
223    /// * `schema_json` - JSON schema from Python/TypeScript decorators
224    ///
225    /// # Returns
226    ///
227    /// Compiled schema with pre-generated SQL templates
228    ///
229    /// # Errors
230    ///
231    /// Returns error if:
232    /// - JSON parsing fails
233    /// - Schema validation fails
234    /// - SQL template generation fails
235    ///
236    /// # Example
237    ///
238    /// ```rust,no_run
239    /// use fraiseql_core::compiler::Compiler;
240    ///
241    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
242    /// let compiler = Compiler::new();
243    /// let schema_json = r#"{"types": [], "queries": []}"#;
244    /// let compiled = compiler.compile(schema_json)?;
245    /// # Ok(())
246    /// # }
247    /// ```
248    pub fn compile(&self, schema_json: &str) -> Result<CompiledSchema> {
249        // Parse JSON → Authoring IR
250        tracing::debug!("Parsing schema...");
251        let ir = self.parser.parse(schema_json)?;
252
253        // Validate IR
254        tracing::debug!("Validating schema...");
255        let validated_ir = self.validator.validate(ir)?;
256
257        // Lower IR → SQL templates
258        tracing::debug!("Generating SQL templates...");
259        let sql_templates = self.lowering.generate(&validated_ir)?;
260
261        // Codegen SQL templates → CompiledSchema
262        tracing::debug!("Generating CompiledSchema...");
263        let compiled = self.codegen.generate(&validated_ir, &sql_templates)?;
264
265        // Note: Fact table metadata will be added by external tools or
266        // through explicit API calls (e.g., from Python decorators)
267
268        tracing::debug!("Compilation complete!");
269
270        Ok(compiled)
271    }
272
273    /// Get compiler configuration.
274    #[must_use]
275    pub const fn config(&self) -> &CompilerConfig {
276        &self.config
277    }
278}
279
280impl Default for Compiler {
281    fn default() -> Self {
282        Self::new()
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_compiler_new() {
292        let compiler = Compiler::new();
293        assert_eq!(compiler.config.database_target, DatabaseTarget::PostgreSQL);
294        assert!(compiler.config.optimize_sql);
295    }
296
297    #[test]
298    fn test_compiler_with_config() {
299        let config = CompilerConfig {
300            database_target: DatabaseTarget::MySQL,
301            optimize_sql:    false,
302            strict_mode:     true,
303            debug:           true,
304            database_url:    None,
305        };
306
307        let compiler = Compiler::with_config(config);
308        assert_eq!(compiler.config.database_target, DatabaseTarget::MySQL);
309        assert!(!compiler.config.optimize_sql);
310        assert!(compiler.config.strict_mode);
311        assert!(compiler.config.debug);
312    }
313
314    #[test]
315    fn test_default_config() {
316        let config = CompilerConfig::default();
317        assert_eq!(config.database_target, DatabaseTarget::PostgreSQL);
318        assert!(config.optimize_sql);
319        assert!(!config.strict_mode);
320        assert!(!config.debug);
321        assert!(config.database_url.is_none());
322    }
323
324    #[test]
325    fn test_compile_schema_with_fact_tables() {
326        let compiler = Compiler::new();
327        let schema_json = r#"{
328            "types": [],
329            "queries": [],
330            "mutations": []
331        }"#;
332
333        let result = compiler.compile(schema_json);
334        assert!(result.is_ok());
335
336        let compiled = result.unwrap();
337        assert_eq!(compiled.fact_tables.len(), 0);
338    }
339
340    #[test]
341    fn test_compiled_schema_fact_table_operations() {
342        let mut schema = CompiledSchema::new();
343
344        // Test adding fact table
345        let metadata = serde_json::json!({
346            "table_name": "tf_sales",
347            "measures": [],
348            "dimensions": {"name": "data"},
349            "denormalized_filters": []
350        });
351
352        schema.add_fact_table("tf_sales".to_string(), metadata.clone());
353
354        // Test has_fact_tables
355        assert!(schema.has_fact_tables());
356
357        // Test list_fact_tables
358        let tables = schema.list_fact_tables();
359        assert_eq!(tables.len(), 1);
360        assert!(tables.contains(&"tf_sales"));
361
362        // Test get_fact_table
363        let retrieved = schema.get_fact_table("tf_sales");
364        assert!(retrieved.is_some());
365        assert_eq!(retrieved.unwrap(), &metadata);
366
367        // Test get non-existent table
368        assert!(schema.get_fact_table("tf_nonexistent").is_none());
369    }
370}