Skip to main content

icydb_core/db/session/sql/
mod.rs

1//! Module: db::session::sql
2//! Responsibility: session-owned SQL dispatch, explain, projection, and
3//! surface-classification helpers above lowered SQL commands.
4//! Does not own: SQL parsing or structural executor runtime behavior.
5//! Boundary: keeps session visibility, authority selection, and SQL surface routing in one subsystem.
6
7mod aggregate;
8mod computed_projection;
9mod dispatch;
10mod explain;
11mod projection;
12mod surface;
13
14use crate::{
15    db::{
16        DbSession, EntityResponse, GroupedTextCursorPageWithTrace, MissingRowPolicy,
17        PagedGroupedExecutionWithTrace, PersistedRow, Query, QueryError,
18        executor::EntityAuthority,
19        identifiers_tail_match,
20        query::{
21            intent::StructuralQuery,
22            plan::{AccessPlannedQuery, VisibleIndexes},
23        },
24        sql::{
25            lowering::{
26                bind_lowered_sql_query, lower_sql_command_from_prepared_statement,
27                prepare_sql_statement,
28            },
29            parser::{SqlStatement, parse_sql},
30        },
31    },
32    traits::{CanisterKind, EntityKind, EntityValue},
33};
34
35use crate::db::session::sql::aggregate::{
36    SqlAggregateSurface, parsed_requires_dedicated_sql_aggregate_lane,
37    unsupported_sql_aggregate_lane_message,
38};
39use crate::db::session::sql::surface::{
40    SqlSurface, session_sql_lane, sql_statement_route_from_statement, unsupported_sql_lane_message,
41};
42
43#[cfg(feature = "structural-read-metrics")]
44pub use crate::db::session::sql::projection::{
45    SqlProjectionMaterializationMetrics, with_sql_projection_materialization_metrics,
46};
47pub use crate::db::session::sql::surface::{
48    SqlDispatchResult, SqlParsedStatement, SqlStatementRoute,
49};
50#[cfg(feature = "perf-attribution")]
51pub use crate::db::{
52    session::sql::dispatch::LoweredSqlDispatchExecutorAttribution,
53    session::sql::projection::SqlProjectionTextExecutorAttribution,
54};
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57enum SqlComputedProjectionSurface {
58    QueryFrom,
59    ExecuteSql,
60    ExecuteSqlGrouped,
61}
62
63const fn unsupported_sql_computed_projection_message(
64    surface: SqlComputedProjectionSurface,
65) -> &'static str {
66    match surface {
67        SqlComputedProjectionSurface::QueryFrom => {
68            "query_from_sql does not accept computed text projection"
69        }
70        SqlComputedProjectionSurface::ExecuteSql => "execute_sql rejects computed text projection",
71        SqlComputedProjectionSurface::ExecuteSqlGrouped => {
72            "execute_sql_grouped rejects scalar computed text projection"
73        }
74    }
75}
76
77const fn unsupported_sql_write_surface_message(
78    surface: SqlSurface,
79    statement: &SqlStatement,
80) -> &'static str {
81    match (surface, statement) {
82        (SqlSurface::QueryFrom, SqlStatement::Insert(_)) => {
83            "query_from_sql rejects INSERT; use create(...) or insert(...)"
84        }
85        (SqlSurface::QueryFrom, SqlStatement::Update(_)) => {
86            "query_from_sql rejects UPDATE; use update(...)"
87        }
88        (SqlSurface::ExecuteSql, SqlStatement::Insert(_)) => {
89            "execute_sql rejects INSERT; use create(...) or insert(...)"
90        }
91        (SqlSurface::ExecuteSql, SqlStatement::Update(_)) => {
92            "execute_sql rejects UPDATE; use update(...)"
93        }
94        (SqlSurface::ExecuteSqlGrouped, SqlStatement::Insert(_)) => {
95            "execute_sql_grouped rejects INSERT; use create(...) or insert(...)"
96        }
97        (SqlSurface::ExecuteSqlGrouped, SqlStatement::Update(_)) => {
98            "execute_sql_grouped rejects UPDATE; use update(...)"
99        }
100        (SqlSurface::Explain, SqlStatement::Insert(_) | SqlStatement::Update(_)) => {
101            "explain_sql requires EXPLAIN"
102        }
103        (
104            _,
105            SqlStatement::Select(_)
106            | SqlStatement::Delete(_)
107            | SqlStatement::Explain(_)
108            | SqlStatement::Describe(_)
109            | SqlStatement::ShowIndexes(_)
110            | SqlStatement::ShowColumns(_)
111            | SqlStatement::ShowEntities(_),
112        ) => unreachable!(),
113    }
114}
115
116const fn unsupported_sql_returning_surface_message(
117    surface: SqlSurface,
118    statement: &SqlStatement,
119) -> &'static str {
120    match (surface, statement) {
121        (SqlSurface::QueryFrom, SqlStatement::Delete(_)) => {
122            "query_from_sql rejects DELETE RETURNING; use delete::<E>().returning..."
123        }
124        (SqlSurface::ExecuteSql, SqlStatement::Delete(_)) => {
125            "execute_sql rejects DELETE RETURNING; use delete::<E>().returning..."
126        }
127        (SqlSurface::ExecuteSqlGrouped, SqlStatement::Delete(_)) => {
128            "execute_sql_grouped rejects DELETE RETURNING; use delete::<E>().returning..."
129        }
130        (SqlSurface::Explain, SqlStatement::Delete(_)) => "explain_sql requires EXPLAIN",
131        (
132            _,
133            SqlStatement::Select(_)
134            | SqlStatement::Insert(_)
135            | SqlStatement::Update(_)
136            | SqlStatement::Explain(_)
137            | SqlStatement::Describe(_)
138            | SqlStatement::ShowIndexes(_)
139            | SqlStatement::ShowColumns(_)
140            | SqlStatement::ShowEntities(_),
141        ) => unreachable!(),
142    }
143}
144
145impl<C: CanisterKind> DbSession<C> {
146    // Enforce that one single-entity SQL endpoint stays hard-bound to the
147    // typed entity `E` instead of silently reusing unrelated entity names.
148    fn ensure_entity_sql_route_matches<E>(route: &SqlStatementRoute) -> Result<(), QueryError>
149    where
150        E: EntityKind<Canister = C>,
151    {
152        let Some(sql_entity) = (match route {
153            SqlStatementRoute::Query { entity }
154            | SqlStatementRoute::Insert { entity }
155            | SqlStatementRoute::Update { entity }
156            | SqlStatementRoute::Explain { entity }
157            | SqlStatementRoute::Describe { entity }
158            | SqlStatementRoute::ShowIndexes { entity }
159            | SqlStatementRoute::ShowColumns { entity } => Some(entity.as_str()),
160            SqlStatementRoute::ShowEntities => None,
161        }) else {
162            return Ok(());
163        };
164
165        if identifiers_tail_match(sql_entity, E::MODEL.name()) {
166            return Ok(());
167        }
168
169        Err(QueryError::unsupported_query(format!(
170            "execute_entity_sql only supports entity '{}', but received '{sql_entity}'",
171            E::MODEL.name()
172        )))
173    }
174
175    // Resolve planner-visible indexes and build one execution-ready
176    // structural plan at the session SQL boundary.
177    pub(in crate::db::session::sql) fn build_structural_plan_with_visible_indexes_for_authority(
178        &self,
179        query: StructuralQuery,
180        authority: EntityAuthority,
181    ) -> Result<(VisibleIndexes<'_>, AccessPlannedQuery), QueryError> {
182        let visible_indexes =
183            self.visible_indexes_for_store_model(authority.store_path(), authority.model())?;
184        let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
185
186        Ok((visible_indexes, plan))
187    }
188
189    // Lower one parsed SQL statement onto the structural query lane while
190    // keeping dedicated global aggregate execution outside this shared path.
191    fn query_from_sql_parsed<E>(
192        parsed: &SqlParsedStatement,
193        lane_surface: SqlSurface,
194        computed_surface: SqlComputedProjectionSurface,
195        surface: SqlAggregateSurface,
196    ) -> Result<Query<E>, QueryError>
197    where
198        E: EntityKind<Canister = C>,
199    {
200        if matches!(
201            &parsed.statement,
202            SqlStatement::Insert(_) | SqlStatement::Update(_)
203        ) {
204            return Err(QueryError::unsupported_query(
205                unsupported_sql_write_surface_message(lane_surface, &parsed.statement),
206            ));
207        }
208        if matches!(&parsed.statement, SqlStatement::Delete(delete) if delete.returning.is_some()) {
209            return Err(QueryError::unsupported_query(
210                unsupported_sql_returning_surface_message(lane_surface, &parsed.statement),
211            ));
212        }
213
214        if computed_projection::computed_sql_projection_plan(&parsed.statement)?.is_some() {
215            return Err(QueryError::unsupported_query(
216                unsupported_sql_computed_projection_message(computed_surface),
217            ));
218        }
219
220        if parsed_requires_dedicated_sql_aggregate_lane(parsed) {
221            return Err(QueryError::unsupported_query(
222                unsupported_sql_aggregate_lane_message(surface),
223            ));
224        }
225
226        let lowered = lower_sql_command_from_prepared_statement(
227            parsed.prepare(E::MODEL.name())?,
228            E::MODEL.primary_key.name,
229        )
230        .map_err(QueryError::from_sql_lowering_error)?;
231        let lane = session_sql_lane(&lowered);
232        let Some(query) = lowered.query().cloned() else {
233            return Err(QueryError::unsupported_query(unsupported_sql_lane_message(
234                lane_surface,
235                lane,
236            )));
237        };
238        let query = bind_lowered_sql_query::<E>(query, MissingRowPolicy::Ignore)
239            .map_err(QueryError::from_sql_lowering_error)?;
240
241        Ok(query)
242    }
243
244    // Lower one session-owned computed grouped SQL projection onto the typed
245    // grouped query lane without widening generic grouped expression support.
246    fn grouped_query_from_computed_sql_projection_plan<E>(
247        plan: &computed_projection::SqlComputedProjectionPlan,
248    ) -> Result<Query<E>, QueryError>
249    where
250        E: EntityKind<Canister = C>,
251    {
252        let lowered = lower_sql_command_from_prepared_statement(
253            prepare_sql_statement(plan.cloned_base_statement(), E::MODEL.name())
254                .map_err(QueryError::from_sql_lowering_error)?,
255            E::MODEL.primary_key.name,
256        )
257        .map_err(QueryError::from_sql_lowering_error)?;
258        let Some(query) = lowered.query().cloned() else {
259            return Err(QueryError::unsupported_query(unsupported_sql_lane_message(
260                SqlSurface::ExecuteSqlGrouped,
261                session_sql_lane(&lowered),
262            )));
263        };
264        let query = bind_lowered_sql_query::<E>(query, MissingRowPolicy::Ignore)
265            .map_err(QueryError::from_sql_lowering_error)?;
266        Self::ensure_sql_query_grouping(&query, dispatch::SqlGroupingSurface::Grouped)?;
267
268        Ok(query)
269    }
270
271    /// Parse one reduced SQL statement and return one reusable parsed envelope.
272    ///
273    /// This method is the SQL parse authority for dynamic route selection.
274    pub fn parse_sql_statement(&self, sql: &str) -> Result<SqlParsedStatement, QueryError> {
275        let statement = parse_sql(sql).map_err(QueryError::from_sql_parse_error)?;
276        let route = sql_statement_route_from_statement(&statement);
277
278        Ok(SqlParsedStatement::new(statement, route))
279    }
280
281    /// Parse one reduced SQL statement into canonical routing metadata.
282    ///
283    /// This method is the SQL dispatch authority for entity/surface routing
284    /// outside typed-entity lowering paths.
285    pub fn sql_statement_route(&self, sql: &str) -> Result<SqlStatementRoute, QueryError> {
286        let parsed = self.parse_sql_statement(sql)?;
287
288        Ok(parsed.route().clone())
289    }
290
291    /// Build one typed query intent from one reduced SQL statement.
292    ///
293    /// This parser/lowering entrypoint is intentionally constrained to the
294    /// executable subset wired in the current release.
295    pub fn query_from_sql<E>(&self, sql: &str) -> Result<Query<E>, QueryError>
296    where
297        E: EntityKind<Canister = C>,
298    {
299        let parsed = self.parse_sql_statement(sql)?;
300
301        Self::query_from_sql_parsed::<E>(
302            &parsed,
303            SqlSurface::QueryFrom,
304            SqlComputedProjectionSurface::QueryFrom,
305            SqlAggregateSurface::QueryFrom,
306        )
307    }
308
309    /// Execute one reduced SQL `SELECT` statement for entity `E`.
310    pub fn execute_sql<E>(&self, sql: &str) -> Result<EntityResponse<E>, QueryError>
311    where
312        E: PersistedRow<Canister = C> + EntityValue,
313    {
314        let parsed = self.parse_sql_statement(sql)?;
315        if matches!(&parsed.statement, SqlStatement::Delete(_)) {
316            return Err(QueryError::unsupported_query(
317                "execute_sql rejects DELETE; use delete::<E>()",
318            ));
319        }
320        let query = Self::query_from_sql_parsed::<E>(
321            &parsed,
322            SqlSurface::ExecuteSql,
323            SqlComputedProjectionSurface::ExecuteSql,
324            SqlAggregateSurface::ExecuteSql,
325        )?;
326        Self::ensure_sql_query_grouping(&query, dispatch::SqlGroupingSurface::Scalar)?;
327
328        self.execute_query(&query)
329    }
330
331    /// Execute one single-entity reduced SQL statement.
332    ///
333    /// This helper is intentionally hard-bound to `E` and exists for canister
334    /// endpoints that want one tiny SQL forwarder without reviving dynamic
335    /// entity dispatch or typed-entity SQL result decoding.
336    pub fn execute_entity_sql<E>(&self, sql: &str) -> Result<SqlDispatchResult, QueryError>
337    where
338        E: PersistedRow<Canister = C> + EntityValue,
339    {
340        let parsed = self.parse_sql_statement(sql)?;
341
342        Self::ensure_entity_sql_route_matches::<E>(parsed.route())?;
343
344        self.execute_sql_dispatch_parsed::<E>(&parsed)
345    }
346
347    /// Execute one reduced SQL grouped `SELECT` statement and return grouped rows.
348    pub fn execute_sql_grouped<E>(
349        &self,
350        sql: &str,
351        cursor_token: Option<&str>,
352    ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
353    where
354        E: PersistedRow<Canister = C> + EntityValue,
355    {
356        let parsed = self.parse_sql_statement(sql)?;
357
358        if matches!(&parsed.statement, SqlStatement::Delete(_)) {
359            return Err(QueryError::unsupported_query(
360                "execute_sql_grouped rejects DELETE; use delete::<E>()",
361            ));
362        }
363
364        if let Some(plan) = computed_projection::computed_sql_projection_plan(&parsed.statement)? {
365            if !plan.is_grouped() {
366                return Err(QueryError::unsupported_query(
367                    unsupported_sql_computed_projection_message(
368                        SqlComputedProjectionSurface::ExecuteSqlGrouped,
369                    ),
370                ));
371            }
372
373            let query = Self::grouped_query_from_computed_sql_projection_plan::<E>(&plan)?;
374            let grouped = self.execute_grouped(&query, cursor_token)?;
375            let (rows, continuation_cursor, execution_trace) = grouped.into_parts();
376            let rows =
377                computed_projection::apply_computed_sql_projection_grouped_rows(rows, &plan)?;
378
379            return Ok(PagedGroupedExecutionWithTrace::new(
380                rows,
381                continuation_cursor,
382                execution_trace,
383            ));
384        }
385
386        let query = Self::query_from_sql_parsed::<E>(
387            &parsed,
388            SqlSurface::ExecuteSqlGrouped,
389            SqlComputedProjectionSurface::ExecuteSqlGrouped,
390            SqlAggregateSurface::ExecuteSqlGrouped,
391        )?;
392        Self::ensure_sql_query_grouping(&query, dispatch::SqlGroupingSurface::Grouped)?;
393
394        self.execute_grouped(&query, cursor_token)
395    }
396
397    /// Execute one reduced SQL grouped `SELECT` statement and return one text cursor directly.
398    #[doc(hidden)]
399    pub fn execute_sql_grouped_text_cursor<E>(
400        &self,
401        sql: &str,
402        cursor_token: Option<&str>,
403    ) -> Result<GroupedTextCursorPageWithTrace, QueryError>
404    where
405        E: PersistedRow<Canister = C> + EntityValue,
406    {
407        let parsed = self.parse_sql_statement(sql)?;
408
409        if matches!(&parsed.statement, SqlStatement::Delete(_)) {
410            return Err(QueryError::unsupported_query(
411                "execute_sql_grouped rejects DELETE; use delete::<E>()",
412            ));
413        }
414
415        if let Some(plan) = computed_projection::computed_sql_projection_plan(&parsed.statement)? {
416            if !plan.is_grouped() {
417                return Err(QueryError::unsupported_query(
418                    unsupported_sql_computed_projection_message(
419                        SqlComputedProjectionSurface::ExecuteSqlGrouped,
420                    ),
421                ));
422            }
423
424            let query = Self::grouped_query_from_computed_sql_projection_plan::<E>(&plan)?;
425            let (rows, continuation_cursor, execution_trace) =
426                self.execute_grouped_text_cursor(&query, cursor_token)?;
427            let rows =
428                computed_projection::apply_computed_sql_projection_grouped_rows(rows, &plan)?;
429
430            return Ok((rows, continuation_cursor, execution_trace));
431        }
432
433        let query = Self::query_from_sql_parsed::<E>(
434            &parsed,
435            SqlSurface::ExecuteSqlGrouped,
436            SqlComputedProjectionSurface::ExecuteSqlGrouped,
437            SqlAggregateSurface::ExecuteSqlGrouped,
438        )?;
439        Self::ensure_sql_query_grouping(&query, dispatch::SqlGroupingSurface::Grouped)?;
440
441        self.execute_grouped_text_cursor(&query, cursor_token)
442    }
443}