Skip to main content

icydb_core/db/contracts/
predicate_schema.rs

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