mod plan_summary;
mod policy;
mod render;
use crate::db::query::plan::AccessPlannedQuery;
use icydb_diagnostic_code::{
Diagnostic, DiagnosticCode, DiagnosticDetail, ErrorCode, ErrorOrigin, QueryReadAdmissionCode,
};
#[cfg(test)]
pub(in crate::db) use policy::GroupedAdmissionPolicy;
pub(in crate::db) use policy::QueryAdmissionPolicy;
pub(in crate::db::query) use policy::{
DEFAULT_BOUNDED_READ_MAX_ROWS, DEFAULT_BOUNDED_READ_RESPONSE_BYTES,
};
#[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,
distinct_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,
distinct_aggregate_count: u32,
max_groups: u64,
max_group_bytes: u64,
having_filter: bool,
) -> Self {
Self {
group_field_count,
aggregate_count,
distinct_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 distinct_aggregate_count(self) -> u32 {
self.distinct_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 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,
PrimaryKeyInputExceedsPolicy,
}
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",
Self::PrimaryKeyInputExceedsPolicy => "primary_key_input_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
}
Self::PrimaryKeyInputExceedsPolicy => {
QueryReadAdmissionCode::PrimaryKeyInputExceedsPolicy
}
}
}
#[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,
primary_key_input_terms: Option<u32>,
primary_key_input_payload_bytes: Option<u32>,
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,
primary_key_input_terms: None,
primary_key_input_payload_bytes: None,
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,
primary_key_input_terms: None,
primary_key_input_payload_bytes: None,
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 {
plan_summary::summary_from_plan(lane, plan)
}
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 primary_key_input_terms(&self) -> Option<u32> {
self.primary_key_input_terms
}
#[must_use]
pub const fn primary_key_input_payload_bytes(&self) -> Option<u32> {
self.primary_key_input_payload_bytes
}
#[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]
#[cfg_attr(not(feature = "sql"), allow(dead_code))]
pub(in crate::db) const fn with_materialization(
mut self,
materialization: QueryMaterializationSummary,
) -> Self {
self.materialization = materialization;
self
}
#[must_use]
pub const fn rejection(&self) -> Option<QueryAdmissionRejection> {
self.rejection
}
#[must_use]
pub(in crate::db) fn render_text_block(&self) -> String {
render::render_text_block(self)
}
}
#[cfg(test)]
mod tests;