1use crate::{
7 db::{
8 DbSession, DynamicQuery, DynamicTypedEntityBinding, ExhaustiveQueryPageOutput,
9 ExhaustiveReadError, GroupedQueryOutput, LiveQueryPageOutput, MissingRowPolicy, QueryError,
10 ReadSetRevisionError, ReadSetRevisionProof, ScalarPageWork,
11 codec::{finalize_hash_sha256, new_hash_sha256_prefixed},
12 commit::{cursor_authentication_key, database_incarnation_id},
13 cursor::{
14 CursorBoundary, CursorBoundarySlot, CursorPlanError, ScalarOrderTermContract,
15 ScalarPageMode, ScalarPageToken, ScalarPageTokenAuthority, ScalarPageTokenProgress,
16 ScalarPageTokenWindow, decode_optional_cursor_token, encode_cursor,
17 },
18 data::{DecodedDataStoreKey, RawDataStoreKey},
19 executor::{
20 AcceptedSchemaCatalogContext, PageWorkEnvelope, ScalarContinuationContext,
21 StructuralProjectionRequest, execute_structural_projection_page,
22 },
23 query::{
24 admission::{
25 QueryAdmissionPolicy, QueryAdmissionSummary, input::validate_dynamic_query_input,
26 },
27 expr::{CompareOperator, FilterExpr, SetOperator},
28 intent::{IntentError, StructuralQuery},
29 plan::{
30 CardinalityTiebreakRoutePin, GroupAggregateSpec, OrderDirection, OrderSpec,
31 OrderTerm,
32 expr::{FieldId, ProjectionSelection},
33 },
34 preparation::PreparationWork,
35 },
36 },
37 traits::CanisterKind,
38};
39use icydb_diagnostic_code::{
40 DiagnosticDecodeReason, DiagnosticExecutionBudgetResource, DiagnosticExecutionLane,
41 QueryReadAdmissionCode,
42};
43use sha2::Digest;
44#[cfg(test)]
45use std::cell::Cell;
46
47#[cfg(not(test))]
48const SCALAR_PAGE_OUTPUT_ROWS: usize = 1_024;
49#[cfg(test)]
50const SCALAR_PAGE_OUTPUT_ROWS: usize = 2;
51#[cfg(test)]
52const SCALAR_PAGE_KEY_ENTRIES: u64 = 4;
53
54#[cfg(test)]
55std::thread_local! {
56 static SCALAR_PAGE_RESULT_BYTES_LIMIT_OVERRIDE: Cell<Option<u64>> = const { Cell::new(None) };
57}
58
59#[cfg(test)]
60struct ScalarPageResultBytesLimitGuard(Option<u64>);
61
62#[cfg(test)]
63impl Drop for ScalarPageResultBytesLimitGuard {
64 fn drop(&mut self) {
65 SCALAR_PAGE_RESULT_BYTES_LIMIT_OVERRIDE.with(|limit| limit.set(self.0));
66 }
67}
68
69#[derive(Clone, Copy)]
70enum DynamicReadLane {
71 Public,
72 Trusted,
73}
74
75impl DynamicReadLane {
76 const fn execution_lane(self) -> DiagnosticExecutionLane {
77 match self {
78 Self::Public => DiagnosticExecutionLane::PublicRead,
79 Self::Trusted => DiagnosticExecutionLane::TrustedRead,
80 }
81 }
82}
83
84struct ScalarCursorContract {
85 signature: crate::db::cursor::ContinuationSignature,
86 authority: ScalarPageTokenAuthority,
87 route_pin: Option<CardinalityTiebreakRoutePin>,
88 window: ScalarPageTokenWindow,
89 order_terms: Vec<ScalarOrderTermContract>,
90}
91
92impl<C: CanisterKind> DbSession<C> {
93 fn may_select_exact_single_primary_key(
94 request: &DynamicQuery,
95 catalog: &AcceptedSchemaCatalogContext,
96 ) -> bool {
97 let [primary_key] = catalog.accepted_schema_info().primary_key_names() else {
98 return false;
99 };
100 let field = match request.filter_expr() {
103 Some(FilterExpr::Compare {
104 operator: CompareOperator::Eq,
105 field,
106 ..
107 }) => field,
108 Some(FilterExpr::Set {
109 operator: SetOperator::In,
110 field,
111 values,
112 }) if values.len() == 1 => field,
113 _ => return false,
114 };
115 field.eq_ignore_ascii_case(primary_key)
116 }
117
118 fn exact_primary_key_candidate_bound(
119 prepared_plan: &crate::db::executor::SharedPreparedExecutionPlan,
120 ) -> Option<usize> {
121 let access = &prepared_plan.logical_plan().access;
122 if access.as_by_key_path().is_some() {
123 return Some(1);
124 }
125
126 access.as_by_keys_path().map(<[crate::value::Value]>::len)
127 }
128
129 pub(super) fn structural_query_from_dynamic_request(
130 &self,
131 request: &DynamicQuery,
132 catalog: &AcceptedSchemaCatalogContext,
133 lane: DiagnosticExecutionLane,
134 ) -> Result<StructuralQuery, QueryError> {
135 self.structural_query_from_dynamic_request_with_page_limit(
136 request, catalog, None, false, lane,
137 )
138 }
139
140 fn structural_query_from_dynamic_request_with_page_limit(
141 &self,
142 request: &DynamicQuery,
143 catalog: &AcceptedSchemaCatalogContext,
144 page_limit: Option<u32>,
145 require_total_order: bool,
146 lane: DiagnosticExecutionLane,
147 ) -> Result<StructuralQuery, QueryError> {
148 validate_dynamic_query_input(request)?;
149 Self::validate_dynamic_query_shape(request)?;
150 PreparationWork::run(self.db.request_execution_scope(), lane, |work| {
151 let schema = catalog.accepted_schema_info();
152 let mut query = StructuralQuery::new(MissingRowPolicy::Ignore);
153 if let Some(filter) = request.filter_expr() {
154 query = query.filter_for_schema(schema, filter, work)?;
155 }
156 let order_fields = if !request.order_terms().is_empty() {
159 work.copy_slice(request.order_terms(), |order| {
160 Ok(order.copy_for_preparation(work)?.lower())
161 })?
162 } else if require_total_order {
163 work.copy_slice(schema.primary_key_names(), |field| {
164 Ok(OrderTerm::field(
165 work.copy_text(field)?,
166 OrderDirection::Asc,
167 ))
168 })?
169 } else {
170 Vec::new()
171 };
172 if !order_fields.is_empty() {
173 query = query.order_spec(OrderSpec {
174 fields: order_fields,
175 });
176 }
177 if !request.selected_fields().is_empty() {
178 query = query.projection_selection(ProjectionSelection::Fields(
179 work.copy_slice(request.selected_fields(), |field| {
180 Ok(FieldId::new(work.copy_text(field)?))
181 })?,
182 ));
183 }
184 #[cfg(test)]
185 if request.projection_is_distinct() {
186 query = query.distinct();
187 }
188 if let Some(limit) = page_limit.or_else(|| request.row_limit()) {
189 query = query.limit(limit);
190 }
191 if !request.group_fields().is_empty() {
192 query = query.group_fields_with_schema(request.group_fields(), schema, work)?;
193 }
194 if !request.aggregates().is_empty() {
195 query =
196 query.group_aggregates(work.copy_slice(request.aggregates(), |aggregate| {
197 Ok(GroupAggregateSpec::from_aggregate_expr(
198 aggregate
199 .copy_for_preparation(work)
200 .map_err(QueryError::execute)?,
201 ))
202 })?);
203 }
204 if let Some((max_groups, max_group_bytes)) = request.grouped_execution_limits() {
205 if max_groups == 0 || max_group_bytes == 0 {
206 return Err(QueryReadAdmissionCode::GroupedQueryRequiresLimits.into());
207 }
208 query = query.grouped_limits(u64::from(max_groups), u64::from(max_group_bytes));
209 }
210
211 Ok(query)
212 })
213 }
214
215 fn validate_dynamic_query_shape(request: &DynamicQuery) -> Result<(), QueryError> {
218 if request.has_grouping() {
219 if request.grouped_execution_limits().is_none() {
220 return Err(QueryReadAdmissionCode::GroupedQueryRequiresLimits.into());
221 }
222 if !request.selected_fields().is_empty() {
223 return Err(QueryError::intent(
224 IntentError::grouped_output_defined_by_group_and_aggregates(),
225 ));
226 }
227 } else if request.grouped_execution_limits().is_some() {
228 return Err(QueryError::intent(
229 IntentError::scalar_terminal_requires_scalar_query(),
230 ));
231 }
232 Ok(())
233 }
234
235 #[doc(hidden)]
238 pub fn explain_query_for_typed_binding(
239 &self,
240 binding: &DynamicTypedEntityBinding,
241 request: &DynamicQuery,
242 ) -> Result<Option<crate::db::ExplainPlan>, QueryError> {
243 let Some(catalog) = self
244 .current_typed_entity_binding_catalog(binding)
245 .map_err(QueryError::execute)?
246 else {
247 return Ok(None);
248 };
249 if request.continuation_cursor().is_some() {
250 return Err(QueryReadAdmissionCode::ExplainDoesNotAcceptCursor.into());
251 }
252 let lane = DiagnosticExecutionLane::Diagnostic;
253 let query = self.structural_query_from_dynamic_request(request, &catalog, lane)?;
254 let plan = self.cached_shared_query_plan_for_accepted_authority_with_catalog(
255 catalog.accepted_entity_authority(),
256 &catalog,
257 &query,
258 lane,
259 )?;
260 PreparationWork::run(self.db.request_execution_scope(), lane, |work| {
261 plan.explain(work).map(Some)
262 })
263 }
264
265 fn scalar_page_cursor_error() -> QueryError {
266 QueryError::from_cursor_plan_error(CursorPlanError::invalid_continuation_cursor_payload(
267 DiagnosticDecodeReason::CursorTokenDecode,
268 ))
269 }
270
271 fn scalar_cursor_contract(
272 request: &DynamicQuery,
273 catalog: &AcceptedSchemaCatalogContext,
274 envelope: PageWorkEnvelope,
275 prepared_plan: &crate::db::executor::SharedPreparedExecutionPlan,
276 mode: ScalarPageMode,
277 proof: Option<&ReadSetRevisionProof>,
278 ) -> Result<ScalarCursorContract, QueryError> {
279 let mut signature = prepared_plan
280 .continuation_signature_for_runtime()
281 .map_err(QueryError::execute)?;
282 match (mode, proof) {
283 (ScalarPageMode::Live, None) => {}
284 (ScalarPageMode::Exhaustive, Some(proof)) => {
285 let mut hasher = new_hash_sha256_prefixed(b"icydb.exhaustive-cursor-proof.v1");
286 hasher.update(signature.into_bytes());
287 hasher.update(proof.signature_bytes());
288 signature = crate::db::cursor::ContinuationSignature::from_bytes(
289 finalize_hash_sha256(hasher),
290 );
291 }
292 _ => return Err(Self::scalar_page_cursor_error()),
293 }
294 let root_identity = catalog.runtime_root_identity();
295 let (root_fingerprint_method, root_fingerprint) = root_identity.fingerprint();
296 let authority = ScalarPageTokenAuthority::new(
297 database_incarnation_id()
298 .map_err(QueryError::execute)?
299 .to_bytes(),
300 root_identity.accepted_root_revision().get(),
301 root_fingerprint_method,
302 root_fingerprint,
303 catalog.fingerprint(),
304 prepared_plan.authority_ref().entity_tag(),
305 );
306 let window =
307 ScalarPageTokenWindow::new(0, request.row_limit(), envelope.profile_identity());
308 let canonical_order = prepared_plan
309 .logical_plan()
310 .scalar_plan()
311 .order
312 .as_ref()
313 .ok_or_else(Self::scalar_page_cursor_error)?;
314 let order_terms = canonical_order
315 .fields
316 .iter()
317 .map(|term| ScalarOrderTermContract::new(term.rendered_label(), term.direction()))
318 .collect::<Vec<_>>();
319
320 Ok(ScalarCursorContract {
321 signature,
322 authority,
323 route_pin: prepared_plan
324 .logical_plan()
325 .cardinality_tiebreak_route_pin(),
326 window,
327 order_terms,
328 })
329 }
330
331 fn validate_scalar_page_token(
332 token: &ScalarPageToken,
333 mode: ScalarPageMode,
334 contract: &ScalarCursorContract,
335 ) -> Result<(), QueryError> {
336 if token.signature() != contract.signature {
337 return Err(QueryError::from_cursor_plan_error(
338 CursorPlanError::continuation_cursor_signature_mismatch(
339 &contract.signature,
340 &token.signature(),
341 ),
342 ));
343 }
344 if token.mode() != mode
345 || token.authority() != contract.authority
346 || token.route_pin() != contract.route_pin
347 || token.window() != contract.window
348 || token.order_terms() != contract.order_terms.as_slice()
349 {
350 return Err(Self::scalar_page_cursor_error());
351 }
352
353 Ok(())
354 }
355
356 fn physical_primary_key_boundary(
357 bytes: &[u8],
358 catalog: &AcceptedSchemaCatalogContext,
359 ) -> Result<CursorBoundary, QueryError> {
360 let raw = RawDataStoreKey::from_persisted_bytes(bytes.to_vec());
361 let key = DecodedDataStoreKey::try_from_raw(&raw)
362 .map_err(|_| Self::scalar_page_cursor_error())?;
363 if key.entity_tag() != catalog.accepted_entity_authority().entity_tag() {
364 return Err(Self::scalar_page_cursor_error());
365 }
366 let primary_key_arity = catalog.accepted_schema_info().primary_key_names().len();
367 let mut slots = Vec::with_capacity(primary_key_arity);
368 for component_index in 0..primary_key_arity {
369 slots.push(CursorBoundarySlot::Present(
370 key.primary_key_component_runtime_value(component_index)
371 .map_err(|_| Self::scalar_page_cursor_error())?,
372 ));
373 }
374
375 Ok(CursorBoundary { slots })
376 }
377
378 fn execute_dynamic_grouped_query_against_catalog(
379 &self,
380 request: &DynamicQuery,
381 lane: DynamicReadLane,
382 catalog: AcceptedSchemaCatalogContext,
383 ) -> Result<GroupedQueryOutput, QueryError> {
384 if !request.has_grouping() {
385 return Err(QueryError::intent(
386 IntentError::grouped_terminal_requires_grouped_query(),
387 ));
388 }
389 let query =
390 self.structural_query_from_dynamic_request(request, &catalog, lane.execution_lane())?;
391 let public_admission = match lane {
392 DynamicReadLane::Public => Some(QueryAdmissionPolicy::default_bounded_read()),
393 DynamicReadLane::Trusted => None,
394 };
395
396 self.execute_structural_grouped_from_query(
397 &query,
398 &catalog,
399 public_admission.as_ref(),
400 request.continuation_cursor(),
401 )
402 }
403
404 #[expect(
405 clippy::too_many_lines,
406 reason = "live-page orchestration keeps planning, cursor validation, execution, and response proof in one auditable boundary"
407 )]
408 fn execute_scalar_page_against_catalog(
409 &self,
410 request: &DynamicQuery,
411 continuation: Option<&str>,
412 lane: DynamicReadLane,
413 catalog: AcceptedSchemaCatalogContext,
414 mode: ScalarPageMode,
415 supplied_proof: Option<&ReadSetRevisionProof>,
416 ) -> Result<(LiveQueryPageOutput, Option<ReadSetRevisionProof>), ExhaustiveReadError> {
417 if request.has_grouping()
418 || request.grouped_execution_limits().is_some()
419 || request.continuation_cursor().is_some()
420 {
421 return Err(
422 QueryError::intent(IntentError::scalar_terminal_requires_scalar_query()).into(),
423 );
424 }
425
426 let exhaustive_proof = match mode {
427 ScalarPageMode::Live => None,
428 ScalarPageMode::Exhaustive => {
429 if continuation.is_some() && supplied_proof.is_none() {
430 return Err(ReadSetRevisionError::ResumeProofRequired.into());
431 }
432 let proof = supplied_proof.cloned().map_or_else(
433 || self.capture_entity_read_set_revision_proof(catalog.identity().store_path()),
434 Ok,
435 )?;
436 Self::ensure_read_set_contains_store(&proof, catalog.identity().store_path())?;
437 if supplied_proof.is_some() {
440 self.verify_durable_read_set_revision_proof(&proof)?;
441 } else {
442 self.verify_read_set_revision_proof(&proof)?;
443 }
444 Some(proof)
445 }
446 };
447
448 let envelope = match lane {
449 DynamicReadLane::Public => PageWorkEnvelope::public_scalar(),
450 DynamicReadLane::Trusted => PageWorkEnvelope::default_scalar(),
451 };
452 #[cfg(test)]
453 let envelope = SCALAR_PAGE_RESULT_BYTES_LIMIT_OVERRIDE.with(|limit| {
454 limit.get().map_or(envelope, |limit| {
455 envelope.with_limit_for_tests(DiagnosticExecutionBudgetResource::ResultBytes, limit)
456 })
457 });
458 #[cfg(test)]
459 let envelope = envelope.with_limit_for_tests(
460 DiagnosticExecutionBudgetResource::KeyIndexEntriesVisited,
461 SCALAR_PAGE_KEY_ENTRIES,
462 );
463 let page_row_limit = envelope
464 .limit(DiagnosticExecutionBudgetResource::ResultRows)
465 .and_then(|limit| usize::try_from(limit).ok())
466 .unwrap_or(SCALAR_PAGE_OUTPUT_ROWS)
467 .min(SCALAR_PAGE_OUTPUT_ROWS);
468 let decoded_token = decode_optional_cursor_token(continuation)
469 .map_err(QueryError::from_cursor_plan_error)?
470 .map(|bytes| {
471 ScalarPageToken::decode(
472 bytes.as_slice(),
473 &cursor_authentication_key().map_err(QueryError::execute)?,
474 )
475 .map_err(|error| {
476 QueryError::from_cursor_plan_error(CursorPlanError::from_token_wire_error(
477 error,
478 ))
479 })
480 })
481 .transpose()?;
482 let prior_rows_emitted = decoded_token
483 .as_ref()
484 .map_or(0, |token| token.progress().rows_emitted());
485 let remaining_limit = request
486 .row_limit()
487 .map(|limit| u64::from(limit).saturating_sub(prior_rows_emitted));
488 let page_output_limit = remaining_limit
489 .unwrap_or(page_row_limit as u64)
490 .min(page_row_limit as u64);
491 let page_output_limit = usize::try_from(page_output_limit).unwrap_or(page_row_limit);
492 let execution_limit = request
496 .row_limit()
497 .unwrap_or(u32::MAX)
498 .min(u32::try_from(page_row_limit).unwrap_or(u32::MAX));
499 let execution_lane = lane.execution_lane();
500 let exact_candidate = decoded_token.is_none()
501 && page_output_limit > 0
502 && Self::may_select_exact_single_primary_key(request, &catalog);
503 let initial_plan = if exact_candidate {
504 let query =
505 self.structural_query_from_dynamic_request(request, &catalog, execution_lane)?;
506 Some(
507 self.structural_projection_prepared_plan_for_accepted_authority(
508 &query,
509 catalog.accepted_entity_authority(),
510 catalog.snapshot(),
511 execution_lane,
512 )?,
513 )
514 } else {
515 None
516 };
517 let initial_is_exact_exhaustion =
518 initial_plan.as_ref().is_some_and(|(prepared_plan, _)| {
519 Self::exact_primary_key_candidate_bound(prepared_plan)
520 .is_some_and(|bound| bound == 1 && bound <= page_output_limit)
521 });
522 let (prepared_plan, projection) = if initial_is_exact_exhaustion {
523 initial_plan.ok_or_else(Self::scalar_page_cursor_error)?
524 } else {
525 let query = self.structural_query_from_dynamic_request_with_page_limit(
526 request,
527 &catalog,
528 Some(execution_limit),
529 true,
530 execution_lane,
531 )?;
532 if let Some(route_pin) = decoded_token.as_ref().and_then(ScalarPageToken::route_pin) {
533 self.structural_projection_prepared_plan_for_accepted_authority_with_route_pin(
534 &query,
535 catalog.accepted_entity_authority(),
536 execution_lane,
537 route_pin,
538 )?
539 .ok_or_else(Self::scalar_page_cursor_error)?
540 } else {
541 self.structural_projection_prepared_plan_for_accepted_authority(
542 &query,
543 catalog.accepted_entity_authority(),
544 catalog.snapshot(),
545 execution_lane,
546 )?
547 }
548 };
549 if matches!(lane, DynamicReadLane::Public) {
550 let policy = QueryAdmissionPolicy::default_bounded_read();
551 let summary = policy.evaluate(
552 QueryAdmissionSummary::from_plan(policy.lane(), prepared_plan.logical_plan())
553 .map_err(QueryError::execute)?,
554 );
555 if let Some(rejection) = summary.rejection() {
556 return Err(QueryError::from(rejection.code()).into());
557 }
558 }
559 let exact_initial_exhaustion = initial_is_exact_exhaustion;
560 let cursor_contract = decoded_token
561 .as_ref()
562 .map(|token| {
563 let contract = Self::scalar_cursor_contract(
564 request,
565 &catalog,
566 envelope,
567 &prepared_plan,
568 mode,
569 exhaustive_proof.as_ref(),
570 )?;
571 Self::validate_scalar_page_token(token, mode, &contract)?;
572 Ok::<_, QueryError>(contract)
573 })
574 .transpose()?;
575 let deferred_cursor_plan =
576 (!exact_initial_exhaustion && decoded_token.is_none()).then(|| prepared_plan.clone());
577 let continuation_context = match decoded_token.as_ref() {
578 None => ScalarContinuationContext::initial(),
579 Some(token) if token.progress().unconsumed_lookahead().is_some() => {
580 return Err(Self::scalar_page_cursor_error().into());
581 }
582 Some(token) => {
583 let logical = token.progress().last_emitted_logical().cloned();
584 match token.progress().last_consumed_physical() {
585 Some(physical) => ScalarContinuationContext::resumed_with_primary_progress(
586 logical,
587 Self::physical_primary_key_boundary(physical, &catalog)?,
588 ),
589 None => logical.map_or_else(
590 ScalarContinuationContext::initial,
591 ScalarContinuationContext::resumed,
592 ),
593 }
594 }
595 };
596 if decoded_token.is_some() && !continuation_context.has_progress() {
597 return Err(Self::scalar_page_cursor_error().into());
598 }
599
600 let value_catalog = prepared_plan
601 .authority_ref()
602 .accepted_schema_info()
603 .value_catalog_handle()
604 .clone();
605 let (columns, _fixed_scales) = projection.into_components();
606 let projection_request = StructuralProjectionRequest::new(prepared_plan, execution_lane)
607 .with_distinct_output_offset(usize::try_from(prior_rows_emitted).unwrap_or(usize::MAX))
608 .with_page_work_envelope(envelope);
609 let projection_request = if exact_initial_exhaustion {
610 projection_request
611 } else {
612 projection_request
613 .with_continuation(continuation_context)
614 .with_cursor_emission(page_output_limit)
615 };
616 let page = execute_structural_projection_page(&self.db, projection_request)
617 .map_err(QueryError::execute)?;
618 let row_count = page.rows.row_count();
619 let rows = page
620 .rows
621 .into_value_rows()
622 .into_iter()
623 .map(|row| {
624 row.into_iter()
625 .map(|value| {
626 crate::db::schema::output_value_from_runtime(
627 value_catalog.enum_catalog(),
628 value,
629 )
630 .map_err(|_| QueryError::invariant())
631 })
632 .collect::<Result<Vec<_>, _>>()
633 })
634 .collect::<Result<Vec<_>, _>>()?;
635 let rows_emitted = prior_rows_emitted.saturating_add(u64::from(row_count));
636 let total_limit_reached = request
637 .row_limit()
638 .is_some_and(|limit| rows_emitted >= u64::from(limit));
639 let continuation = if page.has_more && !total_limit_reached {
640 if page.last_emitted_logical.is_none() && page.last_consumed_physical.is_none() {
641 return Err(Self::scalar_page_cursor_error().into());
642 }
643 let cursor_contract = if let Some(contract) = cursor_contract {
644 contract
645 } else {
646 let prepared_plan = deferred_cursor_plan
647 .as_ref()
648 .ok_or_else(Self::scalar_page_cursor_error)?;
649 Self::scalar_cursor_contract(
650 request,
651 &catalog,
652 envelope,
653 prepared_plan,
654 mode,
655 exhaustive_proof.as_ref(),
656 )?
657 };
658 let token = ScalarPageToken::new(
659 mode,
660 cursor_contract.signature,
661 cursor_contract.authority,
662 cursor_contract.route_pin,
663 cursor_contract.window,
664 cursor_contract.order_terms,
665 ScalarPageTokenProgress::new(
666 page.last_emitted_logical,
667 page.last_consumed_physical,
668 None,
669 decoded_token
670 .as_ref()
671 .map_or(0, |token| token.progress().matching_rows_skipped()),
672 rows_emitted,
673 ),
674 );
675 Some(encode_cursor(
676 token
677 .encode(&cursor_authentication_key().map_err(QueryError::execute)?)
678 .map_err(|error| {
679 QueryError::from_cursor_plan_error(CursorPlanError::from_token_wire_error(
680 error,
681 ))
682 })?
683 .as_slice(),
684 ))
685 } else {
686 None
687 };
688
689 if let Some(proof) = exhaustive_proof.as_ref() {
690 if continuation.is_some() {
691 self.verify_durable_read_set_revision_proof(proof)?;
694 } else {
695 self.verify_read_set_revision_proof(proof)?;
696 }
697 }
698
699 Ok((
700 LiveQueryPageOutput {
701 entity: catalog.snapshot().entity_name().to_string(),
702 columns,
703 rows,
704 row_count,
705 continuation,
706 work: ScalarPageWork {
707 envelope_identity: envelope.identity(),
708 entries_visited: page.scanned_keys as u64,
709 result_rows: row_count,
710 },
711 },
712 exhaustive_proof,
713 ))
714 }
715
716 pub fn execute_public_live_page(
722 &self,
723 request: &DynamicQuery,
724 continuation: Option<&str>,
725 ) -> Result<LiveQueryPageOutput, QueryError> {
726 let catalog = self
727 .accepted_schema_catalog_context_for_entity_name(Some(request.entity()))
728 .map_err(QueryError::execute)?;
729 self.execute_scalar_page_against_catalog(
730 request,
731 continuation,
732 DynamicReadLane::Public,
733 catalog,
734 ScalarPageMode::Live,
735 None,
736 )
737 .map(|(page, _)| page)
738 .map_err(Self::live_page_error)
739 }
740
741 #[doc(hidden)]
744 pub fn execute_public_live_page_for_typed_binding(
745 &self,
746 binding: &DynamicTypedEntityBinding,
747 request: &DynamicQuery,
748 continuation: Option<&str>,
749 ) -> Result<Option<LiveQueryPageOutput>, QueryError> {
750 let Some(catalog) = self
751 .current_typed_entity_binding_catalog(binding)
752 .map_err(QueryError::execute)?
753 else {
754 return Ok(None);
755 };
756 self.execute_scalar_page_against_catalog(
757 request,
758 continuation,
759 DynamicReadLane::Public,
760 catalog,
761 ScalarPageMode::Live,
762 None,
763 )
764 .map(|(page, _)| Some(page))
765 .map_err(Self::live_page_error)
766 }
767
768 pub fn execute_public_dynamic_grouped_query(
773 &self,
774 request: &DynamicQuery,
775 ) -> Result<GroupedQueryOutput, QueryError> {
776 let catalog = self
777 .accepted_schema_catalog_context_for_entity_name(Some(request.entity()))
778 .map_err(QueryError::execute)?;
779 self.execute_dynamic_grouped_query_against_catalog(
780 request,
781 DynamicReadLane::Public,
782 catalog,
783 )
784 }
785
786 #[doc(hidden)]
789 pub fn execute_public_dynamic_grouped_query_for_typed_binding(
790 &self,
791 binding: &DynamicTypedEntityBinding,
792 request: &DynamicQuery,
793 ) -> Result<Option<GroupedQueryOutput>, QueryError> {
794 let Some(catalog) = self
795 .current_typed_entity_binding_catalog(binding)
796 .map_err(QueryError::execute)?
797 else {
798 return Ok(None);
799 };
800 self.execute_dynamic_grouped_query_against_catalog(
801 request,
802 DynamicReadLane::Public,
803 catalog,
804 )
805 .map(Some)
806 }
807
808 pub fn execute_trusted_dynamic_grouped_query(
815 &self,
816 request: &DynamicQuery,
817 ) -> Result<GroupedQueryOutput, QueryError> {
818 let catalog = self
819 .accepted_schema_catalog_context_for_entity_name(Some(request.entity()))
820 .map_err(QueryError::execute)?;
821 self.execute_dynamic_grouped_query_against_catalog(
822 request,
823 DynamicReadLane::Trusted,
824 catalog,
825 )
826 }
827
828 pub fn execute_trusted_live_page(
835 &self,
836 request: &DynamicQuery,
837 continuation: Option<&str>,
838 ) -> Result<LiveQueryPageOutput, QueryError> {
839 let catalog = self
840 .accepted_schema_catalog_context_for_entity_name(Some(request.entity()))
841 .map_err(QueryError::execute)?;
842 self.execute_scalar_page_against_catalog(
843 request,
844 continuation,
845 DynamicReadLane::Trusted,
846 catalog,
847 ScalarPageMode::Live,
848 None,
849 )
850 .map(|(page, _)| page)
851 .map_err(Self::live_page_error)
852 }
853
854 #[cfg(test)]
855 pub(in crate::db) fn execute_trusted_live_page_with_result_bytes_limit_for_tests(
856 &self,
857 request: &DynamicQuery,
858 continuation: Option<&str>,
859 result_bytes_limit: u64,
860 ) -> Result<LiveQueryPageOutput, QueryError> {
861 let previous = SCALAR_PAGE_RESULT_BYTES_LIMIT_OVERRIDE
862 .with(|limit| limit.replace(Some(result_bytes_limit)));
863 let _guard = ScalarPageResultBytesLimitGuard(previous);
864 self.execute_trusted_live_page(request, continuation)
865 }
866
867 pub fn execute_public_exhaustive_page(
869 &self,
870 request: &DynamicQuery,
871 continuation: Option<&str>,
872 proof: Option<&ReadSetRevisionProof>,
873 ) -> Result<ExhaustiveQueryPageOutput, ExhaustiveReadError> {
874 let catalog =
875 self.accepted_schema_catalog_context_for_entity_name(Some(request.entity()))?;
876 let (page, proof) = self.execute_scalar_page_against_catalog(
877 request,
878 continuation,
879 DynamicReadLane::Public,
880 catalog,
881 ScalarPageMode::Exhaustive,
882 proof,
883 )?;
884 let proof = proof.ok_or(ReadSetRevisionError::NonCanonical)?;
885 Ok(ExhaustiveQueryPageOutput::from_live_page(page, proof))
886 }
887
888 #[doc(hidden)]
890 pub fn execute_public_exhaustive_page_for_typed_binding(
891 &self,
892 binding: &DynamicTypedEntityBinding,
893 request: &DynamicQuery,
894 continuation: Option<&str>,
895 proof: Option<&ReadSetRevisionProof>,
896 ) -> Result<Option<ExhaustiveQueryPageOutput>, ExhaustiveReadError> {
897 let Some(catalog) = self.current_typed_entity_binding_catalog(binding)? else {
898 return Ok(None);
899 };
900 let (page, proof) = self.execute_scalar_page_against_catalog(
901 request,
902 continuation,
903 DynamicReadLane::Public,
904 catalog,
905 ScalarPageMode::Exhaustive,
906 proof,
907 )?;
908 let proof = proof.ok_or(ReadSetRevisionError::NonCanonical)?;
909 Ok(Some(ExhaustiveQueryPageOutput::from_live_page(page, proof)))
910 }
911
912 pub fn execute_trusted_exhaustive_page(
914 &self,
915 request: &DynamicQuery,
916 continuation: Option<&str>,
917 proof: Option<&ReadSetRevisionProof>,
918 ) -> Result<ExhaustiveQueryPageOutput, ExhaustiveReadError> {
919 let catalog =
920 self.accepted_schema_catalog_context_for_entity_name(Some(request.entity()))?;
921 let (page, proof) = self.execute_scalar_page_against_catalog(
922 request,
923 continuation,
924 DynamicReadLane::Trusted,
925 catalog,
926 ScalarPageMode::Exhaustive,
927 proof,
928 )?;
929 let proof = proof.ok_or(ReadSetRevisionError::NonCanonical)?;
930 Ok(ExhaustiveQueryPageOutput::from_live_page(page, proof))
931 }
932
933 fn live_page_error(error: ExhaustiveReadError) -> QueryError {
934 match error {
935 ExhaustiveReadError::Query(error) => error,
936 ExhaustiveReadError::Revision(_) => QueryError::invariant(),
937 }
938 }
939}