Skip to main content

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

1//! Module: db::session::sql::dispatch
2//! Responsibility: module-local ownership and contracts for db::session::sql::dispatch.
3//! Does not own: cross-module orchestration outside this module.
4//! Boundary: exposes this module API while keeping implementation details internal.
5
6mod computed;
7mod lowered;
8
9use crate::{
10    db::{
11        DbSession, MissingRowPolicy, PersistedRow, Query, QueryError,
12        executor::{EntityAuthority, execute_sql_projection_rows_for_canister},
13        query::intent::StructuralQuery,
14        session::sql::{
15            SqlDispatchResult, SqlParsedStatement, SqlStatementRoute, computed_projection,
16            projection::{
17                SqlProjectionPayload, projection_labels_from_entity_model,
18                projection_labels_from_structural_query, sql_projection_rows_from_kernel_rows,
19            },
20            surface::{SqlSurface, session_sql_lane, unsupported_sql_lane_message},
21        },
22        sql::lowering::{
23            LoweredSqlQuery, bind_lowered_sql_query, lower_sql_command_from_prepared_statement,
24        },
25    },
26    traits::{CanisterKind, EntityKind, EntityValue},
27};
28
29impl<C: CanisterKind> DbSession<C> {
30    // Lower one parsed SQL statement into the shared query lane and bind the
31    // resulting lowered query shape onto one typed query owner exactly once.
32    pub(in crate::db::session::sql) fn bind_sql_query_lane_from_parsed<E>(
33        parsed: &SqlParsedStatement,
34    ) -> Result<(LoweredSqlQuery, Query<E>), QueryError>
35    where
36        E: EntityKind<Canister = C>,
37    {
38        // Keep `query_from_sql` structural-only: computed text projection is a
39        // session-owned dispatch surface, not part of the lowered typed-query
40        // contract for this slice.
41        if computed_projection::computed_sql_projection_plan(&parsed.statement)?.is_some() {
42            return Err(QueryError::unsupported_query(
43                "query_from_sql does not accept computed text projection; use execute_sql_dispatch(...)",
44            ));
45        }
46
47        let lowered =
48            parsed.lower_query_lane_for_entity(E::MODEL.name(), E::MODEL.primary_key.name)?;
49        let lane = session_sql_lane(&lowered);
50        let Some(query) = lowered.query().cloned() else {
51            return Err(QueryError::unsupported_query(unsupported_sql_lane_message(
52                SqlSurface::QueryFrom,
53                lane,
54            )));
55        };
56        let typed = bind_lowered_sql_query::<E>(query.clone(), MissingRowPolicy::Ignore)
57            .map_err(QueryError::from_sql_lowering_error)?;
58
59        Ok((query, typed))
60    }
61
62    // Execute one structural SQL load query and return only row-oriented SQL
63    // projection values, keeping typed projection rows out of the shared SQL
64    // query-lane path.
65    fn execute_structural_sql_projection(
66        &self,
67        query: StructuralQuery,
68        authority: EntityAuthority,
69    ) -> Result<SqlProjectionPayload, QueryError> {
70        let columns = projection_labels_from_structural_query(&query)?;
71        let projected = execute_sql_projection_rows_for_canister(
72            &self.db,
73            self.debug,
74            authority,
75            query.build_plan()?,
76        )
77        .map_err(QueryError::execute)?;
78        let (rows, row_count) = projected.into_parts();
79
80        Ok(SqlProjectionPayload::new(columns, rows, row_count))
81    }
82
83    // Execute one typed SQL delete query while keeping the row payload on the
84    // typed delete executor boundary that still owns non-runtime-hook delete
85    // commit-window application.
86    fn execute_typed_sql_delete<E>(&self, query: &Query<E>) -> Result<SqlDispatchResult, QueryError>
87    where
88        E: PersistedRow<Canister = C> + EntityValue,
89    {
90        let plan = query.plan()?.into_executable();
91        let deleted = self
92            .with_metrics(|| self.delete_executor::<E>().execute_sql_projection(plan))
93            .map_err(QueryError::execute)?;
94        let (rows, row_count) = deleted.into_parts();
95        let rows = sql_projection_rows_from_kernel_rows(rows);
96
97        Ok(SqlProjectionPayload::new(
98            projection_labels_from_entity_model(E::MODEL),
99            rows,
100            row_count,
101        )
102        .into_dispatch_result())
103    }
104
105    // Validate that one SQL-derived query intent matches the grouped/scalar
106    // execution surface that is about to consume it.
107    pub(in crate::db::session::sql) fn ensure_sql_query_grouping<E>(
108        query: &Query<E>,
109        grouped: bool,
110    ) -> Result<(), QueryError>
111    where
112        E: EntityKind,
113    {
114        match (grouped, query.has_grouping()) {
115            (true, true) | (false, false) => Ok(()),
116            (false, true) => Err(QueryError::grouped_requires_execute_grouped()),
117            (true, false) => Err(QueryError::unsupported_query(
118                "execute_sql_grouped requires grouped SQL query intent",
119            )),
120        }
121    }
122
123    /// Execute one reduced SQL statement into one unified SQL dispatch payload.
124    pub fn execute_sql_dispatch<E>(&self, sql: &str) -> Result<SqlDispatchResult, QueryError>
125    where
126        E: PersistedRow<Canister = C> + EntityValue,
127    {
128        let parsed = self.parse_sql_statement(sql)?;
129
130        self.execute_sql_dispatch_parsed::<E>(&parsed)
131    }
132
133    /// Execute one parsed reduced SQL statement into one unified SQL payload.
134    pub fn execute_sql_dispatch_parsed<E>(
135        &self,
136        parsed: &SqlParsedStatement,
137    ) -> Result<SqlDispatchResult, QueryError>
138    where
139        E: PersistedRow<Canister = C> + EntityValue,
140    {
141        match parsed.route() {
142            SqlStatementRoute::Query { .. } => {
143                if let Some(plan) =
144                    computed_projection::computed_sql_projection_plan(&parsed.statement)?
145                {
146                    return self.execute_computed_sql_projection_dispatch::<E>(plan);
147                }
148
149                let (query, typed_query) = Self::bind_sql_query_lane_from_parsed::<E>(parsed)?;
150
151                Self::ensure_sql_query_grouping(&typed_query, false)?;
152
153                match query {
154                    LoweredSqlQuery::Select(select) => self
155                        .execute_lowered_sql_dispatch_select_core(
156                            &select,
157                            EntityAuthority::for_type::<E>(),
158                        ),
159                    LoweredSqlQuery::Delete(_) => self.execute_typed_sql_delete(&typed_query),
160                }
161            }
162            SqlStatementRoute::Explain { .. } => {
163                if let Some((mode, plan)) =
164                    computed_projection::computed_sql_projection_explain_plan(&parsed.statement)?
165                {
166                    return Self::explain_computed_sql_projection_dispatch::<E>(mode, plan)
167                        .map(SqlDispatchResult::Explain);
168                }
169
170                let lowered = lower_sql_command_from_prepared_statement(
171                    parsed.prepare(E::MODEL.name())?,
172                    E::MODEL.primary_key.name,
173                )
174                .map_err(QueryError::from_sql_lowering_error)?;
175
176                lowered
177                    .explain_for_model(E::MODEL)
178                    .map(SqlDispatchResult::Explain)
179            }
180            SqlStatementRoute::Describe { .. } => {
181                Ok(SqlDispatchResult::Describe(self.describe_entity::<E>()))
182            }
183            SqlStatementRoute::ShowIndexes { .. } => {
184                Ok(SqlDispatchResult::ShowIndexes(self.show_indexes::<E>()))
185            }
186            SqlStatementRoute::ShowColumns { .. } => {
187                Ok(SqlDispatchResult::ShowColumns(self.show_columns::<E>()))
188            }
189            SqlStatementRoute::ShowEntities => {
190                Ok(SqlDispatchResult::ShowEntities(self.show_entities()))
191            }
192        }
193    }
194
195    /// Execute one parsed reduced SQL statement through the generated canister
196    /// query/explain surface for one already-resolved dynamic authority.
197    ///
198    /// This keeps the canister SQL facade on the same reduced SQL ownership
199    /// boundary as typed dispatch without forcing the outer facade to reopen
200    /// typed-generic routing just to preserve parity for computed projections.
201    #[doc(hidden)]
202    pub fn execute_generated_query_surface_dispatch_for_authority(
203        &self,
204        parsed: &SqlParsedStatement,
205        authority: EntityAuthority,
206    ) -> Result<SqlDispatchResult, QueryError> {
207        match parsed.route() {
208            SqlStatementRoute::Query { .. } => {
209                if let Some(plan) =
210                    computed_projection::computed_sql_projection_plan(&parsed.statement)?
211                {
212                    return self
213                        .execute_computed_sql_projection_dispatch_for_authority(plan, authority);
214                }
215
216                let lowered = parsed.lower_generated_query_surface_for_entity(
217                    authority.model().name(),
218                    authority.model().primary_key.name,
219                )?;
220
221                self.execute_lowered_sql_dispatch_query_for_authority(&lowered, authority)
222            }
223            SqlStatementRoute::Explain { .. } => {
224                if let Some((mode, plan)) =
225                    computed_projection::computed_sql_projection_explain_plan(&parsed.statement)?
226                {
227                    return Self::explain_computed_sql_projection_dispatch_for_authority(
228                        mode, plan, authority,
229                    )
230                    .map(SqlDispatchResult::Explain);
231                }
232
233                let lowered = parsed.lower_generated_query_surface_for_entity(
234                    authority.model().name(),
235                    authority.model().primary_key.name,
236                )?;
237
238                lowered
239                    .explain_for_model(authority.model())
240                    .map(SqlDispatchResult::Explain)
241            }
242            SqlStatementRoute::Describe { .. }
243            | SqlStatementRoute::ShowIndexes { .. }
244            | SqlStatementRoute::ShowColumns { .. }
245            | SqlStatementRoute::ShowEntities => Err(QueryError::unsupported_query(
246                "generated SQL query surface requires query or EXPLAIN statement lanes",
247            )),
248        }
249    }
250}