Skip to main content

icydb_core/db/session/sql/dispatch/
mod.rs

1//! Module: db::session::sql::dispatch
2//! Responsibility: session-owned SQL dispatch entrypoints that bind lowered SQL
3//! commands onto structural planning, execution, and outward result shaping.
4//! Does not own: SQL parsing or executor runtime internals.
5//! Boundary: centralizes authority-aware SQL dispatch classification and result packaging.
6
7mod computed;
8mod lowered;
9
10use crate::{
11    db::{
12        DbSession, MissingRowPolicy, PersistedRow, Query, QueryError,
13        data::UpdatePatch,
14        executor::{EntityAuthority, MutationMode},
15        identifiers_tail_match,
16        predicate::{CompareOp, Predicate},
17        query::{intent::StructuralQuery, plan::AccessPlannedQuery},
18        session::sql::{
19            SqlDispatchResult, SqlParsedStatement, SqlStatementRoute,
20            aggregate::parsed_requires_dedicated_sql_aggregate_lane,
21            computed_projection,
22            projection::{
23                SqlProjectionPayload, execute_sql_projection_rows_for_canister,
24                execute_sql_projection_text_rows_for_canister, projection_labels_from_fields,
25                projection_labels_from_projection_spec, sql_projection_rows_from_kernel_rows,
26            },
27        },
28        sql::lowering::{
29            LoweredBaseQueryShape, LoweredSelectShape, LoweredSqlQuery, SqlLoweringError,
30            bind_lowered_sql_query,
31        },
32        sql::parser::{
33            SqlAggregateCall, SqlAggregateKind, SqlInsertStatement, SqlProjection, SqlSelectItem,
34            SqlStatement, SqlTextFunction, SqlUpdateStatement,
35        },
36    },
37    model::{entity::resolve_field_slot, field::FieldKind},
38    traits::{CanisterKind, EntityKind, EntityValue},
39    types::Timestamp,
40    value::Value,
41};
42
43#[cfg(feature = "perf-attribution")]
44pub use lowered::LoweredSqlDispatchExecutorAttribution;
45
46///
47/// GeneratedSqlDispatchAttempt
48///
49/// Hidden generated-query dispatch envelope used by the facade helper to keep
50/// generated route ownership in core while preserving the public EXPLAIN error
51/// rewrite contract at the outer boundary.
52///
53
54#[doc(hidden)]
55pub struct GeneratedSqlDispatchAttempt {
56    entity_name: &'static str,
57    explain_order_field: Option<&'static str>,
58    result: Result<SqlDispatchResult, QueryError>,
59}
60
61impl GeneratedSqlDispatchAttempt {
62    // Build one generated-query dispatch attempt with optional explain-hint context.
63    const fn new(
64        entity_name: &'static str,
65        explain_order_field: Option<&'static str>,
66        result: Result<SqlDispatchResult, QueryError>,
67    ) -> Self {
68        Self {
69            entity_name,
70            explain_order_field,
71            result,
72        }
73    }
74
75    /// Borrow the resolved entity name for this generated-query attempt.
76    #[must_use]
77    pub const fn entity_name(&self) -> &'static str {
78        self.entity_name
79    }
80
81    /// Borrow the suggested deterministic order field for EXPLAIN rewrites.
82    #[must_use]
83    pub const fn explain_order_field(&self) -> Option<&'static str> {
84        self.explain_order_field
85    }
86
87    /// Consume and return the generated-query dispatch result.
88    pub fn into_result(self) -> Result<SqlDispatchResult, QueryError> {
89        self.result
90    }
91}
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub(in crate::db::session::sql) enum SqlGroupingSurface {
95    Scalar,
96    Grouped,
97}
98
99const fn unsupported_sql_grouping_message(surface: SqlGroupingSurface) -> &'static str {
100    match surface {
101        SqlGroupingSurface::Scalar => {
102            "execute_sql rejects grouped SELECT; use execute_sql_grouped(...)"
103        }
104        SqlGroupingSurface::Grouped => "execute_sql_grouped requires grouped SQL query intent",
105    }
106}
107
108// Enforce the generated canister query contract that empty SQL is unsupported
109// before any parser/lowering work occurs.
110fn trim_generated_query_sql_input(sql: &str) -> Result<&str, QueryError> {
111    let sql_trimmed = sql.trim();
112    if sql_trimmed.is_empty() {
113        return Err(QueryError::unsupported_query(
114            "query endpoint requires a non-empty SQL string",
115        ));
116    }
117
118    Ok(sql_trimmed)
119}
120
121// Render the generated-surface entity list from the descriptor table instead
122// of assuming every session-visible entity belongs on the public query export.
123fn generated_sql_entities(authorities: &[EntityAuthority]) -> Vec<String> {
124    let mut entities = Vec::with_capacity(authorities.len());
125
126    for authority in authorities {
127        entities.push(authority.model().name().to_string());
128    }
129
130    entities
131}
132
133// Project parsed SELECT items into one stable outward column contract while
134// allowing parser-owned aliases to override only the final session label.
135fn sql_projection_labels_from_select_statement(
136    statement: &SqlStatement,
137) -> Result<Option<Vec<String>>, QueryError> {
138    let SqlStatement::Select(select) = statement else {
139        return Err(QueryError::invariant(
140            "SQL projection labels require SELECT statement shape",
141        ));
142    };
143    let SqlProjection::Items(items) = &select.projection else {
144        return Ok(None);
145    };
146
147    Ok(Some(
148        items
149            .iter()
150            .enumerate()
151            .map(|(index, item)| {
152                select
153                    .projection_alias(index)
154                    .map_or_else(|| grouped_sql_projection_item_label(item), str::to_string)
155            })
156            .collect(),
157    ))
158}
159
160// Render one grouped SELECT item into the public grouped-column label used by
161// unified dispatch results.
162fn grouped_sql_projection_item_label(item: &SqlSelectItem) -> String {
163    match item {
164        SqlSelectItem::Field(field) => field.clone(),
165        SqlSelectItem::Aggregate(aggregate) => grouped_sql_aggregate_call_label(aggregate),
166        SqlSelectItem::TextFunction(call) => {
167            format!(
168                "{}({})",
169                grouped_sql_text_function_name(call.function),
170                call.field
171            )
172        }
173    }
174}
175
176// Keep the dedicated SQL aggregate lane on parser-owned outward labels
177// without reopening alias semantics in lowering or runtime strategy state.
178fn sql_aggregate_dispatch_label_override(statement: &SqlStatement) -> Option<String> {
179    let SqlStatement::Select(select) = statement else {
180        return None;
181    };
182
183    select.projection_alias(0).map(str::to_string)
184}
185
186// Render one aggregate call into one canonical SQL-style label.
187fn grouped_sql_aggregate_call_label(aggregate: &SqlAggregateCall) -> String {
188    let kind = match aggregate.kind {
189        SqlAggregateKind::Count => "COUNT",
190        SqlAggregateKind::Sum => "SUM",
191        SqlAggregateKind::Avg => "AVG",
192        SqlAggregateKind::Min => "MIN",
193        SqlAggregateKind::Max => "MAX",
194    };
195
196    match aggregate.field.as_deref() {
197        Some(field) => format!("{kind}({field})"),
198        None => format!("{kind}(*)"),
199    }
200}
201
202// Render one reduced SQL text-function identifier into one stable uppercase
203// SQL label for outward column metadata.
204const fn grouped_sql_text_function_name(function: SqlTextFunction) -> &'static str {
205    match function {
206        SqlTextFunction::Trim => "TRIM",
207        SqlTextFunction::Ltrim => "LTRIM",
208        SqlTextFunction::Rtrim => "RTRIM",
209        SqlTextFunction::Lower => "LOWER",
210        SqlTextFunction::Upper => "UPPER",
211        SqlTextFunction::Length => "LENGTH",
212        SqlTextFunction::Left => "LEFT",
213        SqlTextFunction::Right => "RIGHT",
214        SqlTextFunction::StartsWith => "STARTS_WITH",
215        SqlTextFunction::EndsWith => "ENDS_WITH",
216        SqlTextFunction::Contains => "CONTAINS",
217        SqlTextFunction::Position => "POSITION",
218        SqlTextFunction::Replace => "REPLACE",
219        SqlTextFunction::Substring => "SUBSTRING",
220    }
221}
222
223// Resolve one generated query route onto the descriptor-owned authority table.
224fn authority_for_generated_sql_route(
225    route: &SqlStatementRoute,
226    authorities: &[EntityAuthority],
227) -> Result<EntityAuthority, QueryError> {
228    let sql_entity = route.entity();
229
230    for authority in authorities {
231        if identifiers_tail_match(sql_entity, authority.model().name()) {
232            return Ok(*authority);
233        }
234    }
235
236    Err(unsupported_generated_sql_entity_error(
237        sql_entity,
238        authorities,
239    ))
240}
241
242// Keep the generated query-surface unsupported-entity contract stable while
243// moving authority lookup out of the build-generated shim.
244fn unsupported_generated_sql_entity_error(
245    entity_name: &str,
246    authorities: &[EntityAuthority],
247) -> QueryError {
248    let mut supported = String::new();
249
250    for (index, authority) in authorities.iter().enumerate() {
251        if index != 0 {
252            supported.push_str(", ");
253        }
254
255        supported.push_str(authority.model().name());
256    }
257
258    QueryError::unsupported_query(format!(
259        "query endpoint does not support entity '{entity_name}'; supported: {supported}"
260    ))
261}
262
263// Keep typed SQL write routes on the same entity-match contract used by
264// lowered query dispatch, without widening write statements into lowering.
265fn ensure_sql_write_entity_matches<E>(sql_entity: &str) -> Result<(), QueryError>
266where
267    E: EntityKind,
268{
269    if identifiers_tail_match(sql_entity, E::MODEL.name()) {
270        return Ok(());
271    }
272
273    Err(QueryError::from_sql_lowering_error(
274        SqlLoweringError::EntityMismatch {
275            sql_entity: sql_entity.to_string(),
276            expected_entity: E::MODEL.name(),
277        },
278    ))
279}
280
281// Normalize one reduced-SQL primary-key literal onto the concrete entity key
282// type accepted by the structural mutation entrypoint.
283fn sql_write_key_from_literal<E>(value: &Value, pk_name: &str) -> Result<E::Key, QueryError>
284where
285    E: EntityKind,
286{
287    if let Some(key) = <E::Key as crate::traits::FieldValue>::from_value(value) {
288        return Ok(key);
289    }
290
291    let widened = match value {
292        Value::Int(v) if *v >= 0 => Value::Uint(v.cast_unsigned()),
293        Value::Uint(v) if i64::try_from(*v).is_ok() => Value::Int(v.cast_signed()),
294        _ => {
295            return Err(QueryError::unsupported_query(format!(
296                "SQL write primary key literal for '{pk_name}' is not compatible with entity key type"
297            )));
298        }
299    };
300
301    <E::Key as crate::traits::FieldValue>::from_value(&widened).ok_or_else(|| {
302        QueryError::unsupported_query(format!(
303            "SQL write primary key literal for '{pk_name}' is not compatible with entity key type"
304        ))
305    })
306}
307
308// Normalize one reduced-SQL write literal onto the target entity field kind
309// when the parser's numeric literal domain is narrower than the runtime field.
310fn sql_write_value_for_field<E>(field_name: &str, value: &Value) -> Result<Value, QueryError>
311where
312    E: EntityKind,
313{
314    let field_slot = resolve_field_slot(E::MODEL, field_name).ok_or_else(|| {
315        QueryError::invariant("SQL write field must resolve against the target entity model")
316    })?;
317    let field_kind = E::MODEL.fields()[field_slot].kind();
318
319    let normalized = match (field_kind, value) {
320        (FieldKind::Uint, Value::Int(v)) if *v >= 0 => Value::Uint(v.cast_unsigned()),
321        (FieldKind::Int, Value::Uint(v)) if i64::try_from(*v).is_ok() => {
322            Value::Int(v.cast_signed())
323        }
324        _ => value.clone(),
325    };
326
327    Ok(normalized)
328}
329
330// Mirror the derive-owned system timestamp contract on the structural SQL
331// write lane so schema-derived entities stay writable without exposing those
332// slots as required user-authored SQL columns.
333fn sql_write_system_timestamp_fields<E>() -> Option<(&'static str, &'static str)>
334where
335    E: EntityKind,
336{
337    if resolve_field_slot(E::MODEL, "created_at").is_some()
338        && resolve_field_slot(E::MODEL, "updated_at").is_some()
339    {
340        return Some(("created_at", "updated_at"));
341    }
342
343    None
344}
345
346impl<C: CanisterKind> DbSession<C> {
347    // Render one typed entity returned by SQL write dispatch as a single
348    // projection payload row so write statements reuse the same outward result
349    // family as row-producing SELECT and DELETE dispatch.
350    fn sql_write_dispatch_projection<E>(entity: E) -> Result<SqlDispatchResult, QueryError>
351    where
352        E: PersistedRow<Canister = C> + EntityValue,
353    {
354        // Phase 1: freeze the outward full-row SQL column contract from the
355        // persisted model declaration order.
356        let columns = projection_labels_from_fields(E::MODEL.fields());
357        let mut row = Vec::with_capacity(columns.len());
358
359        // Phase 2: project one value row directly from the typed after-image
360        // returned by the shared save/mutation path.
361        for index in 0..columns.len() {
362            let value = entity.get_value_by_index(index).ok_or_else(|| {
363                QueryError::invariant(
364                    "SQL write dispatch projection row must include every declared field",
365                )
366            })?;
367            row.push(value);
368        }
369
370        Ok(SqlDispatchResult::Projection {
371            columns,
372            rows: vec![row],
373            row_count: 1,
374        })
375    }
376
377    // Build the structural insert patch and resolved primary key expected by
378    // the shared structural mutation entrypoint.
379    fn sql_insert_patch_and_key<E>(
380        statement: &SqlInsertStatement,
381    ) -> Result<(E::Key, UpdatePatch), QueryError>
382    where
383        E: PersistedRow<Canister = C> + EntityValue,
384    {
385        // Phase 1: resolve the required primary-key literal from the explicit
386        // INSERT column/value list.
387        let pk_name = E::MODEL.primary_key.name;
388        let Some(pk_index) = statement.columns.iter().position(|field| field == pk_name) else {
389            return Err(QueryError::unsupported_query(format!(
390                "SQL INSERT requires primary key column '{pk_name}' in this release"
391            )));
392        };
393        let pk_value = statement.values.get(pk_index).ok_or_else(|| {
394            QueryError::invariant("INSERT primary key column must align with one VALUES literal")
395        })?;
396        let key = sql_write_key_from_literal::<E>(pk_value, pk_name)?;
397
398        // Phase 2: lower the explicit column/value pairs onto the structural
399        // patch program consumed by the shared save path.
400        let mut patch = UpdatePatch::new();
401        for (field, value) in statement.columns.iter().zip(statement.values.iter()) {
402            let normalized = sql_write_value_for_field::<E>(field, value)?;
403            patch = patch
404                .set_field(E::MODEL, field, normalized)
405                .map_err(QueryError::execute)?;
406        }
407
408        // Phase 3: synthesize the derive-owned system timestamps when the
409        // target entity carries them, matching the typed write surface.
410        if let Some((created_at, updated_at)) = sql_write_system_timestamp_fields::<E>() {
411            let now = Value::Timestamp(Timestamp::now());
412            patch = patch
413                .set_field(E::MODEL, created_at, now.clone())
414                .map_err(QueryError::execute)?;
415            patch = patch
416                .set_field(E::MODEL, updated_at, now)
417                .map_err(QueryError::execute)?;
418        }
419
420        Ok((key, patch))
421    }
422
423    // Build the structural update patch and resolved primary key expected by
424    // the shared structural mutation entrypoint.
425    fn sql_update_patch_and_key<E>(
426        statement: &SqlUpdateStatement,
427    ) -> Result<(E::Key, UpdatePatch), QueryError>
428    where
429        E: PersistedRow<Canister = C> + EntityValue,
430    {
431        // Phase 1: require the narrow `WHERE <pk> = literal` update selector
432        // so this first SQL update slice stays on the existing single-row
433        // structural mutation contract.
434        let pk_name = E::MODEL.primary_key.name;
435        let Some(Predicate::Compare(compare)) = &statement.predicate else {
436            return Err(QueryError::unsupported_query(format!(
437                "SQL UPDATE requires WHERE {pk_name} = literal in this release"
438            )));
439        };
440        if compare.field() != pk_name || compare.op() != CompareOp::Eq {
441            return Err(QueryError::unsupported_query(format!(
442                "SQL UPDATE requires WHERE {pk_name} = literal in this release"
443            )));
444        }
445        let key = sql_write_key_from_literal::<E>(compare.value(), pk_name)?;
446
447        // Phase 2: lower the `SET` list onto the structural patch program
448        // while keeping primary-key mutation out of this first SQL update slice.
449        let mut patch = UpdatePatch::new();
450        for assignment in &statement.assignments {
451            if assignment.field == pk_name {
452                return Err(QueryError::unsupported_query(format!(
453                    "SQL UPDATE does not allow primary key mutation for '{pk_name}' in this release"
454                )));
455            }
456            let normalized =
457                sql_write_value_for_field::<E>(assignment.field.as_str(), &assignment.value)?;
458
459            patch = patch
460                .set_field(E::MODEL, assignment.field.as_str(), normalized)
461                .map_err(QueryError::execute)?;
462        }
463
464        // Phase 3: keep structural SQL UPDATE aligned with the derive-owned
465        // auto-updated timestamp contract when the entity carries that field.
466        if let Some((_, updated_at)) = sql_write_system_timestamp_fields::<E>() {
467            patch = patch
468                .set_field(E::MODEL, updated_at, Value::Timestamp(Timestamp::now()))
469                .map_err(QueryError::execute)?;
470        }
471
472        Ok((key, patch))
473    }
474
475    // Execute one narrow SQL INSERT statement through the existing structural
476    // mutation path and project the returned after-image as one SQL row.
477    fn execute_sql_insert_dispatch<E>(
478        &self,
479        statement: &SqlInsertStatement,
480    ) -> Result<SqlDispatchResult, QueryError>
481    where
482        E: PersistedRow<Canister = C> + EntityValue,
483    {
484        ensure_sql_write_entity_matches::<E>(statement.entity.as_str())?;
485        let (key, patch) = Self::sql_insert_patch_and_key::<E>(statement)?;
486        let entity = self
487            .mutate_structural::<E>(key, patch, MutationMode::Insert)
488            .map_err(QueryError::execute)?;
489
490        Self::sql_write_dispatch_projection(entity)
491    }
492
493    // Execute one narrow SQL UPDATE statement through the existing structural
494    // mutation path and project the returned after-image as one SQL row.
495    fn execute_sql_update_dispatch<E>(
496        &self,
497        statement: &SqlUpdateStatement,
498    ) -> Result<SqlDispatchResult, QueryError>
499    where
500        E: PersistedRow<Canister = C> + EntityValue,
501    {
502        ensure_sql_write_entity_matches::<E>(statement.entity.as_str())?;
503        let (key, patch) = Self::sql_update_patch_and_key::<E>(statement)?;
504        let entity = self
505            .mutate_structural::<E>(key, patch, MutationMode::Update)
506            .map_err(QueryError::execute)?;
507
508        Self::sql_write_dispatch_projection(entity)
509    }
510
511    // Build the shared structural SQL projection execution inputs once so
512    // value-row and rendered-row dispatch surfaces only differ in final packaging.
513    fn prepare_structural_sql_projection_execution(
514        &self,
515        query: StructuralQuery,
516        authority: EntityAuthority,
517    ) -> Result<(Vec<String>, AccessPlannedQuery), QueryError> {
518        // Phase 1: build the structural access plan once and freeze its outward
519        // column contract for all projection materialization surfaces.
520        let (_, plan) =
521            self.build_structural_plan_with_visible_indexes_for_authority(query, authority)?;
522        let projection = plan.projection_spec(authority.model());
523        let columns = projection_labels_from_projection_spec(&projection);
524
525        Ok((columns, plan))
526    }
527
528    // Execute one structural SQL load query and return only row-oriented SQL
529    // projection values, keeping typed projection rows out of the shared SQL
530    // query-lane path.
531    pub(in crate::db::session::sql) fn execute_structural_sql_projection(
532        &self,
533        query: StructuralQuery,
534        authority: EntityAuthority,
535    ) -> Result<SqlProjectionPayload, QueryError> {
536        // Phase 1: build the shared structural plan and outward column contract once.
537        let (columns, plan) = self.prepare_structural_sql_projection_execution(query, authority)?;
538
539        // Phase 2: execute the shared structural load path with the already
540        // derived projection semantics.
541        let projected =
542            execute_sql_projection_rows_for_canister(&self.db, self.debug, authority, plan)
543                .map_err(QueryError::execute)?;
544        let (rows, row_count) = projected.into_parts();
545
546        Ok(SqlProjectionPayload::new(columns, rows, row_count))
547    }
548
549    // Execute one structural SQL load query and return render-ready text rows
550    // for the dispatch lane when the terminal short path can prove them
551    // directly.
552    fn execute_structural_sql_projection_text(
553        &self,
554        query: StructuralQuery,
555        authority: EntityAuthority,
556    ) -> Result<SqlDispatchResult, QueryError> {
557        // Phase 1: build the shared structural plan and outward column contract once.
558        let (columns, plan) = self.prepare_structural_sql_projection_execution(query, authority)?;
559
560        // Phase 2: execute the shared structural load path with the already
561        // derived projection semantics while preferring rendered SQL rows.
562        let projected =
563            execute_sql_projection_text_rows_for_canister(&self.db, self.debug, authority, plan)
564                .map_err(QueryError::execute)?;
565        let (rows, row_count) = projected.into_parts();
566
567        Ok(SqlDispatchResult::ProjectionText {
568            columns,
569            rows,
570            row_count,
571        })
572    }
573
574    // Execute one typed SQL delete query while keeping the row payload on the
575    // typed delete executor boundary that still owns non-runtime-hook delete
576    // commit-window application.
577    fn execute_typed_sql_delete<E>(&self, query: &Query<E>) -> Result<SqlDispatchResult, QueryError>
578    where
579        E: PersistedRow<Canister = C> + EntityValue,
580    {
581        let plan = self
582            .compile_query_with_visible_indexes(query)?
583            .into_prepared_execution_plan();
584        let deleted = self
585            .with_metrics(|| {
586                self.delete_executor::<E>()
587                    .execute_structural_projection(plan)
588            })
589            .map_err(QueryError::execute)?;
590        let (rows, row_count) = deleted.into_parts();
591        let rows = sql_projection_rows_from_kernel_rows(rows).map_err(QueryError::execute)?;
592
593        Ok(SqlProjectionPayload::new(
594            projection_labels_from_fields(E::MODEL.fields()),
595            rows,
596            row_count,
597        )
598        .into_dispatch_result())
599    }
600
601    // Lower one parsed SQL query/explain route once for one resolved authority
602    // and preserve grouped-column metadata for grouped SELECT dispatch.
603    fn lowered_sql_query_dispatch_inputs_for_authority(
604        parsed: &SqlParsedStatement,
605        authority: EntityAuthority,
606        unsupported_message: &'static str,
607    ) -> Result<(LoweredSqlQuery, Option<Vec<String>>), QueryError> {
608        let lowered = parsed.lower_query_lane_for_entity(
609            authority.model().name(),
610            authority.model().primary_key.name,
611        )?;
612        let projection_columns = matches!(lowered.query(), Some(LoweredSqlQuery::Select(_)))
613            .then(|| sql_projection_labels_from_select_statement(&parsed.statement))
614            .transpose()?;
615        let query = lowered
616            .into_query()
617            .ok_or_else(|| QueryError::unsupported_query(unsupported_message))?;
618
619        Ok((query, projection_columns.flatten()))
620    }
621
622    // Execute one parsed SQL query route through the shared aggregate,
623    // computed-projection, and lowered query lane so typed and generated
624    // dispatch only differ at the final SELECT/DELETE packaging boundary.
625    fn dispatch_sql_query_route_for_authority(
626        &self,
627        parsed: &SqlParsedStatement,
628        authority: EntityAuthority,
629        unsupported_message: &'static str,
630        dispatch_select: impl FnOnce(
631            &Self,
632            LoweredSelectShape,
633            EntityAuthority,
634            bool,
635            Option<Vec<String>>,
636        ) -> Result<SqlDispatchResult, QueryError>,
637        dispatch_delete: impl FnOnce(
638            &Self,
639            LoweredBaseQueryShape,
640            EntityAuthority,
641        ) -> Result<SqlDispatchResult, QueryError>,
642    ) -> Result<SqlDispatchResult, QueryError> {
643        // Phase 1: keep aggregate and computed projection classification on the
644        // shared parsed route so both dispatch surfaces honor the same lane split.
645        if parsed_requires_dedicated_sql_aggregate_lane(parsed) {
646            let command =
647                Self::compile_sql_aggregate_command_core_for_authority(parsed, authority)?;
648
649            return self.execute_sql_aggregate_dispatch_for_authority(
650                command,
651                authority,
652                sql_aggregate_dispatch_label_override(&parsed.statement),
653            );
654        }
655
656        if let Some(plan) = computed_projection::computed_sql_projection_plan(&parsed.statement)? {
657            return self.execute_computed_sql_projection_dispatch_for_authority(plan, authority);
658        }
659
660        // Phase 2: lower the remaining query route once, then let the caller
661        // decide only the final outward result packaging.
662        let (query, projection_columns) = Self::lowered_sql_query_dispatch_inputs_for_authority(
663            parsed,
664            authority,
665            unsupported_message,
666        )?;
667        let grouped_surface = query.has_grouping();
668
669        match query {
670            LoweredSqlQuery::Select(select) => {
671                dispatch_select(self, select, authority, grouped_surface, projection_columns)
672            }
673            LoweredSqlQuery::Delete(delete) => dispatch_delete(self, delete, authority),
674        }
675    }
676
677    // Execute one parsed SQL EXPLAIN route through the shared computed-
678    // projection and lowered explain lanes so typed and generated dispatch do
679    // not duplicate the same explain classification tree.
680    fn dispatch_sql_explain_route_for_authority(
681        &self,
682        parsed: &SqlParsedStatement,
683        authority: EntityAuthority,
684    ) -> Result<SqlDispatchResult, QueryError> {
685        // Phase 1: keep computed-projection explain ownership on the same
686        // parsed route boundary as the shared query lane.
687        if let Some((mode, plan)) =
688            computed_projection::computed_sql_projection_explain_plan(&parsed.statement)?
689        {
690            return self
691                .explain_computed_sql_projection_dispatch_for_authority(mode, plan, authority)
692                .map(SqlDispatchResult::Explain);
693        }
694
695        // Phase 2: lower once for execution/logical explain and preserve the
696        // shared execution-first fallback policy across both callers.
697        let lowered = parsed.lower_query_lane_for_entity(
698            authority.model().name(),
699            authority.model().primary_key.name,
700        )?;
701        if let Some(explain) =
702            self.explain_lowered_sql_execution_for_authority(&lowered, authority)?
703        {
704            return Ok(SqlDispatchResult::Explain(explain));
705        }
706
707        self.explain_lowered_sql_for_authority(&lowered, authority)
708            .map(SqlDispatchResult::Explain)
709    }
710
711    // Validate that one SQL-derived query intent matches the grouped/scalar
712    // execution surface that is about to consume it.
713    pub(in crate::db::session::sql) fn ensure_sql_query_grouping<E>(
714        query: &Query<E>,
715        surface: SqlGroupingSurface,
716    ) -> Result<(), QueryError>
717    where
718        E: EntityKind,
719    {
720        match (surface, query.has_grouping()) {
721            (SqlGroupingSurface::Scalar, false) | (SqlGroupingSurface::Grouped, true) => Ok(()),
722            (SqlGroupingSurface::Scalar, true) | (SqlGroupingSurface::Grouped, false) => Err(
723                QueryError::unsupported_query(unsupported_sql_grouping_message(surface)),
724            ),
725        }
726    }
727
728    /// Execute one reduced SQL statement into one unified SQL dispatch payload.
729    pub fn execute_sql_dispatch<E>(&self, sql: &str) -> Result<SqlDispatchResult, QueryError>
730    where
731        E: PersistedRow<Canister = C> + EntityValue,
732    {
733        let parsed = self.parse_sql_statement(sql)?;
734
735        self.execute_sql_dispatch_parsed::<E>(&parsed)
736    }
737
738    /// Execute one parsed reduced SQL statement into one unified SQL payload.
739    pub fn execute_sql_dispatch_parsed<E>(
740        &self,
741        parsed: &SqlParsedStatement,
742    ) -> Result<SqlDispatchResult, QueryError>
743    where
744        E: PersistedRow<Canister = C> + EntityValue,
745    {
746        match parsed.route() {
747            SqlStatementRoute::Query { .. } => self.dispatch_sql_query_route_for_authority(
748                parsed,
749                EntityAuthority::for_type::<E>(),
750                "execute_sql_dispatch accepts SELECT or DELETE only",
751                |session, select, authority, grouped_surface, projection_columns| {
752                    if grouped_surface {
753                        let columns = projection_columns.ok_or_else(|| {
754                            QueryError::unsupported_query(
755                                "grouped SQL dispatch requires explicit grouped projection items",
756                            )
757                        })?;
758
759                        return session.execute_lowered_sql_grouped_dispatch_select_core(
760                            select, authority, columns,
761                        );
762                    }
763
764                    let payload = session.execute_lowered_sql_projection_core(select, authority)?;
765                    if let Some(columns) = projection_columns {
766                        let (_, rows, row_count) = payload.into_parts();
767
768                        return Ok(SqlProjectionPayload::new(columns, rows, row_count)
769                            .into_dispatch_result());
770                    }
771
772                    Ok(payload.into_dispatch_result())
773                },
774                |session, delete, _authority| {
775                    let typed_query = bind_lowered_sql_query::<E>(
776                        LoweredSqlQuery::Delete(delete),
777                        MissingRowPolicy::Ignore,
778                    )
779                    .map_err(QueryError::from_sql_lowering_error)?;
780
781                    session.execute_typed_sql_delete(&typed_query)
782                },
783            ),
784            SqlStatementRoute::Insert { .. } => {
785                let SqlStatement::Insert(statement) = &parsed.statement else {
786                    return Err(QueryError::invariant(
787                        "INSERT SQL route must carry parsed INSERT statement",
788                    ));
789                };
790
791                self.execute_sql_insert_dispatch::<E>(statement)
792            }
793            SqlStatementRoute::Update { .. } => {
794                let SqlStatement::Update(statement) = &parsed.statement else {
795                    return Err(QueryError::invariant(
796                        "UPDATE SQL route must carry parsed UPDATE statement",
797                    ));
798                };
799
800                self.execute_sql_update_dispatch::<E>(statement)
801            }
802            SqlStatementRoute::Explain { .. } => self
803                .dispatch_sql_explain_route_for_authority(parsed, EntityAuthority::for_type::<E>()),
804            SqlStatementRoute::Describe { .. } => {
805                Ok(SqlDispatchResult::Describe(self.describe_entity::<E>()))
806            }
807            SqlStatementRoute::ShowIndexes { .. } => {
808                Ok(SqlDispatchResult::ShowIndexes(self.show_indexes::<E>()))
809            }
810            SqlStatementRoute::ShowColumns { .. } => {
811                Ok(SqlDispatchResult::ShowColumns(self.show_columns::<E>()))
812            }
813            SqlStatementRoute::ShowEntities => {
814                Ok(SqlDispatchResult::ShowEntities(self.show_entities()))
815            }
816        }
817    }
818
819    /// Execute one parsed reduced SQL statement through the generated canister
820    /// query/explain surface for one already-resolved dynamic authority.
821    ///
822    /// This keeps the canister SQL facade on the same reduced SQL ownership
823    /// boundary as typed dispatch without forcing the outer facade to reopen
824    /// typed-generic routing just to preserve parity for computed projections.
825    #[doc(hidden)]
826    pub fn execute_generated_query_surface_dispatch_for_authority(
827        &self,
828        parsed: &SqlParsedStatement,
829        authority: EntityAuthority,
830    ) -> Result<SqlDispatchResult, QueryError> {
831        match parsed.route() {
832            SqlStatementRoute::Query { .. } => self.dispatch_sql_query_route_for_authority(
833                parsed,
834                authority,
835                "generated SQL query surface requires query or EXPLAIN statement lanes",
836                |session, select, authority, grouped_surface, projection_columns| {
837                    if grouped_surface {
838                        let columns = projection_columns.ok_or_else(|| {
839                            QueryError::unsupported_query(
840                                "grouped SQL dispatch requires explicit grouped projection items",
841                            )
842                        })?;
843
844                        return session
845                            .execute_lowered_sql_grouped_dispatch_select_core(select, authority, columns);
846                    }
847
848                    let result =
849                        session.execute_lowered_sql_dispatch_select_text_core(select, authority)?;
850                    if let Some(columns) = projection_columns {
851                        let SqlDispatchResult::ProjectionText {
852                            rows, row_count, ..
853                        } = result
854                        else {
855                            return Err(QueryError::invariant(
856                                "generated scalar SQL dispatch text path must emit projection text rows",
857                            ));
858                        };
859
860                        return Ok(SqlDispatchResult::ProjectionText {
861                            columns,
862                            rows,
863                            row_count,
864                        });
865                    }
866
867                    Ok(result)
868                },
869                |session, delete, authority| {
870                    session.execute_lowered_sql_dispatch_delete_core(&delete, authority)
871                },
872            ),
873            SqlStatementRoute::Explain { .. } => {
874                self.dispatch_sql_explain_route_for_authority(parsed, authority)
875            }
876            SqlStatementRoute::Insert { .. } | SqlStatementRoute::Update { .. }
877            | SqlStatementRoute::Describe { .. }
878            | SqlStatementRoute::ShowIndexes { .. }
879            | SqlStatementRoute::ShowColumns { .. }
880            | SqlStatementRoute::ShowEntities => Err(QueryError::unsupported_query(
881                "generated SQL query surface requires SELECT, DELETE, or EXPLAIN statement lanes",
882            )),
883        }
884    }
885
886    /// Execute one raw SQL string through the generated canister query surface.
887    ///
888    /// This hidden helper keeps parse, route, authority, and metadata/query
889    /// dispatch ownership in core so the build-generated `sql_dispatch` shim
890    /// stays close to a pure descriptor table plus public ABI wrapper.
891    #[doc(hidden)]
892    #[must_use]
893    pub fn execute_generated_query_surface_sql(
894        &self,
895        sql: &str,
896        authorities: &[EntityAuthority],
897    ) -> GeneratedSqlDispatchAttempt {
898        // Phase 1: normalize and parse once so every generated route family
899        // shares the same SQL ownership boundary.
900        let sql_trimmed = match trim_generated_query_sql_input(sql) {
901            Ok(sql_trimmed) => sql_trimmed,
902            Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
903        };
904        let parsed = match self.parse_sql_statement(sql_trimmed) {
905            Ok(parsed) => parsed,
906            Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
907        };
908
909        // Phase 2: keep SHOW ENTITIES descriptor-owned and resolve all other
910        // generated routes against the emitted authority table exactly once.
911        if matches!(parsed.route(), SqlStatementRoute::ShowEntities) {
912            return GeneratedSqlDispatchAttempt::new(
913                "",
914                None,
915                Ok(SqlDispatchResult::ShowEntities(generated_sql_entities(
916                    authorities,
917                ))),
918            );
919        }
920        let authority = match authority_for_generated_sql_route(parsed.route(), authorities) {
921            Ok(authority) => authority,
922            Err(err) => return GeneratedSqlDispatchAttempt::new("", None, Err(err)),
923        };
924
925        // Phase 3: dispatch the resolved route through the existing query,
926        // explain, and metadata helpers without rebuilding route ownership in
927        // the generated build output.
928        let entity_name = authority.model().name();
929        let explain_order_field = parsed
930            .route()
931            .is_explain()
932            .then_some(authority.model().primary_key.name);
933        let result = match parsed.route() {
934            SqlStatementRoute::Query { .. } | SqlStatementRoute::Explain { .. } => {
935                self.execute_generated_query_surface_dispatch_for_authority(&parsed, authority)
936            }
937            SqlStatementRoute::Insert { .. } | SqlStatementRoute::Update { .. } => {
938                Err(QueryError::unsupported_query(
939                    "generated SQL query surface requires SELECT, DELETE, or EXPLAIN statement lanes",
940                ))
941            }
942            SqlStatementRoute::Describe { .. } => Ok(SqlDispatchResult::Describe(
943                self.describe_entity_model(authority.model()),
944            )),
945            SqlStatementRoute::ShowIndexes { .. } => Ok(SqlDispatchResult::ShowIndexes(
946                self.show_indexes_for_store_model(authority.store_path(), authority.model()),
947            )),
948            SqlStatementRoute::ShowColumns { .. } => Ok(SqlDispatchResult::ShowColumns(
949                self.show_columns_for_model(authority.model()),
950            )),
951            SqlStatementRoute::ShowEntities => unreachable!(
952                "SHOW ENTITIES is handled before authority resolution for generated query dispatch"
953            ),
954        };
955
956        GeneratedSqlDispatchAttempt::new(entity_name, explain_order_field, result)
957    }
958}