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 pagination")]
132 DeletePlanWithPagination,
133
134 #[error("load plans must not include delete limits")]
136 LoadPlanWithDeleteLimit,
137
138 #[error("delete limit requires an explicit ordering")]
140 DeleteLimitRequiresOrder,
141
142 #[error(
144 "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."
145 )]
146 UnorderedPagination,
147}
148
149#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
156pub enum CursorPagingPolicyError {
157 #[error(
158 "{message}",
159 message = CursorPlanError::cursor_requires_order_message()
160 )]
161 CursorRequiresOrder,
162
163 #[error(
164 "{message}",
165 message = CursorPlanError::cursor_requires_limit_message()
166 )]
167 CursorRequiresLimit,
168}
169
170#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
177pub enum GroupPlanError {
178 #[error("HAVING is only supported for GROUP BY queries in this release")]
180 HavingRequiresGroupBy,
181
182 #[error("group query validation requires grouped logical plan variant")]
184 GroupedLogicalPlanRequired,
185
186 #[error("group specification must include at least one group field")]
188 EmptyGroupFields,
189
190 #[error(
192 "global DISTINCT aggregate without GROUP BY must declare exactly one DISTINCT field-target aggregate in this release"
193 )]
194 GlobalDistinctAggregateShapeUnsupported,
195
196 #[error("group specification must include at least one aggregate terminal")]
198 EmptyAggregates,
199
200 #[error("unknown group field '{field}'")]
202 UnknownGroupField { field: String },
203
204 #[error("group specification has duplicate group key: '{field}'")]
206 DuplicateGroupField { field: String },
207
208 #[error(
210 "grouped DISTINCT requires adjacency-based ordered-group eligibility proof in this release"
211 )]
212 DistinctAdjacencyEligibilityRequired,
213
214 #[error("grouped ORDER BY must start with GROUP BY key prefix in this release")]
216 OrderPrefixNotAlignedWithGroupKeys,
217
218 #[error("grouped ORDER BY requires LIMIT in this release")]
220 OrderRequiresLimit,
221
222 #[error("grouped HAVING with DISTINCT is not supported in this release")]
224 DistinctHavingUnsupported,
225
226 #[error("grouped HAVING clause at index={index} uses unsupported operator: {op}")]
228 HavingUnsupportedCompareOp { index: usize, op: String },
229
230 #[error("grouped HAVING clause at index={index} references non-group field '{field}'")]
232 HavingNonGroupFieldReference { index: usize, field: String },
233
234 #[error(
236 "grouped HAVING clause at index={index} references aggregate index {aggregate_index} but aggregate_count={aggregate_count}"
237 )]
238 HavingAggregateIndexOutOfBounds {
239 index: usize,
240 aggregate_index: usize,
241 aggregate_count: usize,
242 },
243
244 #[error(
246 "grouped DISTINCT aggregate at index={index} uses unsupported kind '{kind}' in this release"
247 )]
248 DistinctAggregateKindUnsupported { index: usize, kind: String },
249
250 #[error(
252 "grouped DISTINCT aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
253 )]
254 DistinctAggregateFieldTargetUnsupported {
255 index: usize,
256 kind: String,
257 field: String,
258 },
259
260 #[error("unknown grouped aggregate target field at index={index}: '{field}'")]
262 UnknownAggregateTargetField { index: usize, field: String },
263
264 #[error(
266 "global DISTINCT SUM aggregate target field at index={index} is not numeric: '{field}'"
267 )]
268 GlobalDistinctSumTargetNotNumeric { index: usize, field: String },
269
270 #[error(
272 "grouped aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
273 )]
274 FieldTargetAggregatesUnsupported {
275 index: usize,
276 kind: String,
277 field: String,
278 },
279}
280
281#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
288pub enum ExprPlanError {
289 #[error("unknown expression field '{field}'")]
291 UnknownExprField { field: String },
292
293 #[error("aggregate '{kind}' requires numeric target field '{field}'")]
295 NonNumericAggregateTarget { kind: String, field: String },
296
297 #[error("aggregate '{kind}' requires an explicit target field")]
299 AggregateTargetRequired { kind: String },
300
301 #[error("unary operator '{op}' is incompatible with operand type {found}")]
303 InvalidUnaryOperand { op: String, found: String },
304
305 #[error("binary operator '{op}' is incompatible with operand types ({left}, {right})")]
307 InvalidBinaryOperands {
308 op: String,
309 left: String,
310 right: String,
311 },
312
313 #[error(
315 "grouped projection expression at index={index} references fields outside GROUP BY keys"
316 )]
317 GroupedProjectionReferencesNonGroupField { index: usize },
318}
319
320#[derive(Clone, Copy, Debug, Eq, PartialEq)]
327pub(crate) enum CursorOrderPlanShapeError {
328 MissingExplicitOrder,
329 EmptyOrderSpec,
330}
331
332#[derive(Clone, Copy, Debug, Eq, PartialEq)]
339pub(crate) enum IntentKeyAccessKind {
340 Single,
341 Many,
342 Only,
343}
344
345#[derive(Clone, Copy, Debug, Eq, PartialEq)]
351pub(crate) enum IntentKeyAccessPolicyViolation {
352 KeyAccessConflict,
353 ByIdsWithPredicate,
354 OnlyWithPredicate,
355}
356
357#[derive(Clone, Copy, Debug, Eq, PartialEq)]
364pub(crate) enum FluentLoadPolicyViolation {
365 CursorRequiresPagedExecution,
366 GroupedRequiresExecuteGrouped,
367 CursorRequiresOrder,
368 CursorRequiresLimit,
369}
370
371impl From<ValidateError> for PlanError {
372 fn from(err: ValidateError) -> Self {
373 Self::from(PlanUserError::from(err))
374 }
375}
376
377impl From<OrderPlanError> for PlanError {
378 fn from(err: OrderPlanError) -> Self {
379 Self::from(PlanUserError::from(err))
380 }
381}
382
383impl From<AccessPlanError> for PlanError {
384 fn from(err: AccessPlanError) -> Self {
385 Self::from(PlanUserError::from(err))
386 }
387}
388
389impl From<PolicyPlanError> for PlanError {
390 fn from(err: PolicyPlanError) -> Self {
391 Self::from(PlanPolicyError::from(err))
392 }
393}
394
395impl From<CursorPlanError> for PlanError {
396 fn from(err: CursorPlanError) -> Self {
397 Self::Cursor(Box::new(err))
398 }
399}
400
401impl From<GroupPlanError> for PlanError {
402 fn from(err: GroupPlanError) -> Self {
403 if err.belongs_to_policy_axis() {
404 return Self::from(PlanPolicyError::from(err));
405 }
406
407 Self::from(PlanUserError::from(err))
408 }
409}
410
411impl From<ExprPlanError> for PlanError {
412 fn from(err: ExprPlanError) -> Self {
413 Self::from(PlanUserError::from(err))
414 }
415}
416
417impl From<PlanUserError> for PlanError {
418 fn from(err: PlanUserError) -> Self {
419 Self::User(Box::new(err))
420 }
421}
422
423impl From<PlanPolicyError> for PlanError {
424 fn from(err: PlanPolicyError) -> Self {
425 Self::Policy(Box::new(err))
426 }
427}
428
429impl From<ValidateError> for PlanUserError {
430 fn from(err: ValidateError) -> Self {
431 Self::PredicateInvalid(Box::new(err))
432 }
433}
434
435impl From<OrderPlanError> for PlanUserError {
436 fn from(err: OrderPlanError) -> Self {
437 Self::Order(Box::new(err))
438 }
439}
440
441impl From<AccessPlanError> for PlanUserError {
442 fn from(err: AccessPlanError) -> Self {
443 Self::Access(Box::new(err))
444 }
445}
446
447impl From<GroupPlanError> for PlanUserError {
448 fn from(err: GroupPlanError) -> Self {
449 Self::Group(Box::new(err))
450 }
451}
452
453impl From<ExprPlanError> for PlanUserError {
454 fn from(err: ExprPlanError) -> Self {
455 Self::Expr(Box::new(err))
456 }
457}
458
459impl From<PolicyPlanError> for PlanPolicyError {
460 fn from(err: PolicyPlanError) -> Self {
461 Self::Policy(Box::new(err))
462 }
463}
464
465impl From<GroupPlanError> for PlanPolicyError {
466 fn from(err: GroupPlanError) -> Self {
467 Self::Group(Box::new(err))
468 }
469}
470
471impl GroupPlanError {
472 const fn belongs_to_policy_axis(&self) -> bool {
476 matches!(
477 self,
478 Self::GlobalDistinctAggregateShapeUnsupported
479 | Self::DistinctAdjacencyEligibilityRequired
480 | Self::OrderPrefixNotAlignedWithGroupKeys
481 | Self::OrderRequiresLimit
482 | Self::DistinctHavingUnsupported
483 | Self::HavingUnsupportedCompareOp { .. }
484 | Self::DistinctAggregateKindUnsupported { .. }
485 | Self::DistinctAggregateFieldTargetUnsupported { .. }
486 | Self::FieldTargetAggregatesUnsupported { .. }
487 )
488 }
489}