use std::num::NonZeroU32;
use super::{
QueryAdmissionAccessKind, QueryAdmissionLane, QueryAdmissionRejection, QueryAdmissionSummary,
QueryBoundKind, plan_summary,
};
pub(in crate::db::query) const DEFAULT_BOUNDED_READ_MAX_ROWS: u32 = 100;
const DEFAULT_BOUNDED_READ_MAX_GROUPS: u32 = 100;
const DEFAULT_BOUNDED_READ_MAX_GROUP_BYTES: u32 = 64 * 1024;
const DEFAULT_BOUNDED_READ_MAX_DISTINCT_ENTRIES: u32 = 1024;
const DEFAULT_BOUNDED_READ_MAX_PRIMARY_KEY_INPUT_TERMS: u32 = 1024;
const DEFAULT_BOUNDED_READ_MAX_PRIMARY_KEY_INPUT_BYTES: u32 = 64 * 1024;
const fn non_zero_default(value: u32) -> NonZeroU32 {
match NonZeroU32::new(value) {
Some(value) => value,
None => NonZeroU32::MIN,
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) struct GroupedAdmissionPolicy {
groups: Option<NonZeroU32>,
group_bytes: Option<NonZeroU32>,
distinct_entries: Option<NonZeroU32>,
}
impl GroupedAdmissionPolicy {
#[must_use]
pub(in crate::db) const fn disabled() -> Self {
Self {
groups: None,
group_bytes: None,
distinct_entries: None,
}
}
#[must_use]
pub(in crate::db) 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(in crate::db) const fn default_bounded_read() -> Self {
Self::bounded(
non_zero_default(DEFAULT_BOUNDED_READ_MAX_GROUPS),
non_zero_default(DEFAULT_BOUNDED_READ_MAX_GROUP_BYTES),
Some(non_zero_default(DEFAULT_BOUNDED_READ_MAX_DISTINCT_ENTRIES)),
)
}
#[must_use]
pub(in crate::db) const fn max_groups(&self) -> Option<NonZeroU32> {
self.groups
}
#[must_use]
pub(in crate::db) const fn max_group_bytes(&self) -> Option<NonZeroU32> {
self.group_bytes
}
#[must_use]
pub(in crate::db) const fn max_distinct_entries(&self) -> Option<NonZeroU32> {
self.distinct_entries
}
#[must_use]
#[cfg(test)]
pub(in crate::db) 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, Debug, Eq, PartialEq)]
pub(in crate::db) struct QueryAdmissionPolicy {
lane: QueryAdmissionLane,
limit_requirement: LimitRequirement,
max_returned_rows: Option<NonZeroU32>,
max_primary_key_input_terms: Option<NonZeroU32>,
max_primary_key_input_bytes: Option<NonZeroU32>,
index_requirement: IndexRequirement,
full_scan_policy: FullScanPolicy,
materialized_sort_policy: MaterializedSortPolicy,
grouped: GroupedAdmissionPolicy,
}
impl QueryAdmissionPolicy {
#[must_use]
pub(in crate::db) const fn public_read(max_returned_rows: NonZeroU32) -> Self {
Self {
lane: QueryAdmissionLane::PublicRead,
limit_requirement: LimitRequirement::Required,
max_returned_rows: Some(max_returned_rows),
max_primary_key_input_terms: Some(non_zero_default(
DEFAULT_BOUNDED_READ_MAX_PRIMARY_KEY_INPUT_TERMS,
)),
max_primary_key_input_bytes: Some(non_zero_default(
DEFAULT_BOUNDED_READ_MAX_PRIMARY_KEY_INPUT_BYTES,
)),
index_requirement: IndexRequirement::Required,
full_scan_policy: FullScanPolicy::Reject,
materialized_sort_policy: MaterializedSortPolicy::Reject,
grouped: GroupedAdmissionPolicy::disabled(),
}
}
#[must_use]
pub(in crate::db) const fn default_bounded_read() -> Self {
Self::public_read(non_zero_default(DEFAULT_BOUNDED_READ_MAX_ROWS))
.with_grouped_policy(GroupedAdmissionPolicy::default_bounded_read())
}
#[must_use]
pub(in crate::db) const fn with_grouped_policy(
mut self,
grouped: GroupedAdmissionPolicy,
) -> Self {
self.grouped = grouped;
self
}
#[must_use]
pub(in crate::db) const fn diagnostic_explain() -> Self {
Self {
lane: QueryAdmissionLane::DiagnosticExplain,
limit_requirement: LimitRequirement::Optional,
max_returned_rows: None,
max_primary_key_input_terms: None,
max_primary_key_input_bytes: None,
index_requirement: IndexRequirement::Optional,
full_scan_policy: FullScanPolicy::Allow,
materialized_sort_policy: MaterializedSortPolicy::Allow,
grouped: GroupedAdmissionPolicy::disabled(),
}
}
#[must_use]
pub(in crate::db) const fn lane(&self) -> QueryAdmissionLane {
self.lane
}
#[must_use]
pub(in crate::db) const fn require_limit(&self) -> bool {
matches!(self.limit_requirement, LimitRequirement::Required)
}
#[must_use]
#[cfg(test)]
pub(in crate::db) const fn max_returned_rows(&self) -> Option<NonZeroU32> {
self.max_returned_rows
}
#[must_use]
pub(in crate::db) const fn require_index(&self) -> bool {
matches!(self.index_requirement, IndexRequirement::Required)
}
#[must_use]
pub(in crate::db) const fn allow_full_scan(&self) -> bool {
matches!(self.full_scan_policy, FullScanPolicy::Allow)
}
#[must_use]
pub(in crate::db) const fn allow_materialized_sort(&self) -> bool {
matches!(self.materialized_sort_policy, MaterializedSortPolicy::Allow)
}
#[must_use]
#[cfg(test)]
pub(in crate::db) const fn grouped(&self) -> GroupedAdmissionPolicy {
self.grouped
}
#[must_use]
#[cfg(test)]
pub(in crate::db) const fn with_primary_key_input_caps(
mut self,
max_terms: NonZeroU32,
max_bytes: NonZeroU32,
) -> Self {
self.max_primary_key_input_terms = Some(max_terms);
self.max_primary_key_input_bytes = Some(max_bytes);
self
}
#[must_use]
pub(in crate::db) 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 let Some(rejection) = self.grouped_rejection(summary) {
return Some(rejection);
}
if !self.allow_full_scan() && summary.selected_access().is_full_scan() {
return Some(QueryAdmissionRejection::UnboundedFullScanRejected);
}
if self.require_index()
&& !plan_summary::access_satisfies_index_requirement(
summary.selected_access(),
summary.scan_bound(),
)
{
return Some(QueryAdmissionRejection::PublicQueryRequiresIndex);
}
if self.require_limit()
&& summary.limit().is_none()
&& summary.grouped().is_none()
&& !summary.returned_row_bound_kind().admits_public_read()
{
return Some(QueryAdmissionRejection::PublicQueryRequiresLimit);
}
if let Some(rejection) = self.returned_row_bound_rejection(summary) {
return Some(rejection);
}
if let Some(rejection) = self.primary_key_input_rejection(summary) {
return Some(rejection);
}
self.materialization_rejection(summary)
}
fn grouped_rejection(
&self,
summary: &QueryAdmissionSummary,
) -> Option<QueryAdmissionRejection> {
let grouped = summary.grouped()?;
let Some(max_groups) = self.grouped.max_groups() else {
return Some(QueryAdmissionRejection::GroupedQueryRequiresLimits);
};
let Some(max_group_bytes) = self.grouped.max_group_bytes() else {
return Some(QueryAdmissionRejection::GroupedQueryRequiresLimits);
};
if grouped.max_groups() == u64::MAX || grouped.max_group_bytes() == u64::MAX {
return Some(QueryAdmissionRejection::GroupedQueryRequiresLimits);
}
if grouped.max_groups() > u64::from(max_groups.get())
|| grouped.max_group_bytes() > u64::from(max_group_bytes.get())
{
return Some(QueryAdmissionRejection::GroupedQueryExceedsBudget);
}
if grouped.distinct_aggregate_count() > 0 && self.grouped.max_distinct_entries().is_none() {
return Some(QueryAdmissionRejection::GroupedQueryRequiresLimits);
}
None
}
fn returned_row_bound_rejection(
&self,
summary: &QueryAdmissionSummary,
) -> Option<QueryAdmissionRejection> {
let max_returned_rows = self.max_returned_rows?;
let Some(returned_row_bound) = summary
.returned_row_bound()
.filter(|_| summary.returned_row_bound_kind().admits_public_read())
else {
return Some(QueryAdmissionRejection::PublicQueryRequiresLimit);
};
if returned_row_bound > max_returned_rows.get() {
return Some(QueryAdmissionRejection::ReturnedRowBoundExceedsPolicy);
}
None
}
const fn primary_key_input_rejection(
&self,
summary: &QueryAdmissionSummary,
) -> Option<QueryAdmissionRejection> {
if let (Some(bound), Some(max)) = (
summary.primary_key_input_terms(),
self.max_primary_key_input_terms,
) && bound > max.get()
{
return Some(QueryAdmissionRejection::PrimaryKeyInputExceedsPolicy);
}
if let (Some(bound), Some(max)) = (
summary.primary_key_input_payload_bytes(),
self.max_primary_key_input_bytes,
) && bound > max.get()
{
return Some(QueryAdmissionRejection::PrimaryKeyInputExceedsPolicy);
}
None
}
fn materialization_rejection(
&self,
summary: &QueryAdmissionSummary,
) -> Option<QueryAdmissionRejection> {
if !self.allow_materialized_sort()
&& summary.materialization().materialized_sort()
&& !primary_key_materialized_sort_has_exact_candidate_bound(summary)
{
return Some(QueryAdmissionRejection::SortRequiresMaterialization);
}
None
}
}
fn primary_key_materialized_sort_has_exact_candidate_bound(
summary: &QueryAdmissionSummary,
) -> bool {
if !matches!(
summary.selected_access(),
QueryAdmissionAccessKind::ByKey | QueryAdmissionAccessKind::ByKeys
) {
return false;
}
if !matches!(summary.scan_bound_kind(), QueryBoundKind::Exact) {
return false;
}
if !summary
.materialization()
.row_bound_kind()
.admits_public_read()
{
return false;
}
match (
summary.scan_bound(),
summary.materialization().materialized_rows(),
) {
(Some(scan_bound), Some(materialized_rows)) => u64::from(materialized_rows) == scan_bound,
_ => false,
}
}