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