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