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