Skip to main content

icydb_core/db/query/
admission.rs

1//! Module: db::query::admission
2//! Responsibility: shared read-admission vocabulary for query surfaces.
3//! Does not own: physical planning, executor runtime, or SQL/fluent lowering.
4//! Boundary: describes policy, proven bounds, and stable rejection diagnostics.
5
6mod plan_summary;
7mod policy;
8mod render;
9
10use crate::db::query::plan::AccessPlannedQuery;
11use icydb_diagnostic_code::{
12    Diagnostic, DiagnosticCode, DiagnosticDetail, ErrorCode, ErrorOrigin, QueryReadAdmissionCode,
13};
14
15#[cfg(test)]
16pub(in crate::db) use policy::GroupedAdmissionPolicy;
17pub(in crate::db) use policy::QueryAdmissionPolicy;
18pub(in crate::db::query) use policy::{
19    DEFAULT_BOUNDED_READ_MAX_ROWS, DEFAULT_BOUNDED_READ_RESPONSE_BYTES,
20};
21
22/// Query execution lane selected by the public or internal caller surface.
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum QueryAdmissionLane {
25    /// Caller-facing bounded read path for generated canister query endpoints.
26    PublicRead,
27    /// Trusted/admin ad-hoc read path with explicit budgets supplied by the embedder.
28    AdminAdHoc,
29    /// EXPLAIN-only path that describes planning and admission without row execution.
30    DiagnosticExplain,
31    /// Test-only lane for local harnesses that need to bypass production policy.
32    DevTest,
33}
34
35impl QueryAdmissionLane {
36    /// Return a stable lowercase diagnostic label for this lane.
37    #[must_use]
38    pub const fn as_str(self) -> &'static str {
39        match self {
40            Self::PublicRead => "public_read",
41            Self::AdminAdHoc => "admin_ad_hoc",
42            Self::DiagnosticExplain => "diagnostic_explain",
43            Self::DevTest => "dev_test",
44        }
45    }
46
47    /// Return whether this lane may execute and return data rows.
48    #[must_use]
49    pub const fn executes_rows(self) -> bool {
50        !matches!(self, Self::DiagnosticExplain)
51    }
52}
53
54/// Quality of the bound carried into read admission.
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub enum QueryBoundKind {
57    /// Exact count known before execution.
58    Exact,
59    /// Conservative upper bound proven before execution.
60    ConservativeUpperBound,
61    /// Runtime cap enforced by the executor while producing rows.
62    EnforcedRuntimeCap,
63    /// Planner estimate only; not safe as public admission authority.
64    EstimateOnly,
65    /// No bound is available.
66    Unavailable,
67}
68
69impl QueryBoundKind {
70    /// Return a stable lowercase diagnostic label for this bound quality.
71    #[must_use]
72    pub const fn as_str(self) -> &'static str {
73        match self {
74            Self::Exact => "exact",
75            Self::ConservativeUpperBound => "conservative_upper_bound",
76            Self::EnforcedRuntimeCap => "enforced_runtime_cap",
77            Self::EstimateOnly => "estimate_only",
78            Self::Unavailable => "unavailable",
79        }
80    }
81
82    /// Return whether this bound kind is acceptable proof for public reads.
83    #[must_use]
84    pub const fn admits_public_read(self) -> bool {
85        matches!(
86            self,
87            Self::Exact | Self::ConservativeUpperBound | Self::EnforcedRuntimeCap
88        )
89    }
90}
91
92/// Final read-admission decision.
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub enum QueryAdmissionDecision {
95    /// The selected plan is allowed under the lane policy.
96    Admitted,
97    /// The selected plan is rejected before execution.
98    Rejected,
99}
100
101impl QueryAdmissionDecision {
102    /// Return a stable lowercase diagnostic label for this decision.
103    #[must_use]
104    pub const fn as_str(self) -> &'static str {
105        match self {
106            Self::Admitted => "admitted",
107            Self::Rejected => "rejected",
108        }
109    }
110
111    /// Return whether the selected plan may execute.
112    #[must_use]
113    pub const fn is_admitted(self) -> bool {
114        matches!(self, Self::Admitted)
115    }
116}
117
118/// Coarse selected access-path class used by admission and EXPLAIN.
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
120pub enum QueryAdmissionAccessKind {
121    /// Access class has not been summarized yet.
122    Unknown,
123    /// Direct primary-key lookup.
124    ByKey,
125    /// Multiple direct primary-key lookups.
126    ByKeys,
127    /// Primary-key range access.
128    KeyRange,
129    /// Secondary-index prefix access.
130    IndexPrefix,
131    /// Secondary-index multi-lookup access.
132    IndexMultiLookup,
133    /// Secondary-index branch-set access.
134    IndexBranchSet,
135    /// Secondary-index range access.
136    IndexRange,
137    /// Full entity scan.
138    FullScan,
139    /// Union of multiple access paths.
140    Union,
141    /// Intersection of multiple access paths.
142    Intersection,
143}
144
145impl QueryAdmissionAccessKind {
146    /// Return a stable lowercase diagnostic label for this access class.
147    #[must_use]
148    pub const fn as_str(self) -> &'static str {
149        match self {
150            Self::Unknown => "unknown",
151            Self::ByKey => "by_key",
152            Self::ByKeys => "by_keys",
153            Self::KeyRange => "key_range",
154            Self::IndexPrefix => "index_prefix",
155            Self::IndexMultiLookup => "index_multi_lookup",
156            Self::IndexBranchSet => "index_branch_set",
157            Self::IndexRange => "index_range",
158            Self::FullScan => "full_scan",
159            Self::Union => "union",
160            Self::Intersection => "intersection",
161        }
162    }
163
164    /// Return whether this access class is backed by a secondary index.
165    #[must_use]
166    pub const fn is_secondary_index(self) -> bool {
167        matches!(
168            self,
169            Self::IndexPrefix | Self::IndexMultiLookup | Self::IndexBranchSet | Self::IndexRange
170        )
171    }
172
173    /// Return whether this access class is a full entity scan.
174    #[must_use]
175    pub const fn is_full_scan(self) -> bool {
176        matches!(self, Self::FullScan)
177    }
178}
179
180/// Coarse scalar/grouped statement shape used by read admission.
181#[derive(Clone, Copy, Debug, Eq, PartialEq)]
182pub enum QueryAdmissionPlanShape {
183    /// Scalar read shape, including projection-only and global-aggregate scalar plans.
184    ScalarRead,
185    /// Grouped aggregate read shape.
186    GroupedAggregate,
187    /// Delete shape surfaced only for diagnostics; public read lanes must not execute it.
188    Delete,
189}
190
191impl QueryAdmissionPlanShape {
192    /// Return a stable lowercase diagnostic label for this plan shape.
193    #[must_use]
194    pub const fn as_str(self) -> &'static str {
195        match self {
196            Self::ScalarRead => "scalar_read",
197            Self::GroupedAggregate => "grouped_aggregate",
198            Self::Delete => "delete",
199        }
200    }
201}
202
203/// Post-access residual filter shape relevant to admission.
204#[derive(Clone, Copy, Debug, Eq, PartialEq)]
205pub enum QueryAdmissionResidualFilter {
206    /// No residual runtime filter remains after access planning.
207    Absent,
208    /// A predicate-native residual filter remains.
209    Predicate,
210    /// An expression-backed residual filter remains.
211    Expression,
212    /// Both expression and predicate residual forms remain available.
213    ExpressionAndPredicate,
214}
215
216impl QueryAdmissionResidualFilter {
217    /// Return a stable lowercase diagnostic label for this residual shape.
218    #[must_use]
219    pub const fn as_str(self) -> &'static str {
220        match self {
221            Self::Absent => "none",
222            Self::Predicate => "predicate",
223            Self::Expression => "expression",
224            Self::ExpressionAndPredicate => "expression_and_predicate",
225        }
226    }
227
228    /// Return whether no residual runtime filter remains.
229    #[must_use]
230    pub const fn is_absent(self) -> bool {
231        matches!(self, Self::Absent)
232    }
233}
234
235/// ORDER BY facts relevant to read admission.
236#[derive(Clone, Copy, Debug, Eq, PartialEq)]
237pub enum QueryAdmissionOrdering {
238    /// No caller-visible ordering is requested.
239    None,
240    /// Ordering is requested but not yet resolved into executor slots.
241    Requested,
242    /// Ordering has a planner-resolved executor contract.
243    Resolved,
244}
245
246impl QueryAdmissionOrdering {
247    /// Return a stable lowercase diagnostic label for this ordering state.
248    #[must_use]
249    pub const fn as_str(self) -> &'static str {
250        match self {
251            Self::None => "none",
252            Self::Requested => "requested",
253            Self::Resolved => "resolved",
254        }
255    }
256}
257
258/// Grouped query facts relevant to read admission.
259#[derive(Clone, Copy, Debug, Eq, PartialEq)]
260pub struct QueryAdmissionGroupedSummary {
261    group_field_count: u32,
262    aggregate_count: u32,
263    distinct_aggregate_count: u32,
264    max_groups: u64,
265    max_group_bytes: u64,
266    having_filter: bool,
267}
268
269impl QueryAdmissionGroupedSummary {
270    /// Build one grouped admission summary from planner-owned grouped facts.
271    #[must_use]
272    pub const fn new(
273        group_field_count: u32,
274        aggregate_count: u32,
275        distinct_aggregate_count: u32,
276        max_groups: u64,
277        max_group_bytes: u64,
278        having_filter: bool,
279    ) -> Self {
280        Self {
281            group_field_count,
282            aggregate_count,
283            distinct_aggregate_count,
284            max_groups,
285            max_group_bytes,
286            having_filter,
287        }
288    }
289
290    /// Return the number of GROUP BY fields.
291    #[must_use]
292    pub const fn group_field_count(self) -> u32 {
293        self.group_field_count
294    }
295
296    /// Return the number of aggregate expressions.
297    #[must_use]
298    pub const fn aggregate_count(self) -> u32 {
299        self.aggregate_count
300    }
301
302    /// Return the number of aggregate expressions with DISTINCT state.
303    #[must_use]
304    pub const fn distinct_aggregate_count(self) -> u32 {
305        self.distinct_aggregate_count
306    }
307
308    /// Return the grouped execution maximum group count.
309    #[must_use]
310    pub const fn max_groups(self) -> u64 {
311        self.max_groups
312    }
313
314    /// Return the grouped execution maximum bytes per group accumulator.
315    #[must_use]
316    pub const fn max_group_bytes(self) -> u64 {
317        self.max_group_bytes
318    }
319
320    /// Return whether the grouped plan has a HAVING residual expression.
321    #[must_use]
322    pub const fn has_having_filter(self) -> bool {
323        self.having_filter
324    }
325}
326
327/// Materialization facts relevant to read admission.
328#[derive(Clone, Copy, Debug, Eq, PartialEq)]
329pub struct QueryMaterializationSummary {
330    materialized_sort: bool,
331    materialized_rows: Option<u32>,
332    row_bound_kind: QueryBoundKind,
333}
334
335impl QueryMaterializationSummary {
336    /// Build a summary for a plan that does not materialize rows for sorting.
337    #[must_use]
338    pub const fn none() -> Self {
339        Self {
340            materialized_sort: false,
341            materialized_rows: None,
342            row_bound_kind: QueryBoundKind::Unavailable,
343        }
344    }
345
346    /// Build a summary for a plan that materializes rows for sorting.
347    #[must_use]
348    pub const fn sort(materialized_rows: Option<u32>, row_bound_kind: QueryBoundKind) -> Self {
349        Self {
350            materialized_sort: true,
351            materialized_rows,
352            row_bound_kind,
353        }
354    }
355
356    /// Return whether the plan materializes rows for sorting.
357    #[must_use]
358    pub const fn materialized_sort(&self) -> bool {
359        self.materialized_sort
360    }
361
362    /// Return the row materialization bound, if known.
363    #[must_use]
364    pub const fn materialized_rows(&self) -> Option<u32> {
365        self.materialized_rows
366    }
367
368    /// Return the quality of the materialization row bound.
369    #[must_use]
370    pub const fn row_bound_kind(&self) -> QueryBoundKind {
371        self.row_bound_kind
372    }
373}
374
375/// Stable read-admission rejection reason.
376#[derive(Clone, Copy, Debug, Eq, PartialEq)]
377pub enum QueryAdmissionRejection {
378    /// Public reads require a bounded read intent.
379    PublicQueryRequiresLimit,
380    /// Public reads require a proven index-backed access path.
381    PublicQueryRequiresIndex,
382    /// The selected plan is an unbounded full scan.
383    UnboundedFullScanRejected,
384    /// No scan bound was available for a policy that requires one.
385    ScanBoundUnavailable,
386    /// The proven scan bound exceeds the policy.
387    ScanBoundExceedsPolicy,
388    /// Only an estimate was available for a policy that requires proof.
389    EstimatedOnlyBoundRejected,
390    /// ORDER BY requires materializing rows.
391    SortRequiresMaterialization,
392    /// Materialization exceeds the policy.
393    MaterializationExceedsBudget,
394    /// Projection bytes may exceed the response budget.
395    ProjectionResponseMayExceedLimit,
396    /// Grouped reads need explicit group and memory budgets.
397    GroupedQueryRequiresLimits,
398    /// Grouped read planning exceeds the policy.
399    GroupedQueryExceedsBudget,
400    /// Diagnostic lanes do not execute rows.
401    DiagnosticLaneDoesNotExecute,
402    /// Introspection is disabled for the selected lane.
403    IntrospectionDisabledForLane,
404    /// The statement shape is not supported by the selected lane.
405    UnsupportedStatementForQueryLane,
406    /// Public read endpoints do not permit non-zero OFFSET execution.
407    PublicQueryOffsetRejected,
408    /// The returned-row bound exceeds the selected policy.
409    ReturnedRowBoundExceedsPolicy,
410    /// Primary-key predicate input work exceeds the selected policy.
411    PrimaryKeyInputExceedsPolicy,
412}
413
414impl QueryAdmissionRejection {
415    /// Return a stable lowercase diagnostic label for this rejection.
416    #[must_use]
417    pub const fn as_str(self) -> &'static str {
418        match self {
419            Self::PublicQueryRequiresLimit => "public_query_requires_limit",
420            Self::PublicQueryRequiresIndex => "public_query_requires_index",
421            Self::UnboundedFullScanRejected => "unbounded_full_scan_rejected",
422            Self::ScanBoundUnavailable => "scan_bound_unavailable",
423            Self::ScanBoundExceedsPolicy => "scan_bound_exceeds_policy",
424            Self::EstimatedOnlyBoundRejected => "estimated_only_bound_rejected",
425            Self::SortRequiresMaterialization => "sort_requires_materialization",
426            Self::MaterializationExceedsBudget => "materialization_exceeds_budget",
427            Self::ProjectionResponseMayExceedLimit => "projection_response_may_exceed_limit",
428            Self::GroupedQueryRequiresLimits => "grouped_query_requires_limits",
429            Self::GroupedQueryExceedsBudget => "grouped_query_exceeds_budget",
430            Self::DiagnosticLaneDoesNotExecute => "diagnostic_lane_does_not_execute",
431            Self::IntrospectionDisabledForLane => "introspection_disabled_for_lane",
432            Self::UnsupportedStatementForQueryLane => "unsupported_statement_for_query_lane",
433            Self::PublicQueryOffsetRejected => "public_query_offset_rejected",
434            Self::ReturnedRowBoundExceedsPolicy => "returned_row_bound_exceeds_policy",
435            Self::PrimaryKeyInputExceedsPolicy => "primary_key_input_exceeds_policy",
436        }
437    }
438
439    /// Return the compact diagnostic detail code for this rejection.
440    #[must_use]
441    pub const fn code(self) -> QueryReadAdmissionCode {
442        match self {
443            Self::PublicQueryRequiresLimit => QueryReadAdmissionCode::PublicQueryRequiresLimit,
444            Self::PublicQueryRequiresIndex => QueryReadAdmissionCode::PublicQueryRequiresIndex,
445            Self::UnboundedFullScanRejected => QueryReadAdmissionCode::UnboundedFullScanRejected,
446            Self::ScanBoundUnavailable => QueryReadAdmissionCode::ScanBoundUnavailable,
447            Self::ScanBoundExceedsPolicy => QueryReadAdmissionCode::ScanBoundExceedsPolicy,
448            Self::EstimatedOnlyBoundRejected => QueryReadAdmissionCode::EstimatedOnlyBoundRejected,
449            Self::SortRequiresMaterialization => {
450                QueryReadAdmissionCode::SortRequiresMaterialization
451            }
452            Self::MaterializationExceedsBudget => {
453                QueryReadAdmissionCode::MaterializationExceedsBudget
454            }
455            Self::ProjectionResponseMayExceedLimit => {
456                QueryReadAdmissionCode::ProjectionResponseMayExceedLimit
457            }
458            Self::GroupedQueryRequiresLimits => QueryReadAdmissionCode::GroupedQueryRequiresLimits,
459            Self::GroupedQueryExceedsBudget => QueryReadAdmissionCode::GroupedQueryExceedsBudget,
460            Self::DiagnosticLaneDoesNotExecute => {
461                QueryReadAdmissionCode::DiagnosticLaneDoesNotExecute
462            }
463            Self::IntrospectionDisabledForLane => {
464                QueryReadAdmissionCode::IntrospectionDisabledForLane
465            }
466            Self::UnsupportedStatementForQueryLane => {
467                QueryReadAdmissionCode::UnsupportedStatementForQueryLane
468            }
469            Self::PublicQueryOffsetRejected => QueryReadAdmissionCode::PublicQueryOffsetRejected,
470            Self::ReturnedRowBoundExceedsPolicy => {
471                QueryReadAdmissionCode::ReturnedRowBoundExceedsPolicy
472            }
473            Self::PrimaryKeyInputExceedsPolicy => {
474                QueryReadAdmissionCode::PrimaryKeyInputExceedsPolicy
475            }
476        }
477    }
478
479    /// Return a compact diagnostic payload for this rejection.
480    #[must_use]
481    pub const fn diagnostic(self) -> Diagnostic {
482        Diagnostic::new(
483            DiagnosticCode::QueryReadAdmission,
484            ErrorOrigin::Query,
485            Some(DiagnosticDetail::QueryReadAdmission {
486                reason: self.code(),
487            }),
488        )
489    }
490
491    /// Return the public wire code for this rejection.
492    #[must_use]
493    pub const fn error_code(self) -> ErrorCode {
494        self.diagnostic().error_code()
495    }
496}
497
498/// Read-admission result and plan facts for diagnostics and EXPLAIN.
499#[derive(Clone, Debug, Eq, PartialEq)]
500pub struct QueryAdmissionSummary {
501    lane: QueryAdmissionLane,
502    decision: QueryAdmissionDecision,
503    plan_shape: QueryAdmissionPlanShape,
504    selected_access: QueryAdmissionAccessKind,
505    selected_index: Option<String>,
506    limit: Option<u32>,
507    offset: Option<u32>,
508    scan_bound: Option<u64>,
509    scan_bound_kind: QueryBoundKind,
510    returned_row_bound: Option<u32>,
511    returned_row_bound_kind: QueryBoundKind,
512    response_byte_bound: Option<u32>,
513    response_byte_bound_kind: QueryBoundKind,
514    primary_key_input_terms: Option<u32>,
515    primary_key_input_payload_bytes: Option<u32>,
516    residual_filter: QueryAdmissionResidualFilter,
517    ordering: QueryAdmissionOrdering,
518    grouped: Option<QueryAdmissionGroupedSummary>,
519    materialization: QueryMaterializationSummary,
520    rejection: Option<QueryAdmissionRejection>,
521}
522
523impl QueryAdmissionSummary {
524    /// Build an admitted summary with unknown bound details.
525    #[must_use]
526    pub const fn admitted(
527        lane: QueryAdmissionLane,
528        selected_access: QueryAdmissionAccessKind,
529    ) -> Self {
530        Self {
531            lane,
532            decision: QueryAdmissionDecision::Admitted,
533            plan_shape: QueryAdmissionPlanShape::ScalarRead,
534            selected_access,
535            selected_index: None,
536            limit: None,
537            offset: None,
538            scan_bound: None,
539            scan_bound_kind: QueryBoundKind::Unavailable,
540            returned_row_bound: None,
541            returned_row_bound_kind: QueryBoundKind::Unavailable,
542            response_byte_bound: None,
543            response_byte_bound_kind: QueryBoundKind::Unavailable,
544            primary_key_input_terms: None,
545            primary_key_input_payload_bytes: None,
546            residual_filter: QueryAdmissionResidualFilter::Absent,
547            ordering: QueryAdmissionOrdering::None,
548            grouped: None,
549            materialization: QueryMaterializationSummary::none(),
550            rejection: None,
551        }
552    }
553
554    /// Build a rejected summary with unknown bound details.
555    #[must_use]
556    pub const fn rejected(
557        lane: QueryAdmissionLane,
558        selected_access: QueryAdmissionAccessKind,
559        rejection: QueryAdmissionRejection,
560    ) -> Self {
561        Self {
562            lane,
563            decision: QueryAdmissionDecision::Rejected,
564            plan_shape: QueryAdmissionPlanShape::ScalarRead,
565            selected_access,
566            selected_index: None,
567            limit: None,
568            offset: None,
569            scan_bound: None,
570            scan_bound_kind: QueryBoundKind::Unavailable,
571            returned_row_bound: None,
572            returned_row_bound_kind: QueryBoundKind::Unavailable,
573            response_byte_bound: None,
574            response_byte_bound_kind: QueryBoundKind::Unavailable,
575            primary_key_input_terms: None,
576            primary_key_input_payload_bytes: None,
577            residual_filter: QueryAdmissionResidualFilter::Absent,
578            ordering: QueryAdmissionOrdering::None,
579            grouped: None,
580            materialization: QueryMaterializationSummary::none(),
581            rejection: Some(rejection),
582        }
583    }
584
585    /// Build one admitted summary from the already-selected access plan.
586    #[must_use]
587    pub(in crate::db) fn from_plan(lane: QueryAdmissionLane, plan: &AccessPlannedQuery) -> Self {
588        plan_summary::summary_from_plan(lane, plan)
589    }
590
591    const fn admit(mut self) -> Self {
592        self.decision = QueryAdmissionDecision::Admitted;
593        self.rejection = None;
594        self
595    }
596
597    const fn reject(mut self, rejection: QueryAdmissionRejection) -> Self {
598        self.decision = QueryAdmissionDecision::Rejected;
599        self.rejection = Some(rejection);
600        self
601    }
602
603    /// Return the admission lane.
604    #[must_use]
605    pub const fn lane(&self) -> QueryAdmissionLane {
606        self.lane
607    }
608
609    /// Return the final decision.
610    #[must_use]
611    pub const fn decision(&self) -> QueryAdmissionDecision {
612        self.decision
613    }
614
615    /// Return the scalar/grouped statement shape.
616    #[must_use]
617    pub const fn plan_shape(&self) -> QueryAdmissionPlanShape {
618        self.plan_shape
619    }
620
621    /// Return the selected access class.
622    #[must_use]
623    pub const fn selected_access(&self) -> QueryAdmissionAccessKind {
624        self.selected_access
625    }
626
627    /// Return the selected index name, if one exists.
628    #[must_use]
629    pub fn selected_index(&self) -> Option<&str> {
630        self.selected_index.as_deref()
631    }
632
633    /// Return the caller-visible LIMIT, if present.
634    #[must_use]
635    pub const fn limit(&self) -> Option<u32> {
636        self.limit
637    }
638
639    /// Return the caller-visible OFFSET, if present.
640    #[must_use]
641    pub const fn offset(&self) -> Option<u32> {
642        self.offset
643    }
644
645    /// Return the scan bound, if known.
646    #[must_use]
647    pub const fn scan_bound(&self) -> Option<u64> {
648        self.scan_bound
649    }
650
651    /// Return the quality of the scan bound.
652    #[must_use]
653    pub const fn scan_bound_kind(&self) -> QueryBoundKind {
654        self.scan_bound_kind
655    }
656
657    /// Return the returned-row bound, if known.
658    #[must_use]
659    pub const fn returned_row_bound(&self) -> Option<u32> {
660        self.returned_row_bound
661    }
662
663    /// Return the quality of the returned-row bound.
664    #[must_use]
665    pub const fn returned_row_bound_kind(&self) -> QueryBoundKind {
666        self.returned_row_bound_kind
667    }
668
669    /// Return the response-byte bound, if known.
670    #[must_use]
671    pub const fn response_byte_bound(&self) -> Option<u32> {
672        self.response_byte_bound
673    }
674
675    /// Return the quality of the response-byte bound.
676    #[must_use]
677    pub const fn response_byte_bound_kind(&self) -> QueryBoundKind {
678        self.response_byte_bound_kind
679    }
680
681    /// Return primary-key predicate input terms when the selected exact-key
682    /// route carries planner-owned resource facts.
683    #[must_use]
684    pub const fn primary_key_input_terms(&self) -> Option<u32> {
685        self.primary_key_input_terms
686    }
687
688    /// Return estimated primary-key predicate input payload bytes when known.
689    #[must_use]
690    pub const fn primary_key_input_payload_bytes(&self) -> Option<u32> {
691        self.primary_key_input_payload_bytes
692    }
693
694    /// Return post-access residual filter facts.
695    #[must_use]
696    pub const fn residual_filter(&self) -> QueryAdmissionResidualFilter {
697        self.residual_filter
698    }
699
700    /// Return ORDER BY facts.
701    #[must_use]
702    pub const fn ordering(&self) -> QueryAdmissionOrdering {
703        self.ordering
704    }
705
706    /// Return grouped query facts, if this is a grouped plan.
707    #[must_use]
708    pub const fn grouped(&self) -> Option<QueryAdmissionGroupedSummary> {
709        self.grouped
710    }
711
712    /// Return materialization facts.
713    #[must_use]
714    pub const fn materialization(&self) -> QueryMaterializationSummary {
715        self.materialization
716    }
717
718    /// Return a copy of this summary with route-derived materialization facts attached.
719    #[must_use]
720    #[cfg_attr(not(feature = "sql"), allow(dead_code))]
721    pub(in crate::db) const fn with_materialization(
722        mut self,
723        materialization: QueryMaterializationSummary,
724    ) -> Self {
725        self.materialization = materialization;
726        self
727    }
728
729    /// Return the rejection reason, when the decision is rejected.
730    #[must_use]
731    pub const fn rejection(&self) -> Option<QueryAdmissionRejection> {
732        self.rejection
733    }
734
735    /// Render this summary as a stable top-level verbose EXPLAIN block.
736    #[must_use]
737    pub(in crate::db) fn render_text_block(&self) -> String {
738        render::render_text_block(self)
739    }
740}
741
742#[cfg(test)]
743mod tests;