Skip to main content

icydb_core/db/contracts/
predicate_schema.rs

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