Skip to main content

icydb_core/db/query/plan/
validate.rs

1//! Query-plan validation for planner-owned logical semantics.
2//!
3//! Validation ownership contract:
4//! - `validate_query_semantics` owns user-facing query semantics and emits `PlanError`.
5//! - executor-boundary defensive checks live in `db::executor::plan_validate`.
6//!
7//! Future rule changes must declare a semantic owner. Defensive re-check layers may mirror
8//! rules, but must not reinterpret semantics or error class intent.
9
10use crate::{
11    db::{
12        access::{
13            AccessPlanError,
14            validate_access_structure_model as validate_access_structure_model_shared,
15        },
16        contracts::{SchemaInfo, ValidateError},
17        cursor::CursorPlanError,
18        policy::{self, PlanPolicyError},
19        query::{
20            plan::{
21                AccessPlannedQuery, GroupAggregateKind, GroupSpec, LogicalPlan, OrderSpec,
22                ScalarPlan,
23            },
24            predicate,
25        },
26    },
27    model::entity::EntityModel,
28    value::Value,
29};
30use std::collections::BTreeSet;
31use thiserror::Error as ThisError;
32
33///
34/// PlanError
35///
36/// Executor-visible validation failures for logical plans.
37///
38/// These errors indicate that a plan cannot be safely executed against the
39/// current schema or entity definition. They are *not* planner bugs.
40///
41
42#[derive(Debug, ThisError)]
43pub enum PlanError {
44    #[error("predicate validation failed: {0}")]
45    PredicateInvalid(Box<ValidateError>),
46
47    #[error("{0}")]
48    Order(Box<OrderPlanError>),
49
50    #[error("{0}")]
51    Access(Box<AccessPlanError>),
52
53    #[error("{0}")]
54    Policy(Box<PolicyPlanError>),
55
56    #[error("{0}")]
57    Cursor(Box<CursorPlanError>),
58
59    #[error("{0}")]
60    Group(Box<GroupPlanError>),
61}
62
63///
64/// OrderPlanError
65///
66/// ORDER BY-specific validation failures.
67///
68
69#[derive(Debug, ThisError)]
70pub enum OrderPlanError {
71    /// ORDER BY references an unknown field.
72    #[error("unknown order field '{field}'")]
73    UnknownField { field: String },
74
75    /// ORDER BY references a field that cannot be ordered.
76    #[error("order field '{field}' is not orderable")]
77    UnorderableField { field: String },
78
79    /// ORDER BY references the same non-primary-key field multiple times.
80    #[error("order field '{field}' appears multiple times")]
81    DuplicateOrderField { field: String },
82
83    /// Ordered plans must terminate with the primary-key tie-break.
84    #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
85    MissingPrimaryKeyTieBreak { field: String },
86}
87
88///
89/// PolicyPlanError
90///
91/// Plan-shape policy failures.
92///
93
94#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
95pub enum PolicyPlanError {
96    /// ORDER BY must specify at least one field.
97    #[error("order specification must include at least one field")]
98    EmptyOrderSpec,
99
100    /// Delete plans must not carry pagination.
101    #[error("delete plans must not include pagination")]
102    DeletePlanWithPagination,
103
104    /// Load plans must not carry delete limits.
105    #[error("load plans must not include delete limits")]
106    LoadPlanWithDeleteLimit,
107
108    /// Delete limits require an explicit ordering.
109    #[error("delete limit requires an explicit ordering")]
110    DeleteLimitRequiresOrder,
111
112    /// Pagination requires an explicit ordering.
113    #[error(
114        "Unordered pagination is not allowed.\nThis query uses LIMIT or OFFSET without an ORDER BY clause.\nPagination without a total ordering is non-deterministic.\nAdd an explicit order_by(...) to make the query stable."
115    )]
116    UnorderedPagination,
117}
118
119///
120/// GroupPlanError
121///
122/// GROUP BY wrapper validation failures owned by query planning.
123///
124
125#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
126pub enum GroupPlanError {
127    /// Grouped validation entrypoint received a scalar logical plan.
128    #[error("group query validation requires grouped logical plan variant")]
129    GroupedLogicalPlanRequired,
130
131    /// GROUP BY requires at least one declared grouping field.
132    #[error("group specification must include at least one group field")]
133    EmptyGroupFields,
134
135    /// GROUP BY requires at least one aggregate terminal.
136    #[error("group specification must include at least one aggregate terminal")]
137    EmptyAggregates,
138
139    /// GROUP BY references an unknown group field.
140    #[error("unknown group field '{field}'")]
141    UnknownGroupField { field: String },
142
143    /// Aggregate target fields must resolve in the model schema.
144    #[error("unknown grouped aggregate target field at index={index}: '{field}'")]
145    UnknownAggregateTargetField { index: usize, field: String },
146
147    /// Field-target grouped terminals are currently limited to MIN/MAX.
148    #[error(
149        "grouped aggregate at index={index} requires MIN/MAX when targeting field '{field}': found {kind}"
150    )]
151    FieldTargetRequiresExtrema {
152        index: usize,
153        kind: String,
154        field: String,
155    },
156}
157
158impl From<PlanPolicyError> for PolicyPlanError {
159    fn from(err: PlanPolicyError) -> Self {
160        match err {
161            PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
162            PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
163            PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
164            PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
165            PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
166        }
167    }
168}
169
170impl From<ValidateError> for PlanError {
171    fn from(err: ValidateError) -> Self {
172        Self::PredicateInvalid(Box::new(err))
173    }
174}
175
176impl From<OrderPlanError> for PlanError {
177    fn from(err: OrderPlanError) -> Self {
178        Self::Order(Box::new(err))
179    }
180}
181
182impl From<AccessPlanError> for PlanError {
183    fn from(err: AccessPlanError) -> Self {
184        Self::Access(Box::new(err))
185    }
186}
187
188impl From<PolicyPlanError> for PlanError {
189    fn from(err: PolicyPlanError) -> Self {
190        Self::Policy(Box::new(err))
191    }
192}
193
194impl From<CursorPlanError> for PlanError {
195    fn from(err: CursorPlanError) -> Self {
196        Self::Cursor(Box::new(err))
197    }
198}
199
200impl From<GroupPlanError> for PlanError {
201    fn from(err: GroupPlanError) -> Self {
202        Self::Group(Box::new(err))
203    }
204}
205
206impl From<PlanPolicyError> for PlanError {
207    fn from(err: PlanPolicyError) -> Self {
208        Self::from(PolicyPlanError::from(err))
209    }
210}
211
212/// Validate a logical plan with model-level key values.
213///
214/// Ownership:
215/// - semantic owner for user-facing query validity at planning boundaries
216/// - failures here are user-visible planning failures (`PlanError`)
217///
218/// New user-facing validation rules must be introduced here first, then mirrored
219/// defensively in downstream layers without changing semantics.
220pub(crate) fn validate_query_semantics(
221    schema: &SchemaInfo,
222    model: &EntityModel,
223    plan: &AccessPlannedQuery<Value>,
224) -> Result<(), PlanError> {
225    let logical = plan.scalar_plan();
226
227    validate_plan_core(
228        schema,
229        model,
230        logical,
231        plan,
232        validate_order,
233        |schema, model, plan| {
234            validate_access_structure_model_shared(schema, model, &plan.access)
235                .map_err(PlanError::from)
236        },
237    )?;
238
239    Ok(())
240}
241
242/// Validate grouped query semantics for one grouped plan wrapper.
243///
244/// Ownership:
245/// - semantic owner for GROUP BY wrapper validation
246/// - failures here are user-visible planning failures (`PlanError`)
247pub(crate) fn validate_group_query_semantics(
248    schema: &SchemaInfo,
249    model: &EntityModel,
250    plan: &AccessPlannedQuery<Value>,
251) -> Result<(), PlanError> {
252    let logical = plan.scalar_plan();
253    let group = match &plan.logical {
254        LogicalPlan::Grouped(grouped) => &grouped.group,
255        LogicalPlan::Scalar(_) => {
256            return Err(PlanError::from(GroupPlanError::GroupedLogicalPlanRequired));
257        }
258    };
259
260    validate_plan_core(
261        schema,
262        model,
263        logical,
264        plan,
265        validate_order,
266        |schema, model, plan| {
267            validate_access_structure_model_shared(schema, model, &plan.access)
268                .map_err(PlanError::from)
269        },
270    )?;
271    validate_group_spec(schema, model, group)?;
272
273    Ok(())
274}
275
276/// Validate one grouped declarative spec against schema-level field surface.
277pub(crate) fn validate_group_spec(
278    schema: &SchemaInfo,
279    model: &EntityModel,
280    group: &GroupSpec,
281) -> Result<(), PlanError> {
282    if group.group_fields.is_empty() {
283        return Err(PlanError::from(GroupPlanError::EmptyGroupFields));
284    }
285    if group.aggregates.is_empty() {
286        return Err(PlanError::from(GroupPlanError::EmptyAggregates));
287    }
288
289    for field_slot in &group.group_fields {
290        if model.fields.get(field_slot.index()).is_none() {
291            return Err(PlanError::from(GroupPlanError::UnknownGroupField {
292                field: field_slot.field().to_string(),
293            }));
294        }
295    }
296
297    for (index, aggregate) in group.aggregates.iter().enumerate() {
298        let Some(target_field) = aggregate.target_field.as_ref() else {
299            continue;
300        };
301        if schema.field(target_field).is_none() {
302            return Err(PlanError::from(
303                GroupPlanError::UnknownAggregateTargetField {
304                    index,
305                    field: target_field.clone(),
306                },
307            ));
308        }
309        if !matches!(
310            aggregate.kind,
311            GroupAggregateKind::Min | GroupAggregateKind::Max
312        ) {
313            return Err(PlanError::from(
314                GroupPlanError::FieldTargetRequiresExtrema {
315                    index,
316                    kind: format!("{:?}", aggregate.kind),
317                    field: target_field.clone(),
318                },
319            ));
320        }
321    }
322
323    Ok(())
324}
325
326// Shared logical plan validation core owned by planner semantics.
327fn validate_plan_core<K, FOrder, FAccess>(
328    schema: &SchemaInfo,
329    model: &EntityModel,
330    logical: &ScalarPlan,
331    plan: &AccessPlannedQuery<K>,
332    validate_order_fn: FOrder,
333    validate_access_fn: FAccess,
334) -> Result<(), PlanError>
335where
336    FOrder: Fn(&SchemaInfo, &OrderSpec) -> Result<(), PlanError>,
337    FAccess: Fn(&SchemaInfo, &EntityModel, &AccessPlannedQuery<K>) -> Result<(), PlanError>,
338{
339    if let Some(predicate) = &logical.predicate {
340        predicate::validate(schema, predicate)?;
341    }
342
343    if let Some(order) = &logical.order {
344        validate_order_fn(schema, order)?;
345        validate_no_duplicate_non_pk_order_fields(model, order)?;
346        validate_primary_key_tie_break(model, order)?;
347    }
348
349    validate_access_fn(schema, model, plan)?;
350    policy::validate_plan_shape(&plan.logical)?;
351
352    Ok(())
353}
354// ORDER validation ownership contract:
355// - This module owns ORDER semantic validation (field existence/orderability/tie-break).
356// - ORDER canonicalization (primary-key tie-break insertion) is performed at the
357//   intent boundary via `canonicalize_order_spec` before plan validation.
358// - Shape-policy checks (for example empty ORDER, pagination/order coupling) are owned by
359//   `db::policy`.
360// - Executor/runtime layers may defend execution preconditions only.
361
362/// Validate ORDER BY fields against the schema.
363pub(crate) fn validate_order(schema: &SchemaInfo, order: &OrderSpec) -> Result<(), PlanError> {
364    for (field, _) in &order.fields {
365        let field_type = schema
366            .field(field)
367            .ok_or_else(|| OrderPlanError::UnknownField {
368                field: field.clone(),
369            })
370            .map_err(PlanError::from)?;
371
372        if !field_type.is_orderable() {
373            // CONTRACT: ORDER BY rejects non-queryable or unordered fields.
374            return Err(PlanError::from(OrderPlanError::UnorderableField {
375                field: field.clone(),
376            }));
377        }
378    }
379
380    Ok(())
381}
382
383/// Reject duplicate non-primary-key fields in ORDER BY.
384pub(crate) fn validate_no_duplicate_non_pk_order_fields(
385    model: &EntityModel,
386    order: &OrderSpec,
387) -> Result<(), PlanError> {
388    let mut seen = BTreeSet::new();
389    let pk_field = model.primary_key.name;
390
391    for (field, _) in &order.fields {
392        if field == pk_field {
393            continue;
394        }
395        if !seen.insert(field.as_str()) {
396            return Err(PlanError::from(OrderPlanError::DuplicateOrderField {
397                field: field.clone(),
398            }));
399        }
400    }
401
402    Ok(())
403}
404
405// Ordered plans must include exactly one terminal primary-key field so ordering is total and
406// deterministic across explain, fingerprint, and executor comparison paths.
407pub(crate) fn validate_primary_key_tie_break(
408    model: &EntityModel,
409    order: &OrderSpec,
410) -> Result<(), PlanError> {
411    if order.fields.is_empty() {
412        return Ok(());
413    }
414
415    let pk_field = model.primary_key.name;
416    let pk_count = order
417        .fields
418        .iter()
419        .filter(|(field, _)| field == pk_field)
420        .count();
421    let trailing_pk = order
422        .fields
423        .last()
424        .is_some_and(|(field, _)| field == pk_field);
425
426    if pk_count == 1 && trailing_pk {
427        Ok(())
428    } else {
429        Err(PlanError::from(OrderPlanError::MissingPrimaryKeyTieBreak {
430            field: pk_field.to_string(),
431        }))
432    }
433}