Skip to main content

icydb_core/db/session/
sql.rs

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