use crate::db::{access::AccessPlanError, cursor::CursorPlanError, schema::ValidateError};
use thiserror::Error as ThisError;
#[derive(Debug, ThisError)]
pub enum PlanError {
#[error("{0}")]
User(Box<PlanUserError>),
#[error("{0}")]
Policy(Box<PlanPolicyError>),
#[error("{0}")]
Cursor(Box<CursorPlanError>),
}
impl PlanError {
#[must_use]
pub fn is_unordered_pagination(&self) -> bool {
matches!(
self,
Self::Policy(inner)
if matches!(
inner.as_ref(),
PlanPolicyError::Policy(policy)
if matches!(policy.as_ref(), PolicyPlanError::UnorderedPagination)
)
)
}
}
#[derive(Debug, ThisError)]
pub enum PlanUserError {
#[error("predicate validation failed: {0}")]
PredicateInvalid(Box<ValidateError>),
#[error("{0}")]
Order(Box<OrderPlanError>),
#[error("{0}")]
Access(Box<AccessPlanError>),
#[error("{0}")]
Group(Box<GroupPlanError>),
#[error("{0}")]
Expr(Box<ExprPlanError>),
}
#[derive(Debug, ThisError)]
pub enum PlanPolicyError {
#[error("{0}")]
Policy(Box<PolicyPlanError>),
#[error("{0}")]
Group(Box<GroupPlanError>),
}
#[derive(Debug, ThisError)]
pub enum OrderPlanError {
#[error("unknown order field '{field}'")]
UnknownField { field: String },
#[error("order field '{field}' is not orderable")]
UnorderableField { field: String },
#[error("order field '{field}' appears multiple times")]
DuplicateOrderField { field: String },
#[error("order specification must end with primary key '{field}' as deterministic tie-break")]
MissingPrimaryKeyTieBreak { field: String },
}
impl OrderPlanError {
pub(crate) fn unknown_field(field: impl Into<String>) -> Self {
Self::UnknownField {
field: field.into(),
}
}
pub(crate) fn unorderable_field(field: impl Into<String>) -> Self {
Self::UnorderableField {
field: field.into(),
}
}
pub(crate) fn duplicate_order_field(field: impl Into<String>) -> Self {
Self::DuplicateOrderField {
field: field.into(),
}
}
pub(crate) fn missing_primary_key_tie_break(field: impl Into<String>) -> Self {
Self::MissingPrimaryKeyTieBreak {
field: field.into(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
pub enum PolicyPlanError {
#[error("order specification must include at least one field")]
EmptyOrderSpec,
#[error("delete plans must not include GROUP BY or HAVING")]
DeletePlanWithGrouping,
#[error("delete plans must not include pagination")]
DeletePlanWithPagination,
#[error("load plans must not include delete limits")]
LoadPlanWithDeleteLimit,
#[error("delete LIMIT/OFFSET requires an explicit ordering")]
DeleteWindowRequiresOrder,
#[error(
"Unordered pagination is not allowed.\nLIMIT or OFFSET without ORDER BY is non-deterministic.\nAdd order_by(...) to make the query stable."
)]
UnorderedPagination,
#[error(
"expression ORDER BY requires a matching index-backed access order for bounded execution"
)]
ExpressionOrderRequiresIndexSatisfiedAccess,
}
impl PolicyPlanError {
pub(crate) const fn empty_order_spec() -> Self {
Self::EmptyOrderSpec
}
pub(crate) const fn delete_plan_with_grouping() -> Self {
Self::DeletePlanWithGrouping
}
pub(crate) const fn delete_plan_with_pagination() -> Self {
Self::DeletePlanWithPagination
}
pub(crate) const fn load_plan_with_delete_limit() -> Self {
Self::LoadPlanWithDeleteLimit
}
pub(crate) const fn delete_window_requires_order() -> Self {
Self::DeleteWindowRequiresOrder
}
pub(crate) const fn unordered_pagination() -> Self {
Self::UnorderedPagination
}
pub(crate) const fn expression_order_requires_index_satisfied_access() -> Self {
Self::ExpressionOrderRequiresIndexSatisfiedAccess
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
pub enum CursorPagingPolicyError {
#[error(
"{message}",
message = CursorPlanError::cursor_requires_order_message()
)]
CursorRequiresOrder,
#[error(
"{message}",
message = CursorPlanError::cursor_requires_limit_message()
)]
CursorRequiresLimit,
}
impl CursorPagingPolicyError {
pub(crate) const fn cursor_requires_order() -> Self {
Self::CursorRequiresOrder
}
pub(crate) const fn cursor_requires_limit() -> Self {
Self::CursorRequiresLimit
}
}
#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
pub enum GroupPlanError {
#[error("HAVING requires GROUP BY")]
HavingRequiresGroupBy,
#[error("group validation requires grouped plan")]
GroupedLogicalPlanRequired,
#[error("group specification must include at least one group field")]
EmptyGroupFields,
#[error("global DISTINCT without GROUP BY requires one DISTINCT field aggregate")]
GlobalDistinctAggregateShapeUnsupported,
#[error("group specification must include at least one aggregate terminal")]
EmptyAggregates,
#[error("unknown group field '{field}'")]
UnknownGroupField { field: String },
#[error("group specification has duplicate group key: '{field}'")]
DuplicateGroupField { field: String },
#[error("grouped DISTINCT requires ordered-group adjacency proof")]
DistinctAdjacencyEligibilityRequired,
#[error("grouped ORDER BY must start with GROUP BY key prefix")]
OrderPrefixNotAlignedWithGroupKeys,
#[error("grouped ORDER BY expression is not order-admissible in this release: '{term}'")]
OrderExpressionNotAdmissible { term: String },
#[error("aggregate ORDER BY requires LIMIT for bounded execution")]
OrderRequiresLimit,
#[error("aggregate ORDER BY does not support OFFSET for bounded execution")]
OrderOffsetNotSupported,
#[error("grouped HAVING with DISTINCT is unsupported")]
DistinctHavingUnsupported,
#[error("grouped HAVING clause at index={index} uses unsupported operator: {op}")]
HavingUnsupportedCompareOp { index: usize, op: String },
#[error("grouped HAVING clause at index={index} references non-group field '{field}'")]
HavingNonGroupFieldReference { index: usize, field: String },
#[error(
"grouped HAVING clause at index={index} references aggregate index {aggregate_index} but aggregate_count={aggregate_count}"
)]
HavingAggregateIndexOutOfBounds {
index: usize,
aggregate_index: usize,
aggregate_count: usize,
},
#[error(
"grouped DISTINCT aggregate at index={index} uses unsupported kind '{kind}' in this release"
)]
DistinctAggregateKindUnsupported { index: usize, kind: String },
#[error(
"grouped DISTINCT aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
)]
DistinctAggregateFieldTargetUnsupported {
index: usize,
kind: String,
field: String,
},
#[error("unknown grouped aggregate target field at index={index}: '{field}'")]
UnknownAggregateTargetField { index: usize, field: String },
#[error(
"global DISTINCT SUM aggregate target field at index={index} is not numeric: '{field}'"
)]
GlobalDistinctSumTargetNotNumeric { index: usize, field: String },
#[error(
"grouped aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
)]
FieldTargetAggregatesUnsupported {
index: usize,
kind: String,
field: String,
},
}
impl GroupPlanError {
pub(crate) const fn grouped_logical_plan_required() -> Self {
Self::GroupedLogicalPlanRequired
}
pub(crate) const fn global_distinct_aggregate_shape_unsupported() -> Self {
Self::GlobalDistinctAggregateShapeUnsupported
}
pub(crate) const fn distinct_adjacency_eligibility_required() -> Self {
Self::DistinctAdjacencyEligibilityRequired
}
pub(crate) const fn distinct_having_unsupported() -> Self {
Self::DistinctHavingUnsupported
}
pub(crate) fn unknown_group_field(field: impl Into<String>) -> Self {
Self::UnknownGroupField {
field: field.into(),
}
}
pub(crate) fn duplicate_group_field(field: impl Into<String>) -> Self {
Self::DuplicateGroupField {
field: field.into(),
}
}
pub(crate) const fn order_requires_limit() -> Self {
Self::OrderRequiresLimit
}
pub(crate) const fn order_offset_not_supported() -> Self {
Self::OrderOffsetNotSupported
}
pub(crate) const fn order_prefix_not_aligned_with_group_keys() -> Self {
Self::OrderPrefixNotAlignedWithGroupKeys
}
pub(crate) fn order_expression_not_admissible(term: impl Into<String>) -> Self {
Self::OrderExpressionNotAdmissible { term: term.into() }
}
pub(crate) const fn empty_group_fields() -> Self {
Self::EmptyGroupFields
}
pub(crate) const fn empty_aggregates() -> Self {
Self::EmptyAggregates
}
pub(crate) fn having_non_group_field_reference(index: usize, field: impl Into<String>) -> Self {
Self::HavingNonGroupFieldReference {
index,
field: field.into(),
}
}
pub(crate) const fn having_aggregate_index_out_of_bounds(
index: usize,
aggregate_index: usize,
aggregate_count: usize,
) -> Self {
Self::HavingAggregateIndexOutOfBounds {
index,
aggregate_index,
aggregate_count,
}
}
pub(crate) fn having_unsupported_compare_op(index: usize, op: impl Into<String>) -> Self {
Self::HavingUnsupportedCompareOp {
index,
op: op.into(),
}
}
pub(crate) fn distinct_aggregate_kind_unsupported(
index: usize,
kind: impl Into<String>,
) -> Self {
Self::DistinctAggregateKindUnsupported {
index,
kind: kind.into(),
}
}
pub(crate) fn distinct_aggregate_field_target_unsupported(
index: usize,
kind: impl Into<String>,
field: impl Into<String>,
) -> Self {
Self::DistinctAggregateFieldTargetUnsupported {
index,
kind: kind.into(),
field: field.into(),
}
}
pub(crate) fn field_target_aggregates_unsupported(
index: usize,
kind: impl Into<String>,
field: impl Into<String>,
) -> Self {
Self::FieldTargetAggregatesUnsupported {
index,
kind: kind.into(),
field: field.into(),
}
}
pub(crate) fn global_distinct_sum_target_not_numeric(
index: usize,
field: impl Into<String>,
) -> Self {
Self::GlobalDistinctSumTargetNotNumeric {
index,
field: field.into(),
}
}
pub(crate) fn unknown_aggregate_target_field(index: usize, field: impl Into<String>) -> Self {
Self::UnknownAggregateTargetField {
index,
field: field.into(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
pub enum ExprPlanError {
#[error("unknown expression field '{field}'")]
UnknownExprField { field: String },
#[error("aggregate '{kind}' requires numeric target field '{field}'")]
NonNumericAggregateTarget { kind: String, field: String },
#[error("aggregate '{kind}' requires an explicit target field")]
AggregateTargetRequired { kind: String },
#[error("function '{function}' argument at index={index} is incompatible with type {found}")]
InvalidFunctionArgument {
function: String,
index: usize,
found: String,
},
#[error("unary operator '{op}' is incompatible with operand type {found}")]
InvalidUnaryOperand { op: String, found: String },
#[error("CASE branch condition is incompatible with type {found}")]
InvalidCaseConditionType { found: String },
#[error("CASE result branches are incompatible with types ({left}, {right})")]
IncompatibleCaseBranchTypes { left: String, right: String },
#[error("binary operator '{op}' is incompatible with operand types ({left}, {right})")]
InvalidBinaryOperands {
op: String,
left: String,
right: String,
},
#[error(
"grouped projection expression at index={index} references fields outside GROUP BY keys"
)]
GroupedProjectionReferencesNonGroupField { index: usize },
}
impl ExprPlanError {
pub(crate) fn unknown_expr_field(field: impl Into<String>) -> Self {
Self::UnknownExprField {
field: field.into(),
}
}
pub(crate) fn aggregate_target_required(kind: impl Into<String>) -> Self {
Self::AggregateTargetRequired { kind: kind.into() }
}
pub(crate) fn non_numeric_aggregate_target(
kind: impl Into<String>,
field: impl Into<String>,
) -> Self {
Self::NonNumericAggregateTarget {
kind: kind.into(),
field: field.into(),
}
}
pub(crate) fn invalid_function_argument(
function: impl Into<String>,
index: usize,
found: impl Into<String>,
) -> Self {
Self::InvalidFunctionArgument {
function: function.into(),
index,
found: found.into(),
}
}
pub(crate) fn invalid_unary_operand(op: impl Into<String>, found: impl Into<String>) -> Self {
Self::InvalidUnaryOperand {
op: op.into(),
found: found.into(),
}
}
pub(crate) fn invalid_case_condition_type(found: impl Into<String>) -> Self {
Self::InvalidCaseConditionType {
found: found.into(),
}
}
pub(crate) fn incompatible_case_branch_types(
left: impl Into<String>,
right: impl Into<String>,
) -> Self {
Self::IncompatibleCaseBranchTypes {
left: left.into(),
right: right.into(),
}
}
pub(crate) fn invalid_binary_operands(
op: impl Into<String>,
left: impl Into<String>,
right: impl Into<String>,
) -> Self {
Self::InvalidBinaryOperands {
op: op.into(),
left: left.into(),
right: right.into(),
}
}
pub(crate) const fn grouped_projection_references_non_group_field(index: usize) -> Self {
Self::GroupedProjectionReferencesNonGroupField { index }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum CursorOrderPlanShapeError {
MissingExplicitOrder,
EmptyOrderSpec,
}
impl CursorOrderPlanShapeError {
pub(crate) const fn missing_explicit_order() -> Self {
Self::MissingExplicitOrder
}
pub(crate) const fn empty_order_spec() -> Self {
Self::EmptyOrderSpec
}
pub(crate) fn to_cursor_plan_error(
self,
missing_order_message: &'static str,
) -> CursorPlanError {
match self {
Self::MissingExplicitOrder => {
CursorPlanError::continuation_cursor_invariant(missing_order_message)
}
Self::EmptyOrderSpec => CursorPlanError::cursor_requires_non_empty_order(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum IntentKeyAccessKind {
Single,
Many,
Only,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum IntentKeyAccessPolicyViolation {
KeyAccessConflict,
ByIdsWithPredicate,
OnlyWithPredicate,
}
impl IntentKeyAccessPolicyViolation {
pub(crate) const fn key_access_conflict() -> Self {
Self::KeyAccessConflict
}
pub(crate) const fn by_ids_with_predicate() -> Self {
Self::ByIdsWithPredicate
}
pub(crate) const fn only_with_predicate() -> Self {
Self::OnlyWithPredicate
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum FluentLoadPolicyViolation {
CursorRequiresPagedExecution,
GroupedRequiresDirectExecute,
CursorRequiresOrder,
CursorRequiresLimit,
}
impl FluentLoadPolicyViolation {
pub(crate) const fn cursor_requires_paged_execution() -> Self {
Self::CursorRequiresPagedExecution
}
pub(crate) const fn grouped_requires_direct_execute() -> Self {
Self::GroupedRequiresDirectExecute
}
pub(crate) const fn cursor_requires_order() -> Self {
Self::CursorRequiresOrder
}
pub(crate) const fn cursor_requires_limit() -> Self {
Self::CursorRequiresLimit
}
}
impl From<CursorPagingPolicyError> for FluentLoadPolicyViolation {
fn from(err: CursorPagingPolicyError) -> Self {
match err {
CursorPagingPolicyError::CursorRequiresOrder => Self::cursor_requires_order(),
CursorPagingPolicyError::CursorRequiresLimit => Self::cursor_requires_limit(),
}
}
}
impl From<ValidateError> for PlanError {
fn from(err: ValidateError) -> Self {
Self::from(PlanUserError::from(err))
}
}
impl From<OrderPlanError> for PlanError {
fn from(err: OrderPlanError) -> Self {
Self::from(PlanUserError::from(err))
}
}
impl From<AccessPlanError> for PlanError {
fn from(err: AccessPlanError) -> Self {
Self::from(PlanUserError::from(err))
}
}
impl From<PolicyPlanError> for PlanError {
fn from(err: PolicyPlanError) -> Self {
Self::from(PlanPolicyError::from(err))
}
}
impl From<CursorPlanError> for PlanError {
fn from(err: CursorPlanError) -> Self {
Self::Cursor(Box::new(err))
}
}
impl From<GroupPlanError> for PlanError {
fn from(err: GroupPlanError) -> Self {
if err.belongs_to_policy_axis() {
return Self::from(PlanPolicyError::from(err));
}
Self::from(PlanUserError::from(err))
}
}
impl From<ExprPlanError> for PlanError {
fn from(err: ExprPlanError) -> Self {
Self::from(PlanUserError::from(err))
}
}
impl From<PlanUserError> for PlanError {
fn from(err: PlanUserError) -> Self {
Self::User(Box::new(err))
}
}
impl From<PlanPolicyError> for PlanError {
fn from(err: PlanPolicyError) -> Self {
Self::Policy(Box::new(err))
}
}
impl From<ValidateError> for PlanUserError {
fn from(err: ValidateError) -> Self {
Self::PredicateInvalid(Box::new(err))
}
}
impl From<OrderPlanError> for PlanUserError {
fn from(err: OrderPlanError) -> Self {
Self::Order(Box::new(err))
}
}
impl From<AccessPlanError> for PlanUserError {
fn from(err: AccessPlanError) -> Self {
Self::Access(Box::new(err))
}
}
impl From<GroupPlanError> for PlanUserError {
fn from(err: GroupPlanError) -> Self {
Self::Group(Box::new(err))
}
}
impl From<ExprPlanError> for PlanUserError {
fn from(err: ExprPlanError) -> Self {
Self::Expr(Box::new(err))
}
}
impl From<PolicyPlanError> for PlanPolicyError {
fn from(err: PolicyPlanError) -> Self {
Self::Policy(Box::new(err))
}
}
impl From<GroupPlanError> for PlanPolicyError {
fn from(err: GroupPlanError) -> Self {
Self::Group(Box::new(err))
}
}
impl GroupPlanError {
const fn belongs_to_policy_axis(&self) -> bool {
matches!(
self,
Self::GlobalDistinctAggregateShapeUnsupported
| Self::DistinctAdjacencyEligibilityRequired
| Self::OrderPrefixNotAlignedWithGroupKeys
| Self::OrderExpressionNotAdmissible { .. }
| Self::OrderRequiresLimit
| Self::OrderOffsetNotSupported
| Self::DistinctHavingUnsupported
| Self::HavingUnsupportedCompareOp { .. }
| Self::DistinctAggregateKindUnsupported { .. }
| Self::DistinctAggregateFieldTargetUnsupported { .. }
| Self::FieldTargetAggregatesUnsupported { .. }
)
}
}