icydb_core/db/query/plan/validate/
mod.rs1mod core;
16mod cursor_policy;
17mod fluent_policy;
18mod grouped;
19mod intent_policy;
20mod order;
21mod plan_shape;
22mod symbols;
23
24use crate::db::{access::AccessPlanError, cursor::CursorPlanError, predicate::ValidateError};
25use thiserror::Error as ThisError;
26
27pub(crate) use core::{validate_group_query_semantics, validate_query_semantics};
28pub(crate) use cursor_policy::{
29 validate_cursor_order_plan_shape, validate_cursor_paging_requirements,
30};
31pub(crate) use fluent_policy::{validate_fluent_non_paged_mode, validate_fluent_paged_mode};
32#[cfg(test)]
33pub(in crate::db::query) use grouped::validate_group_projection_expr_compatibility_for_test;
34pub(crate) use intent_policy::{validate_intent_key_access_policy, validate_intent_plan_shape};
35pub(crate) use order::validate_order;
36pub(crate) use plan_shape::{has_explicit_order, validate_order_shape, validate_plan_shape};
37pub(crate) use symbols::resolve_group_field_slot;
38
39#[derive(Debug, ThisError)]
49pub enum PlanError {
50 #[error("{0}")]
51 User(Box<PlanUserError>),
52
53 #[error("{0}")]
54 Policy(Box<PlanPolicyError>),
55
56 #[error("{0}")]
57 Cursor(Box<CursorPlanError>),
58}
59
60#[derive(Debug, ThisError)]
69pub enum PlanUserError {
70 #[error("predicate validation failed: {0}")]
71 PredicateInvalid(Box<ValidateError>),
72
73 #[error("{0}")]
74 Order(Box<OrderPlanError>),
75
76 #[error("{0}")]
77 Access(Box<AccessPlanError>),
78
79 #[error("{0}")]
80 Group(Box<GroupPlanError>),
81
82 #[error("{0}")]
83 Expr(Box<ExprPlanError>),
84}
85
86#[derive(Debug, ThisError)]
95pub enum PlanPolicyError {
96 #[error("{0}")]
97 Policy(Box<PolicyPlanError>),
98
99 #[error("{0}")]
100 Group(Box<GroupPlanError>),
101}
102
103#[derive(Debug, ThisError)]
110pub enum OrderPlanError {
111 #[error("unknown order field '{field}'")]
113 UnknownField { field: String },
114
115 #[error("order field '{field}' is not orderable")]
117 UnorderableField { field: String },
118
119 #[error("order field '{field}' appears multiple times")]
121 DuplicateOrderField { field: String },
122
123 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
125 MissingPrimaryKeyTieBreak { field: String },
126}
127
128#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
135pub enum PolicyPlanError {
136 #[error("order specification must include at least one field")]
138 EmptyOrderSpec,
139
140 #[error("delete plans must not include OFFSET")]
142 DeletePlanWithOffset,
143
144 #[error("delete plans must not include GROUP BY or HAVING")]
146 DeletePlanWithGrouping,
147
148 #[error("delete plans must not include pagination")]
150 DeletePlanWithPagination,
151
152 #[error("load plans must not include delete limits")]
154 LoadPlanWithDeleteLimit,
155
156 #[error("delete limit requires an explicit ordering")]
158 DeleteLimitRequiresOrder,
159
160 #[error(
162 "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."
163 )]
164 UnorderedPagination,
165}
166
167#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
174pub enum CursorPagingPolicyError {
175 #[error(
176 "{message}",
177 message = CursorPlanError::cursor_requires_order_message()
178 )]
179 CursorRequiresOrder,
180
181 #[error(
182 "{message}",
183 message = CursorPlanError::cursor_requires_limit_message()
184 )]
185 CursorRequiresLimit,
186}
187
188#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
195pub enum GroupPlanError {
196 #[error("HAVING is only supported for GROUP BY queries in this release")]
198 HavingRequiresGroupBy,
199
200 #[error("group query validation requires grouped logical plan variant")]
202 GroupedLogicalPlanRequired,
203
204 #[error("group specification must include at least one group field")]
206 EmptyGroupFields,
207
208 #[error(
210 "global DISTINCT aggregate without GROUP BY must declare exactly one DISTINCT field-target aggregate in this release"
211 )]
212 GlobalDistinctAggregateShapeUnsupported,
213
214 #[error("group specification must include at least one aggregate terminal")]
216 EmptyAggregates,
217
218 #[error("unknown group field '{field}'")]
220 UnknownGroupField { field: String },
221
222 #[error("group specification has duplicate group key: '{field}'")]
224 DuplicateGroupField { field: String },
225
226 #[error(
228 "grouped DISTINCT requires adjacency-based ordered-group eligibility proof in this release"
229 )]
230 DistinctAdjacencyEligibilityRequired,
231
232 #[error("grouped ORDER BY must start with GROUP BY key prefix in this release")]
234 OrderPrefixNotAlignedWithGroupKeys,
235
236 #[error("grouped ORDER BY requires LIMIT in this release")]
238 OrderRequiresLimit,
239
240 #[error("grouped HAVING with DISTINCT is not supported in this release")]
242 DistinctHavingUnsupported,
243
244 #[error("grouped HAVING clause at index={index} uses unsupported operator: {op}")]
246 HavingUnsupportedCompareOp { index: usize, op: String },
247
248 #[error("grouped HAVING clause at index={index} references non-group field '{field}'")]
250 HavingNonGroupFieldReference { index: usize, field: String },
251
252 #[error(
254 "grouped HAVING clause at index={index} references aggregate index {aggregate_index} but aggregate_count={aggregate_count}"
255 )]
256 HavingAggregateIndexOutOfBounds {
257 index: usize,
258 aggregate_index: usize,
259 aggregate_count: usize,
260 },
261
262 #[error(
264 "grouped DISTINCT aggregate at index={index} uses unsupported kind '{kind}' in this release"
265 )]
266 DistinctAggregateKindUnsupported { index: usize, kind: String },
267
268 #[error(
270 "grouped DISTINCT aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
271 )]
272 DistinctAggregateFieldTargetUnsupported {
273 index: usize,
274 kind: String,
275 field: String,
276 },
277
278 #[error("unknown grouped aggregate target field at index={index}: '{field}'")]
280 UnknownAggregateTargetField { index: usize, field: String },
281
282 #[error(
284 "global DISTINCT SUM aggregate target field at index={index} is not numeric: '{field}'"
285 )]
286 GlobalDistinctSumTargetNotNumeric { index: usize, field: String },
287
288 #[error(
290 "grouped aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
291 )]
292 FieldTargetAggregatesUnsupported {
293 index: usize,
294 kind: String,
295 field: String,
296 },
297}
298
299#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
306pub enum ExprPlanError {
307 #[error("unknown expression field '{field}'")]
309 UnknownExprField { field: String },
310
311 #[error("aggregate '{kind}' requires numeric target field '{field}'")]
313 NonNumericAggregateTarget { kind: String, field: String },
314
315 #[error("aggregate '{kind}' requires an explicit target field")]
317 AggregateTargetRequired { kind: String },
318
319 #[error("unary operator '{op}' is incompatible with operand type {found}")]
321 InvalidUnaryOperand { op: String, found: String },
322
323 #[error("binary operator '{op}' is incompatible with operand types ({left}, {right})")]
325 InvalidBinaryOperands {
326 op: String,
327 left: String,
328 right: String,
329 },
330
331 #[error(
333 "grouped projection expression at index={index} references fields outside GROUP BY keys"
334 )]
335 GroupedProjectionReferencesNonGroupField { index: usize },
336}
337
338#[derive(Clone, Copy, Debug, Eq, PartialEq)]
345pub(crate) enum CursorOrderPlanShapeError {
346 MissingExplicitOrder,
347 EmptyOrderSpec,
348}
349
350#[derive(Clone, Copy, Debug, Eq, PartialEq)]
357pub(crate) enum IntentKeyAccessKind {
358 Single,
359 Many,
360 Only,
361}
362
363#[derive(Clone, Copy, Debug, Eq, PartialEq)]
369pub(crate) enum IntentKeyAccessPolicyViolation {
370 KeyAccessConflict,
371 ByIdsWithPredicate,
372 OnlyWithPredicate,
373}
374
375#[derive(Clone, Copy, Debug, Eq, PartialEq)]
382pub(crate) enum FluentLoadPolicyViolation {
383 CursorRequiresPagedExecution,
384 GroupedRequiresExecuteGrouped,
385 CursorRequiresOrder,
386 CursorRequiresLimit,
387}
388
389impl From<ValidateError> for PlanError {
390 fn from(err: ValidateError) -> Self {
391 Self::from(PlanUserError::from(err))
392 }
393}
394
395impl From<OrderPlanError> for PlanError {
396 fn from(err: OrderPlanError) -> Self {
397 Self::from(PlanUserError::from(err))
398 }
399}
400
401impl From<AccessPlanError> for PlanError {
402 fn from(err: AccessPlanError) -> Self {
403 Self::from(PlanUserError::from(err))
404 }
405}
406
407impl From<PolicyPlanError> for PlanError {
408 fn from(err: PolicyPlanError) -> Self {
409 Self::from(PlanPolicyError::from(err))
410 }
411}
412
413impl From<CursorPlanError> for PlanError {
414 fn from(err: CursorPlanError) -> Self {
415 Self::Cursor(Box::new(err))
416 }
417}
418
419impl From<GroupPlanError> for PlanError {
420 fn from(err: GroupPlanError) -> Self {
421 if err.belongs_to_policy_axis() {
422 return Self::from(PlanPolicyError::from(err));
423 }
424
425 Self::from(PlanUserError::from(err))
426 }
427}
428
429impl From<ExprPlanError> for PlanError {
430 fn from(err: ExprPlanError) -> Self {
431 Self::from(PlanUserError::from(err))
432 }
433}
434
435impl From<PlanUserError> for PlanError {
436 fn from(err: PlanUserError) -> Self {
437 Self::User(Box::new(err))
438 }
439}
440
441impl From<PlanPolicyError> for PlanError {
442 fn from(err: PlanPolicyError) -> Self {
443 Self::Policy(Box::new(err))
444 }
445}
446
447impl From<ValidateError> for PlanUserError {
448 fn from(err: ValidateError) -> Self {
449 Self::PredicateInvalid(Box::new(err))
450 }
451}
452
453impl From<OrderPlanError> for PlanUserError {
454 fn from(err: OrderPlanError) -> Self {
455 Self::Order(Box::new(err))
456 }
457}
458
459impl From<AccessPlanError> for PlanUserError {
460 fn from(err: AccessPlanError) -> Self {
461 Self::Access(Box::new(err))
462 }
463}
464
465impl From<GroupPlanError> for PlanUserError {
466 fn from(err: GroupPlanError) -> Self {
467 Self::Group(Box::new(err))
468 }
469}
470
471impl From<ExprPlanError> for PlanUserError {
472 fn from(err: ExprPlanError) -> Self {
473 Self::Expr(Box::new(err))
474 }
475}
476
477impl From<PolicyPlanError> for PlanPolicyError {
478 fn from(err: PolicyPlanError) -> Self {
479 Self::Policy(Box::new(err))
480 }
481}
482
483impl From<GroupPlanError> for PlanPolicyError {
484 fn from(err: GroupPlanError) -> Self {
485 Self::Group(Box::new(err))
486 }
487}
488
489impl GroupPlanError {
490 const fn belongs_to_policy_axis(&self) -> bool {
494 matches!(
495 self,
496 Self::GlobalDistinctAggregateShapeUnsupported
497 | Self::DistinctAdjacencyEligibilityRequired
498 | Self::OrderPrefixNotAlignedWithGroupKeys
499 | Self::OrderRequiresLimit
500 | Self::DistinctHavingUnsupported
501 | Self::HavingUnsupportedCompareOp { .. }
502 | Self::DistinctAggregateKindUnsupported { .. }
503 | Self::DistinctAggregateFieldTargetUnsupported { .. }
504 | Self::FieldTargetAggregatesUnsupported { .. }
505 )
506 }
507}