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