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}