Skip to main content

icydb_core/db/schema/
errors.rs

1//! Module: db::schema::errors
2//! Responsibility: schema validation error taxonomy for runtime schema contracts.
3//! Does not own: predicate AST or planning policy logic.
4//! Boundary: error surface for schema construction and predicate-schema validation.
5
6use crate::{
7    db::predicate::{CoercionId, CompareOp, UnsupportedQueryFeature},
8    model::index::{IndexExpression, IndexModel},
9};
10use std::fmt;
11
12/// Compact predicate operator identity for schema validation diagnostics.
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum SchemaValidationOperator {
15    Compare(CompareOp),
16    CompareField { op: CompareOp, right_field: String },
17    IsEmpty,
18    IsNotEmpty,
19    TextContains,
20    TextContainsCi,
21}
22
23impl SchemaValidationOperator {
24    pub(crate) const fn compare(op: CompareOp) -> Self {
25        Self::Compare(op)
26    }
27
28    pub(crate) fn compare_field(op: CompareOp, right_field: &str) -> Self {
29        Self::CompareField {
30            op,
31            right_field: right_field.to_string(),
32        }
33    }
34}
35
36impl fmt::Display for SchemaValidationOperator {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match self {
39            Self::Compare(op) => write!(f, "{op:?}"),
40            Self::CompareField { op, right_field } => {
41                write!(f, "{op:?} against field '{right_field}'")
42            }
43            Self::IsEmpty => f.write_str("is_empty"),
44            Self::IsNotEmpty => f.write_str("is_not_empty"),
45            Self::TextContains => f.write_str("text_contains"),
46            Self::TextContainsCi => f.write_str("text_contains_ci"),
47        }
48    }
49}
50
51/// Compact literal validation reason for schema validation diagnostics.
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub enum SchemaLiteralValidationReason {
54    ExpectedList,
55    ExpectedText,
56    ExpectedScalar,
57    LiteralTypeMismatch,
58    ListElementTypeMismatch,
59    EnumPathMismatch,
60    UnknownEnumVariant,
61    EnumBodyMismatch,
62}
63
64impl fmt::Display for SchemaLiteralValidationReason {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            Self::ExpectedList => f.write_str("expected list literal"),
68            Self::ExpectedText => f.write_str("expected text literal"),
69            Self::ExpectedScalar => f.write_str("expected scalar literal"),
70            Self::LiteralTypeMismatch => f.write_str("literal type does not match field type"),
71            Self::ListElementTypeMismatch => {
72                f.write_str("list literal does not match field element type")
73            }
74            Self::EnumPathMismatch => f.write_str("enum path does not match field enum type"),
75            Self::UnknownEnumVariant => f.write_str("enum variant is not accepted for field type"),
76            Self::EnumBodyMismatch => {
77                f.write_str("enum payload does not match the accepted variant contract")
78            }
79        }
80    }
81}
82
83/// Predicate/schema validation failures, including invalid model contracts.
84#[derive(Debug, thiserror::Error)]
85pub enum ValidateError {
86    #[error("unknown field '{field}'")]
87    UnknownField { field: String },
88
89    #[error("field '{field}' is not queryable")]
90    NonQueryableFieldType { field: String },
91
92    #[error("duplicate field '{field}'")]
93    DuplicateField { field: String },
94
95    #[error("unsupported query feature")]
96    UnsupportedQueryFeature(UnsupportedQueryFeature),
97
98    #[error("primary key '{field}' not present in entity fields")]
99    InvalidPrimaryKey { field: String },
100
101    #[error("primary key '{field}' has a non-keyable type")]
102    InvalidPrimaryKeyType { field: String },
103
104    #[error("index '{index}' references unknown field '{field}'")]
105    IndexFieldUnknown {
106        index: Box<IndexModel>,
107        field: String,
108    },
109
110    #[error("index '{index}' references non-queryable field '{field}'")]
111    IndexFieldNotQueryable {
112        index: Box<IndexModel>,
113        field: String,
114    },
115
116    #[error(
117        "index '{index}' references map field '{field}'; map fields are not queryable in icydb 0.7"
118    )]
119    IndexFieldMapNotQueryable {
120        index: Box<IndexModel>,
121        field: String,
122    },
123
124    #[error("index '{index}' repeats field '{field}'")]
125    IndexFieldDuplicate {
126        index: Box<IndexModel>,
127        field: String,
128    },
129
130    #[error("index '{index}' expression key item '{expression}' requires {expected}")]
131    IndexExpressionFieldTypeInvalid {
132        index: &'static str,
133        expression: IndexExpression,
134        expected: &'static str,
135    },
136
137    #[error("duplicate index name '{name}'")]
138    DuplicateIndexName { name: String },
139
140    #[error("index '{index}' predicate '{predicate}' has invalid SQL syntax")]
141    InvalidIndexPredicateSyntax {
142        index: Box<IndexModel>,
143        predicate: &'static str,
144    },
145
146    #[error("index '{index}' predicate '{predicate}' is invalid for schema")]
147    InvalidIndexPredicateSchema {
148        index: Box<IndexModel>,
149        predicate: &'static str,
150    },
151
152    #[error("operator {operator} is not valid for field '{field}'")]
153    InvalidOperator {
154        field: String,
155        operator: SchemaValidationOperator,
156    },
157
158    #[error("coercion {coercion:?} is not valid for field '{field}'")]
159    InvalidCoercion { field: String, coercion: CoercionId },
160
161    #[error("invalid literal for field '{field}': {reason}")]
162    InvalidLiteral {
163        field: String,
164        reason: SchemaLiteralValidationReason,
165    },
166}
167
168impl From<UnsupportedQueryFeature> for ValidateError {
169    fn from(err: UnsupportedQueryFeature) -> Self {
170        Self::UnsupportedQueryFeature(err)
171    }
172}
173
174impl ValidateError {
175    pub(crate) fn invalid_operator(field: &str, operator: SchemaValidationOperator) -> Self {
176        Self::InvalidOperator {
177            field: field.to_string(),
178            operator,
179        }
180    }
181
182    pub(crate) fn invalid_literal(field: &str, reason: SchemaLiteralValidationReason) -> Self {
183        Self::InvalidLiteral {
184            field: field.to_string(),
185            reason,
186        }
187    }
188}