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