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, EntityValue},
22};
23
24#[cfg(test)]
25use crate::db::{
26    MissingRowPolicy, PagedGroupedExecutionWithTrace,
27    sql::lowering::{
28        LoweredSelectQueryShape, bind_lowered_sql_query, lower_sql_command_from_prepared_statement,
29        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    // Keep the public SQL query surface aligned with its name and with
94    // query-shaped canister entrypoints.
95    fn ensure_sql_query_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
96        match statement {
97            SqlStatement::Select(_)
98            | SqlStatement::Explain(_)
99            | SqlStatement::Describe(_)
100            | SqlStatement::ShowIndexes(_)
101            | SqlStatement::ShowColumns(_)
102            | SqlStatement::ShowEntities(_) => Ok(()),
103            SqlStatement::Insert(_) => Err(QueryError::unsupported_query(
104                "execute_sql_query rejects INSERT; use execute_sql_update::<E>()",
105            )),
106            SqlStatement::Update(_) => Err(QueryError::unsupported_query(
107                "execute_sql_query rejects UPDATE; use execute_sql_update::<E>()",
108            )),
109            SqlStatement::Delete(_) => Err(QueryError::unsupported_query(
110                "execute_sql_query rejects DELETE; use execute_sql_update::<E>()",
111            )),
112        }
113    }
114
115    // Keep the public SQL mutation surface aligned with state-changing SQL
116    // while preserving one explicit read/introspection owner.
117    fn ensure_sql_update_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
118        match statement {
119            SqlStatement::Insert(_) | SqlStatement::Update(_) | SqlStatement::Delete(_) => Ok(()),
120            SqlStatement::Select(_) => Err(QueryError::unsupported_query(
121                "execute_sql_update rejects SELECT; use execute_sql_query::<E>()",
122            )),
123            SqlStatement::Explain(_) => Err(QueryError::unsupported_query(
124                "execute_sql_update rejects EXPLAIN; use execute_sql_query::<E>()",
125            )),
126            SqlStatement::Describe(_) => Err(QueryError::unsupported_query(
127                "execute_sql_update rejects DESCRIBE; use execute_sql_query::<E>()",
128            )),
129            SqlStatement::ShowIndexes(_) => Err(QueryError::unsupported_query(
130                "execute_sql_update rejects SHOW INDEXES; use execute_sql_query::<E>()",
131            )),
132            SqlStatement::ShowColumns(_) => Err(QueryError::unsupported_query(
133                "execute_sql_update rejects SHOW COLUMNS; use execute_sql_query::<E>()",
134            )),
135            SqlStatement::ShowEntities(_) => Err(QueryError::unsupported_query(
136                "execute_sql_update rejects SHOW ENTITIES; use execute_sql_query::<E>()",
137            )),
138        }
139    }
140
141    /// Execute one single-entity reduced SQL query or introspection statement.
142    ///
143    /// This surface stays hard-bound to `E`, rejects state-changing SQL, and
144    /// returns SQL-shaped statement output instead of typed entities.
145    pub fn execute_sql_query<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
146    where
147        E: PersistedRow<Canister = C> + EntityValue,
148    {
149        let parsed = parse_sql_statement(sql)?;
150
151        Self::ensure_sql_query_statement_supported(&parsed)?;
152
153        self.execute_sql_statement_inner::<E>(&parsed)
154    }
155
156    /// Execute one single-entity reduced SQL mutation statement.
157    ///
158    /// This surface stays hard-bound to `E`, rejects read-only SQL, and
159    /// returns SQL-shaped mutation output such as counts or `RETURNING` rows.
160    pub fn execute_sql_update<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
161    where
162        E: PersistedRow<Canister = C> + EntityValue,
163    {
164        let parsed = parse_sql_statement(sql)?;
165
166        Self::ensure_sql_update_statement_supported(&parsed)?;
167
168        self.execute_sql_statement_inner::<E>(&parsed)
169    }
170
171    #[cfg(test)]
172    pub(in crate::db) fn execute_grouped_sql_query_for_tests<E>(
173        &self,
174        sql: &str,
175        cursor_token: Option<&str>,
176    ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
177    where
178        E: PersistedRow<Canister = C> + EntityValue,
179    {
180        let parsed = parse_sql_statement(sql)?;
181
182        let lowered = lower_sql_command_from_prepared_statement(
183            prepare_sql_statement(parsed, E::MODEL.name())
184                .map_err(QueryError::from_sql_lowering_error)?,
185            E::MODEL.primary_key.name,
186        )
187        .map_err(QueryError::from_sql_lowering_error)?;
188        let Some(query) = lowered.query().cloned() else {
189            return Err(QueryError::unsupported_query(
190                "grouped SELECT helper requires grouped SELECT",
191            ));
192        };
193        if query.select_shape() != Some(LoweredSelectQueryShape::Grouped) {
194            return Err(QueryError::unsupported_query(
195                "grouped SELECT helper requires grouped SELECT",
196            ));
197        }
198        let query = bind_lowered_sql_query::<E>(query, MissingRowPolicy::Ignore)
199            .map_err(QueryError::from_sql_lowering_error)?;
200
201        self.execute_grouped(&query, cursor_token)
202    }
203}