1mod 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum QueryAdmissionLane {
25 PublicRead,
27 AdminAdHoc,
29 DiagnosticExplain,
31 DevTest,
33}
34
35impl QueryAdmissionLane {
36 #[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 #[must_use]
49 pub const fn executes_rows(self) -> bool {
50 !matches!(self, Self::DiagnosticExplain)
51 }
52}
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub enum QueryBoundKind {
57 Exact,
59 ConservativeUpperBound,
61 EnforcedRuntimeCap,
63 EstimateOnly,
65 Unavailable,
67}
68
69impl QueryBoundKind {
70 #[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 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub enum QueryAdmissionDecision {
95 Admitted,
97 Rejected,
99}
100
101impl QueryAdmissionDecision {
102 #[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 #[must_use]
113 pub const fn is_admitted(self) -> bool {
114 matches!(self, Self::Admitted)
115 }
116}
117
118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
120pub enum QueryAdmissionAccessKind {
121 Unknown,
123 ByKey,
125 ByKeys,
127 KeyRange,
129 IndexPrefix,
131 IndexMultiLookup,
133 IndexBranchSet,
135 IndexRange,
137 FullScan,
139 Union,
141 Intersection,
143}
144
145impl QueryAdmissionAccessKind {
146 #[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 #[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 #[must_use]
175 pub const fn is_full_scan(self) -> bool {
176 matches!(self, Self::FullScan)
177 }
178}
179
180#[derive(Clone, Copy, Debug, Eq, PartialEq)]
182pub enum QueryAdmissionPlanShape {
183 ScalarRead,
185 GroupedAggregate,
187 Delete,
189}
190
191impl QueryAdmissionPlanShape {
192 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
205pub enum QueryAdmissionResidualFilter {
206 Absent,
208 Predicate,
210 Expression,
212 ExpressionAndPredicate,
214}
215
216impl QueryAdmissionResidualFilter {
217 #[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 #[must_use]
230 pub const fn is_absent(self) -> bool {
231 matches!(self, Self::Absent)
232 }
233}
234
235#[derive(Clone, Copy, Debug, Eq, PartialEq)]
237pub enum QueryAdmissionOrdering {
238 None,
240 Requested,
242 Resolved,
244}
245
246impl QueryAdmissionOrdering {
247 #[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#[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 #[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 #[must_use]
292 pub const fn group_field_count(self) -> u32 {
293 self.group_field_count
294 }
295
296 #[must_use]
298 pub const fn aggregate_count(self) -> u32 {
299 self.aggregate_count
300 }
301
302 #[must_use]
304 pub const fn distinct_aggregate_count(self) -> u32 {
305 self.distinct_aggregate_count
306 }
307
308 #[must_use]
310 pub const fn max_groups(self) -> u64 {
311 self.max_groups
312 }
313
314 #[must_use]
316 pub const fn max_group_bytes(self) -> u64 {
317 self.max_group_bytes
318 }
319
320 #[must_use]
322 pub const fn has_having_filter(self) -> bool {
323 self.having_filter
324 }
325}
326
327#[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 #[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 #[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 #[must_use]
358 pub const fn materialized_sort(&self) -> bool {
359 self.materialized_sort
360 }
361
362 #[must_use]
364 pub const fn materialized_rows(&self) -> Option<u32> {
365 self.materialized_rows
366 }
367
368 #[must_use]
370 pub const fn row_bound_kind(&self) -> QueryBoundKind {
371 self.row_bound_kind
372 }
373}
374
375#[derive(Clone, Copy, Debug, Eq, PartialEq)]
377pub enum QueryAdmissionRejection {
378 PublicQueryRequiresLimit,
380 PublicQueryRequiresIndex,
382 UnboundedFullScanRejected,
384 ScanBoundUnavailable,
386 ScanBoundExceedsPolicy,
388 EstimatedOnlyBoundRejected,
390 SortRequiresMaterialization,
392 MaterializationExceedsBudget,
394 ProjectionResponseMayExceedLimit,
396 GroupedQueryRequiresLimits,
398 GroupedQueryExceedsBudget,
400 DiagnosticLaneDoesNotExecute,
402 IntrospectionDisabledForLane,
404 UnsupportedStatementForQueryLane,
406 PublicQueryOffsetRejected,
408 ReturnedRowBoundExceedsPolicy,
410 PrimaryKeyInputExceedsPolicy,
412}
413
414impl QueryAdmissionRejection {
415 #[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 #[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 #[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 #[must_use]
493 pub const fn error_code(self) -> ErrorCode {
494 self.diagnostic().error_code()
495 }
496}
497
498#[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 #[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 #[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 #[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 #[must_use]
605 pub const fn lane(&self) -> QueryAdmissionLane {
606 self.lane
607 }
608
609 #[must_use]
611 pub const fn decision(&self) -> QueryAdmissionDecision {
612 self.decision
613 }
614
615 #[must_use]
617 pub const fn plan_shape(&self) -> QueryAdmissionPlanShape {
618 self.plan_shape
619 }
620
621 #[must_use]
623 pub const fn selected_access(&self) -> QueryAdmissionAccessKind {
624 self.selected_access
625 }
626
627 #[must_use]
629 pub fn selected_index(&self) -> Option<&str> {
630 self.selected_index.as_deref()
631 }
632
633 #[must_use]
635 pub const fn limit(&self) -> Option<u32> {
636 self.limit
637 }
638
639 #[must_use]
641 pub const fn offset(&self) -> Option<u32> {
642 self.offset
643 }
644
645 #[must_use]
647 pub const fn scan_bound(&self) -> Option<u64> {
648 self.scan_bound
649 }
650
651 #[must_use]
653 pub const fn scan_bound_kind(&self) -> QueryBoundKind {
654 self.scan_bound_kind
655 }
656
657 #[must_use]
659 pub const fn returned_row_bound(&self) -> Option<u32> {
660 self.returned_row_bound
661 }
662
663 #[must_use]
665 pub const fn returned_row_bound_kind(&self) -> QueryBoundKind {
666 self.returned_row_bound_kind
667 }
668
669 #[must_use]
671 pub const fn response_byte_bound(&self) -> Option<u32> {
672 self.response_byte_bound
673 }
674
675 #[must_use]
677 pub const fn response_byte_bound_kind(&self) -> QueryBoundKind {
678 self.response_byte_bound_kind
679 }
680
681 #[must_use]
684 pub const fn primary_key_input_terms(&self) -> Option<u32> {
685 self.primary_key_input_terms
686 }
687
688 #[must_use]
690 pub const fn primary_key_input_payload_bytes(&self) -> Option<u32> {
691 self.primary_key_input_payload_bytes
692 }
693
694 #[must_use]
696 pub const fn residual_filter(&self) -> QueryAdmissionResidualFilter {
697 self.residual_filter
698 }
699
700 #[must_use]
702 pub const fn ordering(&self) -> QueryAdmissionOrdering {
703 self.ordering
704 }
705
706 #[must_use]
708 pub const fn grouped(&self) -> Option<QueryAdmissionGroupedSummary> {
709 self.grouped
710 }
711
712 #[must_use]
714 pub const fn materialization(&self) -> QueryMaterializationSummary {
715 self.materialization
716 }
717
718 #[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 #[must_use]
731 pub const fn rejection(&self) -> Option<QueryAdmissionRejection> {
732 self.rejection
733 }
734
735 #[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;