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}
61
62impl fmt::Display for SchemaLiteralValidationReason {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            Self::ExpectedList => f.write_str("expected list literal"),
66            Self::ExpectedText => f.write_str("expected text literal"),
67            Self::ExpectedScalar => f.write_str("expected scalar literal"),
68            Self::LiteralTypeMismatch => f.write_str("literal type does not match field type"),
69            Self::ListElementTypeMismatch => {
70                f.write_str("list literal does not match field element type")
71            }
72            Self::EnumPathMismatch => f.write_str("enum path does not match field enum type"),
73        }
74    }
75}
76
77/// Predicate/schema validation failures, including invalid model contracts.
78#[derive(Debug, thiserror::Error)]
79pub enum ValidateError {
80    #[error("unknown field '{field}'")]
81    UnknownField { field: String },
82
83    #[error("field '{field}' is not queryable")]
84    NonQueryableFieldType { field: String },
85
86    #[error("duplicate field '{field}'")]
87    DuplicateField { field: String },
88
89    #[error("unsupported query feature")]
90    UnsupportedQueryFeature(UnsupportedQueryFeature),
91
92    #[error("primary key '{field}' not present in entity fields")]
93    InvalidPrimaryKey { field: String },
94
95    #[error("primary key '{field}' has a non-keyable type")]
96    InvalidPrimaryKeyType { field: String },
97
98    #[error("index '{index}' references unknown field '{field}'")]
99    IndexFieldUnknown {
100        index: Box<IndexModel>,
101        field: String,
102    },
103
104    #[error("index '{index}' references non-queryable field '{field}'")]
105    IndexFieldNotQueryable {
106        index: Box<IndexModel>,
107        field: String,
108    },
109
110    #[error(
111        "index '{index}' references map field '{field}'; map fields are not queryable in icydb 0.7"
112    )]
113    IndexFieldMapNotQueryable {
114        index: Box<IndexModel>,
115        field: String,
116    },
117
118    #[error("index '{index}' repeats field '{field}'")]
119    IndexFieldDuplicate {
120        index: Box<IndexModel>,
121        field: String,
122    },
123
124    #[error("index '{index}' expression key item '{expression}' requires {expected}")]
125    IndexExpressionFieldTypeInvalid {
126        index: &'static str,
127        expression: IndexExpression,
128        expected: &'static str,
129    },
130
131    #[error("duplicate index name '{name}'")]
132    DuplicateIndexName { name: String },
133
134    #[error("index '{index}' predicate '{predicate}' has invalid SQL syntax")]
135    InvalidIndexPredicateSyntax {
136        index: Box<IndexModel>,
137        predicate: &'static str,
138    },
139
140    #[error("index '{index}' predicate '{predicate}' is invalid for schema")]
141    InvalidIndexPredicateSchema {
142        index: Box<IndexModel>,
143        predicate: &'static str,
144    },
145
146    #[error("operator {operator} is not valid for field '{field}'")]
147    InvalidOperator {
148        field: String,
149        operator: SchemaValidationOperator,
150    },
151
152    #[error("coercion {coercion:?} is not valid for field '{field}'")]
153    InvalidCoercion { field: String, coercion: CoercionId },
154
155    #[error("invalid literal for field '{field}': {reason}")]
156    InvalidLiteral {
157        field: String,
158        reason: SchemaLiteralValidationReason,
159    },
160}
161
162impl From<UnsupportedQueryFeature> for ValidateError {
163    fn from(err: UnsupportedQueryFeature) -> Self {
164        Self::UnsupportedQueryFeature(err)
165    }
166}
167
168impl ValidateError {
169    pub(crate) fn invalid_operator(field: &str, operator: SchemaValidationOperator) -> Self {
170        Self::InvalidOperator {
171            field: field.to_string(),
172            operator,
173        }
174    }
175
176    pub(crate) fn invalid_literal(field: &str, reason: SchemaLiteralValidationReason) -> Self {
177        Self::InvalidLiteral {
178            field: field.to_string(),
179            reason,
180        }
181    }
182}