icydb_core/db/query/plan/validate/
mod.rs1mod 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#[derive(Debug, ThisError)]
39pub enum PlanError {
40 #[error("{0}")]
41 User(Box<PlanUserError>),
42
43 #[error("{0}")]
44 Policy(Box<PlanPolicyError>),
45
46 #[error("{0}")]
47 Cursor(Box<CursorPlanError>),
48}
49
50#[derive(Debug, ThisError)]
59pub enum PlanUserError {
60 #[error("predicate validation failed: {0}")]
61 PredicateInvalid(Box<ValidateError>),
62
63 #[error("{0}")]
64 Order(Box<OrderPlanError>),
65
66 #[error("{0}")]
67 Access(Box<AccessPlanError>),
68
69 #[error("{0}")]
70 Group(Box<GroupPlanError>),
71
72 #[error("{0}")]
73 Expr(Box<ExprPlanError>),
74}
75
76#[derive(Debug, ThisError)]
85pub enum PlanPolicyError {
86 #[error("{0}")]
87 Policy(Box<PolicyPlanError>),
88
89 #[error("{0}")]
90 Group(Box<GroupPlanError>),
91}
92
93#[derive(Debug, ThisError)]
100pub enum OrderPlanError {
101 #[error("unknown order field '{field}'")]
103 UnknownField { field: String },
104
105 #[error("order field '{field}' is not orderable")]
107 UnorderableField { field: String },
108
109 #[error("order field '{field}' appears multiple times")]
111 DuplicateOrderField { field: String },
112
113 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
115 MissingPrimaryKeyTieBreak { field: String },
116}
117
118#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
125pub enum PolicyPlanError {
126 #[error("order specification must include at least one field")]
128 EmptyOrderSpec,
129
130 #[error("delete plans must not include OFFSET")]
132 DeletePlanWithOffset,
133
134 #[error("delete plans must not include GROUP BY or HAVING")]
136 DeletePlanWithGrouping,
137
138 #[error("delete plans must not include pagination")]
140 DeletePlanWithPagination,
141
142 #[error("load plans must not include delete limits")]
144 LoadPlanWithDeleteLimit,
145
146 #[error("delete limit requires an explicit ordering")]
148 DeleteLimitRequiresOrder,
149
150 #[error(
152 "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."
153 )]
154 UnorderedPagination,
155}
156
157#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
164pub enum CursorPagingPolicyError {
165 #[error(
166 "{message}",
167 message = CursorPlanError::cursor_requires_order_message()
168 )]
169 CursorRequiresOrder,
170
171 #[error(
172 "{message}",
173 message = CursorPlanError::cursor_requires_limit_message()
174 )]
175 CursorRequiresLimit,
176}
177
178#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
185pub enum GroupPlanError {
186 #[error("HAVING is only supported for GROUP BY queries in this release")]
188 HavingRequiresGroupBy,
189
190 #[error("group query validation requires grouped logical plan variant")]
192 GroupedLogicalPlanRequired,
193
194 #[error("group specification must include at least one group field")]
196 EmptyGroupFields,
197
198 #[error(
200 "global DISTINCT aggregate without GROUP BY must declare exactly one DISTINCT field-target aggregate in this release"
201 )]
202 GlobalDistinctAggregateShapeUnsupported,
203
204 #[error("group specification must include at least one aggregate terminal")]
206 EmptyAggregates,
207
208 #[error("unknown group field '{field}'")]
210 UnknownGroupField { field: String },
211
212 #[error("group specification has duplicate group key: '{field}'")]
214 DuplicateGroupField { field: String },
215
216 #[error(
218 "grouped DISTINCT requires adjacency-based ordered-group eligibility proof in this release"
219 )]
220 DistinctAdjacencyEligibilityRequired,
221
222 #[error("grouped ORDER BY must start with GROUP BY key prefix in this release")]
224 OrderPrefixNotAlignedWithGroupKeys,
225
226 #[error("grouped ORDER BY requires LIMIT in this release")]
228 OrderRequiresLimit,
229
230 #[error("grouped HAVING with DISTINCT is not supported in this release")]
232 DistinctHavingUnsupported,
233
234 #[error("grouped HAVING clause at index={index} uses unsupported operator: {op}")]
236 HavingUnsupportedCompareOp { index: usize, op: String },
237
238 #[error("grouped HAVING clause at index={index} references non-group field '{field}'")]
240 HavingNonGroupFieldReference { index: usize, field: String },
241
242 #[error(
244 "grouped HAVING clause at index={index} references aggregate index {aggregate_index} but aggregate_count={aggregate_count}"
245 )]
246 HavingAggregateIndexOutOfBounds {
247 index: usize,
248 aggregate_index: usize,
249 aggregate_count: usize,
250 },
251
252 #[error(
254 "grouped DISTINCT aggregate at index={index} uses unsupported kind '{kind}' in this release"
255 )]
256 DistinctAggregateKindUnsupported { index: usize, kind: String },
257
258 #[error(
260 "grouped DISTINCT aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
261 )]
262 DistinctAggregateFieldTargetUnsupported {
263 index: usize,
264 kind: String,
265 field: String,
266 },
267
268 #[error("unknown grouped aggregate target field at index={index}: '{field}'")]
270 UnknownAggregateTargetField { index: usize, field: String },
271
272 #[error(
274 "global DISTINCT SUM aggregate target field at index={index} is not numeric: '{field}'"
275 )]
276 GlobalDistinctSumTargetNotNumeric { index: usize, field: String },
277
278 #[error(
280 "grouped aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
281 )]
282 FieldTargetAggregatesUnsupported {
283 index: usize,
284 kind: String,
285 field: String,
286 },
287}
288
289#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
296pub enum ExprPlanError {
297 #[error("unknown expression field '{field}'")]
299 UnknownExprField { field: String },
300
301 #[error("aggregate '{kind}' requires numeric target field '{field}'")]
303 NonNumericAggregateTarget { kind: String, field: String },
304
305 #[error("aggregate '{kind}' requires an explicit target field")]
307 AggregateTargetRequired { kind: String },
308
309 #[error("unary operator '{op}' is incompatible with operand type {found}")]
311 InvalidUnaryOperand { op: String, found: String },
312
313 #[error("binary operator '{op}' is incompatible with operand types ({left}, {right})")]
315 InvalidBinaryOperands {
316 op: String,
317 left: String,
318 right: String,
319 },
320
321 #[error(
323 "grouped projection expression at index={index} references fields outside GROUP BY keys"
324 )]
325 GroupedProjectionReferencesNonGroupField { index: usize },
326}
327
328#[derive(Clone, Copy, Debug, Eq, PartialEq)]
335pub(crate) enum CursorOrderPlanShapeError {
336 MissingExplicitOrder,
337 EmptyOrderSpec,
338}
339
340#[derive(Clone, Copy, Debug, Eq, PartialEq)]
347pub(crate) enum IntentKeyAccessKind {
348 Single,
349 Many,
350 Only,
351}
352
353#[derive(Clone, Copy, Debug, Eq, PartialEq)]
359pub(crate) enum IntentKeyAccessPolicyViolation {
360 KeyAccessConflict,
361 ByIdsWithPredicate,
362 OnlyWithPredicate,
363}
364
365#[derive(Clone, Copy, Debug, Eq, PartialEq)]
372pub(crate) enum FluentLoadPolicyViolation {
373 CursorRequiresPagedExecution,
374 GroupedRequiresExecuteGrouped,
375 CursorRequiresOrder,
376 CursorRequiresLimit,
377}
378
379impl From<ValidateError> for PlanError {
380 fn from(err: ValidateError) -> Self {
381 Self::from(PlanUserError::from(err))
382 }
383}
384
385impl From<OrderPlanError> for PlanError {
386 fn from(err: OrderPlanError) -> Self {
387 Self::from(PlanUserError::from(err))
388 }
389}
390
391impl From<AccessPlanError> for PlanError {
392 fn from(err: AccessPlanError) -> Self {
393 Self::from(PlanUserError::from(err))
394 }
395}
396
397impl From<PolicyPlanError> for PlanError {
398 fn from(err: PolicyPlanError) -> Self {
399 Self::from(PlanPolicyError::from(err))
400 }
401}
402
403impl From<CursorPlanError> for PlanError {
404 fn from(err: CursorPlanError) -> Self {
405 Self::Cursor(Box::new(err))
406 }
407}
408
409impl From<GroupPlanError> for PlanError {
410 fn from(err: GroupPlanError) -> Self {
411 if err.belongs_to_policy_axis() {
412 return Self::from(PlanPolicyError::from(err));
413 }
414
415 Self::from(PlanUserError::from(err))
416 }
417}
418
419impl From<ExprPlanError> for PlanError {
420 fn from(err: ExprPlanError) -> Self {
421 Self::from(PlanUserError::from(err))
422 }
423}
424
425impl From<PlanUserError> for PlanError {
426 fn from(err: PlanUserError) -> Self {
427 Self::User(Box::new(err))
428 }
429}
430
431impl From<PlanPolicyError> for PlanError {
432 fn from(err: PlanPolicyError) -> Self {
433 Self::Policy(Box::new(err))
434 }
435}
436
437impl From<ValidateError> for PlanUserError {
438 fn from(err: ValidateError) -> Self {
439 Self::PredicateInvalid(Box::new(err))
440 }
441}
442
443impl From<OrderPlanError> for PlanUserError {
444 fn from(err: OrderPlanError) -> Self {
445 Self::Order(Box::new(err))
446 }
447}
448
449impl From<AccessPlanError> for PlanUserError {
450 fn from(err: AccessPlanError) -> Self {
451 Self::Access(Box::new(err))
452 }
453}
454
455impl From<GroupPlanError> for PlanUserError {
456 fn from(err: GroupPlanError) -> Self {
457 Self::Group(Box::new(err))
458 }
459}
460
461impl From<ExprPlanError> for PlanUserError {
462 fn from(err: ExprPlanError) -> Self {
463 Self::Expr(Box::new(err))
464 }
465}
466
467impl From<PolicyPlanError> for PlanPolicyError {
468 fn from(err: PolicyPlanError) -> Self {
469 Self::Policy(Box::new(err))
470 }
471}
472
473impl From<GroupPlanError> for PlanPolicyError {
474 fn from(err: GroupPlanError) -> Self {
475 Self::Group(Box::new(err))
476 }
477}
478
479impl GroupPlanError {
480 const fn belongs_to_policy_axis(&self) -> bool {
484 matches!(
485 self,
486 Self::GlobalDistinctAggregateShapeUnsupported
487 | Self::DistinctAdjacencyEligibilityRequired
488 | Self::OrderPrefixNotAlignedWithGroupKeys
489 | Self::OrderRequiresLimit
490 | Self::DistinctHavingUnsupported
491 | Self::HavingUnsupportedCompareOp { .. }
492 | Self::DistinctAggregateKindUnsupported { .. }
493 | Self::DistinctAggregateFieldTargetUnsupported { .. }
494 | Self::FieldTargetAggregatesUnsupported { .. }
495 )
496 }
497}