Skip to main content

icydb_core/db/session/
sql.rs

1use crate::{
2    db::{
3        DbSession, EntityFieldDescription, EntityResponse, EntitySchemaDescription,
4        MissingRowPolicy, PagedGroupedExecutionWithTrace, ProjectedRow, ProjectionResponse, Query,
5        QueryError,
6        query::{
7            builder::aggregate::{AggregateExpr, avg, count, count_by, max_by, min_by, sum},
8            intent::IntentError,
9            plan::{
10                AggregateKind, FieldSlot, QueryMode,
11                expr::{Expr, ProjectionField},
12            },
13        },
14        sql::lowering::{
15            PreparedSqlStatement as CorePreparedSqlStatement, SqlCommand,
16            SqlGlobalAggregateCommand, SqlGlobalAggregateTerminal, SqlLoweringError,
17            compile_sql_command, compile_sql_command_from_prepared_statement,
18            compile_sql_global_aggregate_command, prepare_sql_statement,
19        },
20        sql::parser::{SqlExplainMode, SqlExplainTarget, SqlStatement, parse_sql},
21    },
22    error::{ErrorClass, ErrorOrigin, InternalError},
23    traits::{CanisterKind, EntityKind, EntityValue},
24    value::Value,
25};
26
27///
28/// SqlStatementRoute
29///
30/// Canonical SQL statement routing metadata derived from reduced SQL parser output.
31/// Carries surface kind (`Query` / `Explain` / `Describe` / `ShowIndexes` /
32/// `ShowColumns` / `ShowEntities`) and canonical parsed entity identifier.
33///
34#[derive(Clone, Debug, Eq, PartialEq)]
35pub enum SqlStatementRoute {
36    Query { entity: String },
37    Explain { entity: String },
38    Describe { entity: String },
39    ShowIndexes { entity: String },
40    ShowColumns { entity: String },
41    ShowEntities,
42}
43
44///
45/// SqlDispatchResult
46///
47/// Unified SQL dispatch payload returned by shared SQL lane execution.
48///
49#[derive(Debug)]
50pub enum SqlDispatchResult<E: EntityKind> {
51    Projection {
52        columns: Vec<String>,
53        projection: ProjectionResponse<E>,
54    },
55    Explain(String),
56    Describe(EntitySchemaDescription),
57    ShowIndexes(Vec<String>),
58    ShowColumns(Vec<EntityFieldDescription>),
59    ShowEntities(Vec<String>),
60}
61
62///
63/// SqlParsedStatement
64///
65/// Opaque parsed SQL statement envelope with stable route metadata.
66/// This allows callers to parse once and reuse parsed authority across
67/// route classification and typed dispatch lowering.
68///
69#[derive(Clone, Debug)]
70pub struct SqlParsedStatement {
71    statement: SqlStatement,
72    route: SqlStatementRoute,
73}
74
75impl SqlParsedStatement {
76    /// Borrow canonical route metadata for this parsed statement.
77    #[must_use]
78    pub const fn route(&self) -> &SqlStatementRoute {
79        &self.route
80    }
81}
82
83///
84/// SqlPreparedStatement
85///
86/// Opaque reduced SQL envelope prepared for one concrete entity route.
87/// This wraps entity-scope normalization and fail-closed entity matching
88/// so dynamic dispatch can share prepare/lower control flow before execution.
89///
90
91#[derive(Clone, Debug)]
92pub struct SqlPreparedStatement {
93    prepared: CorePreparedSqlStatement,
94}
95
96impl SqlStatementRoute {
97    /// Borrow the parsed SQL entity identifier for this statement.
98    ///
99    /// `SHOW ENTITIES` does not carry an entity identifier and returns an
100    /// empty string for this accessor.
101    #[must_use]
102    pub const fn entity(&self) -> &str {
103        match self {
104            Self::Query { entity }
105            | Self::Explain { entity }
106            | Self::Describe { entity }
107            | Self::ShowIndexes { entity }
108            | Self::ShowColumns { entity } => entity.as_str(),
109            Self::ShowEntities => "",
110        }
111    }
112
113    /// Return whether this route targets the EXPLAIN surface.
114    #[must_use]
115    pub const fn is_explain(&self) -> bool {
116        matches!(self, Self::Explain { .. })
117    }
118
119    /// Return whether this route targets the DESCRIBE surface.
120    #[must_use]
121    pub const fn is_describe(&self) -> bool {
122        matches!(self, Self::Describe { .. })
123    }
124
125    /// Return whether this route targets the `SHOW INDEXES` surface.
126    #[must_use]
127    pub const fn is_show_indexes(&self) -> bool {
128        matches!(self, Self::ShowIndexes { .. })
129    }
130
131    /// Return whether this route targets the `SHOW COLUMNS` surface.
132    #[must_use]
133    pub const fn is_show_columns(&self) -> bool {
134        matches!(self, Self::ShowColumns { .. })
135    }
136
137    /// Return whether this route targets the `SHOW ENTITIES` surface.
138    #[must_use]
139    pub const fn is_show_entities(&self) -> bool {
140        matches!(self, Self::ShowEntities)
141    }
142}
143
144// Canonical reduced SQL lane kind used by session entrypoint gate checks.
145#[derive(Clone, Copy, Debug, Eq, PartialEq)]
146enum SqlLaneKind {
147    Query,
148    Explain,
149    Describe,
150    ShowIndexes,
151    ShowColumns,
152    ShowEntities,
153}
154
155// Session SQL surfaces that enforce explicit wrong-lane fail-closed contracts.
156#[derive(Clone, Copy, Debug, Eq, PartialEq)]
157enum SqlSurface {
158    QueryFrom,
159    Explain,
160}
161
162// Resolve one lowered SQL command to its canonical lane kind.
163const fn sql_command_lane<E: EntityKind>(command: &SqlCommand<E>) -> SqlLaneKind {
164    match command {
165        SqlCommand::Query(_) => SqlLaneKind::Query,
166        SqlCommand::Explain { .. } | SqlCommand::ExplainGlobalAggregate { .. } => {
167            SqlLaneKind::Explain
168        }
169        SqlCommand::DescribeEntity => SqlLaneKind::Describe,
170        SqlCommand::ShowIndexesEntity => SqlLaneKind::ShowIndexes,
171        SqlCommand::ShowColumnsEntity => SqlLaneKind::ShowColumns,
172        SqlCommand::ShowEntities => SqlLaneKind::ShowEntities,
173    }
174}
175
176// Render one deterministic unsupported-lane message for one SQL surface.
177const fn unsupported_sql_lane_message(surface: SqlSurface, lane: SqlLaneKind) -> &'static str {
178    match (surface, lane) {
179        (SqlSurface::QueryFrom, SqlLaneKind::Explain) => {
180            "query_from_sql does not accept EXPLAIN statements; use execute_sql_dispatch(...)"
181        }
182        (SqlSurface::QueryFrom, SqlLaneKind::Describe) => {
183            "query_from_sql does not accept DESCRIBE statements; use execute_sql_dispatch(...)"
184        }
185        (SqlSurface::QueryFrom, SqlLaneKind::ShowIndexes) => {
186            "query_from_sql does not accept SHOW INDEXES statements; use execute_sql_dispatch(...)"
187        }
188        (SqlSurface::QueryFrom, SqlLaneKind::ShowColumns) => {
189            "query_from_sql does not accept SHOW COLUMNS statements; use execute_sql_dispatch(...)"
190        }
191        (SqlSurface::QueryFrom, SqlLaneKind::ShowEntities) => {
192            "query_from_sql does not accept SHOW ENTITIES/SHOW TABLES statements; use execute_sql_dispatch(...)"
193        }
194        (SqlSurface::QueryFrom, SqlLaneKind::Query) => {
195            "query_from_sql requires one executable SELECT or DELETE statement"
196        }
197        (SqlSurface::Explain, SqlLaneKind::Describe) => {
198            "explain_sql does not accept DESCRIBE statements; use execute_sql_dispatch(...)"
199        }
200        (SqlSurface::Explain, SqlLaneKind::ShowIndexes) => {
201            "explain_sql does not accept SHOW INDEXES statements; use execute_sql_dispatch(...)"
202        }
203        (SqlSurface::Explain, SqlLaneKind::ShowColumns) => {
204            "explain_sql does not accept SHOW COLUMNS statements; use execute_sql_dispatch(...)"
205        }
206        (SqlSurface::Explain, SqlLaneKind::ShowEntities) => {
207            "explain_sql does not accept SHOW ENTITIES/SHOW TABLES statements; use execute_sql_dispatch(...)"
208        }
209        (SqlSurface::Explain, SqlLaneKind::Query | SqlLaneKind::Explain) => {
210            "explain_sql requires an EXPLAIN statement"
211        }
212    }
213}
214
215// Build one unsupported execution error for wrong-lane SQL surface usage.
216fn unsupported_sql_lane_error(surface: SqlSurface, lane: SqlLaneKind) -> QueryError {
217    QueryError::execute(InternalError::classified(
218        ErrorClass::Unsupported,
219        ErrorOrigin::Query,
220        unsupported_sql_lane_message(surface, lane),
221    ))
222}
223
224// Compile one reduced SQL statement with default lane behavior used by SQL surfaces.
225fn compile_sql_command_ignore<E: EntityKind>(sql: &str) -> Result<SqlCommand<E>, QueryError> {
226    compile_sql_command::<E>(sql, MissingRowPolicy::Ignore).map_err(map_sql_lowering_error)
227}
228
229// Map SQL frontend parse/lowering failures into query-facing execution errors.
230fn map_sql_lowering_error(err: SqlLoweringError) -> QueryError {
231    match err {
232        SqlLoweringError::Query(err) => err,
233        SqlLoweringError::Parse(crate::db::sql::parser::SqlParseError::UnsupportedFeature {
234            feature,
235        }) => QueryError::execute(InternalError::query_unsupported_sql_feature(feature)),
236        other => QueryError::execute(InternalError::classified(
237            ErrorClass::Unsupported,
238            ErrorOrigin::Query,
239            format!("SQL query is not executable in this release: {other}"),
240        )),
241    }
242}
243
244// Map reduced SQL parse failures through the same query-facing classification
245// policy used by SQL lowering entrypoints.
246fn map_sql_parse_error(err: crate::db::sql::parser::SqlParseError) -> QueryError {
247    map_sql_lowering_error(SqlLoweringError::Parse(err))
248}
249
250// Resolve one parsed reduced SQL statement to canonical surface route metadata.
251fn sql_statement_route_from_statement(statement: &SqlStatement) -> SqlStatementRoute {
252    match statement {
253        SqlStatement::Select(select) => SqlStatementRoute::Query {
254            entity: select.entity.clone(),
255        },
256        SqlStatement::Delete(delete) => SqlStatementRoute::Query {
257            entity: delete.entity.clone(),
258        },
259        SqlStatement::Explain(explain) => match &explain.statement {
260            SqlExplainTarget::Select(select) => SqlStatementRoute::Explain {
261                entity: select.entity.clone(),
262            },
263            SqlExplainTarget::Delete(delete) => SqlStatementRoute::Explain {
264                entity: delete.entity.clone(),
265            },
266        },
267        SqlStatement::Describe(describe) => SqlStatementRoute::Describe {
268            entity: describe.entity.clone(),
269        },
270        SqlStatement::ShowIndexes(show_indexes) => SqlStatementRoute::ShowIndexes {
271            entity: show_indexes.entity.clone(),
272        },
273        SqlStatement::ShowColumns(show_columns) => SqlStatementRoute::ShowColumns {
274            entity: show_columns.entity.clone(),
275        },
276        SqlStatement::ShowEntities(_) => SqlStatementRoute::ShowEntities,
277    }
278}
279
280// Resolve one aggregate target field through planner slot contracts before
281// aggregate terminal execution.
282fn resolve_sql_aggregate_target_slot<E: EntityKind>(field: &str) -> Result<FieldSlot, QueryError> {
283    FieldSlot::resolve(E::MODEL, field).ok_or_else(|| {
284        QueryError::execute(crate::db::error::executor_unsupported(format!(
285            "unknown aggregate target field: {field}",
286        )))
287    })
288}
289
290// Convert one lowered global SQL aggregate terminal into aggregate expression
291// contracts used by aggregate explain execution descriptors.
292fn sql_global_aggregate_terminal_to_expr<E: EntityKind>(
293    terminal: &SqlGlobalAggregateTerminal,
294) -> Result<AggregateExpr, QueryError> {
295    match terminal {
296        SqlGlobalAggregateTerminal::CountRows => Ok(count()),
297        SqlGlobalAggregateTerminal::CountField(field) => {
298            let _ = resolve_sql_aggregate_target_slot::<E>(field)?;
299
300            Ok(count_by(field.as_str()))
301        }
302        SqlGlobalAggregateTerminal::SumField(field) => {
303            let _ = resolve_sql_aggregate_target_slot::<E>(field)?;
304
305            Ok(sum(field.as_str()))
306        }
307        SqlGlobalAggregateTerminal::AvgField(field) => {
308            let _ = resolve_sql_aggregate_target_slot::<E>(field)?;
309
310            Ok(avg(field.as_str()))
311        }
312        SqlGlobalAggregateTerminal::MinField(field) => {
313            let _ = resolve_sql_aggregate_target_slot::<E>(field)?;
314
315            Ok(min_by(field.as_str()))
316        }
317        SqlGlobalAggregateTerminal::MaxField(field) => {
318            let _ = resolve_sql_aggregate_target_slot::<E>(field)?;
319
320            Ok(max_by(field.as_str()))
321        }
322    }
323}
324
325// Render one aggregate expression into a canonical projection column label.
326fn projection_label_from_aggregate(aggregate: &AggregateExpr) -> String {
327    let kind = match aggregate.kind() {
328        AggregateKind::Count => "COUNT",
329        AggregateKind::Sum => "SUM",
330        AggregateKind::Avg => "AVG",
331        AggregateKind::Exists => "EXISTS",
332        AggregateKind::First => "FIRST",
333        AggregateKind::Last => "LAST",
334        AggregateKind::Min => "MIN",
335        AggregateKind::Max => "MAX",
336    };
337    let distinct = if aggregate.is_distinct() {
338        "DISTINCT "
339    } else {
340        ""
341    };
342
343    if let Some(field) = aggregate.target_field() {
344        return format!("{kind}({distinct}{field})");
345    }
346
347    format!("{kind}({distinct}*)")
348}
349
350// Render one projection expression into a canonical output label.
351fn projection_label_from_expr(expr: &Expr, ordinal: usize) -> String {
352    match expr {
353        Expr::Field(field) => field.as_str().to_string(),
354        Expr::Aggregate(aggregate) => projection_label_from_aggregate(aggregate),
355        Expr::Alias { name, .. } => name.as_str().to_string(),
356        Expr::Literal(_) | Expr::Unary { .. } | Expr::Binary { .. } => {
357            format!("expr_{ordinal}")
358        }
359    }
360}
361
362// Derive canonical projection column labels from one planned query projection spec.
363fn projection_labels_from_query<E: EntityKind>(
364    query: &Query<E>,
365) -> Result<Vec<String>, QueryError> {
366    let projection = query.plan()?.projection_spec();
367    let mut labels = Vec::with_capacity(projection.len());
368
369    for (ordinal, field) in projection.fields().enumerate() {
370        match field {
371            ProjectionField::Scalar {
372                expr: _,
373                alias: Some(alias),
374            } => labels.push(alias.as_str().to_string()),
375            ProjectionField::Scalar { expr, alias: None } => {
376                labels.push(projection_label_from_expr(expr, ordinal));
377            }
378        }
379    }
380
381    Ok(labels)
382}
383
384// Derive canonical full-entity projection labels in declared model order.
385fn projection_labels_from_entity_model<E: EntityKind>() -> Vec<String> {
386    E::MODEL
387        .fields
388        .iter()
389        .map(|field| field.name.to_string())
390        .collect()
391}
392
393// Rebind one materialized entity response to a projection response in declared
394// model field order so unified SQL dispatch can surface DELETE row payloads
395// through the same row-oriented result contract as SELECT.
396fn projection_from_entity_response<E>(response: EntityResponse<E>) -> ProjectionResponse<E>
397where
398    E: EntityKind + EntityValue,
399{
400    let projected = response
401        .rows()
402        .into_iter()
403        .map(|row| {
404            let (id, entity) = row.into_parts();
405            let values = (0..E::MODEL.fields.len())
406                .map(|index| entity.get_value_by_index(index).unwrap_or(Value::Null))
407                .collect();
408
409            ProjectedRow::new(id, values)
410        })
411        .collect();
412
413    ProjectionResponse::new(projected)
414}
415
416impl<C: CanisterKind> DbSession<C> {
417    // Execute one lowered query/explain SQL command and reject non-query lanes.
418    fn execute_sql_dispatch_query_lane_from_command<E>(
419        &self,
420        command: SqlCommand<E>,
421        lane: SqlLaneKind,
422    ) -> Result<SqlDispatchResult<E>, QueryError>
423    where
424        E: EntityKind<Canister = C> + EntityValue,
425    {
426        match command {
427            SqlCommand::Query(query) => {
428                if query.has_grouping() {
429                    return Err(QueryError::Intent(
430                        IntentError::GroupedRequiresExecuteGrouped,
431                    ));
432                }
433
434                match query.mode() {
435                    QueryMode::Load(_) => {
436                        let columns = projection_labels_from_query(&query)?;
437                        let projection = self.execute_load_query_with(&query, |load, plan| {
438                            load.execute_projection(plan)
439                        })?;
440
441                        Ok(SqlDispatchResult::Projection {
442                            columns,
443                            projection,
444                        })
445                    }
446                    QueryMode::Delete(_) => {
447                        let columns = projection_labels_from_entity_model::<E>();
448                        let deleted = self.execute_query(&query)?;
449                        let projection = projection_from_entity_response(deleted);
450
451                        Ok(SqlDispatchResult::Projection {
452                            columns,
453                            projection,
454                        })
455                    }
456                }
457            }
458            SqlCommand::Explain { .. } | SqlCommand::ExplainGlobalAggregate { .. } => {
459                Self::explain_sql_from_command::<E>(command, lane).map(SqlDispatchResult::Explain)
460            }
461            SqlCommand::DescribeEntity
462            | SqlCommand::ShowIndexesEntity
463            | SqlCommand::ShowColumnsEntity
464            | SqlCommand::ShowEntities => Err(QueryError::execute(InternalError::classified(
465                ErrorClass::Unsupported,
466                ErrorOrigin::Query,
467                "query-lane SQL dispatch only accepts SELECT, DELETE, and EXPLAIN statements",
468            ))),
469        }
470    }
471
472    // Render one EXPLAIN payload from one already-lowered SQL command.
473    fn explain_sql_from_command<E>(
474        command: SqlCommand<E>,
475        lane: SqlLaneKind,
476    ) -> Result<String, QueryError>
477    where
478        E: EntityKind<Canister = C> + EntityValue,
479    {
480        match command {
481            SqlCommand::Query(_)
482            | SqlCommand::DescribeEntity
483            | SqlCommand::ShowIndexesEntity
484            | SqlCommand::ShowColumnsEntity
485            | SqlCommand::ShowEntities => {
486                Err(unsupported_sql_lane_error(SqlSurface::Explain, lane))
487            }
488            SqlCommand::Explain { mode, query } => match mode {
489                SqlExplainMode::Plan => Ok(query.explain()?.render_text_canonical()),
490                SqlExplainMode::Execution => query.explain_execution_text(),
491                SqlExplainMode::Json => Ok(query.explain()?.render_json_canonical()),
492            },
493            SqlCommand::ExplainGlobalAggregate { mode, command } => {
494                Self::explain_sql_global_aggregate::<E>(mode, command)
495            }
496        }
497    }
498
499    /// Parse one reduced SQL statement and return one reusable parsed envelope.
500    ///
501    /// This method is the SQL parse authority for dynamic route selection.
502    pub fn parse_sql_statement(&self, sql: &str) -> Result<SqlParsedStatement, QueryError> {
503        let statement = parse_sql(sql).map_err(map_sql_parse_error)?;
504        let route = sql_statement_route_from_statement(&statement);
505
506        Ok(SqlParsedStatement { statement, route })
507    }
508
509    /// Parse one reduced SQL statement into canonical routing metadata.
510    ///
511    /// This method is the SQL dispatch authority for entity/surface routing
512    /// outside typed-entity lowering paths.
513    pub fn sql_statement_route(&self, sql: &str) -> Result<SqlStatementRoute, QueryError> {
514        let parsed = self.parse_sql_statement(sql)?;
515
516        Ok(parsed.route().clone())
517    }
518
519    /// Prepare one parsed reduced SQL statement for one concrete entity route.
520    ///
521    /// This method is the shared lowering authority for dynamic SQL dispatch
522    /// before lane callback execution.
523    pub fn prepare_sql_dispatch_parsed(
524        &self,
525        parsed: &SqlParsedStatement,
526        expected_entity: &'static str,
527    ) -> Result<SqlPreparedStatement, QueryError> {
528        let prepared = prepare_sql_statement(parsed.statement.clone(), expected_entity)
529            .map_err(map_sql_lowering_error)?;
530
531        Ok(SqlPreparedStatement { prepared })
532    }
533
534    /// Build one typed query intent from one reduced SQL statement.
535    ///
536    /// This parser/lowering entrypoint is intentionally constrained to the
537    /// executable subset wired in the current release.
538    pub fn query_from_sql<E>(&self, sql: &str) -> Result<Query<E>, QueryError>
539    where
540        E: EntityKind<Canister = C>,
541    {
542        let command = compile_sql_command_ignore::<E>(sql)?;
543        let lane = sql_command_lane(&command);
544
545        match command {
546            SqlCommand::Query(query) => Ok(query),
547            SqlCommand::Explain { .. }
548            | SqlCommand::ExplainGlobalAggregate { .. }
549            | SqlCommand::DescribeEntity
550            | SqlCommand::ShowIndexesEntity
551            | SqlCommand::ShowColumnsEntity
552            | SqlCommand::ShowEntities => {
553                Err(unsupported_sql_lane_error(SqlSurface::QueryFrom, lane))
554            }
555        }
556    }
557
558    /// Execute one reduced SQL `SELECT`/`DELETE` statement for entity `E`.
559    pub fn execute_sql<E>(&self, sql: &str) -> Result<EntityResponse<E>, QueryError>
560    where
561        E: EntityKind<Canister = C> + EntityValue,
562    {
563        let query = self.query_from_sql::<E>(sql)?;
564        if query.has_grouping() {
565            return Err(QueryError::Intent(
566                IntentError::GroupedRequiresExecuteGrouped,
567            ));
568        }
569
570        self.execute_query(&query)
571    }
572
573    /// Execute one reduced SQL global aggregate `SELECT` statement.
574    ///
575    /// This entrypoint is intentionally constrained to one aggregate terminal
576    /// shape per statement and preserves existing terminal semantics.
577    pub fn execute_sql_aggregate<E>(&self, sql: &str) -> Result<Value, QueryError>
578    where
579        E: EntityKind<Canister = C> + EntityValue,
580    {
581        let command = compile_sql_global_aggregate_command::<E>(sql, MissingRowPolicy::Ignore)
582            .map_err(map_sql_lowering_error)?;
583
584        match command.terminal() {
585            SqlGlobalAggregateTerminal::CountRows => self
586                .execute_load_query_with(command.query(), |load, plan| load.aggregate_count(plan))
587                .map(|count| Value::Uint(u64::from(count))),
588            SqlGlobalAggregateTerminal::CountField(field) => {
589                let target_slot = resolve_sql_aggregate_target_slot::<E>(field)?;
590                self.execute_load_query_with(command.query(), |load, plan| {
591                    load.values_by_slot(plan, target_slot)
592                })
593                .map(|values| {
594                    let count = values
595                        .into_iter()
596                        .filter(|value| !matches!(value, Value::Null))
597                        .count();
598                    Value::Uint(u64::try_from(count).unwrap_or(u64::MAX))
599                })
600            }
601            SqlGlobalAggregateTerminal::SumField(field) => {
602                let target_slot = resolve_sql_aggregate_target_slot::<E>(field)?;
603                self.execute_load_query_with(command.query(), |load, plan| {
604                    load.aggregate_sum_by_slot(plan, target_slot)
605                })
606                .map(|value| value.map_or(Value::Null, Value::Decimal))
607            }
608            SqlGlobalAggregateTerminal::AvgField(field) => {
609                let target_slot = resolve_sql_aggregate_target_slot::<E>(field)?;
610                self.execute_load_query_with(command.query(), |load, plan| {
611                    load.aggregate_avg_by_slot(plan, target_slot)
612                })
613                .map(|value| value.map_or(Value::Null, Value::Decimal))
614            }
615            SqlGlobalAggregateTerminal::MinField(field) => {
616                let target_slot = resolve_sql_aggregate_target_slot::<E>(field)?;
617                let min_id = self.execute_load_query_with(command.query(), |load, plan| {
618                    load.aggregate_min_by_slot(plan, target_slot)
619                })?;
620
621                match min_id {
622                    Some(id) => self
623                        .load::<E>()
624                        .by_id(id)
625                        .first_value_by(field)
626                        .map(|value| value.unwrap_or(Value::Null)),
627                    None => Ok(Value::Null),
628                }
629            }
630            SqlGlobalAggregateTerminal::MaxField(field) => {
631                let target_slot = resolve_sql_aggregate_target_slot::<E>(field)?;
632                let max_id = self.execute_load_query_with(command.query(), |load, plan| {
633                    load.aggregate_max_by_slot(plan, target_slot)
634                })?;
635
636                match max_id {
637                    Some(id) => self
638                        .load::<E>()
639                        .by_id(id)
640                        .first_value_by(field)
641                        .map(|value| value.unwrap_or(Value::Null)),
642                    None => Ok(Value::Null),
643                }
644            }
645        }
646    }
647
648    /// Execute one reduced SQL grouped `SELECT` statement and return grouped rows.
649    pub fn execute_sql_grouped<E>(
650        &self,
651        sql: &str,
652        cursor_token: Option<&str>,
653    ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
654    where
655        E: EntityKind<Canister = C> + EntityValue,
656    {
657        let query = self.query_from_sql::<E>(sql)?;
658        if !query.has_grouping() {
659            return Err(QueryError::execute(InternalError::classified(
660                ErrorClass::Unsupported,
661                ErrorOrigin::Query,
662                "execute_sql_grouped requires grouped SQL query intent",
663            )));
664        }
665
666        self.execute_grouped(&query, cursor_token)
667    }
668
669    /// Execute one reduced SQL statement into one unified SQL dispatch payload.
670    pub fn execute_sql_dispatch<E>(&self, sql: &str) -> Result<SqlDispatchResult<E>, QueryError>
671    where
672        E: EntityKind<Canister = C> + EntityValue,
673    {
674        let parsed = self.parse_sql_statement(sql)?;
675
676        self.execute_sql_dispatch_parsed::<E>(&parsed)
677    }
678
679    /// Execute one parsed reduced SQL statement into one unified SQL payload.
680    pub fn execute_sql_dispatch_parsed<E>(
681        &self,
682        parsed: &SqlParsedStatement,
683    ) -> Result<SqlDispatchResult<E>, QueryError>
684    where
685        E: EntityKind<Canister = C> + EntityValue,
686    {
687        let prepared = self.prepare_sql_dispatch_parsed(parsed, E::MODEL.entity_name())?;
688
689        self.execute_sql_dispatch_prepared::<E>(&prepared)
690    }
691
692    /// Execute one prepared reduced SQL statement into one unified SQL payload.
693    pub fn execute_sql_dispatch_prepared<E>(
694        &self,
695        prepared: &SqlPreparedStatement,
696    ) -> Result<SqlDispatchResult<E>, QueryError>
697    where
698        E: EntityKind<Canister = C> + EntityValue,
699    {
700        let command = compile_sql_command_from_prepared_statement::<E>(
701            prepared.prepared.clone(),
702            MissingRowPolicy::Ignore,
703        )
704        .map_err(map_sql_lowering_error)?;
705        let lane = sql_command_lane(&command);
706
707        match command {
708            SqlCommand::Query(_)
709            | SqlCommand::Explain { .. }
710            | SqlCommand::ExplainGlobalAggregate { .. } => {
711                self.execute_sql_dispatch_query_lane_from_command::<E>(command, lane)
712            }
713            SqlCommand::DescribeEntity => {
714                Ok(SqlDispatchResult::Describe(self.describe_entity::<E>()))
715            }
716            SqlCommand::ShowIndexesEntity => {
717                Ok(SqlDispatchResult::ShowIndexes(self.show_indexes::<E>()))
718            }
719            SqlCommand::ShowColumnsEntity => {
720                Ok(SqlDispatchResult::ShowColumns(self.show_columns::<E>()))
721            }
722            SqlCommand::ShowEntities => Ok(SqlDispatchResult::ShowEntities(self.show_entities())),
723        }
724    }
725
726    /// Execute one prepared reduced SQL statement limited to query/explain lanes.
727    pub fn execute_sql_dispatch_query_lane_prepared<E>(
728        &self,
729        prepared: &SqlPreparedStatement,
730    ) -> Result<SqlDispatchResult<E>, QueryError>
731    where
732        E: EntityKind<Canister = C> + EntityValue,
733    {
734        let command = compile_sql_command_from_prepared_statement::<E>(
735            prepared.prepared.clone(),
736            MissingRowPolicy::Ignore,
737        )
738        .map_err(map_sql_lowering_error)?;
739        let lane = sql_command_lane(&command);
740
741        self.execute_sql_dispatch_query_lane_from_command::<E>(command, lane)
742    }
743
744    // Render one EXPLAIN payload for constrained global aggregate SQL command.
745    fn explain_sql_global_aggregate<E>(
746        mode: SqlExplainMode,
747        command: SqlGlobalAggregateCommand<E>,
748    ) -> Result<String, QueryError>
749    where
750        E: EntityKind<Canister = C> + EntityValue,
751    {
752        match mode {
753            SqlExplainMode::Plan => {
754                // Keep explain validation parity with execution by requiring the
755                // target field to resolve before returning explain output.
756                let _ = sql_global_aggregate_terminal_to_expr::<E>(command.terminal())?;
757
758                Ok(command.query().explain()?.render_text_canonical())
759            }
760            SqlExplainMode::Execution => {
761                let aggregate = sql_global_aggregate_terminal_to_expr::<E>(command.terminal())?;
762                let plan = Self::explain_load_query_terminal_with(command.query(), aggregate)?;
763
764                Ok(plan.execution_node_descriptor().render_text_tree())
765            }
766            SqlExplainMode::Json => {
767                // Keep explain validation parity with execution by requiring the
768                // target field to resolve before returning explain output.
769                let _ = sql_global_aggregate_terminal_to_expr::<E>(command.terminal())?;
770
771                Ok(command.query().explain()?.render_json_canonical())
772            }
773        }
774    }
775}