Skip to main content

icydb_core/db/query/predicate/validate/
schema.rs

1use crate::{
2    db::{
3        identity::{EntityName, EntityNameError, IndexName, IndexNameError},
4        query::predicate::{UnsupportedQueryFeature, coercion::CoercionId},
5    },
6    model::{entity::EntityModel, field::FieldKind, index::IndexModel},
7};
8use std::collections::{BTreeMap, BTreeSet};
9use std::fmt;
10
11use crate::db::query::predicate::validate::model::{FieldType, field_type_from_model_kind};
12
13fn validate_index_fields(
14    fields: &BTreeMap<String, FieldType>,
15    indexes: &[&IndexModel],
16) -> Result<(), ValidateError> {
17    let mut seen_names = BTreeSet::new();
18    for index in indexes {
19        if seen_names.contains(index.name) {
20            return Err(ValidateError::DuplicateIndexName {
21                name: index.name.to_string(),
22            });
23        }
24        seen_names.insert(index.name);
25
26        let mut seen = BTreeSet::new();
27        for field in index.fields {
28            if !fields.contains_key(*field) {
29                return Err(ValidateError::IndexFieldUnknown {
30                    index: **index,
31                    field: (*field).to_string(),
32                });
33            }
34            if seen.contains(*field) {
35                return Err(ValidateError::IndexFieldDuplicate {
36                    index: **index,
37                    field: (*field).to_string(),
38                });
39            }
40            seen.insert(*field);
41
42            let field_type = fields
43                .get(*field)
44                .expect("index field existence checked above");
45            // Guardrail: map fields are deterministic stored values but remain
46            // non-queryable and non-indexable in 0.7.
47            if matches!(field_type, FieldType::Map { .. }) {
48                return Err(ValidateError::IndexFieldMapNotQueryable {
49                    index: **index,
50                    field: (*field).to_string(),
51                });
52            }
53            if !field_type.value_kind().is_queryable() {
54                return Err(ValidateError::IndexFieldNotQueryable {
55                    index: **index,
56                    field: (*field).to_string(),
57                });
58            }
59        }
60    }
61
62    Ok(())
63}
64
65///
66/// SchemaInfo
67///
68/// Lightweight, runtime-usable field-type map for one entity.
69/// This is the *only* schema surface the predicate validator depends on.
70///
71
72#[derive(Clone, Debug)]
73pub(crate) struct SchemaInfo {
74    fields: BTreeMap<String, FieldType>,
75    field_kinds: BTreeMap<String, FieldKind>,
76}
77
78impl SchemaInfo {
79    #[must_use]
80    pub(crate) fn field(&self, name: &str) -> Option<&FieldType> {
81        self.fields.get(name)
82    }
83
84    #[must_use]
85    pub(crate) fn field_kind(&self, name: &str) -> Option<&FieldKind> {
86        self.field_kinds.get(name)
87    }
88
89    /// Builds runtime predicate schema information from an entity model.
90    pub(crate) fn from_entity_model(model: &EntityModel) -> Result<Self, ValidateError> {
91        // Validate identity constraints before building schema maps.
92        let entity_name = EntityName::try_from_str(model.entity_name).map_err(|err| {
93            ValidateError::InvalidEntityName {
94                name: model.entity_name.to_string(),
95                source: err,
96            }
97        })?;
98
99        if !model
100            .fields
101            .iter()
102            .any(|field| std::ptr::eq(field, model.primary_key))
103        {
104            return Err(ValidateError::InvalidPrimaryKey {
105                field: model.primary_key.name.to_string(),
106            });
107        }
108
109        let mut fields = BTreeMap::new();
110        let mut field_kinds = BTreeMap::new();
111        for field in model.fields {
112            if fields.contains_key(field.name) {
113                return Err(ValidateError::DuplicateField {
114                    field: field.name.to_string(),
115                });
116            }
117            let ty = field_type_from_model_kind(&field.kind);
118            fields.insert(field.name.to_string(), ty);
119            field_kinds.insert(field.name.to_string(), field.kind);
120        }
121
122        let pk_field_type = fields
123            .get(model.primary_key.name)
124            .expect("primary key verified above");
125        if !pk_field_type.is_keyable() {
126            return Err(ValidateError::InvalidPrimaryKeyType {
127                field: model.primary_key.name.to_string(),
128            });
129        }
130
131        validate_index_fields(&fields, model.indexes)?;
132        for index in model.indexes {
133            IndexName::try_from_parts(&entity_name, index.fields).map_err(|err| {
134                ValidateError::InvalidIndexName {
135                    index: **index,
136                    source: err,
137                }
138            })?;
139        }
140
141        Ok(Self {
142            fields,
143            field_kinds,
144        })
145    }
146}
147
148/// Predicate/schema validation failures, including invalid model contracts.
149#[derive(Debug, thiserror::Error)]
150pub enum ValidateError {
151    #[error("invalid entity name '{name}': {source}")]
152    InvalidEntityName {
153        name: String,
154        #[source]
155        source: EntityNameError,
156    },
157
158    #[error("invalid index name for '{index}': {source}")]
159    InvalidIndexName {
160        index: IndexModel,
161        #[source]
162        source: IndexNameError,
163    },
164
165    #[error("unknown field '{field}'")]
166    UnknownField { field: String },
167
168    #[error("field '{field}' is not queryable")]
169    NonQueryableFieldType { field: String },
170
171    #[error("duplicate field '{field}'")]
172    DuplicateField { field: String },
173
174    #[error("{0}")]
175    UnsupportedQueryFeature(#[from] UnsupportedQueryFeature),
176
177    #[error("primary key '{field}' not present in entity fields")]
178    InvalidPrimaryKey { field: String },
179
180    #[error("primary key '{field}' has a non-keyable type")]
181    InvalidPrimaryKeyType { field: String },
182
183    #[error("index '{index}' references unknown field '{field}'")]
184    IndexFieldUnknown { index: IndexModel, field: String },
185
186    #[error("index '{index}' references non-queryable field '{field}'")]
187    IndexFieldNotQueryable { index: IndexModel, field: String },
188
189    #[error(
190        "index '{index}' references map field '{field}'; map fields are not queryable in icydb 0.7"
191    )]
192    IndexFieldMapNotQueryable { index: IndexModel, field: String },
193
194    #[error("index '{index}' repeats field '{field}'")]
195    IndexFieldDuplicate { index: IndexModel, field: String },
196
197    #[error("duplicate index name '{name}'")]
198    DuplicateIndexName { name: String },
199
200    #[error("operator {op} is not valid for field '{field}'")]
201    InvalidOperator { field: String, op: String },
202
203    #[error("coercion {coercion:?} is not valid for field '{field}'")]
204    InvalidCoercion { field: String, coercion: CoercionId },
205
206    #[error("invalid literal for field '{field}': {message}")]
207    InvalidLiteral { field: String, message: String },
208}
209
210impl ValidateError {
211    pub(crate) fn invalid_operator(field: &str, op: impl fmt::Display) -> Self {
212        Self::InvalidOperator {
213            field: field.to_string(),
214            op: op.to_string(),
215        }
216    }
217
218    pub(crate) fn invalid_literal(field: &str, msg: &str) -> Self {
219        Self::InvalidLiteral {
220            field: field.to_string(),
221            message: msg.to_string(),
222        }
223    }
224}