use std::fmt::Write as _;
use std::num::{NonZeroU32, NonZeroU64};
use std::ops::Bound;
use crate::{
db::{
access::IndexBranchSetOrderedSuffix,
query::plan::{
AccessPlanProjection, AccessPlannedQuery, GroupPlan, QueryMode, ResidualFilterShape,
ScalarPlan, project_access_plan,
},
},
value::Value,
};
use icydb_diagnostic_code::{
Diagnostic, DiagnosticCode, DiagnosticDetail, ErrorCode, ErrorOrigin, QueryReadAdmissionCode,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QueryAdmissionLane {
PublicRead,
AdminAdHoc,
DiagnosticExplain,
DevTest,
}
impl QueryAdmissionLane {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::PublicRead => "public_read",
Self::AdminAdHoc => "admin_ad_hoc",
Self::DiagnosticExplain => "diagnostic_explain",
Self::DevTest => "dev_test",
}
}
#[must_use]
pub const fn executes_rows(self) -> bool {
!matches!(self, Self::DiagnosticExplain)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QueryBoundKind {
Exact,
ConservativeUpperBound,
EnforcedRuntimeCap,
EstimateOnly,
Unavailable,
}
impl QueryBoundKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Exact => "exact",
Self::ConservativeUpperBound => "conservative_upper_bound",
Self::EnforcedRuntimeCap => "enforced_runtime_cap",
Self::EstimateOnly => "estimate_only",
Self::Unavailable => "unavailable",
}
}
#[must_use]
pub const fn admits_public_read(self) -> bool {
matches!(
self,
Self::Exact | Self::ConservativeUpperBound | Self::EnforcedRuntimeCap
)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QueryAdmissionDecision {
Admitted,
Rejected,
}
impl QueryAdmissionDecision {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Admitted => "admitted",
Self::Rejected => "rejected",
}
}
#[must_use]
pub const fn is_admitted(self) -> bool {
matches!(self, Self::Admitted)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QueryAdmissionAccessKind {
Unknown,
ByKey,
ByKeys,
KeyRange,
IndexPrefix,
IndexMultiLookup,
IndexBranchSet,
IndexRange,
FullScan,
Union,
Intersection,
}
impl QueryAdmissionAccessKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Unknown => "unknown",
Self::ByKey => "by_key",
Self::ByKeys => "by_keys",
Self::KeyRange => "key_range",
Self::IndexPrefix => "index_prefix",
Self::IndexMultiLookup => "index_multi_lookup",
Self::IndexBranchSet => "index_branch_set",
Self::IndexRange => "index_range",
Self::FullScan => "full_scan",
Self::Union => "union",
Self::Intersection => "intersection",
}
}
#[must_use]
pub const fn is_secondary_index(self) -> bool {
matches!(
self,
Self::IndexPrefix | Self::IndexMultiLookup | Self::IndexBranchSet | Self::IndexRange
)
}
#[must_use]
pub const fn is_full_scan(self) -> bool {
matches!(self, Self::FullScan)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QueryAdmissionPlanShape {
ScalarRead,
GroupedAggregate,
Delete,
}
impl QueryAdmissionPlanShape {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::ScalarRead => "scalar_read",
Self::GroupedAggregate => "grouped_aggregate",
Self::Delete => "delete",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QueryAdmissionResidualFilter {
Absent,
Predicate,
Expression,
ExpressionAndPredicate,
}
impl QueryAdmissionResidualFilter {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Absent => "none",
Self::Predicate => "predicate",
Self::Expression => "expression",
Self::ExpressionAndPredicate => "expression_and_predicate",
}
}
#[must_use]
pub const fn is_absent(self) -> bool {
matches!(self, Self::Absent)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QueryAdmissionOrdering {
None,
Requested,
Resolved,
}
impl QueryAdmissionOrdering {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::Requested => "requested",
Self::Resolved => "resolved",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct QueryAdmissionGroupedSummary {
group_field_count: u32,
aggregate_count: u32,
max_groups: u64,
max_group_bytes: u64,
having_filter: bool,
}
impl QueryAdmissionGroupedSummary {
#[must_use]
pub const fn new(
group_field_count: u32,
aggregate_count: u32,
max_groups: u64,
max_group_bytes: u64,
having_filter: bool,
) -> Self {
Self {
group_field_count,
aggregate_count,
max_groups,
max_group_bytes,
having_filter,
}
}
#[must_use]
pub const fn group_field_count(self) -> u32 {
self.group_field_count
}
#[must_use]
pub const fn aggregate_count(self) -> u32 {
self.aggregate_count
}
#[must_use]
pub const fn max_groups(self) -> u64 {
self.max_groups
}
#[must_use]
pub const fn max_group_bytes(self) -> u64 {
self.max_group_bytes
}
#[must_use]
pub const fn has_having_filter(self) -> bool {
self.having_filter
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GroupedAdmissionPolicy {
groups: Option<NonZeroU32>,
group_bytes: Option<NonZeroU32>,
distinct_entries: Option<NonZeroU32>,
}
impl GroupedAdmissionPolicy {
#[must_use]
pub const fn disabled() -> Self {
Self {
groups: None,
group_bytes: None,
distinct_entries: None,
}
}
#[must_use]
pub const fn bounded(
max_groups: NonZeroU32,
max_group_bytes: NonZeroU32,
max_distinct_entries: Option<NonZeroU32>,
) -> Self {
Self {
groups: Some(max_groups),
group_bytes: Some(max_group_bytes),
distinct_entries: max_distinct_entries,
}
}
#[must_use]
pub const fn max_groups(&self) -> Option<NonZeroU32> {
self.groups
}
#[must_use]
pub const fn max_group_bytes(&self) -> Option<NonZeroU32> {
self.group_bytes
}
#[must_use]
pub const fn max_distinct_entries(&self) -> Option<NonZeroU32> {
self.distinct_entries
}
#[must_use]
pub const fn has_hard_limits(&self) -> bool {
self.groups.is_some() && self.group_bytes.is_some()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum LimitRequirement {
Required,
Optional,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum IndexRequirement {
Required,
Optional,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum FullScanPolicy {
Allow,
Reject,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MaterializedSortPolicy {
Allow,
Reject,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum OffsetPolicy {
Allow,
RejectNonZero,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QueryAdmissionPolicy {
lane: QueryAdmissionLane,
limit_requirement: LimitRequirement,
max_returned_rows: Option<NonZeroU32>,
max_scanned_rows: Option<NonZeroU64>,
max_response_bytes: Option<NonZeroU32>,
index_requirement: IndexRequirement,
offset_policy: OffsetPolicy,
full_scan_policy: FullScanPolicy,
materialized_sort_policy: MaterializedSortPolicy,
max_materialized_rows: Option<NonZeroU32>,
max_projection_columns: Option<NonZeroU32>,
grouped: GroupedAdmissionPolicy,
}
impl QueryAdmissionPolicy {
#[must_use]
pub const fn public_read(
max_returned_rows: NonZeroU32,
max_response_bytes: NonZeroU32,
) -> Self {
Self {
lane: QueryAdmissionLane::PublicRead,
limit_requirement: LimitRequirement::Required,
max_returned_rows: Some(max_returned_rows),
max_scanned_rows: None,
max_response_bytes: Some(max_response_bytes),
index_requirement: IndexRequirement::Required,
offset_policy: OffsetPolicy::RejectNonZero,
full_scan_policy: FullScanPolicy::Reject,
materialized_sort_policy: MaterializedSortPolicy::Reject,
max_materialized_rows: None,
max_projection_columns: None,
grouped: GroupedAdmissionPolicy::disabled(),
}
}
#[must_use]
pub const fn admin_ad_hoc(
max_returned_rows: NonZeroU32,
max_scanned_rows: NonZeroU64,
max_response_bytes: NonZeroU32,
) -> Self {
Self {
lane: QueryAdmissionLane::AdminAdHoc,
limit_requirement: LimitRequirement::Optional,
max_returned_rows: Some(max_returned_rows),
max_scanned_rows: Some(max_scanned_rows),
max_response_bytes: Some(max_response_bytes),
index_requirement: IndexRequirement::Optional,
offset_policy: OffsetPolicy::Allow,
full_scan_policy: FullScanPolicy::Allow,
materialized_sort_policy: MaterializedSortPolicy::Allow,
max_materialized_rows: Some(max_returned_rows),
max_projection_columns: None,
grouped: GroupedAdmissionPolicy::disabled(),
}
}
#[must_use]
pub const fn diagnostic_explain() -> Self {
Self {
lane: QueryAdmissionLane::DiagnosticExplain,
limit_requirement: LimitRequirement::Optional,
max_returned_rows: None,
max_scanned_rows: None,
max_response_bytes: None,
index_requirement: IndexRequirement::Optional,
offset_policy: OffsetPolicy::Allow,
full_scan_policy: FullScanPolicy::Allow,
materialized_sort_policy: MaterializedSortPolicy::Allow,
max_materialized_rows: None,
max_projection_columns: None,
grouped: GroupedAdmissionPolicy::disabled(),
}
}
#[must_use]
pub const fn dev_test_unbounded() -> Self {
Self {
lane: QueryAdmissionLane::DevTest,
limit_requirement: LimitRequirement::Optional,
max_returned_rows: None,
max_scanned_rows: None,
max_response_bytes: None,
index_requirement: IndexRequirement::Optional,
offset_policy: OffsetPolicy::Allow,
full_scan_policy: FullScanPolicy::Allow,
materialized_sort_policy: MaterializedSortPolicy::Allow,
max_materialized_rows: None,
max_projection_columns: None,
grouped: GroupedAdmissionPolicy::disabled(),
}
}
#[must_use]
pub const fn lane(&self) -> QueryAdmissionLane {
self.lane
}
#[must_use]
pub const fn require_limit(&self) -> bool {
matches!(self.limit_requirement, LimitRequirement::Required)
}
#[must_use]
pub const fn max_returned_rows(&self) -> Option<NonZeroU32> {
self.max_returned_rows
}
#[must_use]
pub const fn max_scanned_rows(&self) -> Option<NonZeroU64> {
self.max_scanned_rows
}
#[must_use]
pub const fn max_response_bytes(&self) -> Option<NonZeroU32> {
self.max_response_bytes
}
#[must_use]
pub const fn require_index(&self) -> bool {
matches!(self.index_requirement, IndexRequirement::Required)
}
#[must_use]
pub const fn reject_non_zero_offset(&self) -> bool {
matches!(self.offset_policy, OffsetPolicy::RejectNonZero)
}
#[must_use]
pub const fn allow_full_scan(&self) -> bool {
matches!(self.full_scan_policy, FullScanPolicy::Allow)
}
#[must_use]
pub const fn allow_materialized_sort(&self) -> bool {
matches!(self.materialized_sort_policy, MaterializedSortPolicy::Allow)
}
#[must_use]
pub const fn max_materialized_rows(&self) -> Option<NonZeroU32> {
self.max_materialized_rows
}
#[must_use]
pub const fn max_projection_columns(&self) -> Option<NonZeroU32> {
self.max_projection_columns
}
#[must_use]
pub const fn grouped(&self) -> GroupedAdmissionPolicy {
self.grouped
}
#[must_use]
pub const fn public_caps_are_finite(&self) -> bool {
!matches!(self.lane, QueryAdmissionLane::PublicRead)
|| (self.max_returned_rows.is_some() && self.max_response_bytes.is_some())
}
#[must_use]
pub fn evaluate(&self, mut summary: QueryAdmissionSummary) -> QueryAdmissionSummary {
summary.lane = self.lane;
match self.rejection_for_summary(&summary) {
Some(rejection) => summary.reject(rejection),
None => summary.admit(),
}
}
fn rejection_for_summary(
&self,
summary: &QueryAdmissionSummary,
) -> Option<QueryAdmissionRejection> {
if !self.lane.executes_rows() {
return Some(QueryAdmissionRejection::DiagnosticLaneDoesNotExecute);
}
if matches!(summary.plan_shape(), QueryAdmissionPlanShape::Delete) {
return Some(QueryAdmissionRejection::UnsupportedStatementForQueryLane);
}
if matches!(
summary.plan_shape(),
QueryAdmissionPlanShape::GroupedAggregate
) && !self.grouped.has_hard_limits()
{
return Some(QueryAdmissionRejection::GroupedQueryRequiresLimits);
}
if self.require_limit() && summary.limit().is_none() {
return Some(QueryAdmissionRejection::PublicQueryRequiresLimit);
}
if self.reject_non_zero_offset() && summary.offset().unwrap_or_default() != 0 {
return Some(QueryAdmissionRejection::PublicQueryOffsetRejected);
}
if let Some(rejection) = self.returned_row_bound_rejection(summary) {
return Some(rejection);
}
if !self.allow_full_scan() && summary.selected_access().is_full_scan() {
return Some(QueryAdmissionRejection::UnboundedFullScanRejected);
}
if self.require_index()
&& !access_satisfies_index_requirement(summary.selected_access(), summary.scan_bound())
{
return Some(QueryAdmissionRejection::PublicQueryRequiresIndex);
}
if let Some(rejection) = self.scan_bound_rejection(summary) {
return Some(rejection);
}
self.materialization_rejection(summary)
}
fn returned_row_bound_rejection(
&self,
summary: &QueryAdmissionSummary,
) -> Option<QueryAdmissionRejection> {
let max_returned_rows = self.max_returned_rows?;
if matches!(
summary.returned_row_bound_kind(),
QueryBoundKind::EstimateOnly
) {
return Some(QueryAdmissionRejection::EstimatedOnlyBoundRejected);
}
if !summary.returned_row_bound_kind().admits_public_read() {
return Some(QueryAdmissionRejection::ScanBoundUnavailable);
}
let Some(returned_row_bound) = summary.returned_row_bound() else {
return Some(QueryAdmissionRejection::ScanBoundUnavailable);
};
if returned_row_bound > max_returned_rows.get() {
return Some(QueryAdmissionRejection::ReturnedRowBoundExceedsPolicy);
}
None
}
fn scan_bound_rejection(
&self,
summary: &QueryAdmissionSummary,
) -> Option<QueryAdmissionRejection> {
let max_scanned_rows = self.max_scanned_rows?;
if matches!(summary.scan_bound_kind(), QueryBoundKind::EstimateOnly) {
return Some(QueryAdmissionRejection::EstimatedOnlyBoundRejected);
}
if !summary.scan_bound_kind().admits_public_read() {
return Some(QueryAdmissionRejection::ScanBoundUnavailable);
}
let Some(scan_bound) = summary.scan_bound() else {
return Some(QueryAdmissionRejection::ScanBoundUnavailable);
};
if scan_bound > max_scanned_rows.get() {
return Some(QueryAdmissionRejection::ScanBoundExceedsPolicy);
}
None
}
fn materialization_rejection(
&self,
summary: &QueryAdmissionSummary,
) -> Option<QueryAdmissionRejection> {
if !self.allow_materialized_sort() && summary.materialization().materialized_sort() {
return Some(QueryAdmissionRejection::SortRequiresMaterialization);
}
let max_materialized_rows = self.max_materialized_rows?;
let materialized_rows = summary.materialization().materialized_rows()?;
if materialized_rows > max_materialized_rows.get() {
Some(QueryAdmissionRejection::MaterializationExceedsBudget)
} else {
None
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct QueryMaterializationSummary {
materialized_sort: bool,
materialized_rows: Option<u32>,
row_bound_kind: QueryBoundKind,
}
impl QueryMaterializationSummary {
#[must_use]
pub const fn none() -> Self {
Self {
materialized_sort: false,
materialized_rows: None,
row_bound_kind: QueryBoundKind::Unavailable,
}
}
#[must_use]
pub const fn sort(materialized_rows: Option<u32>, row_bound_kind: QueryBoundKind) -> Self {
Self {
materialized_sort: true,
materialized_rows,
row_bound_kind,
}
}
#[must_use]
pub const fn materialized_sort(&self) -> bool {
self.materialized_sort
}
#[must_use]
pub const fn materialized_rows(&self) -> Option<u32> {
self.materialized_rows
}
#[must_use]
pub const fn row_bound_kind(&self) -> QueryBoundKind {
self.row_bound_kind
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QueryAdmissionRejection {
PublicQueryRequiresLimit,
PublicQueryRequiresIndex,
UnboundedFullScanRejected,
ScanBoundUnavailable,
ScanBoundExceedsPolicy,
EstimatedOnlyBoundRejected,
SortRequiresMaterialization,
MaterializationExceedsBudget,
ProjectionResponseMayExceedLimit,
GroupedQueryRequiresLimits,
GroupedQueryExceedsBudget,
DiagnosticLaneDoesNotExecute,
IntrospectionDisabledForLane,
UnsupportedStatementForQueryLane,
PublicQueryOffsetRejected,
ReturnedRowBoundExceedsPolicy,
}
impl QueryAdmissionRejection {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::PublicQueryRequiresLimit => "public_query_requires_limit",
Self::PublicQueryRequiresIndex => "public_query_requires_index",
Self::UnboundedFullScanRejected => "unbounded_full_scan_rejected",
Self::ScanBoundUnavailable => "scan_bound_unavailable",
Self::ScanBoundExceedsPolicy => "scan_bound_exceeds_policy",
Self::EstimatedOnlyBoundRejected => "estimated_only_bound_rejected",
Self::SortRequiresMaterialization => "sort_requires_materialization",
Self::MaterializationExceedsBudget => "materialization_exceeds_budget",
Self::ProjectionResponseMayExceedLimit => "projection_response_may_exceed_limit",
Self::GroupedQueryRequiresLimits => "grouped_query_requires_limits",
Self::GroupedQueryExceedsBudget => "grouped_query_exceeds_budget",
Self::DiagnosticLaneDoesNotExecute => "diagnostic_lane_does_not_execute",
Self::IntrospectionDisabledForLane => "introspection_disabled_for_lane",
Self::UnsupportedStatementForQueryLane => "unsupported_statement_for_query_lane",
Self::PublicQueryOffsetRejected => "public_query_offset_rejected",
Self::ReturnedRowBoundExceedsPolicy => "returned_row_bound_exceeds_policy",
}
}
#[must_use]
pub const fn code(self) -> QueryReadAdmissionCode {
match self {
Self::PublicQueryRequiresLimit => QueryReadAdmissionCode::PublicQueryRequiresLimit,
Self::PublicQueryRequiresIndex => QueryReadAdmissionCode::PublicQueryRequiresIndex,
Self::UnboundedFullScanRejected => QueryReadAdmissionCode::UnboundedFullScanRejected,
Self::ScanBoundUnavailable => QueryReadAdmissionCode::ScanBoundUnavailable,
Self::ScanBoundExceedsPolicy => QueryReadAdmissionCode::ScanBoundExceedsPolicy,
Self::EstimatedOnlyBoundRejected => QueryReadAdmissionCode::EstimatedOnlyBoundRejected,
Self::SortRequiresMaterialization => {
QueryReadAdmissionCode::SortRequiresMaterialization
}
Self::MaterializationExceedsBudget => {
QueryReadAdmissionCode::MaterializationExceedsBudget
}
Self::ProjectionResponseMayExceedLimit => {
QueryReadAdmissionCode::ProjectionResponseMayExceedLimit
}
Self::GroupedQueryRequiresLimits => QueryReadAdmissionCode::GroupedQueryRequiresLimits,
Self::GroupedQueryExceedsBudget => QueryReadAdmissionCode::GroupedQueryExceedsBudget,
Self::DiagnosticLaneDoesNotExecute => {
QueryReadAdmissionCode::DiagnosticLaneDoesNotExecute
}
Self::IntrospectionDisabledForLane => {
QueryReadAdmissionCode::IntrospectionDisabledForLane
}
Self::UnsupportedStatementForQueryLane => {
QueryReadAdmissionCode::UnsupportedStatementForQueryLane
}
Self::PublicQueryOffsetRejected => QueryReadAdmissionCode::PublicQueryOffsetRejected,
Self::ReturnedRowBoundExceedsPolicy => {
QueryReadAdmissionCode::ReturnedRowBoundExceedsPolicy
}
}
}
#[must_use]
pub const fn diagnostic(self) -> Diagnostic {
Diagnostic::new(
DiagnosticCode::QueryReadAdmission,
ErrorOrigin::Query,
Some(DiagnosticDetail::QueryReadAdmission {
reason: self.code(),
}),
)
}
#[must_use]
pub const fn error_code(self) -> ErrorCode {
self.diagnostic().error_code()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QueryAdmissionSummary {
lane: QueryAdmissionLane,
decision: QueryAdmissionDecision,
plan_shape: QueryAdmissionPlanShape,
selected_access: QueryAdmissionAccessKind,
selected_index: Option<String>,
limit: Option<u32>,
offset: Option<u32>,
scan_bound: Option<u64>,
scan_bound_kind: QueryBoundKind,
returned_row_bound: Option<u32>,
returned_row_bound_kind: QueryBoundKind,
response_byte_bound: Option<u32>,
response_byte_bound_kind: QueryBoundKind,
residual_filter: QueryAdmissionResidualFilter,
ordering: QueryAdmissionOrdering,
grouped: Option<QueryAdmissionGroupedSummary>,
materialization: QueryMaterializationSummary,
rejection: Option<QueryAdmissionRejection>,
}
impl QueryAdmissionSummary {
#[must_use]
pub const fn admitted(
lane: QueryAdmissionLane,
selected_access: QueryAdmissionAccessKind,
) -> Self {
Self {
lane,
decision: QueryAdmissionDecision::Admitted,
plan_shape: QueryAdmissionPlanShape::ScalarRead,
selected_access,
selected_index: None,
limit: None,
offset: None,
scan_bound: None,
scan_bound_kind: QueryBoundKind::Unavailable,
returned_row_bound: None,
returned_row_bound_kind: QueryBoundKind::Unavailable,
response_byte_bound: None,
response_byte_bound_kind: QueryBoundKind::Unavailable,
residual_filter: QueryAdmissionResidualFilter::Absent,
ordering: QueryAdmissionOrdering::None,
grouped: None,
materialization: QueryMaterializationSummary::none(),
rejection: None,
}
}
#[must_use]
pub const fn rejected(
lane: QueryAdmissionLane,
selected_access: QueryAdmissionAccessKind,
rejection: QueryAdmissionRejection,
) -> Self {
Self {
lane,
decision: QueryAdmissionDecision::Rejected,
plan_shape: QueryAdmissionPlanShape::ScalarRead,
selected_access,
selected_index: None,
limit: None,
offset: None,
scan_bound: None,
scan_bound_kind: QueryBoundKind::Unavailable,
returned_row_bound: None,
returned_row_bound_kind: QueryBoundKind::Unavailable,
response_byte_bound: None,
response_byte_bound_kind: QueryBoundKind::Unavailable,
residual_filter: QueryAdmissionResidualFilter::Absent,
ordering: QueryAdmissionOrdering::None,
grouped: None,
materialization: QueryMaterializationSummary::none(),
rejection: Some(rejection),
}
}
#[must_use]
pub(in crate::db) fn from_plan(lane: QueryAdmissionLane, plan: &AccessPlannedQuery) -> Self {
let access = summarize_access_plan(plan);
let grouped = plan.grouped_plan().map(summarize_grouped_plan);
let (limit, offset) = scalar_limit_and_offset(plan.scalar_plan());
let (returned_row_bound, returned_row_bound_kind) =
returned_row_bound_from_plan(limit, grouped);
let scan_bound_kind = access.scan_bound_kind();
Self {
lane,
decision: QueryAdmissionDecision::Admitted,
plan_shape: plan_shape(plan),
selected_access: access.kind,
selected_index: access.selected_index,
limit,
offset: Some(offset),
scan_bound: access.exact_scan_bound,
scan_bound_kind,
returned_row_bound,
returned_row_bound_kind,
response_byte_bound: None,
response_byte_bound_kind: QueryBoundKind::Unavailable,
residual_filter: admission_residual_filter(plan.residual_filter_shape()),
ordering: admission_ordering(plan),
grouped,
materialization: QueryMaterializationSummary::none(),
rejection: None,
}
}
const fn admit(mut self) -> Self {
self.decision = QueryAdmissionDecision::Admitted;
self.rejection = None;
self
}
const fn reject(mut self, rejection: QueryAdmissionRejection) -> Self {
self.decision = QueryAdmissionDecision::Rejected;
self.rejection = Some(rejection);
self
}
#[must_use]
pub const fn lane(&self) -> QueryAdmissionLane {
self.lane
}
#[must_use]
pub const fn decision(&self) -> QueryAdmissionDecision {
self.decision
}
#[must_use]
pub const fn plan_shape(&self) -> QueryAdmissionPlanShape {
self.plan_shape
}
#[must_use]
pub const fn selected_access(&self) -> QueryAdmissionAccessKind {
self.selected_access
}
#[must_use]
pub fn selected_index(&self) -> Option<&str> {
self.selected_index.as_deref()
}
#[must_use]
pub const fn limit(&self) -> Option<u32> {
self.limit
}
#[must_use]
pub const fn offset(&self) -> Option<u32> {
self.offset
}
#[must_use]
pub const fn scan_bound(&self) -> Option<u64> {
self.scan_bound
}
#[must_use]
pub const fn scan_bound_kind(&self) -> QueryBoundKind {
self.scan_bound_kind
}
#[must_use]
pub const fn returned_row_bound(&self) -> Option<u32> {
self.returned_row_bound
}
#[must_use]
pub const fn returned_row_bound_kind(&self) -> QueryBoundKind {
self.returned_row_bound_kind
}
#[must_use]
pub const fn response_byte_bound(&self) -> Option<u32> {
self.response_byte_bound
}
#[must_use]
pub const fn response_byte_bound_kind(&self) -> QueryBoundKind {
self.response_byte_bound_kind
}
#[must_use]
pub const fn residual_filter(&self) -> QueryAdmissionResidualFilter {
self.residual_filter
}
#[must_use]
pub const fn ordering(&self) -> QueryAdmissionOrdering {
self.ordering
}
#[must_use]
pub const fn grouped(&self) -> Option<QueryAdmissionGroupedSummary> {
self.grouped
}
#[must_use]
pub const fn materialization(&self) -> QueryMaterializationSummary {
self.materialization
}
#[must_use]
pub const fn rejection(&self) -> Option<QueryAdmissionRejection> {
self.rejection
}
#[must_use]
pub(in crate::db) fn render_text_block(&self) -> String {
let mut out = String::from("admission:");
push_text_field(&mut out, "lane", self.lane().as_str());
push_text_field(&mut out, "decision", self.decision().as_str());
push_text_field(
&mut out,
"reason",
self.rejection()
.map_or("none", QueryAdmissionRejection::as_str),
);
push_text_field(&mut out, "plan_shape", self.plan_shape().as_str());
push_text_field(&mut out, "selected_access", self.selected_access().as_str());
push_text_field(
&mut out,
"selected_index",
self.selected_index().unwrap_or("none"),
);
push_text_option_u32(&mut out, "limit", self.limit());
push_text_option_u32(&mut out, "offset", self.offset());
push_text_option_u64(&mut out, "scan_bound", self.scan_bound());
push_text_field(&mut out, "scan_bound_kind", self.scan_bound_kind().as_str());
push_text_option_u32(&mut out, "returned_row_bound", self.returned_row_bound());
push_text_field(
&mut out,
"returned_row_bound_kind",
self.returned_row_bound_kind().as_str(),
);
push_text_option_u32(&mut out, "response_byte_bound", self.response_byte_bound());
push_text_field(
&mut out,
"response_byte_bound_kind",
self.response_byte_bound_kind().as_str(),
);
push_text_field(&mut out, "residual_filter", self.residual_filter().as_str());
push_text_field(&mut out, "ordering", self.ordering().as_str());
push_text_bool(
&mut out,
"materialized_sort",
self.materialization().materialized_sort(),
);
push_text_option_u32(
&mut out,
"materialized_rows",
self.materialization().materialized_rows(),
);
push_text_field(
&mut out,
"materialized_row_bound_kind",
self.materialization().row_bound_kind().as_str(),
);
if let Some(grouped) = self.grouped() {
push_text_bool(&mut out, "grouped", true);
push_text_u64(
&mut out,
"group_field_count",
u64::from(grouped.group_field_count()),
);
push_text_u64(
&mut out,
"aggregate_count",
u64::from(grouped.aggregate_count()),
);
push_text_u64(&mut out, "max_groups", grouped.max_groups());
push_text_u64(&mut out, "max_group_bytes", grouped.max_group_bytes());
push_text_bool(&mut out, "having_filter", grouped.has_having_filter());
} else {
push_text_bool(&mut out, "grouped", false);
}
out
}
}
fn push_text_field(out: &mut String, key: &str, value: &str) {
out.push('\n');
out.push_str(" ");
out.push_str(key);
out.push('=');
out.push_str(value);
}
fn push_text_bool(out: &mut String, key: &str, value: bool) {
push_text_field(out, key, if value { "true" } else { "false" });
}
fn push_text_u64(out: &mut String, key: &str, value: u64) {
out.push('\n');
out.push_str(" ");
out.push_str(key);
out.push('=');
let _ = write!(out, "{value}");
}
fn push_text_option_u32(out: &mut String, key: &str, value: Option<u32>) {
match value {
Some(value) => push_text_u64(out, key, u64::from(value)),
None => push_text_field(out, key, "none"),
}
}
fn push_text_option_u64(out: &mut String, key: &str, value: Option<u64>) {
match value {
Some(value) => push_text_u64(out, key, value),
None => push_text_field(out, key, "none"),
}
}
const _: fn(QueryAdmissionLane, &AccessPlannedQuery) -> QueryAdmissionSummary =
QueryAdmissionSummary::from_plan;
const fn access_satisfies_index_requirement(
kind: QueryAdmissionAccessKind,
scan_bound: Option<u64>,
) -> bool {
kind.is_secondary_index()
|| matches!(
(kind, scan_bound),
(
QueryAdmissionAccessKind::ByKey | QueryAdmissionAccessKind::ByKeys,
Some(_)
)
)
}
struct AdmissionAccessProjection;
#[derive(Clone, Debug, Eq, PartialEq)]
struct AdmissionAccessSummary {
kind: QueryAdmissionAccessKind,
selected_index: Option<String>,
exact_scan_bound: Option<u64>,
}
impl AdmissionAccessSummary {
const fn non_index(kind: QueryAdmissionAccessKind, exact_scan_bound: Option<u64>) -> Self {
Self {
kind,
selected_index: None,
exact_scan_bound,
}
}
fn secondary_index(kind: QueryAdmissionAccessKind, index_name: &str) -> Self {
Self {
kind,
selected_index: Some(index_name.to_string()),
exact_scan_bound: None,
}
}
const fn composite(kind: QueryAdmissionAccessKind) -> Self {
Self {
kind,
selected_index: None,
exact_scan_bound: None,
}
}
const fn scan_bound_kind(&self) -> QueryBoundKind {
if self.exact_scan_bound.is_some() {
QueryBoundKind::Exact
} else {
QueryBoundKind::Unavailable
}
}
}
impl AccessPlanProjection<Value> for AdmissionAccessProjection {
type Output = AdmissionAccessSummary;
fn by_key(&mut self, _key: &Value) -> Self::Output {
AdmissionAccessSummary::non_index(QueryAdmissionAccessKind::ByKey, Some(1))
}
fn by_keys(&mut self, keys: &[Value]) -> Self::Output {
AdmissionAccessSummary::non_index(
QueryAdmissionAccessKind::ByKeys,
Some(u64::try_from(keys.len()).unwrap_or(u64::MAX)),
)
}
fn key_range(&mut self, _start: &Value, _end: &Value) -> Self::Output {
AdmissionAccessSummary::non_index(QueryAdmissionAccessKind::KeyRange, None)
}
fn index_prefix(
&mut self,
index_name: &str,
_index_fields: &[String],
_prefix_len: usize,
_values: &[Value],
) -> Self::Output {
AdmissionAccessSummary::secondary_index(QueryAdmissionAccessKind::IndexPrefix, index_name)
}
fn index_multi_lookup(
&mut self,
index_name: &str,
_index_fields: &[String],
_values: &[Value],
) -> Self::Output {
AdmissionAccessSummary::secondary_index(
QueryAdmissionAccessKind::IndexMultiLookup,
index_name,
)
}
fn index_branch_set(
&mut self,
index_name: &str,
_index_fields: &[String],
_fixed_values: &[Value],
_branch_values: &[Value],
_ordered_suffix: IndexBranchSetOrderedSuffix,
) -> Self::Output {
AdmissionAccessSummary::secondary_index(
QueryAdmissionAccessKind::IndexBranchSet,
index_name,
)
}
fn index_range(
&mut self,
index_name: &str,
_index_fields: &[String],
_prefix_len: usize,
_prefix: &[Value],
_lower: &Bound<Value>,
_upper: &Bound<Value>,
) -> Self::Output {
AdmissionAccessSummary::secondary_index(QueryAdmissionAccessKind::IndexRange, index_name)
}
fn full_scan(&mut self) -> Self::Output {
AdmissionAccessSummary::non_index(QueryAdmissionAccessKind::FullScan, None)
}
fn union(&mut self, _children: Vec<Self::Output>) -> Self::Output {
AdmissionAccessSummary::composite(QueryAdmissionAccessKind::Union)
}
fn intersection(&mut self, _children: Vec<Self::Output>) -> Self::Output {
AdmissionAccessSummary::composite(QueryAdmissionAccessKind::Intersection)
}
}
fn summarize_access_plan(plan: &AccessPlannedQuery) -> AdmissionAccessSummary {
project_access_plan(&plan.access, &mut AdmissionAccessProjection)
}
fn summarize_grouped_plan(plan: &GroupPlan) -> QueryAdmissionGroupedSummary {
QueryAdmissionGroupedSummary::new(
u32::try_from(plan.group.group_fields.len()).unwrap_or(u32::MAX),
u32::try_from(plan.group.aggregates.len()).unwrap_or(u32::MAX),
plan.group.execution.max_groups(),
plan.group.execution.max_group_bytes(),
plan.having_expr.is_some(),
)
}
const fn scalar_limit_and_offset(plan: &ScalarPlan) -> (Option<u32>, u32) {
match plan.mode {
QueryMode::Load(load) => match &plan.page {
Some(page) => (page.limit, page.offset),
None => (load.limit(), load.offset()),
},
QueryMode::Delete(delete) => match plan.delete_limit {
Some(delete_limit) => (delete_limit.limit, delete_limit.offset),
None => (delete.limit(), delete.offset()),
},
}
}
fn returned_row_bound_from_plan(
limit: Option<u32>,
grouped: Option<QueryAdmissionGroupedSummary>,
) -> (Option<u32>, QueryBoundKind) {
if let Some(limit) = limit {
return (Some(limit), QueryBoundKind::EnforcedRuntimeCap);
}
let Some(grouped) = grouped else {
return (None, QueryBoundKind::Unavailable);
};
if grouped.max_groups() == u64::MAX {
return (None, QueryBoundKind::Unavailable);
}
(
Some(u32::try_from(grouped.max_groups()).unwrap_or(u32::MAX)),
QueryBoundKind::ConservativeUpperBound,
)
}
const fn admission_residual_filter(shape: ResidualFilterShape) -> QueryAdmissionResidualFilter {
match shape {
ResidualFilterShape::Absent => QueryAdmissionResidualFilter::Absent,
ResidualFilterShape::Predicate => QueryAdmissionResidualFilter::Predicate,
ResidualFilterShape::Expression => QueryAdmissionResidualFilter::Expression,
ResidualFilterShape::ExpressionAndPredicate => {
QueryAdmissionResidualFilter::ExpressionAndPredicate
}
}
}
fn admission_ordering(plan: &AccessPlannedQuery) -> QueryAdmissionOrdering {
if plan.scalar_plan().order.is_none() {
return QueryAdmissionOrdering::None;
}
if plan.resolved_order().is_some() {
QueryAdmissionOrdering::Resolved
} else {
QueryAdmissionOrdering::Requested
}
}
const fn plan_shape(plan: &AccessPlannedQuery) -> QueryAdmissionPlanShape {
if plan.grouped_plan().is_some() {
return QueryAdmissionPlanShape::GroupedAggregate;
}
match plan.scalar_plan().mode {
QueryMode::Load(_) => QueryAdmissionPlanShape::ScalarRead,
QueryMode::Delete(_) => QueryAdmissionPlanShape::Delete,
}
}
#[cfg(test)]
mod tests {
use std::num::{NonZeroU32, NonZeroU64};
use crate::{
db::{
access::{AccessPath, SemanticIndexAccessContract},
predicate::{MissingRowPolicy, Predicate},
query::plan::{
AccessPlannedQuery, AggregateKind, DeleteLimitSpec, DeleteSpec, FieldSlot,
GroupAggregateSpec, GroupSpec, GroupedExecutionConfig, OrderDirection, OrderSpec,
OrderTerm, PageSpec, QueryMode,
expr::{Expr, FieldId},
},
},
model::index::IndexModel,
value::Value,
};
use super::{
GroupedAdmissionPolicy, QueryAdmissionAccessKind, QueryAdmissionDecision,
QueryAdmissionLane, QueryAdmissionOrdering, QueryAdmissionPlanShape, QueryAdmissionPolicy,
QueryAdmissionRejection, QueryAdmissionResidualFilter, QueryAdmissionSummary,
QueryBoundKind,
};
const ADMISSION_INDEX_FIELDS: [&str; 1] = ["tag"];
const ADMISSION_INDEX: IndexModel = IndexModel::generated(
"admission::tag",
"admission::tag_store",
&ADMISSION_INDEX_FIELDS,
false,
);
#[test]
fn public_read_policy_has_safe_finite_defaults() {
let max_rows = NonZeroU32::new(50).expect("test max rows is non-zero");
let max_bytes = NonZeroU32::new(32_768).expect("test max bytes is non-zero");
let policy = QueryAdmissionPolicy::public_read(max_rows, max_bytes);
assert_eq!(policy.lane(), QueryAdmissionLane::PublicRead);
assert!(policy.require_limit());
assert!(policy.require_index());
assert!(policy.reject_non_zero_offset());
assert!(!policy.allow_full_scan());
assert!(!policy.allow_materialized_sort());
assert_eq!(policy.max_returned_rows(), Some(max_rows));
assert_eq!(policy.max_response_bytes(), Some(max_bytes));
assert!(policy.public_caps_are_finite());
assert!(!policy.grouped().has_hard_limits());
}
#[test]
fn admin_policy_is_broader_but_still_budgeted() {
let max_rows = NonZeroU32::new(100).expect("test max rows is non-zero");
let max_scanned = NonZeroU64::new(1_000).expect("test scan cap is non-zero");
let max_bytes = NonZeroU32::new(65_536).expect("test max bytes is non-zero");
let policy = QueryAdmissionPolicy::admin_ad_hoc(max_rows, max_scanned, max_bytes);
assert_eq!(policy.lane(), QueryAdmissionLane::AdminAdHoc);
assert!(!policy.require_limit());
assert!(!policy.require_index());
assert!(policy.allow_full_scan());
assert!(policy.allow_materialized_sort());
assert_eq!(policy.max_scanned_rows(), Some(max_scanned));
assert_eq!(policy.max_materialized_rows(), Some(max_rows));
}
#[test]
fn diagnostic_explain_lane_does_not_execute_rows() {
let policy = QueryAdmissionPolicy::diagnostic_explain();
assert_eq!(policy.lane().as_str(), "diagnostic_explain");
assert!(!policy.lane().executes_rows());
}
#[test]
fn grouped_policy_requires_group_and_memory_budgets() {
let max_groups = NonZeroU32::new(8).expect("test group cap is non-zero");
let max_bytes = NonZeroU32::new(4096).expect("test byte cap is non-zero");
let policy = GroupedAdmissionPolicy::bounded(max_groups, max_bytes, None);
assert!(policy.has_hard_limits());
assert_eq!(policy.max_groups(), Some(max_groups));
assert_eq!(policy.max_group_bytes(), Some(max_bytes));
}
#[test]
fn only_proven_or_enforced_bounds_admit_public_reads() {
assert!(QueryBoundKind::Exact.admits_public_read());
assert!(QueryBoundKind::ConservativeUpperBound.admits_public_read());
assert!(QueryBoundKind::EnforcedRuntimeCap.admits_public_read());
assert!(!QueryBoundKind::EstimateOnly.admits_public_read());
assert!(!QueryBoundKind::Unavailable.admits_public_read());
}
#[test]
fn access_kind_classifies_secondary_indexes_and_full_scans() {
assert!(QueryAdmissionAccessKind::IndexPrefix.is_secondary_index());
assert!(QueryAdmissionAccessKind::FullScan.is_full_scan());
assert!(!QueryAdmissionAccessKind::ByKey.is_secondary_index());
}
#[test]
fn rejection_maps_to_stable_diagnostic() {
let rejection = QueryAdmissionRejection::PublicQueryRequiresLimit;
let diagnostic = rejection.diagnostic();
assert_eq!(
rejection.error_code(),
icydb_diagnostic_code::ErrorCode::QUERY_READ_PUBLIC_REQUIRES_LIMIT
);
assert_eq!(
diagnostic.code(),
icydb_diagnostic_code::DiagnosticCode::QueryReadAdmission
);
}
#[test]
fn summaries_keep_decision_and_rejection_aligned() {
let admitted = QueryAdmissionSummary::admitted(
QueryAdmissionLane::PublicRead,
QueryAdmissionAccessKind::ByKey,
);
let rejected = QueryAdmissionSummary::rejected(
QueryAdmissionLane::PublicRead,
QueryAdmissionAccessKind::FullScan,
QueryAdmissionRejection::UnboundedFullScanRejected,
);
assert_eq!(admitted.decision(), QueryAdmissionDecision::Admitted);
assert_eq!(admitted.rejection(), None);
assert_eq!(rejected.decision(), QueryAdmissionDecision::Rejected);
assert_eq!(
rejected.rejection(),
Some(QueryAdmissionRejection::UnboundedFullScanRejected)
);
}
#[test]
fn admission_summary_renders_stable_verbose_explain_block() {
let summary = QueryAdmissionSummary::rejected(
QueryAdmissionLane::PublicRead,
QueryAdmissionAccessKind::FullScan,
QueryAdmissionRejection::UnboundedFullScanRejected,
);
let rendered = summary.render_text_block();
assert!(
rendered.starts_with("admission:\n lane=public_read\n decision=rejected"),
"admission block should start with stable lane and decision fields: {rendered}",
);
assert!(
rendered.contains("\n reason=unbounded_full_scan_rejected"),
"admission block should include a stable rejection reason: {rendered}",
);
assert!(
rendered.contains("\n selected_access=full_scan"),
"admission block should include the selected access class: {rendered}",
);
assert!(
rendered.contains("\n grouped=false"),
"admission block should include grouped classification: {rendered}",
);
}
#[test]
fn plan_summary_classifies_full_scan_without_overclaiming_bounds() {
let plan = AccessPlannedQuery::new(AccessPath::<Value>::FullScan, MissingRowPolicy::Ignore);
let summary = QueryAdmissionSummary::from_plan(QueryAdmissionLane::PublicRead, &plan);
assert_eq!(summary.plan_shape(), QueryAdmissionPlanShape::ScalarRead);
assert_eq!(
summary.selected_access(),
QueryAdmissionAccessKind::FullScan
);
assert_eq!(summary.selected_index(), None);
assert_eq!(summary.limit(), None);
assert_eq!(summary.offset(), Some(0));
assert_eq!(summary.scan_bound(), None);
assert_eq!(summary.scan_bound_kind(), QueryBoundKind::Unavailable);
assert_eq!(summary.returned_row_bound(), None);
assert_eq!(
summary.returned_row_bound_kind(),
QueryBoundKind::Unavailable
);
assert_eq!(
summary.residual_filter(),
QueryAdmissionResidualFilter::Absent
);
assert_eq!(summary.ordering(), QueryAdmissionOrdering::None);
}
#[test]
fn plan_summary_uses_point_lookup_and_limit_as_proven_bounds() {
let mut plan =
AccessPlannedQuery::new(AccessPath::ByKey(Value::Nat64(7)), MissingRowPolicy::Ignore);
plan.scalar_plan_mut().page = Some(PageSpec {
limit: Some(5),
offset: 2,
});
let summary = QueryAdmissionSummary::from_plan(QueryAdmissionLane::PublicRead, &plan);
assert_eq!(summary.selected_access(), QueryAdmissionAccessKind::ByKey);
assert_eq!(summary.limit(), Some(5));
assert_eq!(summary.offset(), Some(2));
assert_eq!(summary.scan_bound(), Some(1));
assert_eq!(summary.scan_bound_kind(), QueryBoundKind::Exact);
assert_eq!(summary.returned_row_bound(), Some(5));
assert_eq!(
summary.returned_row_bound_kind(),
QueryBoundKind::EnforcedRuntimeCap
);
}
#[test]
fn plan_summary_preserves_selected_index_identity() {
let plan = AccessPlannedQuery::new(
AccessPath::IndexPrefix {
index: SemanticIndexAccessContract::model_only_from_generated_index(
ADMISSION_INDEX,
),
values: vec![Value::Text("alpha".to_string())],
},
MissingRowPolicy::Ignore,
);
let summary = QueryAdmissionSummary::from_plan(QueryAdmissionLane::PublicRead, &plan);
assert_eq!(
summary.selected_access(),
QueryAdmissionAccessKind::IndexPrefix
);
assert_eq!(summary.selected_index(), Some("admission::tag"));
assert_eq!(summary.scan_bound(), None);
assert_eq!(summary.scan_bound_kind(), QueryBoundKind::Unavailable);
}
#[test]
fn plan_summary_classifies_residual_and_requested_ordering() {
let mut plan =
AccessPlannedQuery::new(AccessPath::<Value>::FullScan, MissingRowPolicy::Ignore);
plan.scalar_plan_mut().predicate = Some(Predicate::eq(
"tag".to_string(),
Value::Text("alpha".to_string()),
));
plan.scalar_plan_mut().order = Some(OrderSpec {
fields: vec![OrderTerm::field("tag", OrderDirection::Asc)],
});
let summary = QueryAdmissionSummary::from_plan(QueryAdmissionLane::AdminAdHoc, &plan);
assert_eq!(
summary.residual_filter(),
QueryAdmissionResidualFilter::Predicate
);
assert_eq!(summary.ordering(), QueryAdmissionOrdering::Requested);
}
#[test]
fn plan_summary_carries_grouped_execution_budgets() {
let grouped =
AccessPlannedQuery::new(AccessPath::<Value>::FullScan, MissingRowPolicy::Ignore)
.into_grouped_with_having_expr(
GroupSpec {
group_fields: vec![FieldSlot::from_test_slot(0, "tag")],
aggregates: vec![GroupAggregateSpec {
kind: AggregateKind::Count,
input_expr: None,
filter_expr: None,
distinct: false,
}],
execution: GroupedExecutionConfig::with_hard_limits(12, 4096),
},
Some(Expr::Field(FieldId::new("tag"))),
);
let summary =
QueryAdmissionSummary::from_plan(QueryAdmissionLane::DiagnosticExplain, &grouped);
let grouped = summary
.grouped()
.expect("summary should include grouped facts");
assert_eq!(
summary.plan_shape(),
QueryAdmissionPlanShape::GroupedAggregate
);
assert_eq!(grouped.group_field_count(), 1);
assert_eq!(grouped.aggregate_count(), 1);
assert_eq!(grouped.max_groups(), 12);
assert_eq!(grouped.max_group_bytes(), 4096);
assert!(grouped.has_having_filter());
assert_eq!(summary.returned_row_bound(), Some(12));
assert_eq!(
summary.returned_row_bound_kind(),
QueryBoundKind::ConservativeUpperBound
);
}
#[test]
fn plan_summary_reads_delete_window_without_executing_it() {
let mut plan =
AccessPlannedQuery::new(AccessPath::<Value>::FullScan, MissingRowPolicy::Ignore);
plan.scalar_plan_mut().mode = QueryMode::Delete(DeleteSpec::new());
plan.scalar_plan_mut().delete_limit = Some(DeleteLimitSpec {
limit: Some(3),
offset: 1,
});
let summary =
QueryAdmissionSummary::from_plan(QueryAdmissionLane::DiagnosticExplain, &plan);
assert_eq!(summary.plan_shape(), QueryAdmissionPlanShape::Delete);
assert_eq!(summary.limit(), Some(3));
assert_eq!(summary.offset(), Some(1));
assert_eq!(summary.returned_row_bound(), Some(3));
}
#[test]
fn public_read_evaluation_rejects_missing_limit_before_access_shape() {
let policy = public_read_policy();
let summary = summary_for_index_prefix(None, 0);
let evaluated = policy.evaluate(summary);
assert_eq!(evaluated.decision(), QueryAdmissionDecision::Rejected);
assert_eq!(
evaluated.rejection(),
Some(QueryAdmissionRejection::PublicQueryRequiresLimit)
);
}
#[test]
fn public_read_evaluation_rejects_full_scan_even_with_limit() {
let policy = public_read_policy();
let summary = summary_for_path(AccessPath::<Value>::FullScan, Some(5), 0);
let evaluated = policy.evaluate(summary);
assert_eq!(evaluated.decision(), QueryAdmissionDecision::Rejected);
assert_eq!(
evaluated.rejection(),
Some(QueryAdmissionRejection::UnboundedFullScanRejected)
);
}
#[test]
fn public_read_evaluation_admits_indexed_bounded_scalar_read() {
let policy = public_read_policy();
let summary = summary_for_index_prefix(Some(5), 0);
let evaluated = policy.evaluate(summary);
assert_eq!(evaluated.decision(), QueryAdmissionDecision::Admitted);
assert_eq!(evaluated.rejection(), None);
}
#[test]
fn public_read_evaluation_admits_exact_primary_key_read() {
let policy = public_read_policy();
let summary = summary_for_path(
AccessPath::ByKey(Value::Text("primary".to_string())),
Some(1),
0,
);
let evaluated = policy.evaluate(summary);
assert_eq!(evaluated.decision(), QueryAdmissionDecision::Admitted);
assert_eq!(evaluated.scan_bound(), Some(1));
}
#[test]
fn public_read_evaluation_rejects_non_zero_offset() {
let policy = public_read_policy();
let summary = summary_for_index_prefix(Some(5), 1);
let evaluated = policy.evaluate(summary);
assert_eq!(evaluated.decision(), QueryAdmissionDecision::Rejected);
assert_eq!(
evaluated.rejection(),
Some(QueryAdmissionRejection::PublicQueryOffsetRejected)
);
}
#[test]
fn public_read_evaluation_rejects_returned_row_cap_overflow() {
let policy = public_read_policy();
let summary = summary_for_index_prefix(Some(51), 0);
let evaluated = policy.evaluate(summary);
assert_eq!(evaluated.decision(), QueryAdmissionDecision::Rejected);
assert_eq!(
evaluated.rejection(),
Some(QueryAdmissionRejection::ReturnedRowBoundExceedsPolicy)
);
}
#[test]
fn diagnostic_explain_policy_rejects_row_execution() {
let policy = QueryAdmissionPolicy::diagnostic_explain();
let summary = summary_for_index_prefix(Some(5), 0);
let evaluated = policy.evaluate(summary);
assert_eq!(evaluated.decision(), QueryAdmissionDecision::Rejected);
assert_eq!(
evaluated.rejection(),
Some(QueryAdmissionRejection::DiagnosticLaneDoesNotExecute)
);
}
fn public_read_policy() -> QueryAdmissionPolicy {
QueryAdmissionPolicy::public_read(
NonZeroU32::new(50).expect("test public row cap is non-zero"),
NonZeroU32::new(32_768).expect("test public byte cap is non-zero"),
)
}
fn summary_for_index_prefix(limit: Option<u32>, offset: u32) -> QueryAdmissionSummary {
summary_for_path(
AccessPath::IndexPrefix {
index: SemanticIndexAccessContract::model_only_from_generated_index(
ADMISSION_INDEX,
),
values: vec![Value::Text("alpha".to_string())],
},
limit,
offset,
)
}
fn summary_for_path(
path: AccessPath<Value>,
limit: Option<u32>,
offset: u32,
) -> QueryAdmissionSummary {
let mut plan = AccessPlannedQuery::new(path, MissingRowPolicy::Ignore);
plan.scalar_plan_mut().page = Some(PageSpec { limit, offset });
QueryAdmissionSummary::from_plan(QueryAdmissionLane::PublicRead, &plan)
}
}