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