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,
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 plan: AccessPlannedQuery,
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 plan: AccessPlannedQuery,
301 columns: Vec<String>,
302 fixed_scales: Vec<Option<u32>>,
303 ) -> Self {
304 Self {
305 plan,
306 columns,
307 fixed_scales,
308 }
309 }
310
311 #[must_use]
312 pub(in crate::db) fn into_parts(self) -> (AccessPlannedQuery, Vec<String>, Vec<Option<u32>>) {
313 (self.plan, self.columns, self.fixed_scales)
314 }
315}
316
317impl SqlCompiledCommandCacheKey {
318 fn query_for_entity<E>(sql: &str) -> Self
319 where
320 E: PersistedRow + EntityValue,
321 {
322 Self::for_entity::<E>(SqlCompiledCommandSurface::Query, sql)
323 }
324
325 fn update_for_entity<E>(sql: &str) -> Self
326 where
327 E: PersistedRow + EntityValue,
328 {
329 Self::for_entity::<E>(SqlCompiledCommandSurface::Update, sql)
330 }
331
332 fn for_entity<E>(surface: SqlCompiledCommandSurface, sql: &str) -> Self
333 where
334 E: PersistedRow + EntityValue,
335 {
336 Self {
337 cache_method_version: SQL_COMPILED_COMMAND_CACHE_METHOD_VERSION,
338 surface,
339 entity_path: E::PATH,
340 schema_fingerprint: commit_schema_fingerprint_for_entity::<E>(),
341 sql: sql.to_string(),
342 }
343 }
344
345 #[must_use]
346 pub(in crate::db) const fn schema_fingerprint(&self) -> CommitSchemaFingerprint {
347 self.schema_fingerprint
348 }
349}
350
351impl SqlSelectPlanCacheKey {
352 const fn from_compiled_key(
353 compiled: SqlCompiledCommandCacheKey,
354 visibility: crate::db::session::query::QueryPlanVisibility,
355 ) -> Self {
356 Self {
357 cache_method_version: SQL_SELECT_PLAN_CACHE_METHOD_VERSION,
358 compiled,
359 visibility,
360 }
361 }
362}
363
364#[cfg(test)]
365impl SqlCompiledCommandCacheKey {
366 pub(in crate::db) fn query_for_entity_with_method_version<E>(
367 sql: &str,
368 cache_method_version: u8,
369 ) -> Self
370 where
371 E: PersistedRow + EntityValue,
372 {
373 Self::for_entity_with_method_version::<E>(
374 SqlCompiledCommandSurface::Query,
375 sql,
376 cache_method_version,
377 )
378 }
379
380 pub(in crate::db) fn update_for_entity_with_method_version<E>(
381 sql: &str,
382 cache_method_version: u8,
383 ) -> Self
384 where
385 E: PersistedRow + EntityValue,
386 {
387 Self::for_entity_with_method_version::<E>(
388 SqlCompiledCommandSurface::Update,
389 sql,
390 cache_method_version,
391 )
392 }
393
394 fn for_entity_with_method_version<E>(
395 surface: SqlCompiledCommandSurface,
396 sql: &str,
397 cache_method_version: u8,
398 ) -> Self
399 where
400 E: PersistedRow + EntityValue,
401 {
402 Self {
403 cache_method_version,
404 surface,
405 entity_path: E::PATH,
406 schema_fingerprint: commit_schema_fingerprint_for_entity::<E>(),
407 sql: sql.to_string(),
408 }
409 }
410}
411
412#[cfg(test)]
413impl SqlSelectPlanCacheKey {
414 pub(in crate::db) const fn from_compiled_key_with_method_version(
415 compiled: SqlCompiledCommandCacheKey,
416 visibility: crate::db::session::query::QueryPlanVisibility,
417 cache_method_version: u8,
418 ) -> Self {
419 Self {
420 cache_method_version,
421 compiled,
422 visibility,
423 }
424 }
425}
426
427pub(in crate::db) type SqlCompiledCommandCache =
428 HashMap<SqlCompiledCommandCacheKey, CompiledSqlCommand, CacheBuildHasher>;
429pub(in crate::db) type SqlSelectPlanCache =
430 HashMap<SqlSelectPlanCacheKey, SqlSelectPlanCacheEntry, CacheBuildHasher>;
431
432thread_local! {
433 static SQL_COMPILED_COMMAND_CACHES: RefCell<HashMap<usize, SqlCompiledCommandCache, CacheBuildHasher>> =
437 RefCell::new(HashMap::default());
438 static SQL_SELECT_PLAN_CACHES: RefCell<HashMap<usize, SqlSelectPlanCache, CacheBuildHasher>> =
439 RefCell::new(HashMap::default());
440}
441
442#[derive(Clone, Debug)]
446pub(in crate::db) enum CompiledSqlCommand {
447 Select {
448 query: StructuralQuery,
449 compiled_cache_key: Option<SqlCompiledCommandCacheKey>,
450 },
451 Delete {
452 query: LoweredBaseQueryShape,
453 statement: SqlDeleteStatement,
454 },
455 GlobalAggregate {
456 command: SqlGlobalAggregateCommandCore,
457 label_overrides: Vec<Option<String>>,
458 },
459 Explain(LoweredSqlCommand),
460 Insert(SqlInsertStatement),
461 Update(SqlUpdateStatement),
462 DescribeEntity,
463 ShowIndexesEntity,
464 ShowColumnsEntity,
465 ShowEntities,
466}
467
468pub(in crate::db) fn parse_sql_statement(sql: &str) -> Result<SqlStatement, QueryError> {
471 parse_sql(sql).map_err(QueryError::from_sql_parse_error)
472}
473
474#[cfg(feature = "perf-attribution")]
475#[expect(
476 clippy::missing_const_for_fn,
477 reason = "the wasm32 branch reads the runtime performance counter and cannot be const"
478)]
479fn read_sql_local_instruction_counter() -> u64 {
480 #[cfg(target_arch = "wasm32")]
481 {
482 canic_cdk::api::performance_counter(1)
483 }
484
485 #[cfg(not(target_arch = "wasm32"))]
486 {
487 0
488 }
489}
490
491#[cfg(feature = "perf-attribution")]
492fn measure_sql_stage<T, E>(run: impl FnOnce() -> Result<T, E>) -> (u64, Result<T, E>) {
493 let start = read_sql_local_instruction_counter();
494 let result = run();
495 let delta = read_sql_local_instruction_counter().saturating_sub(start);
496
497 (delta, result)
498}
499
500impl<C: CanisterKind> DbSession<C> {
501 fn sql_cache_scope_id(&self) -> usize {
502 self.db.cache_scope_id()
503 }
504
505 fn with_sql_compiled_command_cache<R>(
506 &self,
507 f: impl FnOnce(&mut SqlCompiledCommandCache) -> R,
508 ) -> R {
509 let scope_id = self.sql_cache_scope_id();
510
511 SQL_COMPILED_COMMAND_CACHES.with(|caches| {
512 let mut caches = caches.borrow_mut();
513 let cache = caches.entry(scope_id).or_default();
514
515 f(cache)
516 })
517 }
518
519 fn with_sql_select_plan_cache<R>(&self, f: impl FnOnce(&mut SqlSelectPlanCache) -> R) -> R {
520 let scope_id = self.sql_cache_scope_id();
521
522 SQL_SELECT_PLAN_CACHES.with(|caches| {
523 let mut caches = caches.borrow_mut();
524 let cache = caches.entry(scope_id).or_default();
525
526 f(cache)
527 })
528 }
529
530 #[cfg(test)]
531 pub(in crate::db) fn sql_compiled_command_cache_len(&self) -> usize {
532 self.with_sql_compiled_command_cache(|cache| cache.len())
533 }
534
535 #[cfg(test)]
536 pub(in crate::db) fn sql_select_plan_cache_len(&self) -> usize {
537 self.with_sql_select_plan_cache(|cache| cache.len())
538 }
539
540 #[cfg(test)]
541 pub(in crate::db) fn clear_sql_caches_for_tests(&self) {
542 self.with_sql_compiled_command_cache(SqlCompiledCommandCache::clear);
543 self.with_sql_select_plan_cache(SqlSelectPlanCache::clear);
544 }
545
546 fn planned_sql_select_with_visibility(
547 &self,
548 query: &StructuralQuery,
549 authority: EntityAuthority,
550 compiled_cache_key: Option<&SqlCompiledCommandCacheKey>,
551 ) -> Result<(SqlSelectPlanCacheEntry, SqlCacheAttribution), QueryError> {
552 let visibility = self.query_plan_visibility_for_store_path(authority.store_path())?;
553 let fallback_schema_fingerprint = crate::db::schema::commit_schema_fingerprint_for_model(
554 authority.model().path,
555 authority.model(),
556 );
557 let cache_schema_fingerprint = compiled_cache_key.map_or(
558 fallback_schema_fingerprint,
559 SqlCompiledCommandCacheKey::schema_fingerprint,
560 );
561
562 let Some(compiled_cache_key) = compiled_cache_key else {
563 let (entry, cache_attribution) = self.cached_query_plan_entry_for_authority(
564 authority,
565 cache_schema_fingerprint,
566 query,
567 )?;
568 let projection = entry.logical_plan().projection_spec(authority.model());
569 let columns = projection_labels_from_projection_spec(&projection);
570 let fixed_scales = projection_fixed_scales_from_projection_spec(&projection);
571
572 return Ok((
573 SqlSelectPlanCacheEntry::new(entry.logical_plan().clone(), columns, fixed_scales),
574 SqlCacheAttribution::from_shared_query_plan_cache(cache_attribution),
575 ));
576 };
577
578 let plan_cache_key =
579 SqlSelectPlanCacheKey::from_compiled_key(compiled_cache_key.clone(), visibility);
580 {
581 let cached =
582 self.with_sql_select_plan_cache(|cache| cache.get(&plan_cache_key).cloned());
583 if let Some(plan) = cached {
584 return Ok((plan, SqlCacheAttribution::sql_select_plan_cache_hit()));
585 }
586 }
587
588 let (entry, cache_attribution) =
589 self.cached_query_plan_entry_for_authority(authority, cache_schema_fingerprint, query)?;
590 let projection = entry.logical_plan().projection_spec(authority.model());
591 let columns = projection_labels_from_projection_spec(&projection);
592 let fixed_scales = projection_fixed_scales_from_projection_spec(&projection);
593 let entry =
594 SqlSelectPlanCacheEntry::new(entry.logical_plan().clone(), columns, fixed_scales);
595 self.with_sql_select_plan_cache(|cache| {
596 cache.insert(plan_cache_key, entry.clone());
597 });
598
599 Ok((
600 entry,
601 SqlCacheAttribution::sql_select_plan_cache_miss().merge(
602 SqlCacheAttribution::from_shared_query_plan_cache(cache_attribution),
603 ),
604 ))
605 }
606
607 pub(in crate::db::session::sql) fn build_structural_plan_with_visible_indexes_for_authority(
610 &self,
611 query: StructuralQuery,
612 authority: EntityAuthority,
613 ) -> Result<(VisibleIndexes<'_>, AccessPlannedQuery), QueryError> {
614 let visible_indexes =
615 self.visible_indexes_for_store_model(authority.store_path(), authority.model())?;
616 let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
617
618 Ok((visible_indexes, plan))
619 }
620
621 fn ensure_sql_query_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
624 match statement {
625 SqlStatement::Select(_)
626 | SqlStatement::Explain(_)
627 | SqlStatement::Describe(_)
628 | SqlStatement::ShowIndexes(_)
629 | SqlStatement::ShowColumns(_)
630 | SqlStatement::ShowEntities(_) => Ok(()),
631 SqlStatement::Insert(_) => Err(QueryError::unsupported_query(
632 "execute_sql_query rejects INSERT; use execute_sql_update::<E>()",
633 )),
634 SqlStatement::Update(_) => Err(QueryError::unsupported_query(
635 "execute_sql_query rejects UPDATE; use execute_sql_update::<E>()",
636 )),
637 SqlStatement::Delete(_) => Err(QueryError::unsupported_query(
638 "execute_sql_query rejects DELETE; use execute_sql_update::<E>()",
639 )),
640 }
641 }
642
643 fn ensure_sql_update_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
646 match statement {
647 SqlStatement::Insert(_) | SqlStatement::Update(_) | SqlStatement::Delete(_) => Ok(()),
648 SqlStatement::Select(_) => Err(QueryError::unsupported_query(
649 "execute_sql_update rejects SELECT; use execute_sql_query::<E>()",
650 )),
651 SqlStatement::Explain(_) => Err(QueryError::unsupported_query(
652 "execute_sql_update rejects EXPLAIN; use execute_sql_query::<E>()",
653 )),
654 SqlStatement::Describe(_) => Err(QueryError::unsupported_query(
655 "execute_sql_update rejects DESCRIBE; use execute_sql_query::<E>()",
656 )),
657 SqlStatement::ShowIndexes(_) => Err(QueryError::unsupported_query(
658 "execute_sql_update rejects SHOW INDEXES; use execute_sql_query::<E>()",
659 )),
660 SqlStatement::ShowColumns(_) => Err(QueryError::unsupported_query(
661 "execute_sql_update rejects SHOW COLUMNS; use execute_sql_query::<E>()",
662 )),
663 SqlStatement::ShowEntities(_) => Err(QueryError::unsupported_query(
664 "execute_sql_update rejects SHOW ENTITIES; use execute_sql_query::<E>()",
665 )),
666 }
667 }
668
669 pub fn execute_sql_query<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
674 where
675 E: PersistedRow<Canister = C> + EntityValue,
676 {
677 let compiled = self.compile_sql_query::<E>(sql)?;
678
679 self.execute_compiled_sql::<E>(&compiled)
680 }
681
682 #[cfg(feature = "perf-attribution")]
685 #[doc(hidden)]
686 pub fn execute_sql_query_with_attribution<E>(
687 &self,
688 sql: &str,
689 ) -> Result<(SqlStatementResult, SqlQueryExecutionAttribution), QueryError>
690 where
691 E: PersistedRow<Canister = C> + EntityValue,
692 {
693 let (compile_local_instructions, compiled) =
696 measure_sql_stage(|| self.compile_sql_query_with_cache_attribution::<E>(sql));
697 let (compiled, compile_cache_attribution) = compiled?;
698
699 let store_get_calls_before = DataStore::current_get_call_count();
702 let pure_covering_decode_before = current_pure_covering_decode_local_instructions();
703 let pure_covering_row_assembly_before =
704 current_pure_covering_row_assembly_local_instructions();
705 let (result, execute_cache_attribution, execute_phase_attribution) =
706 self.execute_compiled_sql_with_phase_attribution::<E>(&compiled)?;
707 let store_get_calls =
708 DataStore::current_get_call_count().saturating_sub(store_get_calls_before);
709 let pure_covering_decode_local_instructions =
710 current_pure_covering_decode_local_instructions()
711 .saturating_sub(pure_covering_decode_before);
712 let pure_covering_row_assembly_local_instructions =
713 current_pure_covering_row_assembly_local_instructions()
714 .saturating_sub(pure_covering_row_assembly_before);
715 let execute_local_instructions = execute_phase_attribution
716 .planner_local_instructions
717 .saturating_add(execute_phase_attribution.store_local_instructions)
718 .saturating_add(execute_phase_attribution.executor_local_instructions);
719 let cache_attribution = compile_cache_attribution.merge(execute_cache_attribution);
720 let total_local_instructions =
721 compile_local_instructions.saturating_add(execute_local_instructions);
722
723 Ok((
724 result,
725 SqlQueryExecutionAttribution {
726 compile_local_instructions,
727 planner_local_instructions: execute_phase_attribution.planner_local_instructions,
728 store_local_instructions: execute_phase_attribution.store_local_instructions,
729 executor_local_instructions: execute_phase_attribution.executor_local_instructions,
730 pure_covering_decode_local_instructions,
731 pure_covering_row_assembly_local_instructions,
732 store_get_calls,
733 response_decode_local_instructions: 0,
734 execute_local_instructions,
735 total_local_instructions,
736 sql_compiled_command_cache_hits: cache_attribution.sql_compiled_command_cache_hits,
737 sql_compiled_command_cache_misses: cache_attribution
738 .sql_compiled_command_cache_misses,
739 sql_select_plan_cache_hits: cache_attribution.sql_select_plan_cache_hits,
740 sql_select_plan_cache_misses: cache_attribution.sql_select_plan_cache_misses,
741 shared_query_plan_cache_hits: cache_attribution.shared_query_plan_cache_hits,
742 shared_query_plan_cache_misses: cache_attribution.shared_query_plan_cache_misses,
743 },
744 ))
745 }
746
747 pub fn execute_sql_update<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
752 where
753 E: PersistedRow<Canister = C> + EntityValue,
754 {
755 let compiled = self.compile_sql_update::<E>(sql)?;
756
757 self.execute_compiled_sql::<E>(&compiled)
758 }
759
760 #[cfg(test)]
761 pub(in crate::db) fn execute_grouped_sql_query_for_tests<E>(
762 &self,
763 sql: &str,
764 cursor_token: Option<&str>,
765 ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
766 where
767 E: PersistedRow<Canister = C> + EntityValue,
768 {
769 let parsed = parse_sql_statement(sql)?;
770
771 let lowered = lower_sql_command_from_prepared_statement(
772 prepare_sql_statement(parsed, E::MODEL.name())
773 .map_err(QueryError::from_sql_lowering_error)?,
774 E::MODEL,
775 )
776 .map_err(QueryError::from_sql_lowering_error)?;
777 let Some(query) = lowered.query().cloned() else {
778 return Err(QueryError::unsupported_query(
779 "grouped SELECT helper requires grouped SELECT",
780 ));
781 };
782 let query = bind_lowered_sql_query::<E>(query, MissingRowPolicy::Ignore)
783 .map_err(QueryError::from_sql_lowering_error)?;
784 if !query.has_grouping() {
785 return Err(QueryError::unsupported_query(
786 "grouped SELECT helper requires grouped SELECT",
787 ));
788 }
789
790 self.execute_grouped(&query, cursor_token)
791 }
792
793 pub(in crate::db) fn compile_sql_query<E>(
796 &self,
797 sql: &str,
798 ) -> Result<CompiledSqlCommand, QueryError>
799 where
800 E: PersistedRow<Canister = C> + EntityValue,
801 {
802 self.compile_sql_query_with_cache_attribution::<E>(sql)
803 .map(|(compiled, _)| compiled)
804 }
805
806 fn compile_sql_query_with_cache_attribution<E>(
807 &self,
808 sql: &str,
809 ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
810 where
811 E: PersistedRow<Canister = C> + EntityValue,
812 {
813 self.compile_sql_statement_with_cache::<E>(
814 SqlCompiledCommandCacheKey::query_for_entity::<E>(sql),
815 sql,
816 Self::ensure_sql_query_statement_supported,
817 )
818 }
819
820 pub(in crate::db) fn compile_sql_update<E>(
823 &self,
824 sql: &str,
825 ) -> Result<CompiledSqlCommand, QueryError>
826 where
827 E: PersistedRow<Canister = C> + EntityValue,
828 {
829 self.compile_sql_update_with_cache_attribution::<E>(sql)
830 .map(|(compiled, _)| compiled)
831 }
832
833 fn compile_sql_update_with_cache_attribution<E>(
834 &self,
835 sql: &str,
836 ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
837 where
838 E: PersistedRow<Canister = C> + EntityValue,
839 {
840 self.compile_sql_statement_with_cache::<E>(
841 SqlCompiledCommandCacheKey::update_for_entity::<E>(sql),
842 sql,
843 Self::ensure_sql_update_statement_supported,
844 )
845 }
846
847 fn compile_sql_statement_with_cache<E>(
850 &self,
851 cache_key: SqlCompiledCommandCacheKey,
852 sql: &str,
853 ensure_surface_supported: fn(&SqlStatement) -> Result<(), QueryError>,
854 ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
855 where
856 E: PersistedRow<Canister = C> + EntityValue,
857 {
858 {
859 let cached =
860 self.with_sql_compiled_command_cache(|cache| cache.get(&cache_key).cloned());
861 if let Some(compiled) = cached {
862 return Ok((
863 compiled,
864 SqlCacheAttribution::sql_compiled_command_cache_hit(),
865 ));
866 }
867 }
868
869 let parsed = parse_sql_statement(sql)?;
870 ensure_surface_supported(&parsed)?;
871 let mut compiled = Self::compile_sql_statement_inner::<E>(&parsed)?;
872 if let CompiledSqlCommand::Select {
873 compiled_cache_key, ..
874 } = &mut compiled
875 {
876 *compiled_cache_key = Some(cache_key.clone());
877 }
878
879 self.with_sql_compiled_command_cache(|cache| {
880 cache.insert(cache_key, compiled.clone());
881 });
882
883 Ok((
884 compiled,
885 SqlCacheAttribution::sql_compiled_command_cache_miss(),
886 ))
887 }
888
889 pub(in crate::db) fn compile_sql_statement_inner<E>(
892 sql_statement: &SqlStatement,
893 ) -> Result<CompiledSqlCommand, QueryError>
894 where
895 E: PersistedRow<Canister = C> + EntityValue,
896 {
897 Self::compile_sql_statement_for_authority(sql_statement, EntityAuthority::for_type::<E>())
898 }
899}