1mod execute;
8mod explain;
9mod projection;
10
11#[cfg(feature = "perf-attribution")]
12use candid::CandidType;
13#[cfg(feature = "perf-attribution")]
14use serde::Deserialize;
15use std::{cell::RefCell, collections::HashMap};
16
17use crate::db::sql::parser::{SqlDeleteStatement, SqlInsertStatement, SqlUpdateStatement};
18use crate::{
19 db::{
20 DbSession, GroupedRow, PersistedRow, QueryError,
21 commit::CommitSchemaFingerprint,
22 executor::EntityAuthority,
23 query::{
24 intent::StructuralQuery,
25 plan::{AccessPlannedQuery, VisibleIndexes},
26 },
27 schema::commit_schema_fingerprint_for_entity,
28 session::sql::projection::projection_labels_from_projection_spec,
29 sql::lowering::{LoweredBaseQueryShape, LoweredSqlCommand, SqlGlobalAggregateCommandCore},
30 sql::parser::{SqlStatement, parse_sql},
31 },
32 traits::{CanisterKind, EntityValue},
33};
34
35#[cfg(test)]
36use crate::db::{
37 MissingRowPolicy, PagedGroupedExecutionWithTrace,
38 sql::lowering::{
39 bind_lowered_sql_query, lower_sql_command_from_prepared_statement, prepare_sql_statement,
40 },
41};
42
43#[cfg(feature = "structural-read-metrics")]
44pub use crate::db::session::sql::projection::{
45 SqlProjectionMaterializationMetrics, with_sql_projection_materialization_metrics,
46};
47
48#[derive(Debug)]
50pub enum SqlStatementResult {
51 Count {
52 row_count: u32,
53 },
54 Projection {
55 columns: Vec<String>,
56 rows: Vec<Vec<crate::value::Value>>,
57 row_count: u32,
58 },
59 ProjectionText {
60 columns: Vec<String>,
61 rows: Vec<Vec<String>>,
62 row_count: u32,
63 },
64 Grouped {
65 columns: Vec<String>,
66 rows: Vec<GroupedRow>,
67 row_count: u32,
68 next_cursor: Option<String>,
69 },
70 Explain(String),
71 Describe(crate::db::EntitySchemaDescription),
72 ShowIndexes(Vec<String>),
73 ShowColumns(Vec<crate::db::EntityFieldDescription>),
74 ShowEntities(Vec<String>),
75}
76
77#[cfg(feature = "perf-attribution")]
88#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
89pub struct SqlQueryExecutionAttribution {
90 pub compile_local_instructions: u64,
91 pub execute_local_instructions: u64,
92 pub total_local_instructions: u64,
93}
94
95#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
96enum SqlCompiledCommandSurface {
97 Query,
98 Update,
99}
100
101#[derive(Clone, Debug, Eq, Hash, PartialEq)]
112pub(in crate::db) struct SqlCompiledCommandCacheKey {
113 surface: SqlCompiledCommandSurface,
114 entity_path: &'static str,
115 schema_fingerprint: CommitSchemaFingerprint,
116 sql: String,
117}
118
119#[derive(Clone, Debug, Eq, Hash, PartialEq)]
120pub(in crate::db) struct SqlSelectPlanCacheKey {
121 compiled: SqlCompiledCommandCacheKey,
122 visibility: crate::db::session::query::QueryPlanVisibility,
123}
124
125#[derive(Clone, Debug)]
135pub(in crate::db) struct SqlSelectPlanCacheEntry {
136 plan: AccessPlannedQuery,
137 columns: Vec<String>,
138}
139
140impl SqlSelectPlanCacheEntry {
141 #[must_use]
142 pub(in crate::db) const fn new(plan: AccessPlannedQuery, columns: Vec<String>) -> Self {
143 Self { plan, columns }
144 }
145
146 #[must_use]
147 pub(in crate::db) fn into_parts(self) -> (AccessPlannedQuery, Vec<String>) {
148 (self.plan, self.columns)
149 }
150}
151
152impl SqlCompiledCommandCacheKey {
153 fn query_for_entity<E>(sql: &str) -> Self
154 where
155 E: PersistedRow + EntityValue,
156 {
157 Self::for_entity::<E>(SqlCompiledCommandSurface::Query, sql)
158 }
159
160 fn update_for_entity<E>(sql: &str) -> Self
161 where
162 E: PersistedRow + EntityValue,
163 {
164 Self::for_entity::<E>(SqlCompiledCommandSurface::Update, sql)
165 }
166
167 fn for_entity<E>(surface: SqlCompiledCommandSurface, sql: &str) -> Self
168 where
169 E: PersistedRow + EntityValue,
170 {
171 Self {
172 surface,
173 entity_path: E::PATH,
174 schema_fingerprint: commit_schema_fingerprint_for_entity::<E>(),
175 sql: sql.to_string(),
176 }
177 }
178
179 #[must_use]
180 pub(in crate::db) const fn entity_path(&self) -> &'static str {
181 self.entity_path
182 }
183
184 #[must_use]
185 pub(in crate::db) const fn schema_fingerprint(&self) -> CommitSchemaFingerprint {
186 self.schema_fingerprint
187 }
188}
189
190impl SqlSelectPlanCacheKey {
191 const fn from_compiled_key(
192 compiled: SqlCompiledCommandCacheKey,
193 visibility: crate::db::session::query::QueryPlanVisibility,
194 ) -> Self {
195 Self {
196 compiled,
197 visibility,
198 }
199 }
200}
201
202pub(in crate::db) type SqlCompiledCommandCache =
203 HashMap<SqlCompiledCommandCacheKey, CompiledSqlCommand>;
204pub(in crate::db) type SqlSelectPlanCache = HashMap<SqlSelectPlanCacheKey, SqlSelectPlanCacheEntry>;
205
206#[derive(Clone, Debug)]
210pub(in crate::db) enum CompiledSqlCommand {
211 Select {
212 query: StructuralQuery,
213 compiled_cache_key: Option<SqlCompiledCommandCacheKey>,
214 },
215 Delete {
216 query: LoweredBaseQueryShape,
217 statement: SqlDeleteStatement,
218 },
219 GlobalAggregate {
220 command: SqlGlobalAggregateCommandCore,
221 label_override: Option<String>,
222 },
223 Explain(LoweredSqlCommand),
224 Insert(SqlInsertStatement),
225 Update(SqlUpdateStatement),
226 DescribeEntity,
227 ShowIndexesEntity,
228 ShowColumnsEntity,
229 ShowEntities,
230}
231
232pub(in crate::db) fn parse_sql_statement(sql: &str) -> Result<SqlStatement, QueryError> {
235 parse_sql(sql).map_err(QueryError::from_sql_parse_error)
236}
237
238#[cfg(feature = "perf-attribution")]
239#[expect(
240 clippy::missing_const_for_fn,
241 reason = "the wasm32 branch reads the runtime performance counter and cannot be const"
242)]
243fn read_sql_local_instruction_counter() -> u64 {
244 #[cfg(target_arch = "wasm32")]
245 {
246 canic_cdk::api::performance_counter(1)
247 }
248
249 #[cfg(not(target_arch = "wasm32"))]
250 {
251 0
252 }
253}
254
255#[cfg(feature = "perf-attribution")]
256fn measure_sql_stage<T, E>(run: impl FnOnce() -> Result<T, E>) -> (u64, Result<T, E>) {
257 let start = read_sql_local_instruction_counter();
258 let result = run();
259 let delta = read_sql_local_instruction_counter().saturating_sub(start);
260
261 (delta, result)
262}
263
264impl<C: CanisterKind> DbSession<C> {
265 fn sql_compiled_command_cache(&self) -> &RefCell<SqlCompiledCommandCache> {
268 self.sql_compiled_command_cache
269 .get_or_init(|| RefCell::new(SqlCompiledCommandCache::new()))
270 }
271
272 fn sql_select_plan_cache(&self) -> &RefCell<SqlSelectPlanCache> {
275 self.sql_select_plan_cache
276 .get_or_init(|| RefCell::new(SqlSelectPlanCache::new()))
277 }
278
279 #[cfg(test)]
280 pub(in crate::db) fn sql_compiled_command_cache_len(&self) -> usize {
281 self.sql_compiled_command_cache().borrow().len()
282 }
283
284 #[cfg(test)]
285 pub(in crate::db) fn sql_select_plan_cache_len(&self) -> usize {
286 self.sql_select_plan_cache().borrow().len()
287 }
288
289 fn planned_sql_select_with_visibility(
290 &self,
291 query: &StructuralQuery,
292 authority: EntityAuthority,
293 compiled_cache_key: Option<&SqlCompiledCommandCacheKey>,
294 ) -> Result<SqlSelectPlanCacheEntry, QueryError> {
295 let visibility = self.query_plan_visibility_for_store_path(authority.store_path())?;
296 let fallback_schema_fingerprint = crate::db::schema::commit_schema_fingerprint_for_model(
297 authority.model().path,
298 authority.model(),
299 );
300 let cache_entity_path = compiled_cache_key.map_or_else(
301 || authority.model().path,
302 SqlCompiledCommandCacheKey::entity_path,
303 );
304 let cache_schema_fingerprint = compiled_cache_key.map_or(
305 fallback_schema_fingerprint,
306 SqlCompiledCommandCacheKey::schema_fingerprint,
307 );
308
309 let Some(compiled_cache_key) = compiled_cache_key else {
310 let plan = self.cached_structural_plan_for_authority(
311 cache_entity_path,
312 cache_schema_fingerprint,
313 authority.store_path(),
314 authority.model(),
315 query,
316 )?;
317 let columns =
318 projection_labels_from_projection_spec(&plan.projection_spec(authority.model()));
319
320 return Ok(SqlSelectPlanCacheEntry::new(plan, columns));
321 };
322
323 let plan_cache_key =
324 SqlSelectPlanCacheKey::from_compiled_key(compiled_cache_key.clone(), visibility);
325 {
326 let cache = self.sql_select_plan_cache().borrow();
327 if let Some(plan) = cache.get(&plan_cache_key) {
328 return Ok(plan.clone());
329 }
330 }
331
332 let plan = self.cached_structural_plan_for_authority(
333 cache_entity_path,
334 cache_schema_fingerprint,
335 authority.store_path(),
336 authority.model(),
337 query,
338 )?;
339 let columns =
340 projection_labels_from_projection_spec(&plan.projection_spec(authority.model()));
341 let entry = SqlSelectPlanCacheEntry::new(plan, columns);
342 self.sql_select_plan_cache()
343 .borrow_mut()
344 .insert(plan_cache_key, entry.clone());
345
346 Ok(entry)
347 }
348
349 pub(in crate::db::session::sql) fn build_structural_plan_with_visible_indexes_for_authority(
352 &self,
353 query: StructuralQuery,
354 authority: EntityAuthority,
355 ) -> Result<(VisibleIndexes<'_>, AccessPlannedQuery), QueryError> {
356 let visible_indexes =
357 self.visible_indexes_for_store_model(authority.store_path(), authority.model())?;
358 let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
359
360 Ok((visible_indexes, plan))
361 }
362
363 fn ensure_sql_query_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
366 match statement {
367 SqlStatement::Select(_)
368 | SqlStatement::Explain(_)
369 | SqlStatement::Describe(_)
370 | SqlStatement::ShowIndexes(_)
371 | SqlStatement::ShowColumns(_)
372 | SqlStatement::ShowEntities(_) => Ok(()),
373 SqlStatement::Insert(_) => Err(QueryError::unsupported_query(
374 "execute_sql_query rejects INSERT; use execute_sql_update::<E>()",
375 )),
376 SqlStatement::Update(_) => Err(QueryError::unsupported_query(
377 "execute_sql_query rejects UPDATE; use execute_sql_update::<E>()",
378 )),
379 SqlStatement::Delete(_) => Err(QueryError::unsupported_query(
380 "execute_sql_query rejects DELETE; use execute_sql_update::<E>()",
381 )),
382 }
383 }
384
385 fn ensure_sql_update_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
388 match statement {
389 SqlStatement::Insert(_) | SqlStatement::Update(_) | SqlStatement::Delete(_) => Ok(()),
390 SqlStatement::Select(_) => Err(QueryError::unsupported_query(
391 "execute_sql_update rejects SELECT; use execute_sql_query::<E>()",
392 )),
393 SqlStatement::Explain(_) => Err(QueryError::unsupported_query(
394 "execute_sql_update rejects EXPLAIN; use execute_sql_query::<E>()",
395 )),
396 SqlStatement::Describe(_) => Err(QueryError::unsupported_query(
397 "execute_sql_update rejects DESCRIBE; use execute_sql_query::<E>()",
398 )),
399 SqlStatement::ShowIndexes(_) => Err(QueryError::unsupported_query(
400 "execute_sql_update rejects SHOW INDEXES; use execute_sql_query::<E>()",
401 )),
402 SqlStatement::ShowColumns(_) => Err(QueryError::unsupported_query(
403 "execute_sql_update rejects SHOW COLUMNS; use execute_sql_query::<E>()",
404 )),
405 SqlStatement::ShowEntities(_) => Err(QueryError::unsupported_query(
406 "execute_sql_update rejects SHOW ENTITIES; use execute_sql_query::<E>()",
407 )),
408 }
409 }
410
411 pub fn execute_sql_query<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
416 where
417 E: PersistedRow<Canister = C> + EntityValue,
418 {
419 let compiled = self.compile_sql_query::<E>(sql)?;
420
421 self.execute_compiled_sql::<E>(&compiled)
422 }
423
424 #[cfg(feature = "perf-attribution")]
427 #[doc(hidden)]
428 pub fn execute_sql_query_with_attribution<E>(
429 &self,
430 sql: &str,
431 ) -> Result<(SqlStatementResult, SqlQueryExecutionAttribution), QueryError>
432 where
433 E: PersistedRow<Canister = C> + EntityValue,
434 {
435 let (compile_local_instructions, compiled) =
438 measure_sql_stage(|| self.compile_sql_query::<E>(sql));
439 let compiled = compiled?;
440
441 let (execute_local_instructions, result) =
444 measure_sql_stage(|| self.execute_compiled_sql::<E>(&compiled));
445 let result = result?;
446 let total_local_instructions =
447 compile_local_instructions.saturating_add(execute_local_instructions);
448
449 Ok((
450 result,
451 SqlQueryExecutionAttribution {
452 compile_local_instructions,
453 execute_local_instructions,
454 total_local_instructions,
455 },
456 ))
457 }
458
459 pub fn execute_sql_update<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
464 where
465 E: PersistedRow<Canister = C> + EntityValue,
466 {
467 let compiled = self.compile_sql_update::<E>(sql)?;
468
469 self.execute_compiled_sql::<E>(&compiled)
470 }
471
472 #[cfg(test)]
473 pub(in crate::db) fn execute_grouped_sql_query_for_tests<E>(
474 &self,
475 sql: &str,
476 cursor_token: Option<&str>,
477 ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
478 where
479 E: PersistedRow<Canister = C> + EntityValue,
480 {
481 let parsed = parse_sql_statement(sql)?;
482
483 let lowered = lower_sql_command_from_prepared_statement(
484 prepare_sql_statement(parsed, E::MODEL.name())
485 .map_err(QueryError::from_sql_lowering_error)?,
486 E::MODEL.primary_key.name,
487 )
488 .map_err(QueryError::from_sql_lowering_error)?;
489 let Some(query) = lowered.query().cloned() else {
490 return Err(QueryError::unsupported_query(
491 "grouped SELECT helper requires grouped SELECT",
492 ));
493 };
494 let query = bind_lowered_sql_query::<E>(query, MissingRowPolicy::Ignore)
495 .map_err(QueryError::from_sql_lowering_error)?;
496 if !query.has_grouping() {
497 return Err(QueryError::unsupported_query(
498 "grouped SELECT helper requires grouped SELECT",
499 ));
500 }
501
502 self.execute_grouped(&query, cursor_token)
503 }
504
505 pub(in crate::db) fn compile_sql_query<E>(
508 &self,
509 sql: &str,
510 ) -> Result<CompiledSqlCommand, QueryError>
511 where
512 E: PersistedRow<Canister = C> + EntityValue,
513 {
514 self.compile_sql_statement_with_cache::<E>(
515 SqlCompiledCommandCacheKey::query_for_entity::<E>(sql),
516 sql,
517 Self::ensure_sql_query_statement_supported,
518 )
519 }
520
521 pub(in crate::db) fn compile_sql_update<E>(
524 &self,
525 sql: &str,
526 ) -> Result<CompiledSqlCommand, QueryError>
527 where
528 E: PersistedRow<Canister = C> + EntityValue,
529 {
530 self.compile_sql_statement_with_cache::<E>(
531 SqlCompiledCommandCacheKey::update_for_entity::<E>(sql),
532 sql,
533 Self::ensure_sql_update_statement_supported,
534 )
535 }
536
537 fn compile_sql_statement_with_cache<E>(
540 &self,
541 cache_key: SqlCompiledCommandCacheKey,
542 sql: &str,
543 ensure_surface_supported: fn(&SqlStatement) -> Result<(), QueryError>,
544 ) -> Result<CompiledSqlCommand, QueryError>
545 where
546 E: PersistedRow<Canister = C> + EntityValue,
547 {
548 {
549 let cache = self.sql_compiled_command_cache().borrow();
550 if let Some(compiled) = cache.get(&cache_key) {
551 return Ok(compiled.clone());
552 }
553 }
554
555 let parsed = parse_sql_statement(sql)?;
556 ensure_surface_supported(&parsed)?;
557 let mut compiled = Self::compile_sql_statement_inner::<E>(&parsed)?;
558 if let CompiledSqlCommand::Select {
559 compiled_cache_key, ..
560 } = &mut compiled
561 {
562 *compiled_cache_key = Some(cache_key.clone());
563 }
564
565 self.sql_compiled_command_cache()
566 .borrow_mut()
567 .insert(cache_key, compiled.clone());
568
569 Ok(compiled)
570 }
571
572 pub(in crate::db) fn compile_sql_statement_inner<E>(
575 sql_statement: &SqlStatement,
576 ) -> Result<CompiledSqlCommand, QueryError>
577 where
578 E: PersistedRow<Canister = C> + EntityValue,
579 {
580 Self::compile_sql_statement_for_authority(sql_statement, EntityAuthority::for_type::<E>())
581 }
582}