1#[cfg(feature = "perf-attribution")]
8use crate::db::executor::ScalarExecutePhaseAttribution;
9use crate::{
10 db::{
11 DbSession, EntityResponse, LoadQueryResult, PagedGroupedExecutionWithTrace,
12 PagedLoadExecutionWithTrace, PersistedRow, Query, QueryError, QueryTracePlan,
13 access::AccessStrategy,
14 commit::CommitSchemaFingerprint,
15 cursor::{
16 CursorPlanError, decode_optional_cursor_token, decode_optional_grouped_cursor_token,
17 },
18 diagnostics::ExecutionTrace,
19 executor::{
20 ExecutionFamily, GroupedCursorPage, LoadExecutor, PreparedExecutionPlan,
21 SharedPreparedExecutionPlan,
22 },
23 query::builder::{
24 PreparedFluentAggregateExplainStrategy, PreparedFluentProjectionStrategy,
25 },
26 query::explain::{
27 ExplainAggregateTerminalPlan, ExplainExecutionNodeDescriptor, ExplainPlan,
28 },
29 query::{
30 intent::{CompiledQuery, PlannedQuery, StructuralQuery},
31 plan::{AccessPlannedQuery, QueryMode, VisibleIndexes},
32 },
33 },
34 error::InternalError,
35 model::entity::EntityModel,
36 traits::{CanisterKind, EntityKind, EntityValue, Path},
37};
38#[cfg(feature = "perf-attribution")]
39use candid::CandidType;
40use icydb_utils::Xxh3;
41#[cfg(feature = "perf-attribution")]
42use serde::Deserialize;
43use std::{cell::RefCell, collections::HashMap, hash::BuildHasherDefault};
44
45type CacheBuildHasher = BuildHasherDefault<Xxh3>;
46
47const SHARED_QUERY_PLAN_CACHE_METHOD_VERSION: u8 = 1;
50
51#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
52pub(in crate::db) enum QueryPlanVisibility {
53 StoreNotReady,
54 StoreReady,
55}
56
57#[derive(Clone, Debug, Eq, Hash, PartialEq)]
58pub(in crate::db) struct QueryPlanCacheKey {
59 cache_method_version: u8,
60 entity_path: &'static str,
61 schema_fingerprint: CommitSchemaFingerprint,
62 visibility: QueryPlanVisibility,
63 structural_query: crate::db::query::intent::StructuralQueryCacheKey,
64}
65
66#[derive(Clone, Debug)]
67pub(in crate::db) struct QueryPlanCacheEntry {
68 logical_plan: AccessPlannedQuery,
69 prepared_plan: SharedPreparedExecutionPlan,
70}
71
72impl QueryPlanCacheEntry {
73 #[must_use]
74 pub(in crate::db) const fn new(
75 logical_plan: AccessPlannedQuery,
76 prepared_plan: SharedPreparedExecutionPlan,
77 ) -> Self {
78 Self {
79 logical_plan,
80 prepared_plan,
81 }
82 }
83
84 #[must_use]
85 pub(in crate::db) const fn logical_plan(&self) -> &AccessPlannedQuery {
86 &self.logical_plan
87 }
88
89 #[must_use]
90 pub(in crate::db) fn typed_prepared_plan<E: EntityKind>(&self) -> PreparedExecutionPlan<E> {
91 self.prepared_plan.typed_clone::<E>()
92 }
93}
94
95pub(in crate::db) type QueryPlanCache =
96 HashMap<QueryPlanCacheKey, QueryPlanCacheEntry, CacheBuildHasher>;
97
98thread_local! {
99 static QUERY_PLAN_CACHES: RefCell<HashMap<usize, QueryPlanCache, CacheBuildHasher>> =
104 RefCell::new(HashMap::default());
105}
106
107#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
108pub(in crate::db) struct QueryPlanCacheAttribution {
109 pub hits: u64,
110 pub misses: u64,
111}
112
113impl QueryPlanCacheAttribution {
114 #[must_use]
115 const fn hit() -> Self {
116 Self { hits: 1, misses: 0 }
117 }
118
119 #[must_use]
120 const fn miss() -> Self {
121 Self { hits: 0, misses: 1 }
122 }
123}
124
125#[cfg(feature = "perf-attribution")]
132#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
133pub struct QueryExecutionAttribution {
134 pub compile_local_instructions: u64,
135 pub runtime_local_instructions: u64,
136 pub finalize_local_instructions: u64,
137 pub direct_data_row_scan_local_instructions: u64,
138 pub direct_data_row_order_window_local_instructions: u64,
139 pub direct_data_row_page_window_local_instructions: u64,
140 pub response_decode_local_instructions: u64,
141 pub execute_local_instructions: u64,
142 pub total_local_instructions: u64,
143 pub shared_query_plan_cache_hits: u64,
144 pub shared_query_plan_cache_misses: u64,
145}
146
147#[cfg(feature = "perf-attribution")]
148#[expect(
149 clippy::missing_const_for_fn,
150 reason = "the wasm32 branch reads the runtime performance counter and cannot be const"
151)]
152fn read_query_local_instruction_counter() -> u64 {
153 #[cfg(target_arch = "wasm32")]
154 {
155 canic_cdk::api::performance_counter(1)
156 }
157
158 #[cfg(not(target_arch = "wasm32"))]
159 {
160 0
161 }
162}
163
164#[cfg(feature = "perf-attribution")]
165fn measure_query_stage<T, E>(run: impl FnOnce() -> Result<T, E>) -> (u64, Result<T, E>) {
166 let start = read_query_local_instruction_counter();
167 let result = run();
168 let delta = read_query_local_instruction_counter().saturating_sub(start);
169
170 (delta, result)
171}
172
173impl<C: CanisterKind> DbSession<C> {
174 #[cfg(feature = "perf-attribution")]
175 const fn empty_scalar_execute_phase_attribution() -> ScalarExecutePhaseAttribution {
176 ScalarExecutePhaseAttribution {
177 runtime_local_instructions: 0,
178 finalize_local_instructions: 0,
179 direct_data_row_scan_local_instructions: 0,
180 direct_data_row_order_window_local_instructions: 0,
181 direct_data_row_page_window_local_instructions: 0,
182 }
183 }
184
185 fn query_plan_cache_scope_id(&self) -> usize {
186 self.db.cache_scope_id()
187 }
188
189 fn with_query_plan_cache<R>(&self, f: impl FnOnce(&mut QueryPlanCache) -> R) -> R {
190 let scope_id = self.query_plan_cache_scope_id();
191
192 QUERY_PLAN_CACHES.with(|caches| {
193 let mut caches = caches.borrow_mut();
194 let cache = caches.entry(scope_id).or_default();
195
196 f(cache)
197 })
198 }
199
200 const fn visible_indexes_for_model(
201 model: &'static EntityModel,
202 visibility: QueryPlanVisibility,
203 ) -> VisibleIndexes<'static> {
204 match visibility {
205 QueryPlanVisibility::StoreReady => VisibleIndexes::planner_visible(model.indexes()),
206 QueryPlanVisibility::StoreNotReady => VisibleIndexes::none(),
207 }
208 }
209
210 #[cfg(test)]
211 pub(in crate::db) fn query_plan_cache_len(&self) -> usize {
212 self.with_query_plan_cache(|cache| cache.len())
213 }
214
215 #[cfg(test)]
216 pub(in crate::db) fn clear_query_plan_cache_for_tests(&self) {
217 self.with_query_plan_cache(QueryPlanCache::clear);
218 }
219
220 pub(in crate::db) fn query_plan_visibility_for_store_path(
221 &self,
222 store_path: &'static str,
223 ) -> Result<QueryPlanVisibility, QueryError> {
224 let store = self
225 .db
226 .recovered_store(store_path)
227 .map_err(QueryError::execute)?;
228 let visibility = if store.index_state() == crate::db::IndexState::Ready {
229 QueryPlanVisibility::StoreReady
230 } else {
231 QueryPlanVisibility::StoreNotReady
232 };
233
234 Ok(visibility)
235 }
236
237 pub(in crate::db) fn cached_query_plan_entry_for_authority(
238 &self,
239 authority: crate::db::executor::EntityAuthority,
240 schema_fingerprint: CommitSchemaFingerprint,
241 query: &StructuralQuery,
242 ) -> Result<(QueryPlanCacheEntry, QueryPlanCacheAttribution), QueryError> {
243 let visibility = self.query_plan_visibility_for_store_path(authority.store_path())?;
244 let cache_key =
245 QueryPlanCacheKey::for_authority(authority, schema_fingerprint, visibility, query);
246
247 {
248 let cached = self.with_query_plan_cache(|cache| cache.get(&cache_key).cloned());
249 if let Some(entry) = cached {
250 return Ok((entry, QueryPlanCacheAttribution::hit()));
251 }
252 }
253
254 let visible_indexes = Self::visible_indexes_for_model(authority.model(), visibility);
255 let plan = query.build_plan_with_visible_indexes(&visible_indexes)?;
256 let entry = QueryPlanCacheEntry::new(
257 plan.clone(),
258 SharedPreparedExecutionPlan::from_plan(authority, plan),
259 );
260 self.with_query_plan_cache(|cache| {
261 cache.insert(cache_key, entry.clone());
262 });
263
264 Ok((entry, QueryPlanCacheAttribution::miss()))
265 }
266
267 #[cfg(test)]
268 pub(in crate::db) fn query_plan_cache_key_for_tests(
269 authority: crate::db::executor::EntityAuthority,
270 schema_fingerprint: CommitSchemaFingerprint,
271 visibility: QueryPlanVisibility,
272 query: &StructuralQuery,
273 cache_method_version: u8,
274 ) -> QueryPlanCacheKey {
275 QueryPlanCacheKey::for_authority_with_method_version(
276 authority,
277 schema_fingerprint,
278 visibility,
279 query,
280 cache_method_version,
281 )
282 }
283
284 pub(in crate::db) fn cached_structural_plan_for_authority(
285 &self,
286 authority: crate::db::executor::EntityAuthority,
287 schema_fingerprint: CommitSchemaFingerprint,
288 query: &StructuralQuery,
289 ) -> Result<AccessPlannedQuery, QueryError> {
290 let (entry, _) =
291 self.cached_query_plan_entry_for_authority(authority, schema_fingerprint, query)?;
292
293 Ok(entry.logical_plan().clone())
294 }
295
296 fn with_query_visible_indexes<E, T>(
299 &self,
300 query: &Query<E>,
301 op: impl FnOnce(
302 &Query<E>,
303 &crate::db::query::plan::VisibleIndexes<'static>,
304 ) -> Result<T, QueryError>,
305 ) -> Result<T, QueryError>
306 where
307 E: EntityKind<Canister = C>,
308 {
309 let visibility = self.query_plan_visibility_for_store_path(E::Store::PATH)?;
310 let visible_indexes = Self::visible_indexes_for_model(E::MODEL, visibility);
311
312 op(query, &visible_indexes)
313 }
314
315 fn cached_structural_plan_for_entity<E>(
318 &self,
319 query: &StructuralQuery,
320 ) -> Result<AccessPlannedQuery, QueryError>
321 where
322 E: EntityKind<Canister = C>,
323 {
324 self.cached_structural_plan_for_authority(
325 crate::db::executor::EntityAuthority::for_type::<E>(),
326 crate::db::schema::commit_schema_fingerprint_for_entity::<E>(),
327 query,
328 )
329 }
330
331 pub(in crate::db::session) fn cached_prepared_query_plan_for_entity<E>(
332 &self,
333 query: &StructuralQuery,
334 ) -> Result<(PreparedExecutionPlan<E>, QueryPlanCacheAttribution), QueryError>
335 where
336 E: EntityKind<Canister = C>,
337 {
338 let (entry, attribution) = self.cached_query_plan_entry_for_authority(
339 crate::db::executor::EntityAuthority::for_type::<E>(),
340 crate::db::schema::commit_schema_fingerprint_for_entity::<E>(),
341 query,
342 )?;
343
344 Ok((entry.typed_prepared_plan::<E>(), attribution))
345 }
346
347 pub(in crate::db) fn compile_query_with_visible_indexes<E>(
350 &self,
351 query: &Query<E>,
352 ) -> Result<CompiledQuery<E>, QueryError>
353 where
354 E: EntityKind<Canister = C>,
355 {
356 let plan = self.cached_structural_plan_for_entity::<E>(query.structural())?;
357
358 Ok(Query::<E>::compiled_query_from_plan(plan))
359 }
360
361 pub(in crate::db) fn planned_query_with_visible_indexes<E>(
364 &self,
365 query: &Query<E>,
366 ) -> Result<PlannedQuery<E>, QueryError>
367 where
368 E: EntityKind<Canister = C>,
369 {
370 let plan = self.cached_structural_plan_for_entity::<E>(query.structural())?;
371
372 Ok(Query::<E>::planned_query_from_plan(plan))
373 }
374
375 pub(in crate::db) fn explain_query_with_visible_indexes<E>(
377 &self,
378 query: &Query<E>,
379 ) -> Result<ExplainPlan, QueryError>
380 where
381 E: EntityKind<Canister = C>,
382 {
383 self.with_query_visible_indexes(query, |query, visible_indexes| {
384 query.explain_with_visible_indexes(visible_indexes)
385 })
386 }
387
388 pub(in crate::db) fn query_plan_hash_hex_with_visible_indexes<E>(
391 &self,
392 query: &Query<E>,
393 ) -> Result<String, QueryError>
394 where
395 E: EntityKind<Canister = C>,
396 {
397 self.with_query_visible_indexes(query, |query, visible_indexes| {
398 query.plan_hash_hex_with_visible_indexes(visible_indexes)
399 })
400 }
401
402 pub(in crate::db) fn explain_query_execution_with_visible_indexes<E>(
405 &self,
406 query: &Query<E>,
407 ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
408 where
409 E: EntityValue + EntityKind<Canister = C>,
410 {
411 self.with_query_visible_indexes(query, |query, visible_indexes| {
412 query.explain_execution_with_visible_indexes(visible_indexes)
413 })
414 }
415
416 pub(in crate::db) fn explain_query_execution_text_with_visible_indexes<E>(
419 &self,
420 query: &Query<E>,
421 ) -> Result<String, QueryError>
422 where
423 E: EntityValue + EntityKind<Canister = C>,
424 {
425 self.with_query_visible_indexes(query, |query, visible_indexes| {
426 query.explain_execution_text_with_visible_indexes(visible_indexes)
427 })
428 }
429
430 pub(in crate::db) fn explain_query_execution_json_with_visible_indexes<E>(
433 &self,
434 query: &Query<E>,
435 ) -> Result<String, QueryError>
436 where
437 E: EntityValue + EntityKind<Canister = C>,
438 {
439 self.with_query_visible_indexes(query, |query, visible_indexes| {
440 query.explain_execution_json_with_visible_indexes(visible_indexes)
441 })
442 }
443
444 pub(in crate::db) fn explain_query_execution_verbose_with_visible_indexes<E>(
447 &self,
448 query: &Query<E>,
449 ) -> Result<String, QueryError>
450 where
451 E: EntityValue + EntityKind<Canister = C>,
452 {
453 self.with_query_visible_indexes(query, |query, visible_indexes| {
454 query.explain_execution_verbose_with_visible_indexes(visible_indexes)
455 })
456 }
457
458 pub(in crate::db) fn explain_query_prepared_aggregate_terminal_with_visible_indexes<E, S>(
461 &self,
462 query: &Query<E>,
463 strategy: &S,
464 ) -> Result<ExplainAggregateTerminalPlan, QueryError>
465 where
466 E: EntityValue + EntityKind<Canister = C>,
467 S: PreparedFluentAggregateExplainStrategy,
468 {
469 self.with_query_visible_indexes(query, |query, visible_indexes| {
470 query
471 .explain_prepared_aggregate_terminal_with_visible_indexes(visible_indexes, strategy)
472 })
473 }
474
475 pub(in crate::db) fn explain_query_bytes_by_with_visible_indexes<E>(
478 &self,
479 query: &Query<E>,
480 target_field: &str,
481 ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
482 where
483 E: EntityValue + EntityKind<Canister = C>,
484 {
485 self.with_query_visible_indexes(query, |query, visible_indexes| {
486 query.explain_bytes_by_with_visible_indexes(visible_indexes, target_field)
487 })
488 }
489
490 pub(in crate::db) fn explain_query_prepared_projection_terminal_with_visible_indexes<E>(
493 &self,
494 query: &Query<E>,
495 strategy: &PreparedFluentProjectionStrategy,
496 ) -> Result<ExplainExecutionNodeDescriptor, QueryError>
497 where
498 E: EntityValue + EntityKind<Canister = C>,
499 {
500 self.with_query_visible_indexes(query, |query, visible_indexes| {
501 query.explain_prepared_projection_terminal_with_visible_indexes(
502 visible_indexes,
503 strategy,
504 )
505 })
506 }
507
508 fn ensure_scalar_paged_execution_family(family: ExecutionFamily) -> Result<(), QueryError> {
511 match family {
512 ExecutionFamily::PrimaryKey => Err(QueryError::invariant(
513 CursorPlanError::cursor_requires_explicit_or_grouped_ordering_message(),
514 )),
515 ExecutionFamily::Ordered => Ok(()),
516 ExecutionFamily::Grouped => Err(QueryError::invariant(
517 "grouped queries execute via execute(), not page().execute()",
518 )),
519 }
520 }
521
522 fn ensure_grouped_execution_family(family: ExecutionFamily) -> Result<(), QueryError> {
525 match family {
526 ExecutionFamily::Grouped => Ok(()),
527 ExecutionFamily::PrimaryKey | ExecutionFamily::Ordered => Err(QueryError::invariant(
528 "grouped execution requires grouped logical plans",
529 )),
530 }
531 }
532
533 pub fn execute_query<E>(&self, query: &Query<E>) -> Result<EntityResponse<E>, QueryError>
535 where
536 E: PersistedRow<Canister = C> + EntityValue,
537 {
538 let mode = query.mode();
540 let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query.structural())?;
541
542 self.execute_query_dyn(mode, plan)
544 }
545
546 #[cfg(feature = "perf-attribution")]
549 #[doc(hidden)]
550 pub fn execute_query_result_with_attribution<E>(
551 &self,
552 query: &Query<E>,
553 ) -> Result<(LoadQueryResult<E>, QueryExecutionAttribution), QueryError>
554 where
555 E: PersistedRow<Canister = C> + EntityValue,
556 {
557 let (compile_local_instructions, plan_and_cache) = measure_query_stage(|| {
562 self.cached_prepared_query_plan_for_entity::<E>(query.structural())
563 });
564 let (plan, cache_attribution) = plan_and_cache?;
565
566 let (execute_local_instructions, result) = measure_query_stage(|| {
569 if query.has_grouping() {
570 self.execute_grouped_plan_with_trace(plan, None)
571 .map(|(page, trace)| {
572 let next_cursor = page
573 .next_cursor
574 .map(|token| {
575 let Some(token) = token.as_grouped() else {
576 return Err(
577 QueryError::grouped_paged_emitted_scalar_continuation(),
578 );
579 };
580
581 token.encode().map_err(|err| {
582 QueryError::serialize_internal(format!(
583 "failed to serialize grouped continuation cursor: {err}"
584 ))
585 })
586 })
587 .transpose()?;
588
589 Ok::<(LoadQueryResult<E>, ScalarExecutePhaseAttribution, u64), QueryError>(
590 (
591 LoadQueryResult::Grouped(PagedGroupedExecutionWithTrace::new(
592 page.rows,
593 next_cursor,
594 trace,
595 )),
596 Self::empty_scalar_execute_phase_attribution(),
597 0,
598 ),
599 )
600 })?
601 } else {
602 match query.mode() {
603 QueryMode::Load(_) => {
604 let (rows, phase_attribution, response_decode_local_instructions) = self
605 .load_executor::<E>()
606 .execute_with_phase_attribution(plan)
607 .map_err(QueryError::execute)?;
608
609 Ok::<(LoadQueryResult<E>, ScalarExecutePhaseAttribution, u64), QueryError>(
610 (
611 LoadQueryResult::Rows(rows),
612 phase_attribution,
613 response_decode_local_instructions,
614 ),
615 )
616 }
617 QueryMode::Delete(_) => {
618 let result = self.execute_query_dyn(query.mode(), plan)?;
619
620 Ok((
621 LoadQueryResult::Rows(result),
622 Self::empty_scalar_execute_phase_attribution(),
623 0,
624 ))
625 }
626 }
627 }
628 });
629 let (result, execute_phase_attribution, response_decode_local_instructions) = result?;
630 let total_local_instructions =
631 compile_local_instructions.saturating_add(execute_local_instructions);
632
633 Ok((
634 result,
635 QueryExecutionAttribution {
636 compile_local_instructions,
637 runtime_local_instructions: execute_phase_attribution.runtime_local_instructions,
638 finalize_local_instructions: execute_phase_attribution.finalize_local_instructions,
639 direct_data_row_scan_local_instructions: execute_phase_attribution
640 .direct_data_row_scan_local_instructions,
641 direct_data_row_order_window_local_instructions: execute_phase_attribution
642 .direct_data_row_order_window_local_instructions,
643 direct_data_row_page_window_local_instructions: execute_phase_attribution
644 .direct_data_row_page_window_local_instructions,
645 response_decode_local_instructions,
646 execute_local_instructions,
647 total_local_instructions,
648 shared_query_plan_cache_hits: cache_attribution.hits,
649 shared_query_plan_cache_misses: cache_attribution.misses,
650 },
651 ))
652 }
653
654 #[doc(hidden)]
657 pub fn execute_query_result<E>(
658 &self,
659 query: &Query<E>,
660 ) -> Result<LoadQueryResult<E>, QueryError>
661 where
662 E: PersistedRow<Canister = C> + EntityValue,
663 {
664 if query.has_grouping() {
665 return self
666 .execute_grouped(query, None)
667 .map(LoadQueryResult::Grouped);
668 }
669
670 self.execute_query(query).map(LoadQueryResult::Rows)
671 }
672
673 #[doc(hidden)]
675 pub fn execute_delete_count<E>(&self, query: &Query<E>) -> Result<u32, QueryError>
676 where
677 E: PersistedRow<Canister = C> + EntityValue,
678 {
679 if !query.mode().is_delete() {
681 return Err(QueryError::unsupported_query(
682 "delete count execution requires delete query mode",
683 ));
684 }
685
686 let plan = self
688 .compile_query_with_visible_indexes(query)?
689 .into_prepared_execution_plan();
690
691 self.with_metrics(|| self.delete_executor::<E>().execute_count(plan))
693 .map_err(QueryError::execute)
694 }
695
696 pub(in crate::db) fn execute_query_dyn<E>(
701 &self,
702 mode: QueryMode,
703 plan: PreparedExecutionPlan<E>,
704 ) -> Result<EntityResponse<E>, QueryError>
705 where
706 E: PersistedRow<Canister = C> + EntityValue,
707 {
708 let result = match mode {
709 QueryMode::Load(_) => self.with_metrics(|| self.load_executor::<E>().execute(plan)),
710 QueryMode::Delete(_) => self.with_metrics(|| self.delete_executor::<E>().execute(plan)),
711 };
712
713 result.map_err(QueryError::execute)
714 }
715
716 pub(in crate::db) fn execute_load_query_with<E, T>(
719 &self,
720 query: &Query<E>,
721 op: impl FnOnce(LoadExecutor<E>, PreparedExecutionPlan<E>) -> Result<T, InternalError>,
722 ) -> Result<T, QueryError>
723 where
724 E: PersistedRow<Canister = C> + EntityValue,
725 {
726 let (plan, _) = self.cached_prepared_query_plan_for_entity::<E>(query.structural())?;
727
728 self.with_metrics(|| op(self.load_executor::<E>(), plan))
729 .map_err(QueryError::execute)
730 }
731
732 pub fn trace_query<E>(&self, query: &Query<E>) -> Result<QueryTracePlan, QueryError>
737 where
738 E: EntityKind<Canister = C>,
739 {
740 let compiled = self.compile_query_with_visible_indexes(query)?;
741 let explain = compiled.explain();
742 let plan_hash = compiled.plan_hash_hex();
743
744 let (executable, _) =
745 self.cached_prepared_query_plan_for_entity::<E>(query.structural())?;
746 let access_strategy = AccessStrategy::from_plan(executable.access()).debug_summary();
747 let execution_family = match query.mode() {
748 QueryMode::Load(_) => Some(executable.execution_family().map_err(QueryError::execute)?),
749 QueryMode::Delete(_) => None,
750 };
751
752 Ok(QueryTracePlan::new(
753 plan_hash,
754 access_strategy,
755 execution_family,
756 explain,
757 ))
758 }
759
760 pub(crate) fn execute_load_query_paged_with_trace<E>(
762 &self,
763 query: &Query<E>,
764 cursor_token: Option<&str>,
765 ) -> Result<PagedLoadExecutionWithTrace<E>, QueryError>
766 where
767 E: PersistedRow<Canister = C> + EntityValue,
768 {
769 let plan = self
771 .cached_prepared_query_plan_for_entity::<E>(query.structural())?
772 .0;
773 Self::ensure_scalar_paged_execution_family(
774 plan.execution_family().map_err(QueryError::execute)?,
775 )?;
776
777 let cursor_bytes = decode_optional_cursor_token(cursor_token)
779 .map_err(QueryError::from_cursor_plan_error)?;
780 let cursor = plan
781 .prepare_cursor(cursor_bytes.as_deref())
782 .map_err(QueryError::from_executor_plan_error)?;
783
784 let (page, trace) = self
786 .with_metrics(|| {
787 self.load_executor::<E>()
788 .execute_paged_with_cursor_traced(plan, cursor)
789 })
790 .map_err(QueryError::execute)?;
791 let next_cursor = page
792 .next_cursor
793 .map(|token| {
794 let Some(token) = token.as_scalar() else {
795 return Err(QueryError::scalar_paged_emitted_grouped_continuation());
796 };
797
798 token.encode().map_err(|err| {
799 QueryError::serialize_internal(format!(
800 "failed to serialize continuation cursor: {err}"
801 ))
802 })
803 })
804 .transpose()?;
805
806 Ok(PagedLoadExecutionWithTrace::new(
807 page.items,
808 next_cursor,
809 trace,
810 ))
811 }
812
813 pub(in crate::db) fn execute_grouped<E>(
818 &self,
819 query: &Query<E>,
820 cursor_token: Option<&str>,
821 ) -> Result<PagedGroupedExecutionWithTrace, QueryError>
822 where
823 E: PersistedRow<Canister = C> + EntityValue,
824 {
825 let (page, trace) = self.execute_grouped_page_with_trace(query, cursor_token)?;
826 let next_cursor = page
827 .next_cursor
828 .map(|token| {
829 let Some(token) = token.as_grouped() else {
830 return Err(QueryError::grouped_paged_emitted_scalar_continuation());
831 };
832
833 token.encode().map_err(|err| {
834 QueryError::serialize_internal(format!(
835 "failed to serialize grouped continuation cursor: {err}"
836 ))
837 })
838 })
839 .transpose()?;
840
841 Ok(PagedGroupedExecutionWithTrace::new(
842 page.rows,
843 next_cursor,
844 trace,
845 ))
846 }
847
848 fn execute_grouped_page_with_trace<E>(
851 &self,
852 query: &Query<E>,
853 cursor_token: Option<&str>,
854 ) -> Result<(GroupedCursorPage, Option<ExecutionTrace>), QueryError>
855 where
856 E: PersistedRow<Canister = C> + EntityValue,
857 {
858 let plan = self
860 .cached_prepared_query_plan_for_entity::<E>(query.structural())?
861 .0;
862
863 self.execute_grouped_plan_with_trace(plan, cursor_token)
865 }
866
867 fn execute_grouped_plan_with_trace<E>(
869 &self,
870 plan: PreparedExecutionPlan<E>,
871 cursor_token: Option<&str>,
872 ) -> Result<(GroupedCursorPage, Option<ExecutionTrace>), QueryError>
873 where
874 E: PersistedRow<Canister = C> + EntityValue,
875 {
876 Self::ensure_grouped_execution_family(
878 plan.execution_family().map_err(QueryError::execute)?,
879 )?;
880
881 let cursor = decode_optional_grouped_cursor_token(cursor_token)
883 .map_err(QueryError::from_cursor_plan_error)?;
884 let cursor = plan
885 .prepare_grouped_cursor_token(cursor)
886 .map_err(QueryError::from_executor_plan_error)?;
887
888 self.with_metrics(|| {
891 self.load_executor::<E>()
892 .execute_grouped_paged_with_cursor_traced(plan, cursor)
893 })
894 .map_err(QueryError::execute)
895 }
896}
897
898impl QueryPlanCacheKey {
899 fn for_authority(
900 authority: crate::db::executor::EntityAuthority,
901 schema_fingerprint: CommitSchemaFingerprint,
902 visibility: QueryPlanVisibility,
903 query: &StructuralQuery,
904 ) -> Self {
905 Self::for_authority_with_method_version(
906 authority,
907 schema_fingerprint,
908 visibility,
909 query,
910 SHARED_QUERY_PLAN_CACHE_METHOD_VERSION,
911 )
912 }
913
914 fn for_authority_with_method_version(
915 authority: crate::db::executor::EntityAuthority,
916 schema_fingerprint: CommitSchemaFingerprint,
917 visibility: QueryPlanVisibility,
918 query: &StructuralQuery,
919 cache_method_version: u8,
920 ) -> Self {
921 Self {
922 cache_method_version,
923 entity_path: authority.entity_path(),
924 schema_fingerprint,
925 visibility,
926 structural_query: query.structural_cache_key(),
927 }
928 }
929}