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