Skip to main content

proof_of_sql/sql/proof_plans/
aggregate_exec.rs

1use super::{fold_columns, fold_vals, DynProofPlan};
2use crate::{
3    base::{
4        database::{
5            group_by_util::{aggregate_columns, AggregatedColumns},
6            Column, ColumnField, ColumnRef, ColumnType, LiteralValue, Table, TableEvaluation,
7            TableRef,
8        },
9        map::{IndexMap, IndexSet},
10        proof::{PlaceholderResult, ProofError},
11        scalar::Scalar,
12        slice_ops,
13    },
14    sql::{
15        proof::{
16            FinalRoundBuilder, FirstRoundBuilder, ProofPlan, ProverEvaluate,
17            SumcheckSubpolynomialType, VerificationBuilder,
18        },
19        proof_exprs::{AliasedDynProofExpr, DynProofExpr, ProofExpr},
20        proof_gadgets::{
21            final_round_evaluate_monotonic, first_round_evaluate_monotonic,
22            fold_log_expr::FoldLogExpr, verify_monotonic,
23        },
24    },
25    utils::log,
26};
27use alloc::{boxed::Box, vec, vec::Vec};
28use bumpalo::Bump;
29use core::iter;
30use num_traits::One;
31use serde::{Deserialize, Serialize};
32use snafu::Snafu;
33use sqlparser::ast::Ident;
34use tracing::{span, Level};
35
36/// Errors returned when an aggregate proof plan cannot be constructed.
37#[non_exhaustive]
38#[derive(Clone, Copy, Debug, Eq, PartialEq, Snafu)]
39pub enum AggregateExecError {
40    /// Grouping and deduplication support at most one expression.
41    #[snafu(display("grouping and deduplication support at most one expression, found {count}"))]
42    UnsupportedGroupByExpressionCount {
43        /// Number of grouping expressions found.
44        count: usize,
45    },
46    /// The grouping expression has a datatype for which uniqueness cannot be proven.
47    #[snafu(display("grouping and deduplication do not support expressions of type {data_type}"))]
48    UnsupportedGroupByExpressionType {
49        /// Unsupported grouping datatype.
50        data_type: ColumnType,
51    },
52}
53
54/// Provable expressions for queries of the form
55/// ```ignore
56///     SELECT <group_by_expr1>.expr as <group_by_expr1>.alias, ..., <group_by_exprM>.expr as <group_by_exprM>.alias,
57///         SUM(<sum_expr1>.expr) as <sum_expr1>.alias, ..., SUM(<sum_exprN>.expr) as <sum_exprN>.alias,
58///         COUNT(*) as <count_alias>
59///     FROM <input>
60///     WHERE <where_clause>
61///     GROUP BY <group_by_expr1>.expr, ..., <group_by_exprM>.expr
62/// ```
63///
64/// Note: if `group_by_exprs` is empty, then the query is equivalent to removing the `GROUP BY` clause.
65#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
66pub struct AggregateExec {
67    group_by_exprs: Vec<AliasedDynProofExpr>,
68    sum_expr: Vec<AliasedDynProofExpr>,
69    count_alias: Ident,
70    input: Box<DynProofPlan>,
71    where_clause: DynProofExpr,
72}
73
74impl AggregateExec {
75    /// Creates a new aggregate proof plan.
76    pub fn try_new(
77        group_by_exprs: Vec<AliasedDynProofExpr>,
78        sum_expr: Vec<AliasedDynProofExpr>,
79        count_alias: Ident,
80        input: Box<DynProofPlan>,
81        where_clause: DynProofExpr,
82    ) -> Result<Self, AggregateExecError> {
83        let group_by = Self {
84            group_by_exprs,
85            sum_expr,
86            count_alias,
87            input,
88            where_clause,
89        };
90        group_by.try_get_is_uniqueness_provable()?;
91        Ok(group_by)
92    }
93
94    /// Get a reference to the input plan
95    pub fn input(&self) -> &DynProofPlan {
96        &self.input
97    }
98
99    /// Get a reference to the where clause
100    pub fn where_clause(&self) -> &DynProofExpr {
101        &self.where_clause
102    }
103
104    /// Get a reference to the group by expressions
105    pub fn group_by_exprs(&self) -> &[AliasedDynProofExpr] {
106        &self.group_by_exprs
107    }
108
109    /// Get a reference to the sum expressions
110    pub fn sum_expr(&self) -> &[AliasedDynProofExpr] {
111        &self.sum_expr
112    }
113
114    /// Get a reference to the count alias
115    pub fn count_alias(&self) -> &Ident {
116        &self.count_alias
117    }
118
119    /// Checks if the group by expression can prove uniqueness
120    /// This is true if there is only one group by column and its type is not `VarChar` and not `VarBinary`
121    pub fn try_get_is_uniqueness_provable(&self) -> Result<bool, AggregateExecError> {
122        if let [group_by_expr] = self.group_by_exprs.as_slice() {
123            let data_type = group_by_expr.expr.data_type();
124            if matches!(data_type, ColumnType::VarChar | ColumnType::VarBinary) {
125                Err(AggregateExecError::UnsupportedGroupByExpressionType { data_type })
126            } else {
127                Ok(true)
128            }
129        } else if self.group_by_exprs.is_empty() {
130            Ok(false)
131        } else {
132            Err(AggregateExecError::UnsupportedGroupByExpressionCount {
133                count: self.group_by_exprs.len(),
134            })
135        }
136    }
137}
138
139impl ProofPlan for AggregateExec {
140    fn verifier_evaluate<S: Scalar>(
141        &self,
142        builder: &mut impl VerificationBuilder<S>,
143        accessor: &IndexMap<TableRef, IndexMap<Ident, S>>,
144        chi_eval_map: &IndexMap<TableRef, (S, usize)>,
145        params: &[LiteralValue],
146    ) -> Result<TableEvaluation<S>, ProofError> {
147        let alpha = builder.try_consume_post_result_challenge()?;
148        let beta = builder.try_consume_post_result_challenge()?;
149        let input_eval = self
150            .input
151            .verifier_evaluate(builder, accessor, chi_eval_map, params)?;
152        let input_chi_eval = input_eval.chi_eval();
153        // Build new accessors
154        let input_schema = self.input.get_column_result_fields();
155        // Check for tables - this is just error handling, we don't need the table ref
156        let accessor = input_schema
157            .iter()
158            .zip(input_eval.column_evals())
159            .map(|(field, eval)| (field.name().clone(), *eval))
160            .collect::<IndexMap<_, _>>();
161
162        // Compute g_in_star
163        let fold_gadget = FoldLogExpr::new(alpha, beta);
164        let group_by_evals = self
165            .group_by_exprs
166            .iter()
167            .map(|aliased_expr| {
168                aliased_expr
169                    .expr
170                    .verifier_evaluate(builder, &accessor, input_chi_eval, params)
171            })
172            .collect::<Result<Vec<_>, _>>()?;
173        let g_in_star_eval = fold_gadget
174            .verify_evaluate(builder, &group_by_evals, input_chi_eval)?
175            .0;
176        // End compute g_in_star
177
178        let where_eval =
179            self.where_clause
180                .verifier_evaluate(builder, &accessor, input_chi_eval, params)?;
181
182        // Compute sum_in_fold
183        let aggregate_evals = self
184            .sum_expr
185            .iter()
186            .map(|aliased_expr| {
187                aliased_expr
188                    .expr
189                    .verifier_evaluate(builder, &accessor, input_chi_eval, params)
190            })
191            .collect::<Result<Vec<_>, _>>()?;
192        let sum_in_fold_eval = input_chi_eval + beta * fold_vals(beta, &aggregate_evals);
193        // End compute sum_in_fold
194
195        let output_chi_eval = builder.try_consume_chi_evaluation()?;
196
197        // 3. filtered_columns
198        let group_by_result_columns_evals =
199            builder.try_consume_first_round_mle_evaluations(self.group_by_exprs.len())?;
200        let g_out_star_eval = fold_gadget
201            .verify_evaluate(builder, &group_by_result_columns_evals, output_chi_eval.0)?
202            .0;
203
204        match self.try_get_is_uniqueness_provable() {
205            Ok(true) => {
206                verify_monotonic::<S, true, true>(
207                    builder,
208                    alpha,
209                    beta,
210                    group_by_result_columns_evals[0],
211                    output_chi_eval.0,
212                )?;
213            }
214            Ok(false) => (),
215            Err(_) => {
216                Err(ProofError::UnsupportedQueryPlan {
217                error: "AggregateExec with nonzero grouping columns and without provable uniqueness check not supported.",
218            })?;
219            }
220        }
221
222        let sum_result_columns_evals =
223            builder.try_consume_first_round_mle_evaluations(self.sum_expr.len() + 1)?;
224
225        let sum_out_fold_eval = fold_vals(beta, &sum_result_columns_evals);
226
227        builder.try_produce_sumcheck_subpolynomial_evaluation(
228            SumcheckSubpolynomialType::ZeroSum,
229            g_in_star_eval * where_eval * sum_in_fold_eval - g_out_star_eval * sum_out_fold_eval,
230            3,
231        )?;
232
233        let column_evals = group_by_result_columns_evals
234            .into_iter()
235            .chain(sum_result_columns_evals)
236            .collect::<Vec<_>>();
237        Ok(TableEvaluation::new(column_evals, output_chi_eval))
238    }
239
240    fn get_column_result_fields(&self) -> Vec<ColumnField> {
241        self.group_by_exprs
242            .iter()
243            .map(|aliased_expr| {
244                ColumnField::new(aliased_expr.alias.clone(), aliased_expr.expr.data_type())
245            })
246            .chain(self.sum_expr.iter().map(|aliased_expr| {
247                ColumnField::new(aliased_expr.alias.clone(), aliased_expr.expr.data_type())
248            }))
249            .chain(iter::once(ColumnField::new(
250                self.count_alias.clone(),
251                ColumnType::BigInt,
252            )))
253            .collect()
254    }
255
256    fn get_column_references(&self) -> IndexSet<ColumnRef> {
257        self.input.get_column_references()
258    }
259
260    fn get_table_references(&self) -> IndexSet<TableRef> {
261        self.input.get_table_references()
262    }
263}
264
265impl ProverEvaluate for AggregateExec {
266    #[tracing::instrument(
267        name = "AggregateExec::first_round_evaluate",
268        level = "debug",
269        skip_all
270    )]
271    fn first_round_evaluate<'a, S: Scalar>(
272        &self,
273        builder: &mut FirstRoundBuilder<'a, S>,
274        alloc: &'a Bump,
275        table_map: &IndexMap<TableRef, Table<'a, S>>,
276        params: &[LiteralValue],
277    ) -> PlaceholderResult<Table<'a, S>> {
278        log::log_memory_usage("Start");
279
280        builder.request_post_result_challenges(2);
281
282        let input = self
283            .input
284            .first_round_evaluate(builder, alloc, table_map, params)?;
285
286        // Compute g_in_star
287        let group_by_columns = self
288            .group_by_exprs
289            .iter()
290            .map(|aliased_expr| -> PlaceholderResult<Column<'a, S>> {
291                aliased_expr
292                    .expr
293                    .first_round_evaluate(alloc, &input, params)
294            })
295            .collect::<PlaceholderResult<Vec<_>>>()?;
296        // End compute g_in_star
297
298        let selection_column: Column<'a, S> = self
299            .where_clause
300            .first_round_evaluate(alloc, &input, params)?;
301        let selection = selection_column
302            .as_boolean()
303            .expect("selection is not boolean");
304
305        // Compute sum_in_fold
306        let sum_columns = self
307            .sum_expr
308            .iter()
309            .map(|aliased_expr| -> PlaceholderResult<Column<'a, S>> {
310                aliased_expr
311                    .expr
312                    .first_round_evaluate(alloc, &input, params)
313            })
314            .collect::<PlaceholderResult<Vec<_>>>()?;
315        // End compute sum_in_fold
316
317        // Compute filtered_columns
318        let AggregatedColumns {
319            group_by_columns: group_by_result_columns,
320            sum_columns: sum_result_columns,
321            count_column,
322            ..
323        } = aggregate_columns(alloc, &group_by_columns, &sum_columns, &[], &[], selection)
324            .expect("columns should be aggregatable");
325        for column in &group_by_result_columns {
326            builder.produce_intermediate_mle(*column);
327        }
328
329        builder.produce_chi_evaluation_length(count_column.len());
330
331        let sum_result_columns_iter = sum_result_columns
332            .iter()
333            .map(|col| Column::Scalar(col))
334            .chain(iter::once(Column::BigInt(count_column)));
335        let res = Table::<'a, S>::try_from_iter(
336            self.get_column_result_fields()
337                .into_iter()
338                .map(|field| field.name())
339                .zip(
340                    group_by_result_columns
341                        .iter()
342                        .copied()
343                        .chain(sum_result_columns_iter.clone()),
344                ),
345        )
346        .expect("Failed to create table from column references");
347        // Prove result uniqueness if possible
348        if self
349            .try_get_is_uniqueness_provable()
350            .expect("Group by must be provable")
351        {
352            first_round_evaluate_monotonic(
353                builder,
354                alloc,
355                alloc.alloc_slice_copy(&group_by_result_columns[0].to_scalar()),
356            );
357        }
358        // Produce MLEs
359        for column in sum_result_columns_iter {
360            builder.produce_intermediate_mle(column);
361        }
362
363        log::log_memory_usage("End");
364
365        Ok(res)
366    }
367
368    #[expect(clippy::too_many_lines)]
369    #[tracing::instrument(
370        name = "AggregateExec::final_round_evaluate",
371        level = "debug",
372        skip_all
373    )]
374    fn final_round_evaluate<'a, S: Scalar>(
375        &self,
376        builder: &mut FinalRoundBuilder<'a, S>,
377        alloc: &'a Bump,
378        table_map: &IndexMap<TableRef, Table<'a, S>>,
379        params: &[LiteralValue],
380    ) -> PlaceholderResult<Table<'a, S>> {
381        log::log_memory_usage("Start");
382
383        let alpha = builder.consume_post_result_challenge();
384        let beta = builder.consume_post_result_challenge();
385
386        let input = self
387            .input
388            .final_round_evaluate(builder, alloc, table_map, params)?;
389
390        let n = input.num_rows();
391
392        // Compute g_in_star
393        let group_by_columns = self
394            .group_by_exprs
395            .iter()
396            .map(|aliased_expr| -> PlaceholderResult<Column<'a, S>> {
397                aliased_expr
398                    .expr
399                    .final_round_evaluate(builder, alloc, &input, params)
400            })
401            .collect::<PlaceholderResult<Vec<_>>>()?;
402        let fold_gadget = FoldLogExpr::new(alpha, beta);
403        let g_in_star = fold_gadget
404            .final_round_evaluate(builder, alloc, &group_by_columns, n)
405            .0;
406        // End compute g_in_star
407
408        let selection_column: Column<'a, S> = self
409            .where_clause
410            .final_round_evaluate(builder, alloc, &input, params)?;
411        let selection = selection_column
412            .as_boolean()
413            .expect("selection is not boolean");
414
415        // Compute sum_in_fold
416        let span = span!(
417            Level::DEBUG,
418            "AggregateExec::final_round_evaluate sum_columns"
419        )
420        .entered();
421        let sum_columns = self
422            .sum_expr
423            .iter()
424            .map(|aliased_expr| -> PlaceholderResult<Column<'a, S>> {
425                aliased_expr
426                    .expr
427                    .final_round_evaluate(builder, alloc, &input, params)
428            })
429            .collect::<PlaceholderResult<Vec<_>>>()?;
430        span.exit();
431
432        let span = span!(
433            Level::DEBUG,
434            "AggregateExec::final_round_evaluate allocate sum_in_fold"
435        )
436        .entered();
437        let sum_in_fold = alloc.alloc_slice_fill_copy(n, One::one());
438        span.exit();
439
440        fold_columns(sum_in_fold, beta, beta, &sum_columns);
441        // End compute sum_in_fold
442
443        // 3. Compute filtered_columns
444        let AggregatedColumns {
445            group_by_columns: group_by_result_columns,
446            sum_columns: sum_result_columns,
447            count_column,
448            ..
449        } = aggregate_columns(alloc, &group_by_columns, &sum_columns, &[], &[], selection)
450            .expect("columns should be aggregatable");
451
452        let m = count_column.len();
453
454        let g_out_star = fold_gadget
455            .final_round_evaluate(builder, alloc, &group_by_result_columns, m)
456            .0;
457
458        if self
459            .try_get_is_uniqueness_provable()
460            .expect("Group by must be provable")
461        {
462            let g_out_scalars = group_by_result_columns[0].to_scalar();
463            let alloc_g_out_scalars = alloc.alloc_slice_copy(&g_out_scalars);
464            final_round_evaluate_monotonic::<S, true, true>(
465                builder,
466                alloc,
467                alpha,
468                beta,
469                alloc_g_out_scalars,
470            );
471        }
472
473        // 4. Tally results
474        let sum_result_columns_iter = sum_result_columns.iter().map(|col| Column::Scalar(col));
475        let columns = group_by_result_columns
476            .clone()
477            .into_iter()
478            .chain(sum_result_columns_iter.clone())
479            .chain(iter::once(Column::BigInt(count_column)));
480        let res = Table::<'a, S>::try_from_iter(
481            self.get_column_result_fields()
482                .into_iter()
483                .map(|field| field.name())
484                .zip(columns.clone()),
485        )
486        .expect("Failed to create table from column references");
487        // 5. Prove group by
488
489        let sum_out_fold = alloc.alloc_slice_fill_default(m);
490        slice_ops::slice_cast_mut(count_column, sum_out_fold);
491        fold_columns(sum_out_fold, beta, beta, &sum_result_columns);
492
493        builder.produce_sumcheck_subpolynomial(
494            SumcheckSubpolynomialType::ZeroSum,
495            vec![
496                (
497                    S::one(),
498                    vec![
499                        Box::new(g_in_star as &[_]),
500                        Box::new(selection),
501                        Box::new(sum_in_fold as &[_]),
502                    ],
503                ),
504                (
505                    -S::one(),
506                    vec![Box::new(g_out_star as &[_]), Box::new(sum_out_fold as &[_])],
507                ),
508            ],
509        );
510
511        log::log_memory_usage("End");
512
513        Ok(res)
514    }
515}