Skip to main content

icydb_core/db/session/query/
dynamic.rs

1//! Module: db::session::query::dynamic
2//! Responsibility: lower and execute public dynamic reads against accepted schema.
3//! Does not own: query planning, accepted schema construction, or row projection.
4//! Boundary: entity-name requests converge on the shared structural read lane.
5
6use 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        // This is only a cheap candidate check, not normalization. Multi-value
101        // lists (including duplicates) use the ordinary paged preparation once.
102        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            // Materialize each known-length clause once, then move it into
157            // intent. An absent order remains absent unless paging needs one.
158            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    // Dynamic execution and typed diagnostics accept the same grouped shape.
216    // Execution-only scan/sort policy remains at the execution terminal.
217    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    /// Project a logical query under the binding's current accepted authority.
236    /// `None` means the binding is stale. No row execution or cursor decoding occurs.
237    #[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                // A caller-supplied proof may have crossed an upgrade. Heap
438                // revisions cannot establish continuity across that boundary.
439                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        // Bound physical execution as well as outward projection. Use the
493        // authored total, not the remaining total, to reuse the same prepared
494        // plan across pages. The cursor authenticates that total separately.
495        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                // Do not return an incomplete heap traversal with a cursor
692                // whose revision proof could match unrelated data after reboot.
693                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    /// Execute one revision-tolerant bounded scalar page.
717    ///
718    /// [`DynamicQuery::limit`] caps rows across all resumed pages. Reaching
719    /// it ends continuation even when matching rows remain; each page is
720    /// independently bounded by its read envelope.
721    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    /// Execute one live page through a typed binding's immutable accepted
742    /// entity identity. `None` means the opaque binding is stale.
743    #[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    /// Execute one ordinary entity-name-driven bounded grouped read.
769    ///
770    /// [`DynamicQuery::limit`] bounds groups per page. Following the returned
771    /// grouped cursor can yield more groups in total than that limit.
772    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    /// Execute one grouped typed read through the binding's immutable accepted
787    /// entity identity. `None` means the opaque binding is stale.
788    #[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    /// Execute one trusted entity-name-driven grouped read.
809    ///
810    /// This bypasses ordinary public admission but retains accepted-schema
811    /// planning, explicit grouped limits, cursor validation, and execution.
812    /// [`DynamicQuery::limit`] bounds each grouped page, including resumed
813    /// pages, rather than the total traversal.
814    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    /// Execute one trusted revision-tolerant bounded dynamic page.
829    ///
830    /// Trusted execution bypasses public admission but retains the same
831    /// physical and aggregate request budgets as every other read lane.
832    /// [`DynamicQuery::limit`] remains a total across resumed scalar pages;
833    /// reaching it ends continuation even when matching rows remain.
834    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    /// Execute one revision-strict bounded dynamic page.
868    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    /// Execute one exhaustive page through a typed binding's accepted identity.
889    #[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    /// Execute one trusted revision-strict bounded dynamic page.
913    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}