Skip to main content

icydb_core/db/session/sql/
mod.rs

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