Skip to main content

icydb_core/db/query/plan/validate/
mod.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
10mod core;
11mod grouped;
12mod order;
13mod policy;
14
15use crate::db::{access::AccessPlanError, cursor::CursorPlanError, predicate::ValidateError};
16use thiserror::Error as ThisError;
17
18pub(crate) use core::{validate_group_query_semantics, validate_query_semantics};
19#[cfg(test)]
20pub(in crate::db::query) use grouped::validate_group_projection_expr_compatibility_for_test;
21pub(crate) use order::validate_order;
22pub(crate) use policy::{
23    has_explicit_order, resolve_group_field_slot, validate_cursor_order_plan_shape,
24    validate_cursor_paging_requirements, validate_fluent_non_paged_mode,
25    validate_fluent_paged_mode, validate_intent_key_access_policy, validate_intent_plan_shape,
26    validate_order_shape,
27};
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    #[error("{0}")]
59    Expr(Box<ExprPlanError>),
60}
61
62///
63/// OrderPlanError
64///
65/// ORDER BY-specific validation failures.
66///
67
68#[derive(Debug, ThisError)]
69pub enum OrderPlanError {
70    /// ORDER BY references an unknown field.
71    #[error("unknown order field '{field}'")]
72    UnknownField { field: String },
73
74    /// ORDER BY references a field that cannot be ordered.
75    #[error("order field '{field}' is not orderable")]
76    UnorderableField { field: String },
77
78    /// ORDER BY references the same non-primary-key field multiple times.
79    #[error("order field '{field}' appears multiple times")]
80    DuplicateOrderField { field: String },
81
82    /// Ordered plans must terminate with the primary-key tie-break.
83    #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
84    MissingPrimaryKeyTieBreak { field: String },
85}
86
87///
88/// PolicyPlanError
89///
90/// Plan-shape policy failures.
91///
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
94pub enum PolicyPlanError {
95    /// ORDER BY must specify at least one field.
96    #[error("order specification must include at least one field")]
97    EmptyOrderSpec,
98
99    /// Delete plans must not carry pagination.
100    #[error("delete plans must not include pagination")]
101    DeletePlanWithPagination,
102
103    /// Load plans must not carry delete limits.
104    #[error("load plans must not include delete limits")]
105    LoadPlanWithDeleteLimit,
106
107    /// Delete limits require an explicit ordering.
108    #[error("delete limit requires an explicit ordering")]
109    DeleteLimitRequiresOrder,
110
111    /// Pagination requires an explicit ordering.
112    #[error(
113        "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."
114    )]
115    UnorderedPagination,
116}
117
118///
119/// CursorPagingPolicyError
120///
121/// Cursor pagination readiness errors shared by intent/fluent entry surfaces.
122///
123
124#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
125pub enum CursorPagingPolicyError {
126    #[error(
127        "{message}",
128        message = CursorPlanError::cursor_requires_order_message()
129    )]
130    CursorRequiresOrder,
131
132    #[error(
133        "{message}",
134        message = CursorPlanError::cursor_requires_limit_message()
135    )]
136    CursorRequiresLimit,
137}
138
139///
140/// GroupPlanError
141///
142/// GROUP BY wrapper validation failures owned by query planning.
143///
144
145#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
146pub enum GroupPlanError {
147    /// HAVING requires GROUP BY grouped plan shape.
148    #[error("HAVING is only supported for GROUP BY queries in this release")]
149    HavingRequiresGroupBy,
150
151    /// Grouped validation entrypoint received a scalar logical plan.
152    #[error("group query validation requires grouped logical plan variant")]
153    GroupedLogicalPlanRequired,
154
155    /// GROUP BY requires at least one declared grouping field.
156    #[error("group specification must include at least one group field")]
157    EmptyGroupFields,
158
159    /// Global DISTINCT aggregate shapes without GROUP BY are restricted.
160    #[error(
161        "global DISTINCT aggregate without GROUP BY must declare exactly one DISTINCT field-target aggregate in this release"
162    )]
163    GlobalDistinctAggregateShapeUnsupported,
164
165    /// GROUP BY requires at least one aggregate terminal.
166    #[error("group specification must include at least one aggregate terminal")]
167    EmptyAggregates,
168
169    /// GROUP BY references an unknown group field.
170    #[error("unknown group field '{field}'")]
171    UnknownGroupField { field: String },
172
173    /// GROUP BY must not repeat the same resolved group slot.
174    #[error("group specification has duplicate group key: '{field}'")]
175    DuplicateGroupField { field: String },
176
177    /// GROUP BY v1 does not accept DISTINCT unless adjacency eligibility is explicit.
178    #[error(
179        "grouped DISTINCT requires adjacency-based ordered-group eligibility proof in this release"
180    )]
181    DistinctAdjacencyEligibilityRequired,
182
183    /// GROUP BY ORDER BY shape must start with grouped-key prefix.
184    #[error("grouped ORDER BY must start with GROUP BY key prefix in this release")]
185    OrderPrefixNotAlignedWithGroupKeys,
186
187    /// GROUP BY ORDER BY requires an explicit LIMIT in grouped v1.
188    #[error("grouped ORDER BY requires LIMIT in this release")]
189    OrderRequiresLimit,
190
191    /// HAVING with DISTINCT is deferred until grouped DISTINCT support expands.
192    #[error("grouped HAVING with DISTINCT is not supported in this release")]
193    DistinctHavingUnsupported,
194
195    /// HAVING currently supports compare operators only.
196    #[error("grouped HAVING clause at index={index} uses unsupported operator: {op}")]
197    HavingUnsupportedCompareOp { index: usize, op: String },
198
199    /// HAVING group-field symbols must reference declared grouped keys.
200    #[error("grouped HAVING clause at index={index} references non-group field '{field}'")]
201    HavingNonGroupFieldReference { index: usize, field: String },
202
203    /// HAVING aggregate references must resolve to declared grouped terminals.
204    #[error(
205        "grouped HAVING clause at index={index} references aggregate index {aggregate_index} but aggregate_count={aggregate_count}"
206    )]
207    HavingAggregateIndexOutOfBounds {
208        index: usize,
209        aggregate_index: usize,
210        aggregate_count: usize,
211    },
212
213    /// DISTINCT grouped terminal kinds are intentionally conservative in v1.
214    #[error(
215        "grouped DISTINCT aggregate at index={index} uses unsupported kind '{kind}' in this release"
216    )]
217    DistinctAggregateKindUnsupported { index: usize, kind: String },
218
219    /// DISTINCT over grouped field-target terminals is deferred with field-target support.
220    #[error(
221        "grouped DISTINCT aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
222    )]
223    DistinctAggregateFieldTargetUnsupported {
224        index: usize,
225        kind: String,
226        field: String,
227    },
228
229    /// Aggregate target fields must resolve in the model schema.
230    #[error("unknown grouped aggregate target field at index={index}: '{field}'")]
231    UnknownAggregateTargetField { index: usize, field: String },
232
233    /// Global DISTINCT SUM requires a numeric field target.
234    #[error(
235        "global DISTINCT SUM aggregate target field at index={index} is not numeric: '{field}'"
236    )]
237    GlobalDistinctSumTargetNotNumeric { index: usize, field: String },
238
239    /// Field-target grouped terminals are not enabled in grouped execution v1.
240    #[error(
241        "grouped aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
242    )]
243    FieldTargetAggregatesUnsupported {
244        index: usize,
245        kind: String,
246        field: String,
247    },
248}
249
250///
251/// ExprPlanError
252///
253/// Expression-spine inference failures owned by planner semantics.
254///
255
256#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
257pub enum ExprPlanError {
258    /// Expression references a field that does not exist in schema.
259    #[error("unknown expression field '{field}'")]
260    UnknownExprField { field: String },
261
262    /// Aggregate terminal requires a numeric target field.
263    #[error("aggregate '{kind}' requires numeric target field '{field}'")]
264    NonNumericAggregateTarget { kind: String, field: String },
265
266    /// Aggregate expression requires an explicit target field.
267    #[error("aggregate '{kind}' requires an explicit target field")]
268    AggregateTargetRequired { kind: String },
269
270    /// Unary operation is incompatible with inferred operand type.
271    #[error("unary operator '{op}' is incompatible with operand type {found}")]
272    InvalidUnaryOperand { op: String, found: String },
273
274    /// Binary operation is incompatible with inferred operand types.
275    #[error("binary operator '{op}' is incompatible with operand types ({left}, {right})")]
276    InvalidBinaryOperands {
277        op: String,
278        left: String,
279        right: String,
280    },
281
282    /// GROUP BY projections must not reference fields outside grouped keys.
283    #[error(
284        "grouped projection expression at index={index} references fields outside GROUP BY keys"
285    )]
286    GroupedProjectionReferencesNonGroupField { index: usize },
287}
288
289///
290/// CursorOrderPlanShapeError
291///
292/// Logical cursor-order plan-shape failures used by cursor/runtime boundary adapters.
293///
294
295#[derive(Clone, Copy, Debug, Eq, PartialEq)]
296pub(crate) enum CursorOrderPlanShapeError {
297    MissingExplicitOrder,
298    EmptyOrderSpec,
299}
300
301///
302/// IntentKeyAccessKind
303///
304/// Key-access shape used by intent policy validation.
305///
306
307#[derive(Clone, Copy, Debug, Eq, PartialEq)]
308pub(crate) enum IntentKeyAccessKind {
309    Single,
310    Many,
311    Only,
312}
313
314///
315/// IntentKeyAccessPolicyViolation
316///
317/// Logical key-access policy violations at query-intent boundaries.
318///
319#[derive(Clone, Copy, Debug, Eq, PartialEq)]
320pub(crate) enum IntentKeyAccessPolicyViolation {
321    KeyAccessConflict,
322    ByIdsWithPredicate,
323    OnlyWithPredicate,
324}
325
326///
327/// FluentLoadPolicyViolation
328///
329/// Fluent load-entry policy violations.
330///
331
332#[derive(Clone, Copy, Debug, Eq, PartialEq)]
333pub(crate) enum FluentLoadPolicyViolation {
334    CursorRequiresPagedExecution,
335    GroupedRequiresExecuteGrouped,
336    CursorRequiresOrder,
337    CursorRequiresLimit,
338}
339
340impl From<ValidateError> for PlanError {
341    fn from(err: ValidateError) -> Self {
342        Self::PredicateInvalid(Box::new(err))
343    }
344}
345
346impl From<OrderPlanError> for PlanError {
347    fn from(err: OrderPlanError) -> Self {
348        Self::Order(Box::new(err))
349    }
350}
351
352impl From<AccessPlanError> for PlanError {
353    fn from(err: AccessPlanError) -> Self {
354        Self::Access(Box::new(err))
355    }
356}
357
358impl From<PolicyPlanError> for PlanError {
359    fn from(err: PolicyPlanError) -> Self {
360        Self::Policy(Box::new(err))
361    }
362}
363
364impl From<CursorPlanError> for PlanError {
365    fn from(err: CursorPlanError) -> Self {
366        Self::Cursor(Box::new(err))
367    }
368}
369
370impl From<GroupPlanError> for PlanError {
371    fn from(err: GroupPlanError) -> Self {
372        Self::Group(Box::new(err))
373    }
374}
375
376impl From<ExprPlanError> for PlanError {
377    fn from(err: ExprPlanError) -> Self {
378        Self::Expr(Box::new(err))
379    }
380}