fraiseql_core/runtime/aggregation/
partial_period_builder.rs1use chrono::NaiveDate;
12
13use super::{AggregationSqlGenerator, Result};
14use crate::{
15 backend::where_clause::WhereClause,
16 compiler::{
17 aggregation::{AggregateExpression, AggregationPlan, GroupByExpression},
18 fact_table::{FactTableMetadata, PartialPeriodConfig, TemporalGrain},
19 },
20 runtime::partial_period::BranchPlan,
21};
22
23#[derive(Debug, Clone)]
25pub struct PartialPeriodSql {
26 pub sql: String,
28 pub params: Vec<serde_json::Value>,
30}
31
32impl AggregationSqlGenerator {
33 pub fn generate_partial_period(
44 &self,
45 plan: &AggregationPlan,
46 config: &PartialPeriodConfig,
47 branch_plan: &BranchPlan,
48 extra_where: Option<&WhereClause>,
49 ) -> Result<PartialPeriodSql> {
50 let mut params: Vec<serde_json::Value> = Vec::new();
51 let mut branches: Vec<String> = Vec::new();
52
53 if let Some((gte, lt)) = branch_plan.partial_leading {
55 let sql = self.build_branch(
56 plan,
57 &config.fine_grain_view,
58 &config.time_grain_column,
59 Some(config.time_grain_trunc),
60 gte,
61 lt,
62 extra_where,
63 &plan.metadata,
64 &mut params,
65 )?;
66 branches.push(sql);
67 }
68
69 if let Some((gte, lt)) = branch_plan.complete_middle {
71 let sql = self.build_branch(
72 plan,
73 &plan.metadata.table_name,
75 &config.time_grain_column,
76 None, gte,
78 lt,
79 extra_where,
80 &plan.metadata,
81 &mut params,
82 )?;
83 branches.push(sql);
84 }
85
86 let (gte, lt) = branch_plan.current_period;
88 let sql = self.build_branch(
89 plan,
90 &config.fine_grain_view,
91 &config.time_grain_column,
92 Some(config.time_grain_trunc),
93 gte,
94 lt,
95 extra_where,
96 &plan.metadata,
97 &mut params,
98 )?;
99 branches.push(sql);
100
101 let union_sql = branches.join("\nUNION ALL\n");
103
104 Ok(PartialPeriodSql {
105 sql: union_sql,
106 params,
107 })
108 }
109
110 #[allow(clippy::too_many_arguments)] fn build_branch(
126 &self,
127 plan: &AggregationPlan,
128 view_name: &str,
129 time_col: &str,
130 trunc_grain: Option<TemporalGrain>,
131 date_gte: NaiveDate,
132 date_lt: NaiveDate,
133 extra_where: Option<&WhereClause>,
134 metadata: &FactTableMetadata,
135 params: &mut Vec<serde_json::Value>,
136 ) -> Result<String> {
137 let select_sql = self.build_branch_select(
139 &plan.group_by_expressions,
140 &plan.aggregate_expressions,
141 time_col,
142 trunc_grain,
143 )?;
144
145 let from_sql = format!("FROM {view_name}");
146
147 let where_sql =
149 self.build_branch_where(time_col, date_gte, date_lt, extra_where, metadata, params)?;
150
151 let group_sql = if !plan.group_by_expressions.is_empty() {
153 self.build_branch_group_by(&plan.group_by_expressions, time_col, trunc_grain)?
154 } else {
155 String::new()
156 };
157
158 let mut parts: Vec<&str> = vec![&select_sql, &from_sql, &where_sql, &group_sql];
159 parts.retain(|s| !s.is_empty());
160
161 Ok(parts.join("\n"))
162 }
163
164 fn build_branch_select(
167 &self,
168 group_by_expressions: &[GroupByExpression],
169 aggregate_expressions: &[AggregateExpression],
170 time_col: &str,
171 trunc_grain: Option<TemporalGrain>,
172 ) -> Result<String> {
173 let mut columns = Vec::new();
174
175 for expr in group_by_expressions {
176 let alias = group_by_alias(expr);
177 let sql = self.branch_group_by_expr_sql(expr, time_col, trunc_grain)?;
178 columns.push(format!("{sql} AS {alias}"));
179 }
180
181 for expr in aggregate_expressions {
182 let column = self.aggregate_expression_to_sql(expr)?;
183 let alias = aggregate_alias(expr);
184 columns.push(format!("{column} AS {alias}"));
185 }
186
187 Ok(format!("SELECT\n {}", columns.join(",\n ")))
188 }
189
190 fn build_branch_where(
192 &self,
193 time_col: &str,
194 date_gte: NaiveDate,
195 date_lt: NaiveDate,
196 extra_where: Option<&WhereClause>,
197 metadata: &FactTableMetadata,
198 params: &mut Vec<serde_json::Value>,
199 ) -> Result<String> {
200 let gte_param =
201 self.emit_value_param(&serde_json::Value::String(date_gte.to_string()), params);
202 let lt_param =
203 self.emit_value_param(&serde_json::Value::String(date_lt.to_string()), params);
204
205 let quoted_col = self.quote_identifier(time_col);
206 let mut where_parts = vec![format!(
207 "{quoted_col} >= {gte_param} AND {quoted_col} < {lt_param}"
208 )];
209
210 if let Some(wc) = extra_where {
211 let extra_sql = self.where_clause_to_sql_parameterized(wc, metadata, params)?;
212 if !extra_sql.is_empty() {
213 where_parts.push(extra_sql);
214 }
215 }
216
217 Ok(format!("WHERE {}", where_parts.join(" AND ")))
218 }
219
220 fn build_branch_group_by(
222 &self,
223 group_by_expressions: &[GroupByExpression],
224 time_col: &str,
225 trunc_grain: Option<TemporalGrain>,
226 ) -> Result<String> {
227 let mut exprs = Vec::new();
228 for expr in group_by_expressions {
229 exprs.push(self.branch_group_by_expr_sql(expr, time_col, trunc_grain)?);
230 }
231 Ok(format!("GROUP BY {}", exprs.join(", ")))
232 }
233
234 fn branch_group_by_expr_sql(
240 &self,
241 expr: &GroupByExpression,
242 time_col: &str,
243 trunc_grain: Option<TemporalGrain>,
244 ) -> Result<String> {
245 match (expr, trunc_grain) {
246 (GroupByExpression::TemporalBucket { column, .. }, Some(grain))
249 if column == time_col =>
250 {
251 Ok(self.temporal_bucket_sql(column, grain.to_temporal_bucket()))
252 },
253
254 (GroupByExpression::NativeColumn { column, .. }, Some(grain)) if column == time_col => {
256 Ok(self.temporal_bucket_sql(column, grain.to_temporal_bucket()))
257 },
258
259 (
262 GroupByExpression::TemporalBucket { column, .. }
263 | GroupByExpression::NativeColumn { column, .. },
264 None,
265 ) if column == time_col => Ok(self.quote_identifier(column)),
266
267 _ => self.group_by_expression_to_sql(expr),
269 }
270 }
271}
272
273fn group_by_alias(expr: &GroupByExpression) -> &str {
275 match expr {
276 GroupByExpression::JsonbPath { alias, .. }
277 | GroupByExpression::TemporalBucket { alias, .. }
278 | GroupByExpression::CalendarPath { alias, .. }
279 | GroupByExpression::TreeLevel { alias, .. }
280 | GroupByExpression::NativeColumn { alias, .. } => alias,
281 }
282}
283
284fn aggregate_alias(expr: &AggregateExpression) -> &str {
286 match expr {
287 AggregateExpression::Count { alias }
288 | AggregateExpression::CountDistinct { alias, .. }
289 | AggregateExpression::MeasureAggregate { alias, .. }
290 | AggregateExpression::AdvancedAggregate { alias, .. }
291 | AggregateExpression::BoolAggregate { alias, .. } => alias,
292 }
293}