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)]
469pub(in crate::db) enum CompiledSqlCommand {
470 Select {
471 query: StructuralQuery,
472 compiled_cache_key: Option<SqlCompiledCommandCacheKey>,
473 },
474 Delete {
475 query: LoweredBaseQueryShape,
476 statement: SqlDeleteStatement,
477 },
478 GlobalAggregate {
479 command: SqlGlobalAggregateCommandCore,
480 label_overrides: Vec<Option<String>>,
481 },
482 Explain(LoweredSqlCommand),
483 Insert(SqlInsertStatement),
484 Update(SqlUpdateStatement),
485 DescribeEntity,
486 ShowIndexesEntity,
487 ShowColumnsEntity,
488 ShowEntities,
489}
490
491pub(in crate::db) fn parse_sql_statement(sql: &str) -> Result<SqlStatement, QueryError> {
494 parse_sql(sql).map_err(QueryError::from_sql_parse_error)
495}
496
497#[cfg(feature = "diagnostics")]
498#[expect(
499 clippy::missing_const_for_fn,
500 reason = "the wasm32 branch reads the runtime performance counter and cannot be const"
501)]
502fn read_sql_local_instruction_counter() -> u64 {
503 #[cfg(target_arch = "wasm32")]
504 {
505 canic_cdk::api::performance_counter(1)
506 }
507
508 #[cfg(not(target_arch = "wasm32"))]
509 {
510 0
511 }
512}
513
514#[cfg(feature = "diagnostics")]
515fn measure_sql_stage<T, E>(run: impl FnOnce() -> Result<T, E>) -> (u64, Result<T, E>) {
516 let start = read_sql_local_instruction_counter();
517 let result = run();
518 let delta = read_sql_local_instruction_counter().saturating_sub(start);
519
520 (delta, result)
521}
522
523impl<C: CanisterKind> DbSession<C> {
524 fn sql_cache_scope_id(&self) -> usize {
525 self.db.cache_scope_id()
526 }
527
528 fn with_sql_compiled_command_cache<R>(
529 &self,
530 f: impl FnOnce(&mut SqlCompiledCommandCache) -> R,
531 ) -> R {
532 let scope_id = self.sql_cache_scope_id();
533
534 SQL_COMPILED_COMMAND_CACHES.with(|caches| {
535 let mut caches = caches.borrow_mut();
536 let cache = caches.entry(scope_id).or_default();
537
538 f(cache)
539 })
540 }
541
542 fn with_sql_select_plan_cache<R>(&self, f: impl FnOnce(&mut SqlSelectPlanCache) -> R) -> R {
543 let scope_id = self.sql_cache_scope_id();
544
545 SQL_SELECT_PLAN_CACHES.with(|caches| {
546 let mut caches = caches.borrow_mut();
547 let cache = caches.entry(scope_id).or_default();
548
549 f(cache)
550 })
551 }
552
553 #[cfg(test)]
554 pub(in crate::db) fn sql_compiled_command_cache_len(&self) -> usize {
555 self.with_sql_compiled_command_cache(|cache| cache.len())
556 }
557
558 #[cfg(test)]
559 pub(in crate::db) fn sql_select_plan_cache_len(&self) -> usize {
560 self.with_sql_select_plan_cache(|cache| cache.len())
561 }
562
563 #[cfg(test)]
564 pub(in crate::db) fn clear_sql_caches_for_tests(&self) {
565 self.with_sql_compiled_command_cache(SqlCompiledCommandCache::clear);
566 self.with_sql_select_plan_cache(SqlSelectPlanCache::clear);
567 }
568
569 fn planned_sql_select_with_visibility(
570 &self,
571 query: &StructuralQuery,
572 authority: EntityAuthority,
573 compiled_cache_key: Option<&SqlCompiledCommandCacheKey>,
574 ) -> Result<(SqlSelectPlanCacheEntry, SqlCacheAttribution), QueryError> {
575 let visibility = self.query_plan_visibility_for_store_path(authority.store_path())?;
576 let fallback_schema_fingerprint = crate::db::schema::commit_schema_fingerprint_for_model(
577 authority.model().path,
578 authority.model(),
579 );
580 let cache_schema_fingerprint = compiled_cache_key.map_or(
581 fallback_schema_fingerprint,
582 SqlCompiledCommandCacheKey::schema_fingerprint,
583 );
584
585 let Some(compiled_cache_key) = compiled_cache_key else {
586 let (entry, cache_attribution) = self.cached_query_plan_entry_for_authority(
587 authority,
588 cache_schema_fingerprint,
589 query,
590 )?;
591 let projection = entry.logical_plan().projection_spec(authority.model());
592 let columns = projection_labels_from_projection_spec(&projection);
593 let fixed_scales = projection_fixed_scales_from_projection_spec(&projection);
594
595 return Ok((
596 SqlSelectPlanCacheEntry::new(entry.prepared_plan().clone(), columns, fixed_scales),
597 SqlCacheAttribution::from_shared_query_plan_cache(cache_attribution),
598 ));
599 };
600
601 let plan_cache_key =
602 SqlSelectPlanCacheKey::from_compiled_key(compiled_cache_key.clone(), visibility);
603 {
604 let cached =
605 self.with_sql_select_plan_cache(|cache| cache.get(&plan_cache_key).cloned());
606 if let Some(plan) = cached {
607 return Ok((plan, SqlCacheAttribution::sql_select_plan_cache_hit()));
608 }
609 }
610
611 let (entry, cache_attribution) =
612 self.cached_query_plan_entry_for_authority(authority, cache_schema_fingerprint, query)?;
613 let projection = entry.logical_plan().projection_spec(authority.model());
614 let columns = projection_labels_from_projection_spec(&projection);
615 let fixed_scales = projection_fixed_scales_from_projection_spec(&projection);
616 let entry =
617 SqlSelectPlanCacheEntry::new(entry.prepared_plan().clone(), columns, fixed_scales);
618 self.with_sql_select_plan_cache(|cache| {
619 cache.insert(plan_cache_key, entry.clone());
620 });
621
622 Ok((
623 entry,
624 SqlCacheAttribution::sql_select_plan_cache_miss().merge(
625 SqlCacheAttribution::from_shared_query_plan_cache(cache_attribution),
626 ),
627 ))
628 }
629
630 pub(in crate::db::session::sql) fn build_structural_plan_with_visible_indexes_for_authority(
633 &self,
634 query: StructuralQuery,
635 authority: EntityAuthority,
636 ) -> Result<(VisibleIndexes<'_>, AccessPlannedQuery), QueryError> {
637 let visible_indexes =
638 self.visible_indexes_for_store_model(authority.store_path(), authority.model())?;
639 let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
640
641 Ok((visible_indexes, plan))
642 }
643
644 fn ensure_sql_query_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
647 match statement {
648 SqlStatement::Select(_)
649 | SqlStatement::Explain(_)
650 | SqlStatement::Describe(_)
651 | SqlStatement::ShowIndexes(_)
652 | SqlStatement::ShowColumns(_)
653 | SqlStatement::ShowEntities(_) => Ok(()),
654 SqlStatement::Insert(_) => Err(QueryError::unsupported_query(
655 "execute_sql_query rejects INSERT; use execute_sql_update::<E>()",
656 )),
657 SqlStatement::Update(_) => Err(QueryError::unsupported_query(
658 "execute_sql_query rejects UPDATE; use execute_sql_update::<E>()",
659 )),
660 SqlStatement::Delete(_) => Err(QueryError::unsupported_query(
661 "execute_sql_query rejects DELETE; use execute_sql_update::<E>()",
662 )),
663 }
664 }
665
666 fn ensure_sql_update_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
669 match statement {
670 SqlStatement::Insert(_) | SqlStatement::Update(_) | SqlStatement::Delete(_) => Ok(()),
671 SqlStatement::Select(_) => Err(QueryError::unsupported_query(
672 "execute_sql_update rejects SELECT; use execute_sql_query::<E>()",
673 )),
674 SqlStatement::Explain(_) => Err(QueryError::unsupported_query(
675 "execute_sql_update rejects EXPLAIN; use execute_sql_query::<E>()",
676 )),
677 SqlStatement::Describe(_) => Err(QueryError::unsupported_query(
678 "execute_sql_update rejects DESCRIBE; use execute_sql_query::<E>()",
679 )),
680 SqlStatement::ShowIndexes(_) => Err(QueryError::unsupported_query(
681 "execute_sql_update rejects SHOW INDEXES; use execute_sql_query::<E>()",
682 )),
683 SqlStatement::ShowColumns(_) => Err(QueryError::unsupported_query(
684 "execute_sql_update rejects SHOW COLUMNS; use execute_sql_query::<E>()",
685 )),
686 SqlStatement::ShowEntities(_) => Err(QueryError::unsupported_query(
687 "execute_sql_update rejects SHOW ENTITIES; use execute_sql_query::<E>()",
688 )),
689 }
690 }
691
692 pub fn execute_sql_query<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
697 where
698 E: PersistedRow<Canister = C> + EntityValue,
699 {
700 let compiled = self.compile_sql_query::<E>(sql)?;
701
702 self.execute_compiled_sql::<E>(&compiled)
703 }
704
705 #[cfg(feature = "diagnostics")]
708 #[doc(hidden)]
709 pub fn execute_sql_query_with_attribution<E>(
710 &self,
711 sql: &str,
712 ) -> Result<(SqlStatementResult, SqlQueryExecutionAttribution), QueryError>
713 where
714 E: PersistedRow<Canister = C> + EntityValue,
715 {
716 let (compile_local_instructions, compiled) =
719 measure_sql_stage(|| self.compile_sql_query_with_cache_attribution::<E>(sql));
720 let (compiled, compile_cache_attribution) = compiled?;
721
722 let store_get_calls_before = DataStore::current_get_call_count();
725 let pure_covering_decode_before = current_pure_covering_decode_local_instructions();
726 let pure_covering_row_assembly_before =
727 current_pure_covering_row_assembly_local_instructions();
728 let (result, execute_cache_attribution, execute_phase_attribution) =
729 self.execute_compiled_sql_with_phase_attribution::<E>(&compiled)?;
730 let store_get_calls =
731 DataStore::current_get_call_count().saturating_sub(store_get_calls_before);
732 let pure_covering_decode_local_instructions =
733 current_pure_covering_decode_local_instructions()
734 .saturating_sub(pure_covering_decode_before);
735 let pure_covering_row_assembly_local_instructions =
736 current_pure_covering_row_assembly_local_instructions()
737 .saturating_sub(pure_covering_row_assembly_before);
738 let execute_local_instructions = execute_phase_attribution
739 .planner_local_instructions
740 .saturating_add(execute_phase_attribution.store_local_instructions)
741 .saturating_add(execute_phase_attribution.executor_local_instructions);
742 let cache_attribution = compile_cache_attribution.merge(execute_cache_attribution);
743 let total_local_instructions =
744 compile_local_instructions.saturating_add(execute_local_instructions);
745
746 Ok((
747 result,
748 SqlQueryExecutionAttribution {
749 compile_local_instructions,
750 planner_local_instructions: execute_phase_attribution.planner_local_instructions,
751 store_local_instructions: execute_phase_attribution.store_local_instructions,
752 executor_local_instructions: execute_phase_attribution.executor_local_instructions,
753 grouped_stream_local_instructions: execute_phase_attribution
754 .grouped_stream_local_instructions,
755 grouped_fold_local_instructions: execute_phase_attribution
756 .grouped_fold_local_instructions,
757 grouped_finalize_local_instructions: execute_phase_attribution
758 .grouped_finalize_local_instructions,
759 grouped_count_borrowed_hash_computations: execute_phase_attribution
760 .grouped_count
761 .borrowed_hash_computations,
762 grouped_count_bucket_candidate_checks: execute_phase_attribution
763 .grouped_count
764 .bucket_candidate_checks,
765 grouped_count_existing_group_hits: execute_phase_attribution
766 .grouped_count
767 .existing_group_hits,
768 grouped_count_new_group_inserts: execute_phase_attribution
769 .grouped_count
770 .new_group_inserts,
771 grouped_count_row_materialization_local_instructions: execute_phase_attribution
772 .grouped_count
773 .row_materialization_local_instructions,
774 grouped_count_group_lookup_local_instructions: execute_phase_attribution
775 .grouped_count
776 .group_lookup_local_instructions,
777 grouped_count_existing_group_update_local_instructions: execute_phase_attribution
778 .grouped_count
779 .existing_group_update_local_instructions,
780 grouped_count_new_group_insert_local_instructions: execute_phase_attribution
781 .grouped_count
782 .new_group_insert_local_instructions,
783 pure_covering_decode_local_instructions,
784 pure_covering_row_assembly_local_instructions,
785 store_get_calls,
786 response_decode_local_instructions: 0,
787 execute_local_instructions,
788 total_local_instructions,
789 sql_compiled_command_cache_hits: cache_attribution.sql_compiled_command_cache_hits,
790 sql_compiled_command_cache_misses: cache_attribution
791 .sql_compiled_command_cache_misses,
792 sql_select_plan_cache_hits: cache_attribution.sql_select_plan_cache_hits,
793 sql_select_plan_cache_misses: cache_attribution.sql_select_plan_cache_misses,
794 shared_query_plan_cache_hits: cache_attribution.shared_query_plan_cache_hits,
795 shared_query_plan_cache_misses: cache_attribution.shared_query_plan_cache_misses,
796 },
797 ))
798 }
799
800 pub fn execute_sql_update<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
805 where
806 E: PersistedRow<Canister = C> + EntityValue,
807 {
808 let compiled = self.compile_sql_update::<E>(sql)?;
809
810 self.execute_compiled_sql::<E>(&compiled)
811 }
812
813 #[cfg(test)]
814 pub(in crate::db) fn execute_grouped_sql_query_for_tests<E>(
815 &self,
816 sql: &str,
817 cursor_token: Option<&str>,
818 ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
819 where
820 E: PersistedRow<Canister = C> + EntityValue,
821 {
822 let parsed = parse_sql_statement(sql)?;
823
824 let lowered = lower_sql_command_from_prepared_statement(
825 prepare_sql_statement(parsed, E::MODEL.name())
826 .map_err(QueryError::from_sql_lowering_error)?,
827 E::MODEL,
828 )
829 .map_err(QueryError::from_sql_lowering_error)?;
830 let Some(query) = lowered.query().cloned() else {
831 return Err(QueryError::unsupported_query(
832 "grouped SELECT helper requires grouped SELECT",
833 ));
834 };
835 let query = bind_lowered_sql_query::<E>(query, MissingRowPolicy::Ignore)
836 .map_err(QueryError::from_sql_lowering_error)?;
837 if !query.has_grouping() {
838 return Err(QueryError::unsupported_query(
839 "grouped SELECT helper requires grouped SELECT",
840 ));
841 }
842
843 self.execute_grouped(&query, cursor_token)
844 }
845
846 pub(in crate::db) fn compile_sql_query<E>(
849 &self,
850 sql: &str,
851 ) -> Result<CompiledSqlCommand, QueryError>
852 where
853 E: PersistedRow<Canister = C> + EntityValue,
854 {
855 self.compile_sql_query_with_cache_attribution::<E>(sql)
856 .map(|(compiled, _)| compiled)
857 }
858
859 fn compile_sql_query_with_cache_attribution<E>(
860 &self,
861 sql: &str,
862 ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
863 where
864 E: PersistedRow<Canister = C> + EntityValue,
865 {
866 self.compile_sql_statement_with_cache::<E>(
867 SqlCompiledCommandCacheKey::query_for_entity::<E>(sql),
868 sql,
869 Self::ensure_sql_query_statement_supported,
870 )
871 }
872
873 pub(in crate::db) fn compile_sql_update<E>(
876 &self,
877 sql: &str,
878 ) -> Result<CompiledSqlCommand, QueryError>
879 where
880 E: PersistedRow<Canister = C> + EntityValue,
881 {
882 self.compile_sql_update_with_cache_attribution::<E>(sql)
883 .map(|(compiled, _)| compiled)
884 }
885
886 fn compile_sql_update_with_cache_attribution<E>(
887 &self,
888 sql: &str,
889 ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
890 where
891 E: PersistedRow<Canister = C> + EntityValue,
892 {
893 self.compile_sql_statement_with_cache::<E>(
894 SqlCompiledCommandCacheKey::update_for_entity::<E>(sql),
895 sql,
896 Self::ensure_sql_update_statement_supported,
897 )
898 }
899
900 fn compile_sql_statement_with_cache<E>(
903 &self,
904 cache_key: SqlCompiledCommandCacheKey,
905 sql: &str,
906 ensure_surface_supported: fn(&SqlStatement) -> Result<(), QueryError>,
907 ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
908 where
909 E: PersistedRow<Canister = C> + EntityValue,
910 {
911 {
912 let cached =
913 self.with_sql_compiled_command_cache(|cache| cache.get(&cache_key).cloned());
914 if let Some(compiled) = cached {
915 return Ok((
916 compiled,
917 SqlCacheAttribution::sql_compiled_command_cache_hit(),
918 ));
919 }
920 }
921
922 let parsed = parse_sql_statement(sql)?;
923 ensure_surface_supported(&parsed)?;
924 let mut compiled = Self::compile_sql_statement_inner::<E>(&parsed)?;
925 if let CompiledSqlCommand::Select {
926 compiled_cache_key, ..
927 } = &mut compiled
928 {
929 *compiled_cache_key = Some(cache_key.clone());
930 }
931
932 self.with_sql_compiled_command_cache(|cache| {
933 cache.insert(cache_key, compiled.clone());
934 });
935
936 Ok((
937 compiled,
938 SqlCacheAttribution::sql_compiled_command_cache_miss(),
939 ))
940 }
941
942 pub(in crate::db) fn compile_sql_statement_inner<E>(
945 sql_statement: &SqlStatement,
946 ) -> Result<CompiledSqlCommand, QueryError>
947 where
948 E: PersistedRow<Canister = C> + EntityValue,
949 {
950 Self::compile_sql_statement_for_authority(sql_statement, EntityAuthority::for_type::<E>())
951 }
952}