1mod execute;
8mod explain;
9mod projection;
10
11#[cfg(feature = "diagnostics")]
12use candid::CandidType;
13use icydb_utils::Xxh3;
14#[cfg(feature = "diagnostics")]
15use serde::Deserialize;
16use std::{cell::RefCell, collections::HashMap, hash::BuildHasherDefault};
17
18type CacheBuildHasher = BuildHasherDefault<Xxh3>;
19
20const SQL_COMPILED_COMMAND_CACHE_METHOD_VERSION: u8 = 1;
23const SQL_SELECT_PLAN_CACHE_METHOD_VERSION: u8 = 1;
24
25#[cfg(feature = "diagnostics")]
26use crate::db::DataStore;
27#[cfg(feature = "diagnostics")]
28use crate::db::executor::GroupedCountAttribution;
29#[cfg(feature = "diagnostics")]
30use crate::db::session::sql::projection::{
31 current_pure_covering_decode_local_instructions,
32 current_pure_covering_row_assembly_local_instructions,
33};
34use crate::db::sql::parser::{SqlDeleteStatement, SqlInsertStatement, SqlUpdateStatement};
35use crate::{
36 db::{
37 DbSession, GroupedRow, PersistedRow, QueryError,
38 commit::CommitSchemaFingerprint,
39 executor::{EntityAuthority, SharedPreparedExecutionPlan},
40 query::{
41 intent::StructuralQuery,
42 plan::{AccessPlannedQuery, VisibleIndexes},
43 },
44 schema::commit_schema_fingerprint_for_entity,
45 session::query::QueryPlanCacheAttribution,
46 session::sql::projection::{
47 projection_fixed_scales_from_projection_spec, projection_labels_from_projection_spec,
48 },
49 sql::lowering::{LoweredBaseQueryShape, LoweredSqlCommand, SqlGlobalAggregateCommandCore},
50 sql::parser::{SqlStatement, parse_sql},
51 },
52 traits::{CanisterKind, EntityValue},
53};
54
55#[cfg(test)]
56use crate::db::{
57 MissingRowPolicy, PagedGroupedExecutionWithTrace,
58 sql::lowering::{
59 bind_lowered_sql_query, lower_sql_command_from_prepared_statement, prepare_sql_statement,
60 },
61};
62
63#[cfg(all(test, not(feature = "diagnostics")))]
64pub(crate) use crate::db::session::sql::projection::with_sql_projection_materialization_metrics;
65#[cfg(feature = "diagnostics")]
66pub use crate::db::session::sql::projection::{
67 SqlProjectionMaterializationMetrics, with_sql_projection_materialization_metrics,
68};
69
70#[derive(Debug)]
72pub enum SqlStatementResult {
73 Count {
74 row_count: u32,
75 },
76 Projection {
77 columns: Vec<String>,
78 fixed_scales: Vec<Option<u32>>,
79 rows: Vec<Vec<crate::value::Value>>,
80 row_count: u32,
81 },
82 ProjectionText {
83 columns: Vec<String>,
84 rows: Vec<Vec<String>>,
85 row_count: u32,
86 },
87 Grouped {
88 columns: Vec<String>,
89 fixed_scales: Vec<Option<u32>>,
90 rows: Vec<GroupedRow>,
91 row_count: u32,
92 next_cursor: Option<String>,
93 },
94 Explain(String),
95 Describe(crate::db::EntitySchemaDescription),
96 ShowIndexes(Vec<String>),
97 ShowColumns(Vec<crate::db::EntityFieldDescription>),
98 ShowEntities(Vec<String>),
99}
100
101#[cfg(feature = "diagnostics")]
112#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
113pub struct SqlQueryExecutionAttribution {
114 pub compile_local_instructions: u64,
115 pub planner_local_instructions: u64,
116 pub store_local_instructions: u64,
117 pub executor_local_instructions: u64,
118 pub grouped_stream_local_instructions: u64,
119 pub grouped_fold_local_instructions: u64,
120 pub grouped_finalize_local_instructions: u64,
121 pub grouped_count_borrowed_hash_computations: u64,
122 pub grouped_count_bucket_candidate_checks: u64,
123 pub grouped_count_existing_group_hits: u64,
124 pub grouped_count_new_group_inserts: u64,
125 pub grouped_count_row_materialization_local_instructions: u64,
126 pub grouped_count_group_lookup_local_instructions: u64,
127 pub grouped_count_existing_group_update_local_instructions: u64,
128 pub grouped_count_new_group_insert_local_instructions: u64,
129 pub pure_covering_decode_local_instructions: u64,
130 pub pure_covering_row_assembly_local_instructions: u64,
131 pub store_get_calls: u64,
132 pub response_decode_local_instructions: u64,
133 pub execute_local_instructions: u64,
134 pub total_local_instructions: u64,
135 pub sql_compiled_command_cache_hits: u64,
136 pub sql_compiled_command_cache_misses: u64,
137 pub sql_select_plan_cache_hits: u64,
138 pub sql_select_plan_cache_misses: u64,
139 pub shared_query_plan_cache_hits: u64,
140 pub shared_query_plan_cache_misses: u64,
141}
142
143#[cfg(feature = "diagnostics")]
147#[derive(Clone, Copy, Debug, Eq, PartialEq)]
148pub(in crate::db) struct SqlExecutePhaseAttribution {
149 pub planner_local_instructions: u64,
150 pub store_local_instructions: u64,
151 pub executor_local_instructions: u64,
152 pub grouped_stream_local_instructions: u64,
153 pub grouped_fold_local_instructions: u64,
154 pub grouped_finalize_local_instructions: u64,
155 pub grouped_count: GroupedCountAttribution,
156}
157
158#[cfg(feature = "diagnostics")]
159impl SqlExecutePhaseAttribution {
160 #[must_use]
161 pub(in crate::db) const fn from_execute_total_and_store_total(
162 execute_local_instructions: u64,
163 store_local_instructions: u64,
164 ) -> Self {
165 Self {
166 planner_local_instructions: 0,
167 store_local_instructions,
168 executor_local_instructions: execute_local_instructions
169 .saturating_sub(store_local_instructions),
170 grouped_stream_local_instructions: 0,
171 grouped_fold_local_instructions: 0,
172 grouped_finalize_local_instructions: 0,
173 grouped_count: GroupedCountAttribution::none(),
174 }
175 }
176}
177
178#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
182pub(in crate::db) struct SqlCacheAttribution {
183 pub sql_compiled_command_cache_hits: u64,
184 pub sql_compiled_command_cache_misses: u64,
185 pub sql_select_plan_cache_hits: u64,
186 pub sql_select_plan_cache_misses: u64,
187 pub shared_query_plan_cache_hits: u64,
188 pub shared_query_plan_cache_misses: u64,
189}
190
191impl SqlCacheAttribution {
192 #[must_use]
193 const fn none() -> Self {
194 Self {
195 sql_compiled_command_cache_hits: 0,
196 sql_compiled_command_cache_misses: 0,
197 sql_select_plan_cache_hits: 0,
198 sql_select_plan_cache_misses: 0,
199 shared_query_plan_cache_hits: 0,
200 shared_query_plan_cache_misses: 0,
201 }
202 }
203
204 #[must_use]
205 const fn sql_compiled_command_cache_hit() -> Self {
206 Self {
207 sql_compiled_command_cache_hits: 1,
208 ..Self::none()
209 }
210 }
211
212 #[must_use]
213 const fn sql_compiled_command_cache_miss() -> Self {
214 Self {
215 sql_compiled_command_cache_misses: 1,
216 ..Self::none()
217 }
218 }
219
220 #[must_use]
221 const fn sql_select_plan_cache_hit() -> Self {
222 Self {
223 sql_select_plan_cache_hits: 1,
224 ..Self::none()
225 }
226 }
227
228 #[must_use]
229 const fn sql_select_plan_cache_miss() -> Self {
230 Self {
231 sql_select_plan_cache_misses: 1,
232 ..Self::none()
233 }
234 }
235
236 #[must_use]
237 const fn from_shared_query_plan_cache(attribution: QueryPlanCacheAttribution) -> Self {
238 Self {
239 shared_query_plan_cache_hits: attribution.hits,
240 shared_query_plan_cache_misses: attribution.misses,
241 ..Self::none()
242 }
243 }
244
245 #[must_use]
246 const fn merge(self, other: Self) -> Self {
247 Self {
248 sql_compiled_command_cache_hits: self
249 .sql_compiled_command_cache_hits
250 .saturating_add(other.sql_compiled_command_cache_hits),
251 sql_compiled_command_cache_misses: self
252 .sql_compiled_command_cache_misses
253 .saturating_add(other.sql_compiled_command_cache_misses),
254 sql_select_plan_cache_hits: self
255 .sql_select_plan_cache_hits
256 .saturating_add(other.sql_select_plan_cache_hits),
257 sql_select_plan_cache_misses: self
258 .sql_select_plan_cache_misses
259 .saturating_add(other.sql_select_plan_cache_misses),
260 shared_query_plan_cache_hits: self
261 .shared_query_plan_cache_hits
262 .saturating_add(other.shared_query_plan_cache_hits),
263 shared_query_plan_cache_misses: self
264 .shared_query_plan_cache_misses
265 .saturating_add(other.shared_query_plan_cache_misses),
266 }
267 }
268}
269
270#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
271enum SqlCompiledCommandSurface {
272 Query,
273 Update,
274}
275
276#[derive(Clone, Debug, Eq, Hash, PartialEq)]
287pub(in crate::db) struct SqlCompiledCommandCacheKey {
288 cache_method_version: u8,
289 surface: SqlCompiledCommandSurface,
290 entity_path: &'static str,
291 schema_fingerprint: CommitSchemaFingerprint,
292 sql: String,
293}
294
295#[derive(Clone, Debug, Eq, Hash, PartialEq)]
296pub(in crate::db) struct SqlSelectPlanCacheKey {
297 cache_method_version: u8,
298 compiled: SqlCompiledCommandCacheKey,
299 visibility: crate::db::session::query::QueryPlanVisibility,
300}
301
302#[derive(Clone, Debug)]
312pub(in crate::db) struct SqlSelectPlanCacheEntry {
313 prepared_plan: SharedPreparedExecutionPlan,
314 columns: Vec<String>,
315 fixed_scales: Vec<Option<u32>>,
316}
317
318impl SqlSelectPlanCacheEntry {
319 #[must_use]
320 pub(in crate::db) const fn new(
321 prepared_plan: SharedPreparedExecutionPlan,
322 columns: Vec<String>,
323 fixed_scales: Vec<Option<u32>>,
324 ) -> Self {
325 Self {
326 prepared_plan,
327 columns,
328 fixed_scales,
329 }
330 }
331
332 #[must_use]
333 pub(in crate::db) fn into_parts(
334 self,
335 ) -> (SharedPreparedExecutionPlan, Vec<String>, Vec<Option<u32>>) {
336 (self.prepared_plan, self.columns, self.fixed_scales)
337 }
338}
339
340impl SqlCompiledCommandCacheKey {
341 fn query_for_entity<E>(sql: &str) -> Self
342 where
343 E: PersistedRow + EntityValue,
344 {
345 Self::for_entity::<E>(SqlCompiledCommandSurface::Query, sql)
346 }
347
348 fn update_for_entity<E>(sql: &str) -> Self
349 where
350 E: PersistedRow + EntityValue,
351 {
352 Self::for_entity::<E>(SqlCompiledCommandSurface::Update, sql)
353 }
354
355 fn for_entity<E>(surface: SqlCompiledCommandSurface, sql: &str) -> Self
356 where
357 E: PersistedRow + EntityValue,
358 {
359 Self {
360 cache_method_version: SQL_COMPILED_COMMAND_CACHE_METHOD_VERSION,
361 surface,
362 entity_path: E::PATH,
363 schema_fingerprint: commit_schema_fingerprint_for_entity::<E>(),
364 sql: sql.to_string(),
365 }
366 }
367
368 #[must_use]
369 pub(in crate::db) const fn schema_fingerprint(&self) -> CommitSchemaFingerprint {
370 self.schema_fingerprint
371 }
372}
373
374impl SqlSelectPlanCacheKey {
375 const fn from_compiled_key(
376 compiled: SqlCompiledCommandCacheKey,
377 visibility: crate::db::session::query::QueryPlanVisibility,
378 ) -> Self {
379 Self {
380 cache_method_version: SQL_SELECT_PLAN_CACHE_METHOD_VERSION,
381 compiled,
382 visibility,
383 }
384 }
385}
386
387#[cfg(test)]
388impl SqlCompiledCommandCacheKey {
389 pub(in crate::db) fn query_for_entity_with_method_version<E>(
390 sql: &str,
391 cache_method_version: u8,
392 ) -> Self
393 where
394 E: PersistedRow + EntityValue,
395 {
396 Self::for_entity_with_method_version::<E>(
397 SqlCompiledCommandSurface::Query,
398 sql,
399 cache_method_version,
400 )
401 }
402
403 pub(in crate::db) fn update_for_entity_with_method_version<E>(
404 sql: &str,
405 cache_method_version: u8,
406 ) -> Self
407 where
408 E: PersistedRow + EntityValue,
409 {
410 Self::for_entity_with_method_version::<E>(
411 SqlCompiledCommandSurface::Update,
412 sql,
413 cache_method_version,
414 )
415 }
416
417 fn for_entity_with_method_version<E>(
418 surface: SqlCompiledCommandSurface,
419 sql: &str,
420 cache_method_version: u8,
421 ) -> Self
422 where
423 E: PersistedRow + EntityValue,
424 {
425 Self {
426 cache_method_version,
427 surface,
428 entity_path: E::PATH,
429 schema_fingerprint: commit_schema_fingerprint_for_entity::<E>(),
430 sql: sql.to_string(),
431 }
432 }
433}
434
435#[cfg(test)]
436impl SqlSelectPlanCacheKey {
437 pub(in crate::db) const fn from_compiled_key_with_method_version(
438 compiled: SqlCompiledCommandCacheKey,
439 visibility: crate::db::session::query::QueryPlanVisibility,
440 cache_method_version: u8,
441 ) -> Self {
442 Self {
443 cache_method_version,
444 compiled,
445 visibility,
446 }
447 }
448}
449
450pub(in crate::db) type SqlCompiledCommandCache =
451 HashMap<SqlCompiledCommandCacheKey, CompiledSqlCommand, CacheBuildHasher>;
452pub(in crate::db) type SqlSelectPlanCache =
453 HashMap<SqlSelectPlanCacheKey, SqlSelectPlanCacheEntry, CacheBuildHasher>;
454
455thread_local! {
456 static SQL_COMPILED_COMMAND_CACHES: RefCell<HashMap<usize, SqlCompiledCommandCache, CacheBuildHasher>> =
460 RefCell::new(HashMap::default());
461 static SQL_SELECT_PLAN_CACHES: RefCell<HashMap<usize, SqlSelectPlanCache, CacheBuildHasher>> =
462 RefCell::new(HashMap::default());
463}
464
465#[derive(Clone, Debug)]
469#[expect(
470 clippy::large_enum_variant,
471 reason = "compiled SQL keeps the full global aggregate command owned on the session boundary"
472)]
473pub(in crate::db) enum CompiledSqlCommand {
474 Select {
475 query: StructuralQuery,
476 compiled_cache_key: Option<SqlCompiledCommandCacheKey>,
477 },
478 Delete {
479 query: LoweredBaseQueryShape,
480 statement: SqlDeleteStatement,
481 },
482 GlobalAggregate {
483 command: SqlGlobalAggregateCommandCore,
484 },
485 Explain(LoweredSqlCommand),
486 Insert(SqlInsertStatement),
487 Update(SqlUpdateStatement),
488 DescribeEntity,
489 ShowIndexesEntity,
490 ShowColumnsEntity,
491 ShowEntities,
492}
493
494pub(in crate::db) fn parse_sql_statement(sql: &str) -> Result<SqlStatement, QueryError> {
497 parse_sql(sql).map_err(QueryError::from_sql_parse_error)
498}
499
500#[cfg(feature = "diagnostics")]
501#[expect(
502 clippy::missing_const_for_fn,
503 reason = "the wasm32 branch reads the runtime performance counter and cannot be const"
504)]
505fn read_sql_local_instruction_counter() -> u64 {
506 #[cfg(target_arch = "wasm32")]
507 {
508 canic_cdk::api::performance_counter(1)
509 }
510
511 #[cfg(not(target_arch = "wasm32"))]
512 {
513 0
514 }
515}
516
517#[cfg(feature = "diagnostics")]
518fn measure_sql_stage<T, E>(run: impl FnOnce() -> Result<T, E>) -> (u64, Result<T, E>) {
519 let start = read_sql_local_instruction_counter();
520 let result = run();
521 let delta = read_sql_local_instruction_counter().saturating_sub(start);
522
523 (delta, result)
524}
525
526impl<C: CanisterKind> DbSession<C> {
527 fn sql_cache_scope_id(&self) -> usize {
528 self.db.cache_scope_id()
529 }
530
531 fn with_sql_compiled_command_cache<R>(
532 &self,
533 f: impl FnOnce(&mut SqlCompiledCommandCache) -> R,
534 ) -> R {
535 let scope_id = self.sql_cache_scope_id();
536
537 SQL_COMPILED_COMMAND_CACHES.with(|caches| {
538 let mut caches = caches.borrow_mut();
539 let cache = caches.entry(scope_id).or_default();
540
541 f(cache)
542 })
543 }
544
545 fn with_sql_select_plan_cache<R>(&self, f: impl FnOnce(&mut SqlSelectPlanCache) -> R) -> R {
546 let scope_id = self.sql_cache_scope_id();
547
548 SQL_SELECT_PLAN_CACHES.with(|caches| {
549 let mut caches = caches.borrow_mut();
550 let cache = caches.entry(scope_id).or_default();
551
552 f(cache)
553 })
554 }
555
556 #[cfg(test)]
557 pub(in crate::db) fn sql_compiled_command_cache_len(&self) -> usize {
558 self.with_sql_compiled_command_cache(|cache| cache.len())
559 }
560
561 #[cfg(test)]
562 pub(in crate::db) fn sql_select_plan_cache_len(&self) -> usize {
563 self.with_sql_select_plan_cache(|cache| cache.len())
564 }
565
566 #[cfg(test)]
567 pub(in crate::db) fn clear_sql_caches_for_tests(&self) {
568 self.with_sql_compiled_command_cache(SqlCompiledCommandCache::clear);
569 self.with_sql_select_plan_cache(SqlSelectPlanCache::clear);
570 }
571
572 fn planned_sql_select_with_visibility(
573 &self,
574 query: &StructuralQuery,
575 authority: EntityAuthority,
576 compiled_cache_key: Option<&SqlCompiledCommandCacheKey>,
577 ) -> Result<(SqlSelectPlanCacheEntry, SqlCacheAttribution), QueryError> {
578 let visibility = self.query_plan_visibility_for_store_path(authority.store_path())?;
579 let fallback_schema_fingerprint = crate::db::schema::commit_schema_fingerprint_for_model(
580 authority.model().path,
581 authority.model(),
582 );
583 let cache_schema_fingerprint = compiled_cache_key.map_or(
584 fallback_schema_fingerprint,
585 SqlCompiledCommandCacheKey::schema_fingerprint,
586 );
587
588 let Some(compiled_cache_key) = compiled_cache_key else {
589 let (entry, cache_attribution) = self.cached_query_plan_entry_for_authority(
590 authority,
591 cache_schema_fingerprint,
592 query,
593 )?;
594 let projection = entry.logical_plan().projection_spec(authority.model());
595 let columns = projection_labels_from_projection_spec(&projection);
596 let fixed_scales = projection_fixed_scales_from_projection_spec(&projection);
597
598 return Ok((
599 SqlSelectPlanCacheEntry::new(entry.prepared_plan().clone(), columns, fixed_scales),
600 SqlCacheAttribution::from_shared_query_plan_cache(cache_attribution),
601 ));
602 };
603
604 let plan_cache_key =
605 SqlSelectPlanCacheKey::from_compiled_key(compiled_cache_key.clone(), visibility);
606 {
607 let cached =
608 self.with_sql_select_plan_cache(|cache| cache.get(&plan_cache_key).cloned());
609 if let Some(plan) = cached {
610 return Ok((plan, SqlCacheAttribution::sql_select_plan_cache_hit()));
611 }
612 }
613
614 let (entry, cache_attribution) =
615 self.cached_query_plan_entry_for_authority(authority, cache_schema_fingerprint, query)?;
616 let projection = entry.logical_plan().projection_spec(authority.model());
617 let columns = projection_labels_from_projection_spec(&projection);
618 let fixed_scales = projection_fixed_scales_from_projection_spec(&projection);
619 let entry =
620 SqlSelectPlanCacheEntry::new(entry.prepared_plan().clone(), columns, fixed_scales);
621 self.with_sql_select_plan_cache(|cache| {
622 cache.insert(plan_cache_key, entry.clone());
623 });
624
625 Ok((
626 entry,
627 SqlCacheAttribution::sql_select_plan_cache_miss().merge(
628 SqlCacheAttribution::from_shared_query_plan_cache(cache_attribution),
629 ),
630 ))
631 }
632
633 pub(in crate::db::session::sql) fn build_structural_plan_with_visible_indexes_for_authority(
636 &self,
637 query: StructuralQuery,
638 authority: EntityAuthority,
639 ) -> Result<(VisibleIndexes<'_>, AccessPlannedQuery), QueryError> {
640 let visible_indexes =
641 self.visible_indexes_for_store_model(authority.store_path(), authority.model())?;
642 let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
643
644 Ok((visible_indexes, plan))
645 }
646
647 fn ensure_sql_query_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
650 match statement {
651 SqlStatement::Select(_)
652 | SqlStatement::Explain(_)
653 | SqlStatement::Describe(_)
654 | SqlStatement::ShowIndexes(_)
655 | SqlStatement::ShowColumns(_)
656 | SqlStatement::ShowEntities(_) => Ok(()),
657 SqlStatement::Insert(_) => Err(QueryError::unsupported_query(
658 "execute_sql_query rejects INSERT; use execute_sql_update::<E>()",
659 )),
660 SqlStatement::Update(_) => Err(QueryError::unsupported_query(
661 "execute_sql_query rejects UPDATE; use execute_sql_update::<E>()",
662 )),
663 SqlStatement::Delete(_) => Err(QueryError::unsupported_query(
664 "execute_sql_query rejects DELETE; use execute_sql_update::<E>()",
665 )),
666 }
667 }
668
669 fn ensure_sql_update_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
672 match statement {
673 SqlStatement::Insert(_) | SqlStatement::Update(_) | SqlStatement::Delete(_) => Ok(()),
674 SqlStatement::Select(_) => Err(QueryError::unsupported_query(
675 "execute_sql_update rejects SELECT; use execute_sql_query::<E>()",
676 )),
677 SqlStatement::Explain(_) => Err(QueryError::unsupported_query(
678 "execute_sql_update rejects EXPLAIN; use execute_sql_query::<E>()",
679 )),
680 SqlStatement::Describe(_) => Err(QueryError::unsupported_query(
681 "execute_sql_update rejects DESCRIBE; use execute_sql_query::<E>()",
682 )),
683 SqlStatement::ShowIndexes(_) => Err(QueryError::unsupported_query(
684 "execute_sql_update rejects SHOW INDEXES; use execute_sql_query::<E>()",
685 )),
686 SqlStatement::ShowColumns(_) => Err(QueryError::unsupported_query(
687 "execute_sql_update rejects SHOW COLUMNS; use execute_sql_query::<E>()",
688 )),
689 SqlStatement::ShowEntities(_) => Err(QueryError::unsupported_query(
690 "execute_sql_update rejects SHOW ENTITIES; use execute_sql_query::<E>()",
691 )),
692 }
693 }
694
695 pub fn execute_sql_query<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
700 where
701 E: PersistedRow<Canister = C> + EntityValue,
702 {
703 let compiled = self.compile_sql_query::<E>(sql)?;
704
705 self.execute_compiled_sql::<E>(&compiled)
706 }
707
708 #[cfg(feature = "diagnostics")]
711 #[doc(hidden)]
712 pub fn execute_sql_query_with_attribution<E>(
713 &self,
714 sql: &str,
715 ) -> Result<(SqlStatementResult, SqlQueryExecutionAttribution), QueryError>
716 where
717 E: PersistedRow<Canister = C> + EntityValue,
718 {
719 let (compile_local_instructions, compiled) =
722 measure_sql_stage(|| self.compile_sql_query_with_cache_attribution::<E>(sql));
723 let (compiled, compile_cache_attribution) = compiled?;
724
725 let store_get_calls_before = DataStore::current_get_call_count();
728 let pure_covering_decode_before = current_pure_covering_decode_local_instructions();
729 let pure_covering_row_assembly_before =
730 current_pure_covering_row_assembly_local_instructions();
731 let (result, execute_cache_attribution, execute_phase_attribution) =
732 self.execute_compiled_sql_with_phase_attribution::<E>(&compiled)?;
733 let store_get_calls =
734 DataStore::current_get_call_count().saturating_sub(store_get_calls_before);
735 let pure_covering_decode_local_instructions =
736 current_pure_covering_decode_local_instructions()
737 .saturating_sub(pure_covering_decode_before);
738 let pure_covering_row_assembly_local_instructions =
739 current_pure_covering_row_assembly_local_instructions()
740 .saturating_sub(pure_covering_row_assembly_before);
741 let execute_local_instructions = execute_phase_attribution
742 .planner_local_instructions
743 .saturating_add(execute_phase_attribution.store_local_instructions)
744 .saturating_add(execute_phase_attribution.executor_local_instructions);
745 let cache_attribution = compile_cache_attribution.merge(execute_cache_attribution);
746 let total_local_instructions =
747 compile_local_instructions.saturating_add(execute_local_instructions);
748
749 Ok((
750 result,
751 SqlQueryExecutionAttribution {
752 compile_local_instructions,
753 planner_local_instructions: execute_phase_attribution.planner_local_instructions,
754 store_local_instructions: execute_phase_attribution.store_local_instructions,
755 executor_local_instructions: execute_phase_attribution.executor_local_instructions,
756 grouped_stream_local_instructions: execute_phase_attribution
757 .grouped_stream_local_instructions,
758 grouped_fold_local_instructions: execute_phase_attribution
759 .grouped_fold_local_instructions,
760 grouped_finalize_local_instructions: execute_phase_attribution
761 .grouped_finalize_local_instructions,
762 grouped_count_borrowed_hash_computations: execute_phase_attribution
763 .grouped_count
764 .borrowed_hash_computations,
765 grouped_count_bucket_candidate_checks: execute_phase_attribution
766 .grouped_count
767 .bucket_candidate_checks,
768 grouped_count_existing_group_hits: execute_phase_attribution
769 .grouped_count
770 .existing_group_hits,
771 grouped_count_new_group_inserts: execute_phase_attribution
772 .grouped_count
773 .new_group_inserts,
774 grouped_count_row_materialization_local_instructions: execute_phase_attribution
775 .grouped_count
776 .row_materialization_local_instructions,
777 grouped_count_group_lookup_local_instructions: execute_phase_attribution
778 .grouped_count
779 .group_lookup_local_instructions,
780 grouped_count_existing_group_update_local_instructions: execute_phase_attribution
781 .grouped_count
782 .existing_group_update_local_instructions,
783 grouped_count_new_group_insert_local_instructions: execute_phase_attribution
784 .grouped_count
785 .new_group_insert_local_instructions,
786 pure_covering_decode_local_instructions,
787 pure_covering_row_assembly_local_instructions,
788 store_get_calls,
789 response_decode_local_instructions: 0,
790 execute_local_instructions,
791 total_local_instructions,
792 sql_compiled_command_cache_hits: cache_attribution.sql_compiled_command_cache_hits,
793 sql_compiled_command_cache_misses: cache_attribution
794 .sql_compiled_command_cache_misses,
795 sql_select_plan_cache_hits: cache_attribution.sql_select_plan_cache_hits,
796 sql_select_plan_cache_misses: cache_attribution.sql_select_plan_cache_misses,
797 shared_query_plan_cache_hits: cache_attribution.shared_query_plan_cache_hits,
798 shared_query_plan_cache_misses: cache_attribution.shared_query_plan_cache_misses,
799 },
800 ))
801 }
802
803 pub fn execute_sql_update<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
808 where
809 E: PersistedRow<Canister = C> + EntityValue,
810 {
811 let compiled = self.compile_sql_update::<E>(sql)?;
812
813 self.execute_compiled_sql::<E>(&compiled)
814 }
815
816 #[cfg(test)]
817 pub(in crate::db) fn execute_grouped_sql_query_for_tests<E>(
818 &self,
819 sql: &str,
820 cursor_token: Option<&str>,
821 ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
822 where
823 E: PersistedRow<Canister = C> + EntityValue,
824 {
825 let parsed = parse_sql_statement(sql)?;
826
827 let lowered = lower_sql_command_from_prepared_statement(
828 prepare_sql_statement(parsed, E::MODEL.name())
829 .map_err(QueryError::from_sql_lowering_error)?,
830 E::MODEL,
831 )
832 .map_err(QueryError::from_sql_lowering_error)?;
833 let Some(query) = lowered.query().cloned() else {
834 return Err(QueryError::unsupported_query(
835 "grouped SELECT helper requires grouped SELECT",
836 ));
837 };
838 let query = bind_lowered_sql_query::<E>(query, MissingRowPolicy::Ignore)
839 .map_err(QueryError::from_sql_lowering_error)?;
840 if !query.has_grouping() {
841 return Err(QueryError::unsupported_query(
842 "grouped SELECT helper requires grouped SELECT",
843 ));
844 }
845
846 self.execute_grouped(&query, cursor_token)
847 }
848
849 pub(in crate::db) fn compile_sql_query<E>(
852 &self,
853 sql: &str,
854 ) -> Result<CompiledSqlCommand, QueryError>
855 where
856 E: PersistedRow<Canister = C> + EntityValue,
857 {
858 self.compile_sql_query_with_cache_attribution::<E>(sql)
859 .map(|(compiled, _)| compiled)
860 }
861
862 fn compile_sql_query_with_cache_attribution<E>(
863 &self,
864 sql: &str,
865 ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
866 where
867 E: PersistedRow<Canister = C> + EntityValue,
868 {
869 self.compile_sql_statement_with_cache::<E>(
870 SqlCompiledCommandCacheKey::query_for_entity::<E>(sql),
871 sql,
872 Self::ensure_sql_query_statement_supported,
873 )
874 }
875
876 pub(in crate::db) fn compile_sql_update<E>(
879 &self,
880 sql: &str,
881 ) -> Result<CompiledSqlCommand, QueryError>
882 where
883 E: PersistedRow<Canister = C> + EntityValue,
884 {
885 self.compile_sql_update_with_cache_attribution::<E>(sql)
886 .map(|(compiled, _)| compiled)
887 }
888
889 fn compile_sql_update_with_cache_attribution<E>(
890 &self,
891 sql: &str,
892 ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
893 where
894 E: PersistedRow<Canister = C> + EntityValue,
895 {
896 self.compile_sql_statement_with_cache::<E>(
897 SqlCompiledCommandCacheKey::update_for_entity::<E>(sql),
898 sql,
899 Self::ensure_sql_update_statement_supported,
900 )
901 }
902
903 fn compile_sql_statement_with_cache<E>(
906 &self,
907 cache_key: SqlCompiledCommandCacheKey,
908 sql: &str,
909 ensure_surface_supported: fn(&SqlStatement) -> Result<(), QueryError>,
910 ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
911 where
912 E: PersistedRow<Canister = C> + EntityValue,
913 {
914 {
915 let cached =
916 self.with_sql_compiled_command_cache(|cache| cache.get(&cache_key).cloned());
917 if let Some(compiled) = cached {
918 return Ok((
919 compiled,
920 SqlCacheAttribution::sql_compiled_command_cache_hit(),
921 ));
922 }
923 }
924
925 let parsed = parse_sql_statement(sql)?;
926 ensure_surface_supported(&parsed)?;
927 let mut compiled = Self::compile_sql_statement_inner::<E>(&parsed)?;
928 if let CompiledSqlCommand::Select {
929 compiled_cache_key, ..
930 } = &mut compiled
931 {
932 *compiled_cache_key = Some(cache_key.clone());
933 }
934
935 self.with_sql_compiled_command_cache(|cache| {
936 cache.insert(cache_key, compiled.clone());
937 });
938
939 Ok((
940 compiled,
941 SqlCacheAttribution::sql_compiled_command_cache_miss(),
942 ))
943 }
944
945 pub(in crate::db) fn compile_sql_statement_inner<E>(
948 sql_statement: &SqlStatement,
949 ) -> Result<CompiledSqlCommand, QueryError>
950 where
951 E: PersistedRow<Canister = C> + EntityValue,
952 {
953 Self::compile_sql_statement_for_authority(sql_statement, EntityAuthority::for_type::<E>())
954 }
955}