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