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