Skip to main content

icydb_core/db/session/sql/
mod.rs

1//! Module: db::session::sql
2//! Responsibility: session-owned SQL execution, 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 computed_projection;
8mod execute;
9mod explain;
10mod projection;
11
12use crate::{
13    db::{
14        DbSession, GroupedRow, PersistedRow, QueryError,
15        executor::EntityAuthority,
16        query::{
17            intent::StructuralQuery,
18            plan::{AccessPlannedQuery, VisibleIndexes},
19        },
20        sql::parser::{SqlStatement, parse_sql},
21    },
22    traits::{CanisterKind, EntityKind, EntityValue},
23};
24
25#[cfg(test)]
26use crate::db::{
27    MissingRowPolicy, PagedGroupedExecutionWithTrace,
28    sql::lowering::{
29        bind_lowered_sql_query, lower_sql_command_from_prepared_statement, prepare_sql_statement,
30    },
31};
32
33#[cfg(feature = "structural-read-metrics")]
34pub use crate::db::session::sql::projection::{
35    SqlProjectionMaterializationMetrics, with_sql_projection_materialization_metrics,
36};
37#[cfg(feature = "perf-attribution")]
38pub use crate::db::{
39    session::sql::execute::LoweredSqlStatementExecutorAttribution,
40    session::sql::projection::SqlProjectionTextExecutorAttribution,
41};
42
43/// Unified SQL statement payload returned by shared SQL lane execution.
44#[derive(Debug)]
45pub enum SqlStatementResult {
46    Count {
47        row_count: u32,
48    },
49    Projection {
50        columns: Vec<String>,
51        rows: Vec<Vec<crate::value::Value>>,
52        row_count: u32,
53    },
54    ProjectionText {
55        columns: Vec<String>,
56        rows: Vec<Vec<String>>,
57        row_count: u32,
58    },
59    Grouped {
60        columns: Vec<String>,
61        rows: Vec<GroupedRow>,
62        row_count: u32,
63        next_cursor: Option<String>,
64    },
65    Explain(String),
66    Describe(crate::db::EntitySchemaDescription),
67    ShowIndexes(Vec<String>),
68    ShowColumns(Vec<crate::db::EntityFieldDescription>),
69    ShowEntities(Vec<String>),
70}
71
72// Keep parsing as a module-owned helper instead of hanging a pure parser off
73// `DbSession` as a fake session method.
74pub(in crate::db) fn parse_sql_statement(sql: &str) -> Result<SqlStatement, QueryError> {
75    parse_sql(sql).map_err(QueryError::from_sql_parse_error)
76}
77
78impl<C: CanisterKind> DbSession<C> {
79    // Resolve planner-visible indexes and build one execution-ready
80    // structural plan at the session SQL boundary.
81    pub(in crate::db::session::sql) fn build_structural_plan_with_visible_indexes_for_authority(
82        &self,
83        query: StructuralQuery,
84        authority: EntityAuthority,
85    ) -> Result<(VisibleIndexes<'_>, AccessPlannedQuery), QueryError> {
86        let visible_indexes =
87            self.visible_indexes_for_store_model(authority.store_path(), authority.model())?;
88        let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
89
90        Ok((visible_indexes, plan))
91    }
92
93    // Enforce that the public typed SQL executors stay hard-bound to the
94    // typed entity `E` instead of silently reusing unrelated entity names.
95    fn ensure_typed_sql_statement_matches<E>(statement: &SqlStatement) -> Result<(), QueryError>
96    where
97        E: EntityKind<Canister = C>,
98    {
99        let Some(sql_entity) = (match statement {
100            SqlStatement::Select(select) => Some(select.entity.as_str()),
101            SqlStatement::Delete(delete) => Some(delete.entity.as_str()),
102            SqlStatement::Insert(insert) => Some(insert.entity.as_str()),
103            SqlStatement::Update(update) => Some(update.entity.as_str()),
104            SqlStatement::Explain(explain) => Some(match &explain.statement {
105                crate::db::sql::parser::SqlExplainTarget::Select(select) => select.entity.as_str(),
106                crate::db::sql::parser::SqlExplainTarget::Delete(delete) => delete.entity.as_str(),
107            }),
108            SqlStatement::Describe(describe) => Some(describe.entity.as_str()),
109            SqlStatement::ShowIndexes(show_indexes) => Some(show_indexes.entity.as_str()),
110            SqlStatement::ShowColumns(show_columns) => Some(show_columns.entity.as_str()),
111            SqlStatement::ShowEntities(_) => None,
112        }) else {
113            return Ok(());
114        };
115
116        if crate::db::identifiers_tail_match(sql_entity, E::MODEL.name()) {
117            return Ok(());
118        }
119
120        Err(QueryError::unsupported_query(format!(
121            "typed SQL only supports entity '{}', but received '{sql_entity}'",
122            E::MODEL.name()
123        )))
124    }
125
126    // Keep the public SQL query surface aligned with its name and with
127    // query-shaped canister entrypoints.
128    fn ensure_sql_query_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
129        match statement {
130            SqlStatement::Select(_)
131            | SqlStatement::Explain(_)
132            | SqlStatement::Describe(_)
133            | SqlStatement::ShowIndexes(_)
134            | SqlStatement::ShowColumns(_)
135            | SqlStatement::ShowEntities(_) => Ok(()),
136            SqlStatement::Insert(_) => Err(QueryError::unsupported_query(
137                "execute_sql_query rejects INSERT; use execute_sql_update::<E>()",
138            )),
139            SqlStatement::Update(_) => Err(QueryError::unsupported_query(
140                "execute_sql_query rejects UPDATE; use execute_sql_update::<E>()",
141            )),
142            SqlStatement::Delete(_) => Err(QueryError::unsupported_query(
143                "execute_sql_query rejects DELETE; use execute_sql_update::<E>()",
144            )),
145        }
146    }
147
148    // Keep the public SQL mutation surface aligned with state-changing SQL
149    // while preserving one explicit read/introspection owner.
150    fn ensure_sql_update_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
151        match statement {
152            SqlStatement::Insert(_) | SqlStatement::Update(_) | SqlStatement::Delete(_) => Ok(()),
153            SqlStatement::Select(_) => Err(QueryError::unsupported_query(
154                "execute_sql_update rejects SELECT; use execute_sql_query::<E>()",
155            )),
156            SqlStatement::Explain(_) => Err(QueryError::unsupported_query(
157                "execute_sql_update rejects EXPLAIN; use execute_sql_query::<E>()",
158            )),
159            SqlStatement::Describe(_) => Err(QueryError::unsupported_query(
160                "execute_sql_update rejects DESCRIBE; use execute_sql_query::<E>()",
161            )),
162            SqlStatement::ShowIndexes(_) => Err(QueryError::unsupported_query(
163                "execute_sql_update rejects SHOW INDEXES; use execute_sql_query::<E>()",
164            )),
165            SqlStatement::ShowColumns(_) => Err(QueryError::unsupported_query(
166                "execute_sql_update rejects SHOW COLUMNS; use execute_sql_query::<E>()",
167            )),
168            SqlStatement::ShowEntities(_) => Err(QueryError::unsupported_query(
169                "execute_sql_update rejects SHOW ENTITIES; use execute_sql_query::<E>()",
170            )),
171        }
172    }
173
174    /// Execute one single-entity reduced SQL query or introspection statement.
175    ///
176    /// This surface stays hard-bound to `E`, rejects state-changing SQL, and
177    /// returns SQL-shaped statement output instead of typed entities.
178    pub fn execute_sql_query<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
179    where
180        E: PersistedRow<Canister = C> + EntityValue,
181    {
182        let parsed = parse_sql_statement(sql)?;
183
184        Self::ensure_typed_sql_statement_matches::<E>(&parsed)?;
185        Self::ensure_sql_query_statement_supported(&parsed)?;
186
187        self.execute_sql_statement_inner::<E>(&parsed)
188    }
189
190    /// Execute one single-entity reduced SQL mutation statement.
191    ///
192    /// This surface stays hard-bound to `E`, rejects read-only SQL, and
193    /// returns SQL-shaped mutation output such as counts or `RETURNING` rows.
194    pub fn execute_sql_update<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
195    where
196        E: PersistedRow<Canister = C> + EntityValue,
197    {
198        let parsed = parse_sql_statement(sql)?;
199
200        Self::ensure_typed_sql_statement_matches::<E>(&parsed)?;
201        Self::ensure_sql_update_statement_supported(&parsed)?;
202
203        self.execute_sql_statement_inner::<E>(&parsed)
204    }
205
206    #[cfg(test)]
207    pub(in crate::db) fn execute_grouped_sql_query_for_tests<E>(
208        &self,
209        sql: &str,
210        cursor_token: Option<&str>,
211    ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
212    where
213        E: PersistedRow<Canister = C> + EntityValue,
214    {
215        let parsed = parse_sql_statement(sql)?;
216
217        // Keep grouped computed SQL on the same computed-projection plan used
218        // by the live statement executor while preserving grouped cursor
219        // behavior for the grouped SELECT test helpers.
220        if let Some(plan) = computed_projection::computed_sql_projection_plan(&parsed)? {
221            let lowered = lower_sql_command_from_prepared_statement(
222                prepare_sql_statement(plan.cloned_base_statement(), E::MODEL.name())
223                    .map_err(QueryError::from_sql_lowering_error)?,
224                E::MODEL.primary_key.name,
225            )
226            .map_err(QueryError::from_sql_lowering_error)?;
227            let Some(query) = lowered.query().cloned() else {
228                return Err(QueryError::unsupported_query(
229                    "grouped SELECT helper requires grouped SELECT",
230                ));
231            };
232            let query = bind_lowered_sql_query::<E>(query, MissingRowPolicy::Ignore)
233                .map_err(QueryError::from_sql_lowering_error)?;
234
235            if !query.has_grouping() {
236                return Err(QueryError::unsupported_query(
237                    "grouped SELECT helper rejects scalar computed text projection",
238                ));
239            }
240
241            let execution = self.execute_grouped(&query, cursor_token)?;
242            let (rows, continuation_cursor, execution_trace) = execution.into_parts();
243            let rows =
244                computed_projection::apply_computed_sql_projection_grouped_rows(rows, &plan)?;
245
246            return Ok(PagedGroupedExecutionWithTrace::new(
247                rows,
248                continuation_cursor,
249                execution_trace,
250            ));
251        }
252
253        let lowered = lower_sql_command_from_prepared_statement(
254            prepare_sql_statement(parsed, E::MODEL.name())
255                .map_err(QueryError::from_sql_lowering_error)?,
256            E::MODEL.primary_key.name,
257        )
258        .map_err(QueryError::from_sql_lowering_error)?;
259        let Some(query) = lowered.query().cloned() else {
260            return Err(QueryError::unsupported_query(
261                "grouped SELECT helper requires grouped SELECT",
262            ));
263        };
264        let query = bind_lowered_sql_query::<E>(query, MissingRowPolicy::Ignore)
265            .map_err(QueryError::from_sql_lowering_error)?;
266
267        if !query.has_grouping() {
268            return Err(QueryError::unsupported_query(
269                "grouped SELECT helper requires grouped SELECT",
270            ));
271        }
272
273        self.execute_grouped(&query, cursor_token)
274    }
275}