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