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