Skip to main content

fraiseql_core/compiler/window_functions/
codegen.rs

1use super::{
2    FactTableMetadata, FraiseQLError, OrderByClause, PartitionByColumn, Result, SelectColumn,
3    WindowExecutionPlan, WindowFunction, WindowFunctionRequest, WindowFunctionSpec,
4    WindowFunctionType, WindowOrderBy, WindowRequest, WindowSelectColumn,
5};
6
7// =============================================================================
8// WindowPlanner - Converts high-level WindowRequest to WindowExecutionPlan
9// =============================================================================
10
11/// High-level window planner that validates semantic names against metadata.
12///
13/// Converts `WindowRequest` (user-friendly semantic names) to `WindowExecutionPlan`
14/// (SQL expressions ready for execution).
15///
16/// # Example
17///
18/// ```rust,ignore
19/// let request = WindowRequest { ... };
20/// let metadata = FactTableMetadata { ... };
21/// let plan = WindowPlanner::plan(request, &metadata)?;
22/// // plan now has SQL expressions like "dimensions->>'category'" instead of "category"
23/// ```
24pub struct WindowPlanner;
25
26impl WindowPlanner {
27    /// Convert high-level `WindowRequest` to executable `WindowExecutionPlan`.
28    ///
29    /// # Arguments
30    ///
31    /// * `request` - High-level window request with semantic names
32    /// * `metadata` - Fact table metadata for validation and expression generation
33    ///
34    /// # Errors
35    ///
36    /// Returns error if:
37    /// - Referenced measures don't exist in metadata
38    /// - Referenced filter columns don't exist
39    /// - Window function field references are invalid
40    pub fn plan(
41        request: WindowRequest,
42        metadata: &FactTableMetadata,
43    ) -> Result<WindowExecutionPlan> {
44        // Convert select columns to SQL expressions
45        let select = Self::convert_select_columns(&request.select, metadata)?;
46
47        // Convert window functions to SQL expressions
48        let windows = Self::convert_window_functions(&request.windows, metadata)?;
49
50        // Convert final ORDER BY to SQL expressions
51        let order_by = Self::convert_order_by(&request.order_by, metadata)?;
52
53        Ok(WindowExecutionPlan {
54            table: request.table_name,
55            select,
56            windows,
57            where_clause: request.where_clause,
58            order_by,
59            limit: request.limit,
60            offset: request.offset,
61        })
62    }
63
64    /// Convert semantic select columns to SQL expressions.
65    fn convert_select_columns(
66        columns: &[WindowSelectColumn],
67        metadata: &FactTableMetadata,
68    ) -> Result<Vec<SelectColumn>> {
69        columns
70            .iter()
71            .map(|col| Self::convert_single_select_column(col, metadata))
72            .collect()
73    }
74
75    fn convert_single_select_column(
76        column: &WindowSelectColumn,
77        metadata: &FactTableMetadata,
78    ) -> Result<SelectColumn> {
79        match column {
80            WindowSelectColumn::Measure { name, alias } => {
81                // Validate measure exists
82                if !metadata.measures.iter().any(|m| m.name == *name) {
83                    return Err(FraiseQLError::Validation {
84                        message: format!(
85                            "Measure '{}' not found in fact table '{}'",
86                            name, metadata.table_name
87                        ),
88                        path:    None,
89                    });
90                }
91                // Measure columns are direct SQL columns
92                Ok(SelectColumn {
93                    expression: name.clone(),
94                    alias:      alias.clone(),
95                })
96            },
97            WindowSelectColumn::Dimension { path, alias } => {
98                // Dimension from JSONB - generate extraction expression
99                let expression = format!("{}->>'{}'", metadata.dimensions.name, path);
100                Ok(SelectColumn {
101                    expression,
102                    alias: alias.clone(),
103                })
104            },
105            WindowSelectColumn::Filter { name, alias } => {
106                // Validate filter column exists
107                if !metadata.denormalized_filters.iter().any(|f| f.name == *name) {
108                    return Err(FraiseQLError::Validation {
109                        message: format!(
110                            "Filter column '{}' not found in fact table '{}'",
111                            name, metadata.table_name
112                        ),
113                        path:    None,
114                    });
115                }
116                // Filter columns are direct SQL columns
117                Ok(SelectColumn {
118                    expression: name.clone(),
119                    alias:      alias.clone(),
120                })
121            },
122        }
123    }
124
125    /// Convert semantic window functions to SQL expressions.
126    fn convert_window_functions(
127        windows: &[WindowFunctionRequest],
128        metadata: &FactTableMetadata,
129    ) -> Result<Vec<WindowFunction>> {
130        windows
131            .iter()
132            .map(|w| Self::convert_single_window_function(w, metadata))
133            .collect()
134    }
135
136    fn convert_single_window_function(
137        request: &WindowFunctionRequest,
138        metadata: &FactTableMetadata,
139    ) -> Result<WindowFunction> {
140        // Convert function spec to function type
141        let function = Self::convert_function_spec(&request.function, metadata)?;
142
143        // Convert PARTITION BY columns to SQL expressions
144        let partition_by = request
145            .partition_by
146            .iter()
147            .map(|p| Self::convert_partition_by(p, metadata))
148            .collect::<Result<Vec<_>>>()?;
149
150        // Convert ORDER BY within window to SQL expressions
151        let order_by = request
152            .order_by
153            .iter()
154            .map(|o| Self::convert_window_order_by(o, metadata))
155            .collect::<Result<Vec<_>>>()?;
156
157        Ok(WindowFunction {
158            function,
159            alias: request.alias.clone(),
160            partition_by,
161            order_by,
162            frame: request.frame.clone(),
163        })
164    }
165
166    /// Convert high-level function spec to low-level function type with SQL expressions.
167    fn convert_function_spec(
168        spec: &WindowFunctionSpec,
169        metadata: &FactTableMetadata,
170    ) -> Result<WindowFunctionType> {
171        match spec {
172            // Ranking functions - no field conversion needed
173            WindowFunctionSpec::RowNumber => Ok(WindowFunctionType::RowNumber),
174            WindowFunctionSpec::Rank => Ok(WindowFunctionType::Rank),
175            WindowFunctionSpec::DenseRank => Ok(WindowFunctionType::DenseRank),
176            WindowFunctionSpec::Ntile { n } => Ok(WindowFunctionType::Ntile { n: *n }),
177            WindowFunctionSpec::PercentRank => Ok(WindowFunctionType::PercentRank),
178            WindowFunctionSpec::CumeDist => Ok(WindowFunctionType::CumeDist),
179
180            // Value functions - need field conversion
181            WindowFunctionSpec::Lag {
182                field,
183                offset,
184                default,
185            } => {
186                let sql_field = Self::resolve_field_to_sql(field, metadata)?;
187                Ok(WindowFunctionType::Lag {
188                    field:   sql_field,
189                    offset:  *offset,
190                    default: default.clone(),
191                })
192            },
193            WindowFunctionSpec::Lead {
194                field,
195                offset,
196                default,
197            } => {
198                let sql_field = Self::resolve_field_to_sql(field, metadata)?;
199                Ok(WindowFunctionType::Lead {
200                    field:   sql_field,
201                    offset:  *offset,
202                    default: default.clone(),
203                })
204            },
205            WindowFunctionSpec::FirstValue { field } => {
206                let sql_field = Self::resolve_field_to_sql(field, metadata)?;
207                Ok(WindowFunctionType::FirstValue { field: sql_field })
208            },
209            WindowFunctionSpec::LastValue { field } => {
210                let sql_field = Self::resolve_field_to_sql(field, metadata)?;
211                Ok(WindowFunctionType::LastValue { field: sql_field })
212            },
213            WindowFunctionSpec::NthValue { field, n } => {
214                let sql_field = Self::resolve_field_to_sql(field, metadata)?;
215                Ok(WindowFunctionType::NthValue {
216                    field: sql_field,
217                    n:     *n,
218                })
219            },
220
221            // Aggregate as window functions - need measure conversion
222            WindowFunctionSpec::RunningSum { measure } => {
223                Self::validate_measure(measure, metadata)?;
224                Ok(WindowFunctionType::Sum {
225                    field: measure.clone(),
226                })
227            },
228            WindowFunctionSpec::RunningAvg { measure } => {
229                Self::validate_measure(measure, metadata)?;
230                Ok(WindowFunctionType::Avg {
231                    field: measure.clone(),
232                })
233            },
234            WindowFunctionSpec::RunningCount => Ok(WindowFunctionType::Count { field: None }),
235            WindowFunctionSpec::RunningCountField { field } => {
236                let sql_field = Self::resolve_field_to_sql(field, metadata)?;
237                Ok(WindowFunctionType::Count {
238                    field: Some(sql_field),
239                })
240            },
241            WindowFunctionSpec::RunningMin { measure } => {
242                Self::validate_measure(measure, metadata)?;
243                Ok(WindowFunctionType::Min {
244                    field: measure.clone(),
245                })
246            },
247            WindowFunctionSpec::RunningMax { measure } => {
248                Self::validate_measure(measure, metadata)?;
249                Ok(WindowFunctionType::Max {
250                    field: measure.clone(),
251                })
252            },
253            WindowFunctionSpec::RunningStddev { measure } => {
254                Self::validate_measure(measure, metadata)?;
255                Ok(WindowFunctionType::Stddev {
256                    field: measure.clone(),
257                })
258            },
259            WindowFunctionSpec::RunningVariance { measure } => {
260                Self::validate_measure(measure, metadata)?;
261                Ok(WindowFunctionType::Variance {
262                    field: measure.clone(),
263                })
264            },
265        }
266    }
267
268    /// Convert PARTITION BY column to SQL expression.
269    fn convert_partition_by(
270        partition: &PartitionByColumn,
271        metadata: &FactTableMetadata,
272    ) -> Result<String> {
273        match partition {
274            PartitionByColumn::Dimension { path } => {
275                Ok(format!("{}->>'{}'", metadata.dimensions.name, path))
276            },
277            PartitionByColumn::Filter { name } => {
278                if !metadata.denormalized_filters.iter().any(|f| f.name == *name) {
279                    return Err(FraiseQLError::Validation {
280                        message: format!(
281                            "Filter column '{}' not found in fact table '{}'",
282                            name, metadata.table_name
283                        ),
284                        path:    None,
285                    });
286                }
287                Ok(name.clone())
288            },
289            PartitionByColumn::Measure { name } => {
290                Self::validate_measure(name, metadata)?;
291                Ok(name.clone())
292            },
293        }
294    }
295
296    /// Convert window ORDER BY to SQL expression.
297    fn convert_window_order_by(
298        order: &WindowOrderBy,
299        metadata: &FactTableMetadata,
300    ) -> Result<OrderByClause> {
301        let field = Self::resolve_field_to_sql(&order.field, metadata)?;
302        Ok(OrderByClause {
303            field,
304            direction: order.direction,
305        })
306    }
307
308    /// Convert final ORDER BY to SQL expressions.
309    fn convert_order_by(
310        orders: &[WindowOrderBy],
311        metadata: &FactTableMetadata,
312    ) -> Result<Vec<OrderByClause>> {
313        orders.iter().map(|o| Self::convert_window_order_by(o, metadata)).collect()
314    }
315
316    /// Resolve a semantic field name to its SQL expression.
317    ///
318    /// Priority:
319    /// 1. Check if it's a measure (direct column)
320    /// 2. Check if it's a filter column (direct column)
321    /// 3. Treat as a dimension path (JSONB extraction) — only if the name is a valid GraphQL
322    ///    identifier (`[_A-Za-z][_0-9A-Za-z]*`), to prevent SQL injection via the single-quoted key
323    ///    in `data->>'field'` expressions.
324    fn resolve_field_to_sql(field: &str, metadata: &FactTableMetadata) -> Result<String> {
325        // Check if it's a measure
326        if metadata.measures.iter().any(|m| m.name == field) {
327            return Ok(field.to_string());
328        }
329
330        // Check if it's a filter column
331        if metadata.denormalized_filters.iter().any(|f| f.name == field) {
332            return Ok(field.to_string());
333        }
334
335        // Validate identifier before embedding in JSONB extraction expression.
336        // Without this check, a field like "x'; DROP TABLE t; --" would produce
337        // `data->>'x'; DROP TABLE t; --'`, breaking the SQL structure.
338        Self::validate_field_identifier(field)?;
339
340        // Dimension path
341        Ok(format!("{}->>'{}'", metadata.dimensions.name, field))
342    }
343
344    /// Validate that `field` is a safe GraphQL identifier: `[_A-Za-z][_0-9A-Za-z]*`.
345    ///
346    /// Field names are embedded as single-quoted string keys in JSONB extraction
347    /// expressions (`data->>'field'`). Any character outside this set must be rejected.
348    fn validate_field_identifier(field: &str) -> Result<()> {
349        let mut chars = field.chars();
350        let first_ok = chars.next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
351        let rest_ok = chars.all(|c| c.is_ascii_alphanumeric() || c == '_');
352        if first_ok && rest_ok {
353            Ok(())
354        } else {
355            Err(crate::error::FraiseQLError::Validation {
356                message: format!(
357                    "window field '{field}' contains invalid characters; \
358                     only [_A-Za-z][_0-9A-Za-z]* is allowed"
359                ),
360                path:    None,
361            })
362        }
363    }
364
365    /// Validate that a measure exists in metadata.
366    fn validate_measure(measure: &str, metadata: &FactTableMetadata) -> Result<()> {
367        if !metadata.measures.iter().any(|m| m.name == *measure) {
368            return Err(FraiseQLError::Validation {
369                message: format!(
370                    "Measure '{}' not found in fact table '{}'",
371                    measure, metadata.table_name
372                ),
373                path:    None,
374            });
375        }
376        Ok(())
377    }
378}