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        query::{
20            intent::StructuralQuery,
21            plan::{AccessPlannedQuery, VisibleIndexes},
22        },
23        sql::{
24            lowering::{bind_lowered_sql_query, lower_sql_command_from_prepared_statement},
25            parser::{SqlStatement, parse_sql},
26        },
27    },
28    traits::{CanisterKind, EntityKind, EntityValue},
29};
30
31use crate::db::session::sql::aggregate::{
32    SqlAggregateSurface, parsed_requires_dedicated_sql_aggregate_lane,
33    unsupported_sql_aggregate_lane_message,
34};
35use crate::db::session::sql::surface::{
36    SqlSurface, session_sql_lane, sql_statement_route_from_statement, unsupported_sql_lane_message,
37};
38
39pub use crate::db::session::sql::surface::{
40    SqlDispatchResult, SqlParsedStatement, SqlStatementRoute,
41};
42#[cfg(feature = "perf-attribution")]
43pub use crate::db::{
44    executor::SqlProjectionTextExecutorAttribution,
45    session::sql::dispatch::LoweredSqlDispatchExecutorAttribution,
46};
47
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49enum SqlComputedProjectionSurface {
50    QueryFrom,
51    ExecuteSql,
52    ExecuteSqlGrouped,
53}
54
55const fn unsupported_sql_computed_projection_message(
56    surface: SqlComputedProjectionSurface,
57) -> &'static str {
58    match surface {
59        SqlComputedProjectionSurface::QueryFrom => {
60            "query_from_sql does not accept computed text projection; use execute_sql_dispatch(...)"
61        }
62        SqlComputedProjectionSurface::ExecuteSql => {
63            "execute_sql rejects computed text projection; use execute_sql_dispatch(...)"
64        }
65        SqlComputedProjectionSurface::ExecuteSqlGrouped => {
66            "execute_sql_grouped rejects computed text projection; use execute_sql_dispatch(...)"
67        }
68    }
69}
70
71impl<C: CanisterKind> DbSession<C> {
72    // Resolve planner-visible indexes and build one execution-ready
73    // structural plan at the session SQL boundary.
74    pub(in crate::db::session::sql) fn build_structural_plan_with_visible_indexes_for_authority(
75        &self,
76        query: StructuralQuery,
77        authority: EntityAuthority,
78    ) -> Result<(VisibleIndexes<'_>, AccessPlannedQuery), QueryError> {
79        let visible_indexes =
80            self.visible_indexes_for_store_model(authority.store_path(), authority.model())?;
81        let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
82
83        Ok((visible_indexes, plan))
84    }
85
86    // Lower one parsed SQL statement onto the structural query lane while
87    // keeping dedicated global aggregate execution outside this shared path.
88    fn query_from_sql_parsed<E>(
89        parsed: &SqlParsedStatement,
90        lane_surface: SqlSurface,
91        computed_surface: SqlComputedProjectionSurface,
92        surface: SqlAggregateSurface,
93    ) -> Result<Query<E>, QueryError>
94    where
95        E: EntityKind<Canister = C>,
96    {
97        if computed_projection::computed_sql_projection_plan(&parsed.statement)?.is_some() {
98            return Err(QueryError::unsupported_query(
99                unsupported_sql_computed_projection_message(computed_surface),
100            ));
101        }
102
103        if parsed_requires_dedicated_sql_aggregate_lane(parsed) {
104            return Err(QueryError::unsupported_query(
105                unsupported_sql_aggregate_lane_message(surface),
106            ));
107        }
108
109        let lowered = lower_sql_command_from_prepared_statement(
110            parsed.prepare(E::MODEL.name())?,
111            E::MODEL.primary_key.name,
112        )
113        .map_err(QueryError::from_sql_lowering_error)?;
114        let lane = session_sql_lane(&lowered);
115        let Some(query) = lowered.query().cloned() else {
116            return Err(QueryError::unsupported_query(unsupported_sql_lane_message(
117                lane_surface,
118                lane,
119            )));
120        };
121        let query = bind_lowered_sql_query::<E>(query, MissingRowPolicy::Ignore)
122            .map_err(QueryError::from_sql_lowering_error)?;
123
124        Ok(query)
125    }
126
127    /// Parse one reduced SQL statement and return one reusable parsed envelope.
128    ///
129    /// This method is the SQL parse authority for dynamic route selection.
130    pub fn parse_sql_statement(&self, sql: &str) -> Result<SqlParsedStatement, QueryError> {
131        let statement = parse_sql(sql).map_err(QueryError::from_sql_parse_error)?;
132        let route = sql_statement_route_from_statement(&statement);
133
134        Ok(SqlParsedStatement::new(statement, route))
135    }
136
137    /// Parse one reduced SQL statement into canonical routing metadata.
138    ///
139    /// This method is the SQL dispatch authority for entity/surface routing
140    /// outside typed-entity lowering paths.
141    pub fn sql_statement_route(&self, sql: &str) -> Result<SqlStatementRoute, QueryError> {
142        let parsed = self.parse_sql_statement(sql)?;
143
144        Ok(parsed.route().clone())
145    }
146
147    /// Build one typed query intent from one reduced SQL statement.
148    ///
149    /// This parser/lowering entrypoint is intentionally constrained to the
150    /// executable subset wired in the current release.
151    pub fn query_from_sql<E>(&self, sql: &str) -> Result<Query<E>, QueryError>
152    where
153        E: EntityKind<Canister = C>,
154    {
155        let parsed = self.parse_sql_statement(sql)?;
156
157        Self::query_from_sql_parsed::<E>(
158            &parsed,
159            SqlSurface::QueryFrom,
160            SqlComputedProjectionSurface::QueryFrom,
161            SqlAggregateSurface::QueryFrom,
162        )
163    }
164
165    /// Execute one reduced SQL `SELECT`/`DELETE` statement for entity `E`.
166    pub fn execute_sql<E>(&self, sql: &str) -> Result<EntityResponse<E>, QueryError>
167    where
168        E: PersistedRow<Canister = C> + EntityValue,
169    {
170        let parsed = self.parse_sql_statement(sql)?;
171        let query = Self::query_from_sql_parsed::<E>(
172            &parsed,
173            SqlSurface::ExecuteSql,
174            SqlComputedProjectionSurface::ExecuteSql,
175            SqlAggregateSurface::ExecuteSql,
176        )?;
177        Self::ensure_sql_query_grouping(&query, dispatch::SqlGroupingSurface::Scalar)?;
178
179        self.execute_query(&query)
180    }
181
182    /// Execute one reduced SQL grouped `SELECT` statement and return grouped rows.
183    pub fn execute_sql_grouped<E>(
184        &self,
185        sql: &str,
186        cursor_token: Option<&str>,
187    ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
188    where
189        E: PersistedRow<Canister = C> + EntityValue,
190    {
191        let parsed = self.parse_sql_statement(sql)?;
192
193        if matches!(&parsed.statement, SqlStatement::Delete(_)) {
194            return Err(QueryError::unsupported_query(
195                "execute_sql_grouped rejects DELETE; use execute_sql_dispatch(...)",
196            ));
197        }
198
199        let query = Self::query_from_sql_parsed::<E>(
200            &parsed,
201            SqlSurface::ExecuteSqlGrouped,
202            SqlComputedProjectionSurface::ExecuteSqlGrouped,
203            SqlAggregateSurface::ExecuteSqlGrouped,
204        )?;
205        Self::ensure_sql_query_grouping(&query, dispatch::SqlGroupingSurface::Grouped)?;
206
207        self.execute_grouped(&query, cursor_token)
208    }
209
210    /// Execute one reduced SQL grouped `SELECT` statement and return one text cursor directly.
211    #[doc(hidden)]
212    pub fn execute_sql_grouped_text_cursor<E>(
213        &self,
214        sql: &str,
215        cursor_token: Option<&str>,
216    ) -> Result<GroupedTextCursorPageWithTrace, QueryError>
217    where
218        E: PersistedRow<Canister = C> + EntityValue,
219    {
220        let parsed = self.parse_sql_statement(sql)?;
221
222        if matches!(&parsed.statement, SqlStatement::Delete(_)) {
223            return Err(QueryError::unsupported_query(
224                "execute_sql_grouped rejects DELETE; use execute_sql_dispatch(...)",
225            ));
226        }
227
228        let query = Self::query_from_sql_parsed::<E>(
229            &parsed,
230            SqlSurface::ExecuteSqlGrouped,
231            SqlComputedProjectionSurface::ExecuteSqlGrouped,
232            SqlAggregateSurface::ExecuteSqlGrouped,
233        )?;
234        Self::ensure_sql_query_grouping(&query, dispatch::SqlGroupingSurface::Grouped)?;
235
236        self.execute_grouped_text_cursor(&query, cursor_token)
237    }
238}