1mod execute;
8mod explain;
9mod projection;
10
11#[cfg(feature = "perf-attribution")]
12use candid::CandidType;
13#[cfg(feature = "perf-attribution")]
14use serde::Deserialize;
15use std::{cell::RefCell, collections::HashMap};
16
17use crate::db::sql::parser::{SqlDeleteStatement, SqlInsertStatement, SqlUpdateStatement};
18use crate::{
19 db::{
20 DbSession, GroupedRow, PersistedRow, QueryError,
21 commit::CommitSchemaFingerprint,
22 executor::EntityAuthority,
23 query::{
24 intent::StructuralQuery,
25 plan::{AccessPlannedQuery, VisibleIndexes},
26 },
27 schema::commit_schema_fingerprint_for_entity,
28 session::query::QueryPlanCacheAttribution,
29 session::sql::projection::{
30 projection_fixed_scales_from_projection_spec, projection_labels_from_projection_spec,
31 },
32 sql::lowering::{LoweredBaseQueryShape, LoweredSqlCommand, SqlGlobalAggregateCommandCore},
33 sql::parser::{SqlStatement, parse_sql},
34 },
35 traits::{CanisterKind, EntityValue},
36};
37
38#[cfg(test)]
39use crate::db::{
40 MissingRowPolicy, PagedGroupedExecutionWithTrace,
41 sql::lowering::{
42 bind_lowered_sql_query, lower_sql_command_from_prepared_statement, prepare_sql_statement,
43 },
44};
45
46#[cfg(feature = "structural-read-metrics")]
47pub use crate::db::session::sql::projection::{
48 SqlProjectionMaterializationMetrics, with_sql_projection_materialization_metrics,
49};
50
51#[derive(Debug)]
53pub enum SqlStatementResult {
54 Count {
55 row_count: u32,
56 },
57 Projection {
58 columns: Vec<String>,
59 fixed_scales: Vec<Option<u32>>,
60 rows: Vec<Vec<crate::value::Value>>,
61 row_count: u32,
62 },
63 ProjectionText {
64 columns: Vec<String>,
65 rows: Vec<Vec<String>>,
66 row_count: u32,
67 },
68 Grouped {
69 columns: Vec<String>,
70 rows: Vec<GroupedRow>,
71 row_count: u32,
72 next_cursor: Option<String>,
73 },
74 Explain(String),
75 Describe(crate::db::EntitySchemaDescription),
76 ShowIndexes(Vec<String>),
77 ShowColumns(Vec<crate::db::EntityFieldDescription>),
78 ShowEntities(Vec<String>),
79}
80
81#[cfg(feature = "perf-attribution")]
92#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
93pub struct SqlQueryExecutionAttribution {
94 pub compile_local_instructions: u64,
95 pub planner_local_instructions: u64,
96 pub executor_local_instructions: u64,
97 pub execute_local_instructions: u64,
98 pub total_local_instructions: u64,
99 pub sql_compiled_command_cache_hits: u64,
100 pub sql_compiled_command_cache_misses: u64,
101 pub sql_select_plan_cache_hits: u64,
102 pub sql_select_plan_cache_misses: u64,
103 pub shared_query_plan_cache_hits: u64,
104 pub shared_query_plan_cache_misses: u64,
105}
106
107#[cfg(feature = "perf-attribution")]
110#[derive(Clone, Copy, Debug, Eq, PartialEq)]
111pub(in crate::db) struct SqlExecutePhaseAttribution {
112 pub planner_local_instructions: u64,
113 pub executor_local_instructions: u64,
114}
115
116#[cfg(feature = "perf-attribution")]
117impl SqlExecutePhaseAttribution {
118 #[must_use]
119 pub(in crate::db) const fn from_execute_total(execute_local_instructions: u64) -> Self {
120 Self {
121 planner_local_instructions: 0,
122 executor_local_instructions: execute_local_instructions,
123 }
124 }
125}
126
127#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
131pub(in crate::db) struct SqlCacheAttribution {
132 pub sql_compiled_command_cache_hits: u64,
133 pub sql_compiled_command_cache_misses: u64,
134 pub sql_select_plan_cache_hits: u64,
135 pub sql_select_plan_cache_misses: u64,
136 pub shared_query_plan_cache_hits: u64,
137 pub shared_query_plan_cache_misses: u64,
138}
139
140impl SqlCacheAttribution {
141 #[must_use]
142 const fn none() -> Self {
143 Self {
144 sql_compiled_command_cache_hits: 0,
145 sql_compiled_command_cache_misses: 0,
146 sql_select_plan_cache_hits: 0,
147 sql_select_plan_cache_misses: 0,
148 shared_query_plan_cache_hits: 0,
149 shared_query_plan_cache_misses: 0,
150 }
151 }
152
153 #[must_use]
154 const fn sql_compiled_command_cache_hit() -> Self {
155 Self {
156 sql_compiled_command_cache_hits: 1,
157 ..Self::none()
158 }
159 }
160
161 #[must_use]
162 const fn sql_compiled_command_cache_miss() -> Self {
163 Self {
164 sql_compiled_command_cache_misses: 1,
165 ..Self::none()
166 }
167 }
168
169 #[must_use]
170 const fn sql_select_plan_cache_hit() -> Self {
171 Self {
172 sql_select_plan_cache_hits: 1,
173 ..Self::none()
174 }
175 }
176
177 #[must_use]
178 const fn sql_select_plan_cache_miss() -> Self {
179 Self {
180 sql_select_plan_cache_misses: 1,
181 ..Self::none()
182 }
183 }
184
185 #[must_use]
186 const fn from_shared_query_plan_cache(attribution: QueryPlanCacheAttribution) -> Self {
187 Self {
188 shared_query_plan_cache_hits: attribution.hits,
189 shared_query_plan_cache_misses: attribution.misses,
190 ..Self::none()
191 }
192 }
193
194 #[must_use]
195 const fn merge(self, other: Self) -> Self {
196 Self {
197 sql_compiled_command_cache_hits: self
198 .sql_compiled_command_cache_hits
199 .saturating_add(other.sql_compiled_command_cache_hits),
200 sql_compiled_command_cache_misses: self
201 .sql_compiled_command_cache_misses
202 .saturating_add(other.sql_compiled_command_cache_misses),
203 sql_select_plan_cache_hits: self
204 .sql_select_plan_cache_hits
205 .saturating_add(other.sql_select_plan_cache_hits),
206 sql_select_plan_cache_misses: self
207 .sql_select_plan_cache_misses
208 .saturating_add(other.sql_select_plan_cache_misses),
209 shared_query_plan_cache_hits: self
210 .shared_query_plan_cache_hits
211 .saturating_add(other.shared_query_plan_cache_hits),
212 shared_query_plan_cache_misses: self
213 .shared_query_plan_cache_misses
214 .saturating_add(other.shared_query_plan_cache_misses),
215 }
216 }
217}
218
219#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
220enum SqlCompiledCommandSurface {
221 Query,
222 Update,
223}
224
225#[derive(Clone, Debug, Eq, Hash, PartialEq)]
236pub(in crate::db) struct SqlCompiledCommandCacheKey {
237 surface: SqlCompiledCommandSurface,
238 entity_path: &'static str,
239 schema_fingerprint: CommitSchemaFingerprint,
240 sql: String,
241}
242
243#[derive(Clone, Debug, Eq, Hash, PartialEq)]
244pub(in crate::db) struct SqlSelectPlanCacheKey {
245 compiled: SqlCompiledCommandCacheKey,
246 visibility: crate::db::session::query::QueryPlanVisibility,
247}
248
249#[derive(Clone, Debug)]
259pub(in crate::db) struct SqlSelectPlanCacheEntry {
260 plan: AccessPlannedQuery,
261 columns: Vec<String>,
262 fixed_scales: Vec<Option<u32>>,
263}
264
265impl SqlSelectPlanCacheEntry {
266 #[must_use]
267 pub(in crate::db) const fn new(
268 plan: AccessPlannedQuery,
269 columns: Vec<String>,
270 fixed_scales: Vec<Option<u32>>,
271 ) -> Self {
272 Self {
273 plan,
274 columns,
275 fixed_scales,
276 }
277 }
278
279 #[must_use]
280 pub(in crate::db) fn into_parts(self) -> (AccessPlannedQuery, Vec<String>, Vec<Option<u32>>) {
281 (self.plan, self.columns, self.fixed_scales)
282 }
283}
284
285impl SqlCompiledCommandCacheKey {
286 fn query_for_entity<E>(sql: &str) -> Self
287 where
288 E: PersistedRow + EntityValue,
289 {
290 Self::for_entity::<E>(SqlCompiledCommandSurface::Query, sql)
291 }
292
293 fn update_for_entity<E>(sql: &str) -> Self
294 where
295 E: PersistedRow + EntityValue,
296 {
297 Self::for_entity::<E>(SqlCompiledCommandSurface::Update, sql)
298 }
299
300 fn for_entity<E>(surface: SqlCompiledCommandSurface, sql: &str) -> Self
301 where
302 E: PersistedRow + EntityValue,
303 {
304 Self {
305 surface,
306 entity_path: E::PATH,
307 schema_fingerprint: commit_schema_fingerprint_for_entity::<E>(),
308 sql: sql.to_string(),
309 }
310 }
311
312 #[must_use]
313 pub(in crate::db) const fn schema_fingerprint(&self) -> CommitSchemaFingerprint {
314 self.schema_fingerprint
315 }
316}
317
318impl SqlSelectPlanCacheKey {
319 const fn from_compiled_key(
320 compiled: SqlCompiledCommandCacheKey,
321 visibility: crate::db::session::query::QueryPlanVisibility,
322 ) -> Self {
323 Self {
324 compiled,
325 visibility,
326 }
327 }
328}
329
330pub(in crate::db) type SqlCompiledCommandCache =
331 HashMap<SqlCompiledCommandCacheKey, CompiledSqlCommand>;
332pub(in crate::db) type SqlSelectPlanCache = HashMap<SqlSelectPlanCacheKey, SqlSelectPlanCacheEntry>;
333
334thread_local! {
335 static SQL_COMPILED_COMMAND_CACHES: RefCell<HashMap<usize, SqlCompiledCommandCache>> =
339 RefCell::new(HashMap::new());
340 static SQL_SELECT_PLAN_CACHES: RefCell<HashMap<usize, SqlSelectPlanCache>> =
341 RefCell::new(HashMap::new());
342}
343
344#[derive(Clone, Debug)]
348pub(in crate::db) enum CompiledSqlCommand {
349 Select {
350 query: StructuralQuery,
351 compiled_cache_key: Option<SqlCompiledCommandCacheKey>,
352 },
353 Delete {
354 query: LoweredBaseQueryShape,
355 statement: SqlDeleteStatement,
356 },
357 GlobalAggregate {
358 command: SqlGlobalAggregateCommandCore,
359 label_override: Option<String>,
360 },
361 Explain(LoweredSqlCommand),
362 Insert(SqlInsertStatement),
363 Update(SqlUpdateStatement),
364 DescribeEntity,
365 ShowIndexesEntity,
366 ShowColumnsEntity,
367 ShowEntities,
368}
369
370pub(in crate::db) fn parse_sql_statement(sql: &str) -> Result<SqlStatement, QueryError> {
373 parse_sql(sql).map_err(QueryError::from_sql_parse_error)
374}
375
376#[cfg(feature = "perf-attribution")]
377#[expect(
378 clippy::missing_const_for_fn,
379 reason = "the wasm32 branch reads the runtime performance counter and cannot be const"
380)]
381fn read_sql_local_instruction_counter() -> u64 {
382 #[cfg(target_arch = "wasm32")]
383 {
384 canic_cdk::api::performance_counter(1)
385 }
386
387 #[cfg(not(target_arch = "wasm32"))]
388 {
389 0
390 }
391}
392
393#[cfg(feature = "perf-attribution")]
394fn measure_sql_stage<T, E>(run: impl FnOnce() -> Result<T, E>) -> (u64, Result<T, E>) {
395 let start = read_sql_local_instruction_counter();
396 let result = run();
397 let delta = read_sql_local_instruction_counter().saturating_sub(start);
398
399 (delta, result)
400}
401
402impl<C: CanisterKind> DbSession<C> {
403 fn sql_cache_scope_id(&self) -> usize {
404 self.db.cache_scope_id()
405 }
406
407 fn with_sql_compiled_command_cache<R>(
408 &self,
409 f: impl FnOnce(&mut SqlCompiledCommandCache) -> R,
410 ) -> R {
411 let scope_id = self.sql_cache_scope_id();
412
413 SQL_COMPILED_COMMAND_CACHES.with(|caches| {
414 let mut caches = caches.borrow_mut();
415 let cache = caches.entry(scope_id).or_default();
416
417 f(cache)
418 })
419 }
420
421 fn with_sql_select_plan_cache<R>(&self, f: impl FnOnce(&mut SqlSelectPlanCache) -> R) -> R {
422 let scope_id = self.sql_cache_scope_id();
423
424 SQL_SELECT_PLAN_CACHES.with(|caches| {
425 let mut caches = caches.borrow_mut();
426 let cache = caches.entry(scope_id).or_default();
427
428 f(cache)
429 })
430 }
431
432 #[cfg(test)]
433 pub(in crate::db) fn sql_compiled_command_cache_len(&self) -> usize {
434 self.with_sql_compiled_command_cache(|cache| cache.len())
435 }
436
437 #[cfg(test)]
438 pub(in crate::db) fn sql_select_plan_cache_len(&self) -> usize {
439 self.with_sql_select_plan_cache(|cache| cache.len())
440 }
441
442 #[cfg(test)]
443 pub(in crate::db) fn clear_sql_caches_for_tests(&self) {
444 self.with_sql_compiled_command_cache(SqlCompiledCommandCache::clear);
445 self.with_sql_select_plan_cache(SqlSelectPlanCache::clear);
446 }
447
448 fn planned_sql_select_with_visibility(
449 &self,
450 query: &StructuralQuery,
451 authority: EntityAuthority,
452 compiled_cache_key: Option<&SqlCompiledCommandCacheKey>,
453 ) -> Result<(SqlSelectPlanCacheEntry, SqlCacheAttribution), QueryError> {
454 let visibility = self.query_plan_visibility_for_store_path(authority.store_path())?;
455 let fallback_schema_fingerprint = crate::db::schema::commit_schema_fingerprint_for_model(
456 authority.model().path,
457 authority.model(),
458 );
459 let cache_schema_fingerprint = compiled_cache_key.map_or(
460 fallback_schema_fingerprint,
461 SqlCompiledCommandCacheKey::schema_fingerprint,
462 );
463
464 let Some(compiled_cache_key) = compiled_cache_key else {
465 let (entry, cache_attribution) = self.cached_query_plan_entry_for_authority(
466 authority,
467 cache_schema_fingerprint,
468 query,
469 )?;
470 let projection = entry.logical_plan().projection_spec(authority.model());
471 let columns = projection_labels_from_projection_spec(&projection);
472 let fixed_scales = projection_fixed_scales_from_projection_spec(&projection);
473
474 return Ok((
475 SqlSelectPlanCacheEntry::new(entry.logical_plan().clone(), columns, fixed_scales),
476 SqlCacheAttribution::from_shared_query_plan_cache(cache_attribution),
477 ));
478 };
479
480 let plan_cache_key =
481 SqlSelectPlanCacheKey::from_compiled_key(compiled_cache_key.clone(), visibility);
482 {
483 let cached =
484 self.with_sql_select_plan_cache(|cache| cache.get(&plan_cache_key).cloned());
485 if let Some(plan) = cached {
486 return Ok((plan, SqlCacheAttribution::sql_select_plan_cache_hit()));
487 }
488 }
489
490 let (entry, cache_attribution) =
491 self.cached_query_plan_entry_for_authority(authority, cache_schema_fingerprint, query)?;
492 let projection = entry.logical_plan().projection_spec(authority.model());
493 let columns = projection_labels_from_projection_spec(&projection);
494 let fixed_scales = projection_fixed_scales_from_projection_spec(&projection);
495 let entry =
496 SqlSelectPlanCacheEntry::new(entry.logical_plan().clone(), columns, fixed_scales);
497 self.with_sql_select_plan_cache(|cache| {
498 cache.insert(plan_cache_key, entry.clone());
499 });
500
501 Ok((
502 entry,
503 SqlCacheAttribution::sql_select_plan_cache_miss().merge(
504 SqlCacheAttribution::from_shared_query_plan_cache(cache_attribution),
505 ),
506 ))
507 }
508
509 pub(in crate::db::session::sql) fn build_structural_plan_with_visible_indexes_for_authority(
512 &self,
513 query: StructuralQuery,
514 authority: EntityAuthority,
515 ) -> Result<(VisibleIndexes<'_>, AccessPlannedQuery), QueryError> {
516 let visible_indexes =
517 self.visible_indexes_for_store_model(authority.store_path(), authority.model())?;
518 let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
519
520 Ok((visible_indexes, plan))
521 }
522
523 fn ensure_sql_query_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
526 match statement {
527 SqlStatement::Select(_)
528 | SqlStatement::Explain(_)
529 | SqlStatement::Describe(_)
530 | SqlStatement::ShowIndexes(_)
531 | SqlStatement::ShowColumns(_)
532 | SqlStatement::ShowEntities(_) => Ok(()),
533 SqlStatement::Insert(_) => Err(QueryError::unsupported_query(
534 "execute_sql_query rejects INSERT; use execute_sql_update::<E>()",
535 )),
536 SqlStatement::Update(_) => Err(QueryError::unsupported_query(
537 "execute_sql_query rejects UPDATE; use execute_sql_update::<E>()",
538 )),
539 SqlStatement::Delete(_) => Err(QueryError::unsupported_query(
540 "execute_sql_query rejects DELETE; use execute_sql_update::<E>()",
541 )),
542 }
543 }
544
545 fn ensure_sql_update_statement_supported(statement: &SqlStatement) -> Result<(), QueryError> {
548 match statement {
549 SqlStatement::Insert(_) | SqlStatement::Update(_) | SqlStatement::Delete(_) => Ok(()),
550 SqlStatement::Select(_) => Err(QueryError::unsupported_query(
551 "execute_sql_update rejects SELECT; use execute_sql_query::<E>()",
552 )),
553 SqlStatement::Explain(_) => Err(QueryError::unsupported_query(
554 "execute_sql_update rejects EXPLAIN; use execute_sql_query::<E>()",
555 )),
556 SqlStatement::Describe(_) => Err(QueryError::unsupported_query(
557 "execute_sql_update rejects DESCRIBE; use execute_sql_query::<E>()",
558 )),
559 SqlStatement::ShowIndexes(_) => Err(QueryError::unsupported_query(
560 "execute_sql_update rejects SHOW INDEXES; use execute_sql_query::<E>()",
561 )),
562 SqlStatement::ShowColumns(_) => Err(QueryError::unsupported_query(
563 "execute_sql_update rejects SHOW COLUMNS; use execute_sql_query::<E>()",
564 )),
565 SqlStatement::ShowEntities(_) => Err(QueryError::unsupported_query(
566 "execute_sql_update rejects SHOW ENTITIES; use execute_sql_query::<E>()",
567 )),
568 }
569 }
570
571 pub fn execute_sql_query<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
576 where
577 E: PersistedRow<Canister = C> + EntityValue,
578 {
579 let compiled = self.compile_sql_query::<E>(sql)?;
580
581 self.execute_compiled_sql::<E>(&compiled)
582 }
583
584 #[cfg(feature = "perf-attribution")]
587 #[doc(hidden)]
588 pub fn execute_sql_query_with_attribution<E>(
589 &self,
590 sql: &str,
591 ) -> Result<(SqlStatementResult, SqlQueryExecutionAttribution), QueryError>
592 where
593 E: PersistedRow<Canister = C> + EntityValue,
594 {
595 let (compile_local_instructions, compiled) =
598 measure_sql_stage(|| self.compile_sql_query_with_cache_attribution::<E>(sql));
599 let (compiled, compile_cache_attribution) = compiled?;
600
601 let (result, execute_cache_attribution, execute_phase_attribution) =
604 self.execute_compiled_sql_with_phase_attribution::<E>(&compiled)?;
605 let execute_local_instructions = execute_phase_attribution
606 .planner_local_instructions
607 .saturating_add(execute_phase_attribution.executor_local_instructions);
608 let cache_attribution = compile_cache_attribution.merge(execute_cache_attribution);
609 let total_local_instructions =
610 compile_local_instructions.saturating_add(execute_local_instructions);
611
612 Ok((
613 result,
614 SqlQueryExecutionAttribution {
615 compile_local_instructions,
616 planner_local_instructions: execute_phase_attribution.planner_local_instructions,
617 executor_local_instructions: execute_phase_attribution.executor_local_instructions,
618 execute_local_instructions,
619 total_local_instructions,
620 sql_compiled_command_cache_hits: cache_attribution.sql_compiled_command_cache_hits,
621 sql_compiled_command_cache_misses: cache_attribution
622 .sql_compiled_command_cache_misses,
623 sql_select_plan_cache_hits: cache_attribution.sql_select_plan_cache_hits,
624 sql_select_plan_cache_misses: cache_attribution.sql_select_plan_cache_misses,
625 shared_query_plan_cache_hits: cache_attribution.shared_query_plan_cache_hits,
626 shared_query_plan_cache_misses: cache_attribution.shared_query_plan_cache_misses,
627 },
628 ))
629 }
630
631 pub fn execute_sql_update<E>(&self, sql: &str) -> Result<SqlStatementResult, QueryError>
636 where
637 E: PersistedRow<Canister = C> + EntityValue,
638 {
639 let compiled = self.compile_sql_update::<E>(sql)?;
640
641 self.execute_compiled_sql::<E>(&compiled)
642 }
643
644 #[cfg(test)]
645 pub(in crate::db) fn execute_grouped_sql_query_for_tests<E>(
646 &self,
647 sql: &str,
648 cursor_token: Option<&str>,
649 ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
650 where
651 E: PersistedRow<Canister = C> + EntityValue,
652 {
653 let parsed = parse_sql_statement(sql)?;
654
655 let lowered = lower_sql_command_from_prepared_statement(
656 prepare_sql_statement(parsed, E::MODEL.name())
657 .map_err(QueryError::from_sql_lowering_error)?,
658 E::MODEL.primary_key.name,
659 )
660 .map_err(QueryError::from_sql_lowering_error)?;
661 let Some(query) = lowered.query().cloned() else {
662 return Err(QueryError::unsupported_query(
663 "grouped SELECT helper requires grouped SELECT",
664 ));
665 };
666 let query = bind_lowered_sql_query::<E>(query, MissingRowPolicy::Ignore)
667 .map_err(QueryError::from_sql_lowering_error)?;
668 if !query.has_grouping() {
669 return Err(QueryError::unsupported_query(
670 "grouped SELECT helper requires grouped SELECT",
671 ));
672 }
673
674 self.execute_grouped(&query, cursor_token)
675 }
676
677 pub(in crate::db) fn compile_sql_query<E>(
680 &self,
681 sql: &str,
682 ) -> Result<CompiledSqlCommand, QueryError>
683 where
684 E: PersistedRow<Canister = C> + EntityValue,
685 {
686 self.compile_sql_query_with_cache_attribution::<E>(sql)
687 .map(|(compiled, _)| compiled)
688 }
689
690 fn compile_sql_query_with_cache_attribution<E>(
691 &self,
692 sql: &str,
693 ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
694 where
695 E: PersistedRow<Canister = C> + EntityValue,
696 {
697 self.compile_sql_statement_with_cache::<E>(
698 SqlCompiledCommandCacheKey::query_for_entity::<E>(sql),
699 sql,
700 Self::ensure_sql_query_statement_supported,
701 )
702 }
703
704 pub(in crate::db) fn compile_sql_update<E>(
707 &self,
708 sql: &str,
709 ) -> Result<CompiledSqlCommand, QueryError>
710 where
711 E: PersistedRow<Canister = C> + EntityValue,
712 {
713 self.compile_sql_update_with_cache_attribution::<E>(sql)
714 .map(|(compiled, _)| compiled)
715 }
716
717 fn compile_sql_update_with_cache_attribution<E>(
718 &self,
719 sql: &str,
720 ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
721 where
722 E: PersistedRow<Canister = C> + EntityValue,
723 {
724 self.compile_sql_statement_with_cache::<E>(
725 SqlCompiledCommandCacheKey::update_for_entity::<E>(sql),
726 sql,
727 Self::ensure_sql_update_statement_supported,
728 )
729 }
730
731 fn compile_sql_statement_with_cache<E>(
734 &self,
735 cache_key: SqlCompiledCommandCacheKey,
736 sql: &str,
737 ensure_surface_supported: fn(&SqlStatement) -> Result<(), QueryError>,
738 ) -> Result<(CompiledSqlCommand, SqlCacheAttribution), QueryError>
739 where
740 E: PersistedRow<Canister = C> + EntityValue,
741 {
742 {
743 let cached =
744 self.with_sql_compiled_command_cache(|cache| cache.get(&cache_key).cloned());
745 if let Some(compiled) = cached {
746 return Ok((
747 compiled,
748 SqlCacheAttribution::sql_compiled_command_cache_hit(),
749 ));
750 }
751 }
752
753 let parsed = parse_sql_statement(sql)?;
754 ensure_surface_supported(&parsed)?;
755 let mut compiled = Self::compile_sql_statement_inner::<E>(&parsed)?;
756 if let CompiledSqlCommand::Select {
757 compiled_cache_key, ..
758 } = &mut compiled
759 {
760 *compiled_cache_key = Some(cache_key.clone());
761 }
762
763 self.with_sql_compiled_command_cache(|cache| {
764 cache.insert(cache_key, compiled.clone());
765 });
766
767 Ok((
768 compiled,
769 SqlCacheAttribution::sql_compiled_command_cache_miss(),
770 ))
771 }
772
773 pub(in crate::db) fn compile_sql_statement_inner<E>(
776 sql_statement: &SqlStatement,
777 ) -> Result<CompiledSqlCommand, QueryError>
778 where
779 E: PersistedRow<Canister = C> + EntityValue,
780 {
781 Self::compile_sql_statement_for_authority(sql_statement, EntityAuthority::for_type::<E>())
782 }
783}