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