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