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