icydb_core/db/query/plan/validate/
mod.rs1mod core;
11mod cursor_policy;
12mod fluent_policy;
13mod grouped;
14mod intent_policy;
15mod order;
16mod plan_shape;
17mod symbols;
18
19use crate::db::{access::AccessPlanError, cursor::CursorPlanError, predicate::ValidateError};
20use thiserror::Error as ThisError;
21
22pub(crate) use core::{validate_group_query_semantics, validate_query_semantics};
23pub(crate) use cursor_policy::{
24 validate_cursor_order_plan_shape, validate_cursor_paging_requirements,
25};
26pub(crate) use fluent_policy::{validate_fluent_non_paged_mode, validate_fluent_paged_mode};
27#[cfg(test)]
28pub(in crate::db::query) use grouped::validate_group_projection_expr_compatibility_for_test;
29pub(crate) use intent_policy::{validate_intent_key_access_policy, validate_intent_plan_shape};
30pub(crate) use order::validate_order;
31pub(crate) use plan_shape::{has_explicit_order, validate_order_shape, validate_plan_shape};
32pub(crate) use symbols::resolve_group_field_slot;
33
34#[derive(Debug, ThisError)]
44pub enum PlanError {
45 #[error("{0}")]
46 User(Box<PlanUserError>),
47
48 #[error("{0}")]
49 Policy(Box<PlanPolicyError>),
50
51 #[error("{0}")]
52 Cursor(Box<CursorPlanError>),
53}
54
55#[derive(Debug, ThisError)]
64pub enum PlanUserError {
65 #[error("predicate validation failed: {0}")]
66 PredicateInvalid(Box<ValidateError>),
67
68 #[error("{0}")]
69 Order(Box<OrderPlanError>),
70
71 #[error("{0}")]
72 Access(Box<AccessPlanError>),
73
74 #[error("{0}")]
75 Group(Box<GroupPlanError>),
76
77 #[error("{0}")]
78 Expr(Box<ExprPlanError>),
79}
80
81#[derive(Debug, ThisError)]
90pub enum PlanPolicyError {
91 #[error("{0}")]
92 Policy(Box<PolicyPlanError>),
93
94 #[error("{0}")]
95 Group(Box<GroupPlanError>),
96}
97
98#[derive(Debug, ThisError)]
105pub enum OrderPlanError {
106 #[error("unknown order field '{field}'")]
108 UnknownField { field: String },
109
110 #[error("order field '{field}' is not orderable")]
112 UnorderableField { field: String },
113
114 #[error("order field '{field}' appears multiple times")]
116 DuplicateOrderField { field: String },
117
118 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
120 MissingPrimaryKeyTieBreak { field: String },
121}
122
123#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
130pub enum PolicyPlanError {
131 #[error("order specification must include at least one field")]
133 EmptyOrderSpec,
134
135 #[error("delete plans must not include OFFSET")]
137 DeletePlanWithOffset,
138
139 #[error("delete plans must not include GROUP BY or HAVING")]
141 DeletePlanWithGrouping,
142
143 #[error("delete plans must not include pagination")]
145 DeletePlanWithPagination,
146
147 #[error("load plans must not include delete limits")]
149 LoadPlanWithDeleteLimit,
150
151 #[error("delete limit requires an explicit ordering")]
153 DeleteLimitRequiresOrder,
154
155 #[error(
157 "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."
158 )]
159 UnorderedPagination,
160}
161
162#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
169pub enum CursorPagingPolicyError {
170 #[error(
171 "{message}",
172 message = CursorPlanError::cursor_requires_order_message()
173 )]
174 CursorRequiresOrder,
175
176 #[error(
177 "{message}",
178 message = CursorPlanError::cursor_requires_limit_message()
179 )]
180 CursorRequiresLimit,
181}
182
183#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
190pub enum GroupPlanError {
191 #[error("HAVING is only supported for GROUP BY queries in this release")]
193 HavingRequiresGroupBy,
194
195 #[error("group query validation requires grouped logical plan variant")]
197 GroupedLogicalPlanRequired,
198
199 #[error("group specification must include at least one group field")]
201 EmptyGroupFields,
202
203 #[error(
205 "global DISTINCT aggregate without GROUP BY must declare exactly one DISTINCT field-target aggregate in this release"
206 )]
207 GlobalDistinctAggregateShapeUnsupported,
208
209 #[error("group specification must include at least one aggregate terminal")]
211 EmptyAggregates,
212
213 #[error("unknown group field '{field}'")]
215 UnknownGroupField { field: String },
216
217 #[error("group specification has duplicate group key: '{field}'")]
219 DuplicateGroupField { field: String },
220
221 #[error(
223 "grouped DISTINCT requires adjacency-based ordered-group eligibility proof in this release"
224 )]
225 DistinctAdjacencyEligibilityRequired,
226
227 #[error("grouped ORDER BY must start with GROUP BY key prefix in this release")]
229 OrderPrefixNotAlignedWithGroupKeys,
230
231 #[error("grouped ORDER BY requires LIMIT in this release")]
233 OrderRequiresLimit,
234
235 #[error("grouped HAVING with DISTINCT is not supported in this release")]
237 DistinctHavingUnsupported,
238
239 #[error("grouped HAVING clause at index={index} uses unsupported operator: {op}")]
241 HavingUnsupportedCompareOp { index: usize, op: String },
242
243 #[error("grouped HAVING clause at index={index} references non-group field '{field}'")]
245 HavingNonGroupFieldReference { index: usize, field: String },
246
247 #[error(
249 "grouped HAVING clause at index={index} references aggregate index {aggregate_index} but aggregate_count={aggregate_count}"
250 )]
251 HavingAggregateIndexOutOfBounds {
252 index: usize,
253 aggregate_index: usize,
254 aggregate_count: usize,
255 },
256
257 #[error(
259 "grouped DISTINCT aggregate at index={index} uses unsupported kind '{kind}' in this release"
260 )]
261 DistinctAggregateKindUnsupported { index: usize, kind: String },
262
263 #[error(
265 "grouped DISTINCT aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
266 )]
267 DistinctAggregateFieldTargetUnsupported {
268 index: usize,
269 kind: String,
270 field: String,
271 },
272
273 #[error("unknown grouped aggregate target field at index={index}: '{field}'")]
275 UnknownAggregateTargetField { index: usize, field: String },
276
277 #[error(
279 "global DISTINCT SUM aggregate target field at index={index} is not numeric: '{field}'"
280 )]
281 GlobalDistinctSumTargetNotNumeric { index: usize, field: String },
282
283 #[error(
285 "grouped aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
286 )]
287 FieldTargetAggregatesUnsupported {
288 index: usize,
289 kind: String,
290 field: String,
291 },
292}
293
294#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
301pub enum ExprPlanError {
302 #[error("unknown expression field '{field}'")]
304 UnknownExprField { field: String },
305
306 #[error("aggregate '{kind}' requires numeric target field '{field}'")]
308 NonNumericAggregateTarget { kind: String, field: String },
309
310 #[error("aggregate '{kind}' requires an explicit target field")]
312 AggregateTargetRequired { kind: String },
313
314 #[error("unary operator '{op}' is incompatible with operand type {found}")]
316 InvalidUnaryOperand { op: String, found: String },
317
318 #[error("binary operator '{op}' is incompatible with operand types ({left}, {right})")]
320 InvalidBinaryOperands {
321 op: String,
322 left: String,
323 right: String,
324 },
325
326 #[error(
328 "grouped projection expression at index={index} references fields outside GROUP BY keys"
329 )]
330 GroupedProjectionReferencesNonGroupField { index: usize },
331}
332
333#[derive(Clone, Copy, Debug, Eq, PartialEq)]
340pub(crate) enum CursorOrderPlanShapeError {
341 MissingExplicitOrder,
342 EmptyOrderSpec,
343}
344
345#[derive(Clone, Copy, Debug, Eq, PartialEq)]
352pub(crate) enum IntentKeyAccessKind {
353 Single,
354 Many,
355 Only,
356}
357
358#[derive(Clone, Copy, Debug, Eq, PartialEq)]
364pub(crate) enum IntentKeyAccessPolicyViolation {
365 KeyAccessConflict,
366 ByIdsWithPredicate,
367 OnlyWithPredicate,
368}
369
370#[derive(Clone, Copy, Debug, Eq, PartialEq)]
377pub(crate) enum FluentLoadPolicyViolation {
378 CursorRequiresPagedExecution,
379 GroupedRequiresExecuteGrouped,
380 CursorRequiresOrder,
381 CursorRequiresLimit,
382}
383
384impl From<ValidateError> for PlanError {
385 fn from(err: ValidateError) -> Self {
386 Self::from(PlanUserError::from(err))
387 }
388}
389
390impl From<OrderPlanError> for PlanError {
391 fn from(err: OrderPlanError) -> Self {
392 Self::from(PlanUserError::from(err))
393 }
394}
395
396impl From<AccessPlanError> for PlanError {
397 fn from(err: AccessPlanError) -> Self {
398 Self::from(PlanUserError::from(err))
399 }
400}
401
402impl From<PolicyPlanError> for PlanError {
403 fn from(err: PolicyPlanError) -> Self {
404 Self::from(PlanPolicyError::from(err))
405 }
406}
407
408impl From<CursorPlanError> for PlanError {
409 fn from(err: CursorPlanError) -> Self {
410 Self::Cursor(Box::new(err))
411 }
412}
413
414impl From<GroupPlanError> for PlanError {
415 fn from(err: GroupPlanError) -> Self {
416 if err.belongs_to_policy_axis() {
417 return Self::from(PlanPolicyError::from(err));
418 }
419
420 Self::from(PlanUserError::from(err))
421 }
422}
423
424impl From<ExprPlanError> for PlanError {
425 fn from(err: ExprPlanError) -> Self {
426 Self::from(PlanUserError::from(err))
427 }
428}
429
430impl From<PlanUserError> for PlanError {
431 fn from(err: PlanUserError) -> Self {
432 Self::User(Box::new(err))
433 }
434}
435
436impl From<PlanPolicyError> for PlanError {
437 fn from(err: PlanPolicyError) -> Self {
438 Self::Policy(Box::new(err))
439 }
440}
441
442impl From<ValidateError> for PlanUserError {
443 fn from(err: ValidateError) -> Self {
444 Self::PredicateInvalid(Box::new(err))
445 }
446}
447
448impl From<OrderPlanError> for PlanUserError {
449 fn from(err: OrderPlanError) -> Self {
450 Self::Order(Box::new(err))
451 }
452}
453
454impl From<AccessPlanError> for PlanUserError {
455 fn from(err: AccessPlanError) -> Self {
456 Self::Access(Box::new(err))
457 }
458}
459
460impl From<GroupPlanError> for PlanUserError {
461 fn from(err: GroupPlanError) -> Self {
462 Self::Group(Box::new(err))
463 }
464}
465
466impl From<ExprPlanError> for PlanUserError {
467 fn from(err: ExprPlanError) -> Self {
468 Self::Expr(Box::new(err))
469 }
470}
471
472impl From<PolicyPlanError> for PlanPolicyError {
473 fn from(err: PolicyPlanError) -> Self {
474 Self::Policy(Box::new(err))
475 }
476}
477
478impl From<GroupPlanError> for PlanPolicyError {
479 fn from(err: GroupPlanError) -> Self {
480 Self::Group(Box::new(err))
481 }
482}
483
484impl GroupPlanError {
485 const fn belongs_to_policy_axis(&self) -> bool {
489 matches!(
490 self,
491 Self::GlobalDistinctAggregateShapeUnsupported
492 | Self::DistinctAdjacencyEligibilityRequired
493 | Self::OrderPrefixNotAlignedWithGroupKeys
494 | Self::OrderRequiresLimit
495 | Self::DistinctHavingUnsupported
496 | Self::HavingUnsupportedCompareOp { .. }
497 | Self::DistinctAggregateKindUnsupported { .. }
498 | Self::DistinctAggregateFieldTargetUnsupported { .. }
499 | Self::FieldTargetAggregatesUnsupported { .. }
500 )
501 }
502}