icydb_core/db/session/sql/
mod.rs1mod 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#[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
72pub(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 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 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 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 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 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 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 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}