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