fraiseql_core/runtime/aggregation/
partial_period_builder.rs1use chrono::NaiveDate;
12
13use super::{AggregationSqlGenerator, Result};
14use crate::{
15 compiler::{
16 aggregation::{AggregateExpression, AggregationPlan, GroupByExpression},
17 fact_table::{FactTableMetadata, PartialPeriodConfig, TemporalGrain},
18 },
19 db::where_clause::WhereClause,
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.request.table_name,
74 &config.time_grain_column,
75 None, gte,
77 lt,
78 extra_where,
79 &plan.metadata,
80 &mut params,
81 )?;
82 branches.push(sql);
83 }
84
85 let (gte, lt) = branch_plan.current_period;
87 let sql = self.build_branch(
88 plan,
89 &config.fine_grain_view,
90 &config.time_grain_column,
91 Some(config.time_grain_trunc),
92 gte,
93 lt,
94 extra_where,
95 &plan.metadata,
96 &mut params,
97 )?;
98 branches.push(sql);
99
100 let union_sql = branches.join("\nUNION ALL\n");
102
103 Ok(PartialPeriodSql {
104 sql: union_sql,
105 params,
106 })
107 }
108
109 #[allow(clippy::too_many_arguments)] fn build_branch(
125 &self,
126 plan: &AggregationPlan,
127 view_name: &str,
128 time_col: &str,
129 trunc_grain: Option<TemporalGrain>,
130 date_gte: NaiveDate,
131 date_lt: NaiveDate,
132 extra_where: Option<&WhereClause>,
133 metadata: &FactTableMetadata,
134 params: &mut Vec<serde_json::Value>,
135 ) -> Result<String> {
136 let select_sql = self.build_branch_select(
138 &plan.group_by_expressions,
139 &plan.aggregate_expressions,
140 time_col,
141 trunc_grain,
142 )?;
143
144 let from_sql = format!("FROM {view_name}");
145
146 let where_sql =
148 self.build_branch_where(time_col, date_gte, date_lt, extra_where, metadata, params)?;
149
150 let group_sql = if !plan.group_by_expressions.is_empty() {
152 self.build_branch_group_by(&plan.group_by_expressions, time_col, trunc_grain)?
153 } else {
154 String::new()
155 };
156
157 let mut parts: Vec<&str> = vec![&select_sql, &from_sql, &where_sql, &group_sql];
158 parts.retain(|s| !s.is_empty());
159
160 Ok(parts.join("\n"))
161 }
162
163 fn build_branch_select(
166 &self,
167 group_by_expressions: &[GroupByExpression],
168 aggregate_expressions: &[AggregateExpression],
169 time_col: &str,
170 trunc_grain: Option<TemporalGrain>,
171 ) -> Result<String> {
172 let mut columns = Vec::new();
173
174 for expr in group_by_expressions {
175 let alias = group_by_alias(expr);
176 let sql = self.branch_group_by_expr_sql(expr, time_col, trunc_grain)?;
177 columns.push(format!("{sql} AS {alias}"));
178 }
179
180 for expr in aggregate_expressions {
181 let column = self.aggregate_expression_to_sql(expr)?;
182 let alias = aggregate_alias(expr);
183 columns.push(format!("{column} AS {alias}"));
184 }
185
186 Ok(format!("SELECT\n {}", columns.join(",\n ")))
187 }
188
189 fn build_branch_where(
191 &self,
192 time_col: &str,
193 date_gte: NaiveDate,
194 date_lt: NaiveDate,
195 extra_where: Option<&WhereClause>,
196 metadata: &FactTableMetadata,
197 params: &mut Vec<serde_json::Value>,
198 ) -> Result<String> {
199 let gte_param =
200 self.emit_value_param(&serde_json::Value::String(date_gte.to_string()), params);
201 let lt_param =
202 self.emit_value_param(&serde_json::Value::String(date_lt.to_string()), params);
203
204 let quoted_col = self.quote_identifier(time_col);
205 let mut where_parts = vec![format!(
206 "{quoted_col} >= {gte_param} AND {quoted_col} < {lt_param}"
207 )];
208
209 if let Some(wc) = extra_where {
210 let extra_sql = self.where_clause_to_sql_parameterized(wc, metadata, params)?;
211 if !extra_sql.is_empty() {
212 where_parts.push(extra_sql);
213 }
214 }
215
216 Ok(format!("WHERE {}", where_parts.join(" AND ")))
217 }
218
219 fn build_branch_group_by(
221 &self,
222 group_by_expressions: &[GroupByExpression],
223 time_col: &str,
224 trunc_grain: Option<TemporalGrain>,
225 ) -> Result<String> {
226 let mut exprs = Vec::new();
227 for expr in group_by_expressions {
228 exprs.push(self.branch_group_by_expr_sql(expr, time_col, trunc_grain)?);
229 }
230 Ok(format!("GROUP BY {}", exprs.join(", ")))
231 }
232
233 fn branch_group_by_expr_sql(
239 &self,
240 expr: &GroupByExpression,
241 time_col: &str,
242 trunc_grain: Option<TemporalGrain>,
243 ) -> Result<String> {
244 match (expr, trunc_grain) {
245 (GroupByExpression::TemporalBucket { column, .. }, Some(grain))
248 if column == time_col =>
249 {
250 Ok(self.temporal_bucket_sql(column, grain.to_temporal_bucket()))
251 },
252
253 (GroupByExpression::NativeColumn { column, .. }, Some(grain)) if column == time_col => {
255 Ok(self.temporal_bucket_sql(column, grain.to_temporal_bucket()))
256 },
257
258 (
261 GroupByExpression::TemporalBucket { column, .. }
262 | GroupByExpression::NativeColumn { column, .. },
263 None,
264 ) if column == time_col => Ok(self.quote_identifier(column)),
265
266 _ => self.group_by_expression_to_sql(expr),
268 }
269 }
270}
271
272fn group_by_alias(expr: &GroupByExpression) -> &str {
274 match expr {
275 GroupByExpression::JsonbPath { alias, .. }
276 | GroupByExpression::TemporalBucket { alias, .. }
277 | GroupByExpression::CalendarPath { alias, .. }
278 | GroupByExpression::NativeColumn { alias, .. } => alias,
279 }
280}
281
282fn aggregate_alias(expr: &AggregateExpression) -> &str {
284 match expr {
285 AggregateExpression::Count { alias }
286 | AggregateExpression::CountDistinct { alias, .. }
287 | AggregateExpression::MeasureAggregate { alias, .. }
288 | AggregateExpression::AdvancedAggregate { alias, .. }
289 | AggregateExpression::BoolAggregate { alias, .. } => alias,
290 }
291}