use crate::db::query::plan::{LogicalPlan, OrderSpec, QueryMode, validate::PolicyPlanError};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[expect(clippy::struct_excessive_bools)]
struct PlanShapePolicyContext {
is_delete_mode: bool,
grouped: bool,
has_order: bool,
has_page: bool,
has_delete_window: bool,
}
impl PlanShapePolicyContext {
#[must_use]
#[expect(clippy::fn_params_excessive_bools)]
const fn new(
is_delete_mode: bool,
grouped: bool,
has_order: bool,
has_page: bool,
has_delete_window: bool,
) -> Self {
Self {
is_delete_mode,
grouped,
has_order,
has_page,
has_delete_window,
}
}
}
#[derive(Clone, Copy)]
struct PlanShapePolicyRule {
reason: PolicyPlanError,
violated: fn(PlanShapePolicyContext) -> bool,
}
impl PlanShapePolicyRule {
#[must_use]
const fn new(reason: PolicyPlanError, violated: fn(PlanShapePolicyContext) -> bool) -> Self {
Self { reason, violated }
}
}
const PLAN_SHAPE_POLICY_RULES: &[PlanShapePolicyRule] = &[
PlanShapePolicyRule::new(
PolicyPlanError::delete_plan_with_grouping(),
plan_shape_delete_with_grouping_violated,
),
PlanShapePolicyRule::new(
PolicyPlanError::delete_window_requires_order(),
plan_shape_delete_window_requires_order_violated,
),
PlanShapePolicyRule::new(
PolicyPlanError::delete_plan_with_pagination(),
plan_shape_delete_with_pagination_violated,
),
PlanShapePolicyRule::new(
PolicyPlanError::load_plan_with_delete_limit(),
plan_shape_load_with_delete_limit_violated,
),
PlanShapePolicyRule::new(
PolicyPlanError::unordered_pagination(),
plan_shape_unordered_scalar_load_pagination_violated,
),
];
const fn plan_shape_delete_window_requires_order_violated(ctx: PlanShapePolicyContext) -> bool {
ctx.has_delete_window && !ctx.has_order
}
const fn plan_shape_delete_with_grouping_violated(ctx: PlanShapePolicyContext) -> bool {
ctx.is_delete_mode && ctx.grouped
}
const fn plan_shape_delete_with_pagination_violated(ctx: PlanShapePolicyContext) -> bool {
ctx.is_delete_mode && ctx.has_page
}
const fn plan_shape_load_with_delete_limit_violated(ctx: PlanShapePolicyContext) -> bool {
!ctx.is_delete_mode && ctx.has_delete_window
}
const fn plan_shape_unordered_scalar_load_pagination_violated(ctx: PlanShapePolicyContext) -> bool {
!ctx.is_delete_mode && ctx.has_page && !ctx.has_order && !ctx.grouped
}
fn first_plan_shape_policy_violation(ctx: PlanShapePolicyContext) -> Option<PolicyPlanError> {
for rule in PLAN_SHAPE_POLICY_RULES {
if (rule.violated)(ctx) {
return Some(rule.reason);
}
}
None
}
fn validate_plan_shape_policy_rules(ctx: PlanShapePolicyContext) -> Result<(), PolicyPlanError> {
first_plan_shape_policy_violation(ctx).map_or(Ok(()), Err)
}
#[must_use]
pub(crate) fn has_explicit_order(order: Option<&OrderSpec>) -> bool {
order.is_some_and(|order| !order.fields.is_empty())
}
#[must_use]
pub(crate) fn has_empty_order(order: Option<&OrderSpec>) -> bool {
order.is_some_and(|order| order.fields.is_empty())
}
pub(crate) fn validate_order_shape(order: Option<&OrderSpec>) -> Result<(), PolicyPlanError> {
if has_empty_order(order) {
return Err(PolicyPlanError::empty_order_spec());
}
Ok(())
}
pub(crate) fn validate_plan_shape(plan: &LogicalPlan) -> Result<(), PolicyPlanError> {
let grouped = matches!(plan, LogicalPlan::Grouped(_));
let plan = match plan {
LogicalPlan::Scalar(plan) => plan,
LogicalPlan::Grouped(plan) => &plan.scalar,
};
validate_order_shape(plan.order.as_ref())?;
let context = PlanShapePolicyContext::new(
matches!(plan.mode, QueryMode::Delete(_)),
grouped,
has_explicit_order(plan.order.as_ref()),
plan.page.is_some(),
plan.delete_limit.is_some(),
);
validate_plan_shape_policy_rules(context)
}