Skip to main content

icydb_core/db/query/predicate/
validate.rs

1use crate::{
2    db::identity::{EntityName, EntityNameError, IndexName, IndexNameError},
3    model::{entity::EntityModel, field::EntityFieldKind, index::IndexModel},
4    value::{Value, ValueFamily, ValueFamilyExt},
5};
6use icydb_schema::{
7    node::{
8        Entity, Enum, Item, ItemTarget, List, Map, Newtype, Record, Schema, Set, Tuple,
9        Value as SValue,
10    },
11    types::{Cardinality, Primitive},
12};
13use std::{
14    collections::{BTreeMap, BTreeSet},
15    fmt,
16};
17
18use super::{
19    ast::{CompareOp, ComparePredicate, Predicate},
20    coercion::{CoercionId, CoercionSpec, supports_coercion},
21};
22
23#[cfg(test)]
24use std::cell::Cell;
25
26#[cfg(test)]
27thread_local! {
28    static SCHEMA_LOOKUP_CALLED: Cell<bool> = const { Cell::new(false) };
29}
30
31#[cfg(test)]
32pub(crate) fn reset_schema_lookup_called() {
33    SCHEMA_LOOKUP_CALLED.with(|flag| flag.set(false));
34}
35
36#[cfg(test)]
37pub(crate) fn schema_lookup_called() -> bool {
38    SCHEMA_LOOKUP_CALLED.with(Cell::get)
39}
40
41///
42/// ScalarType
43///
44/// Internal scalar classification used by predicate validation.
45/// This is deliberately *smaller* than the full schema/type system
46/// and exists only to support:
47/// - coercion rules
48/// - literal compatibility checks
49/// - operator validity (ordering, equality)
50///
51
52#[derive(Clone, Debug, Eq, PartialEq)]
53pub(crate) enum ScalarType {
54    Account,
55    Blob,
56    Bool,
57    Date,
58    Decimal,
59    Duration,
60    Enum,
61    E8s,
62    E18s,
63    Float32,
64    Float64,
65    Int,
66    Int128,
67    IntBig,
68    Principal,
69    Subaccount,
70    Text,
71    Timestamp,
72    Uint,
73    Uint128,
74    UintBig,
75    Ulid,
76    Unit,
77}
78
79impl ScalarType {
80    #[must_use]
81    pub const fn family(&self) -> ValueFamily {
82        match self {
83            Self::Text => ValueFamily::Textual,
84            Self::Ulid | Self::Principal | Self::Account => ValueFamily::Identifier,
85            Self::Enum => ValueFamily::Enum,
86            Self::Blob | Self::Subaccount => ValueFamily::Blob,
87            Self::Bool => ValueFamily::Bool,
88            Self::Unit => ValueFamily::Unit,
89            Self::Date
90            | Self::Decimal
91            | Self::Duration
92            | Self::E8s
93            | Self::E18s
94            | Self::Float32
95            | Self::Float64
96            | Self::Int
97            | Self::Int128
98            | Self::IntBig
99            | Self::Timestamp
100            | Self::Uint
101            | Self::Uint128
102            | Self::UintBig => ValueFamily::Numeric,
103        }
104    }
105
106    #[must_use]
107    pub const fn is_orderable(&self) -> bool {
108        !matches!(self, Self::Blob | Self::Unit)
109    }
110
111    #[must_use]
112    pub const fn matches_value(&self, value: &Value) -> bool {
113        matches!(
114            (self, value),
115            (Self::Account, Value::Account(_))
116                | (Self::Blob, Value::Blob(_))
117                | (Self::Bool, Value::Bool(_))
118                | (Self::Date, Value::Date(_))
119                | (Self::Decimal, Value::Decimal(_))
120                | (Self::Duration, Value::Duration(_))
121                | (Self::Enum, Value::Enum(_))
122                | (Self::E8s, Value::E8s(_))
123                | (Self::E18s, Value::E18s(_))
124                | (Self::Float32, Value::Float32(_))
125                | (Self::Float64, Value::Float64(_))
126                | (Self::Int, Value::Int(_))
127                | (Self::Int128, Value::Int128(_))
128                | (Self::IntBig, Value::IntBig(_))
129                | (Self::Principal, Value::Principal(_))
130                | (Self::Subaccount, Value::Subaccount(_))
131                | (Self::Text, Value::Text(_))
132                | (Self::Timestamp, Value::Timestamp(_))
133                | (Self::Uint, Value::Uint(_))
134                | (Self::Uint128, Value::Uint128(_))
135                | (Self::UintBig, Value::UintBig(_))
136                | (Self::Ulid, Value::Ulid(_))
137                | (Self::Unit, Value::Unit)
138        )
139    }
140}
141
142///
143/// FieldType
144///
145/// Reduced runtime type representation used exclusively for predicate validation.
146/// This intentionally drops:
147/// - record structure
148/// - tuple structure
149/// - validator/sanitizer metadata
150///
151
152#[derive(Clone, Debug, Eq, PartialEq)]
153pub(crate) enum FieldType {
154    Scalar(ScalarType),
155    List(Box<Self>),
156    Set(Box<Self>),
157    Map { key: Box<Self>, value: Box<Self> },
158    Unsupported,
159}
160
161impl FieldType {
162    #[must_use]
163    pub const fn family(&self) -> Option<ValueFamily> {
164        match self {
165            Self::Scalar(inner) => Some(inner.family()),
166            Self::List(_) | Self::Set(_) | Self::Map { .. } => Some(ValueFamily::Collection),
167            Self::Unsupported => None,
168        }
169    }
170
171    #[must_use]
172    pub const fn is_text(&self) -> bool {
173        matches!(self, Self::Scalar(ScalarType::Text))
174    }
175
176    #[must_use]
177    pub const fn is_collection(&self) -> bool {
178        matches!(self, Self::List(_) | Self::Set(_) | Self::Map { .. })
179    }
180
181    #[must_use]
182    pub const fn is_list_like(&self) -> bool {
183        matches!(self, Self::List(_) | Self::Set(_))
184    }
185
186    #[must_use]
187    pub const fn is_map(&self) -> bool {
188        matches!(self, Self::Map { .. })
189    }
190
191    #[must_use]
192    pub fn element_type(&self) -> Option<&Self> {
193        match self {
194            Self::List(inner) | Self::Set(inner) => Some(inner),
195            _ => None,
196        }
197    }
198
199    #[must_use]
200    pub fn map_types(&self) -> Option<(&Self, &Self)> {
201        match self {
202            Self::Map { key, value } => Some((key.as_ref(), value.as_ref())),
203            _ => None,
204        }
205    }
206
207    #[must_use]
208    pub const fn is_orderable(&self) -> bool {
209        match self {
210            Self::Scalar(inner) => inner.is_orderable(),
211            _ => false,
212        }
213    }
214
215    #[must_use]
216    pub const fn is_keyable(&self) -> bool {
217        matches!(
218            self,
219            Self::Scalar(
220                ScalarType::Account
221                    | ScalarType::Int
222                    | ScalarType::Principal
223                    | ScalarType::Subaccount
224                    | ScalarType::Timestamp
225                    | ScalarType::Uint
226                    | ScalarType::Ulid
227                    | ScalarType::Unit
228            )
229        )
230    }
231}
232
233fn validate_index_fields(
234    fields: &BTreeMap<String, FieldType>,
235    indexes: &[&IndexModel],
236) -> Result<(), ValidateError> {
237    let mut seen_names = BTreeSet::new();
238    for index in indexes {
239        if seen_names.contains(index.name) {
240            return Err(ValidateError::DuplicateIndexName {
241                name: index.name.to_string(),
242            });
243        }
244        seen_names.insert(index.name);
245
246        let mut seen = BTreeSet::new();
247        for field in index.fields {
248            if !fields.contains_key(*field) {
249                return Err(ValidateError::IndexFieldUnknown {
250                    index: **index,
251                    field: (*field).to_string(),
252                });
253            }
254            if seen.contains(*field) {
255                return Err(ValidateError::IndexFieldDuplicate {
256                    index: **index,
257                    field: (*field).to_string(),
258                });
259            }
260            seen.insert(*field);
261
262            let field_type = fields
263                .get(*field)
264                .expect("index field existence checked above");
265            // Indexing is hash-based across all Value variants; only Unsupported is rejected here.
266            // Collisions are detected during unique enforcement and lookups.
267            if matches!(field_type, FieldType::Unsupported) {
268                return Err(ValidateError::IndexFieldUnsupported {
269                    index: **index,
270                    field: (*field).to_string(),
271                });
272            }
273        }
274    }
275
276    Ok(())
277}
278
279///
280/// SchemaInfo
281///
282/// Lightweight, runtime-usable field-type map for one entity.
283/// This is the *only* schema surface the predicate validator depends on.
284///
285
286#[derive(Clone, Debug)]
287pub struct SchemaInfo {
288    fields: BTreeMap<String, FieldType>,
289}
290
291impl SchemaInfo {
292    #[must_use]
293    #[expect(dead_code)]
294    pub(crate) fn new(fields: impl IntoIterator<Item = (String, FieldType)>) -> Self {
295        Self {
296            fields: fields.into_iter().collect(),
297        }
298    }
299
300    #[must_use]
301    pub(crate) fn field(&self, name: &str) -> Option<&FieldType> {
302        self.fields.get(name)
303    }
304
305    #[must_use]
306    pub fn from_entity_schema(entity: &Entity, schema: &Schema) -> Self {
307        let fields = entity
308            .fields
309            .fields
310            .iter()
311            .map(|field| {
312                let ty = field_type_from_value(&field.value, schema);
313                (field.ident.to_string(), ty)
314            })
315            .collect::<BTreeMap<_, _>>();
316
317        Self { fields }
318    }
319
320    pub fn from_entity_model(model: &EntityModel) -> Result<Self, ValidateError> {
321        // Validate identity constraints before building schema maps.
322        let entity_name = EntityName::try_from_str(model.entity_name).map_err(|err| {
323            ValidateError::InvalidEntityName {
324                name: model.entity_name.to_string(),
325                source: err,
326            }
327        })?;
328
329        if !model
330            .fields
331            .iter()
332            .any(|field| std::ptr::eq(field, model.primary_key))
333        {
334            return Err(ValidateError::InvalidPrimaryKey {
335                field: model.primary_key.name.to_string(),
336            });
337        }
338
339        let mut fields = BTreeMap::new();
340        for field in model.fields {
341            if fields.contains_key(field.name) {
342                return Err(ValidateError::DuplicateField {
343                    field: field.name.to_string(),
344                });
345            }
346            let ty = field_type_from_model_kind(&field.kind);
347            fields.insert(field.name.to_string(), ty);
348        }
349
350        let pk_field_type = fields
351            .get(model.primary_key.name)
352            .expect("primary key verified above");
353        if !pk_field_type.is_keyable() {
354            return Err(ValidateError::InvalidPrimaryKeyType {
355                field: model.primary_key.name.to_string(),
356            });
357        }
358
359        validate_index_fields(&fields, model.indexes)?;
360        for index in model.indexes {
361            IndexName::try_from_parts(&entity_name, index.fields).map_err(|err| {
362                ValidateError::InvalidIndexName {
363                    index: **index,
364                    source: err,
365                }
366            })?;
367        }
368
369        Ok(Self { fields })
370    }
371}
372
373/// Predicate/schema validation failures, including invalid model contracts.
374#[derive(Debug, thiserror::Error)]
375pub enum ValidateError {
376    #[error("invalid entity name '{name}': {source}")]
377    InvalidEntityName {
378        name: String,
379        #[source]
380        source: EntityNameError,
381    },
382
383    #[error("invalid index name for '{index}': {source}")]
384    InvalidIndexName {
385        index: IndexModel,
386        #[source]
387        source: IndexNameError,
388    },
389
390    #[error("unknown field '{field}'")]
391    UnknownField { field: String },
392
393    #[error("unsupported field type for '{field}'")]
394    UnsupportedFieldType { field: String },
395
396    #[error("duplicate field '{field}'")]
397    DuplicateField { field: String },
398
399    #[error("primary key '{field}' not present in entity fields")]
400    InvalidPrimaryKey { field: String },
401
402    #[error("primary key '{field}' has an unsupported type")]
403    InvalidPrimaryKeyType { field: String },
404
405    #[error("index '{index}' references unknown field '{field}'")]
406    IndexFieldUnknown { index: IndexModel, field: String },
407
408    #[error("index '{index}' references unsupported field '{field}'")]
409    IndexFieldUnsupported { index: IndexModel, field: String },
410
411    #[error("index '{index}' repeats field '{field}'")]
412    IndexFieldDuplicate { index: IndexModel, field: String },
413
414    #[error("duplicate index name '{name}'")]
415    DuplicateIndexName { name: String },
416
417    #[error("operator {op} is not valid for field '{field}'")]
418    InvalidOperator { field: String, op: String },
419
420    #[error("coercion {coercion:?} is not valid for field '{field}'")]
421    InvalidCoercion { field: String, coercion: CoercionId },
422
423    #[error("invalid literal for field '{field}': {message}")]
424    InvalidLiteral { field: String, message: String },
425
426    #[error("schema unavailable: {0}")]
427    SchemaUnavailable(String),
428}
429
430pub fn validate(schema: &SchemaInfo, predicate: &Predicate) -> Result<(), ValidateError> {
431    match predicate {
432        Predicate::True | Predicate::False => Ok(()),
433        Predicate::And(children) | Predicate::Or(children) => {
434            for child in children {
435                validate(schema, child)?;
436            }
437            Ok(())
438        }
439        Predicate::Not(inner) => validate(schema, inner),
440        Predicate::Compare(cmp) => validate_compare(schema, cmp),
441        Predicate::IsNull { field } | Predicate::IsMissing { field } => {
442            ensure_field(schema, field).map(|_| ())
443        }
444        Predicate::IsEmpty { field } => {
445            let field_type = ensure_field(schema, field)?;
446            if field_type.is_text() || field_type.is_collection() {
447                Ok(())
448            } else {
449                Err(ValidateError::InvalidOperator {
450                    field: field.clone(),
451                    op: "is_empty".to_string(),
452                })
453            }
454        }
455        Predicate::IsNotEmpty { field } => {
456            let field_type = ensure_field(schema, field)?;
457            if field_type.is_text() || field_type.is_collection() {
458                Ok(())
459            } else {
460                Err(ValidateError::InvalidOperator {
461                    field: field.clone(),
462                    op: "is_not_empty".to_string(),
463                })
464            }
465        }
466        Predicate::MapContainsKey {
467            field,
468            key,
469            coercion,
470        } => validate_map_key(schema, field, key, coercion),
471        Predicate::MapContainsValue {
472            field,
473            value,
474            coercion,
475        } => validate_map_value(schema, field, value, coercion),
476        Predicate::MapContainsEntry {
477            field,
478            key,
479            value,
480            coercion,
481        } => validate_map_entry(schema, field, key, value, coercion),
482    }
483}
484
485pub fn validate_model(model: &EntityModel, predicate: &Predicate) -> Result<(), ValidateError> {
486    let schema = SchemaInfo::from_entity_model(model)?;
487    validate(&schema, predicate)
488}
489
490fn validate_compare(schema: &SchemaInfo, cmp: &ComparePredicate) -> Result<(), ValidateError> {
491    let field_type = ensure_field(schema, &cmp.field)?;
492
493    match cmp.op {
494        CompareOp::Eq | CompareOp::Ne => {
495            validate_eq_ne(&cmp.field, field_type, &cmp.value, &cmp.coercion)
496        }
497        CompareOp::Lt | CompareOp::Lte | CompareOp::Gt | CompareOp::Gte => {
498            validate_ordering(&cmp.field, field_type, &cmp.value, &cmp.coercion, cmp.op)
499        }
500        CompareOp::In | CompareOp::NotIn => {
501            validate_in(&cmp.field, field_type, &cmp.value, &cmp.coercion)
502        }
503        CompareOp::AnyIn | CompareOp::AllIn => {
504            validate_any_all_in(&cmp.field, field_type, &cmp.value, &cmp.coercion)
505        }
506        CompareOp::Contains => validate_contains(&cmp.field, field_type, &cmp.value, &cmp.coercion),
507        CompareOp::StartsWith | CompareOp::EndsWith => {
508            validate_text_op(&cmp.field, field_type, &cmp.value, &cmp.coercion, cmp.op)
509        }
510    }
511}
512
513fn validate_eq_ne(
514    field: &str,
515    field_type: &FieldType,
516    value: &Value,
517    coercion: &CoercionSpec,
518) -> Result<(), ValidateError> {
519    if field_type.is_list_like() {
520        ensure_list_literal(field, value, field_type)?;
521    } else if field_type.is_map() {
522        ensure_map_literal(field, value, field_type)?;
523    } else if matches!(value, Value::List(_)) {
524        return Err(ValidateError::InvalidLiteral {
525            field: field.to_string(),
526            message: "expected scalar literal".to_string(),
527        });
528    }
529
530    ensure_coercion(field, field_type, value, coercion)
531}
532
533fn validate_ordering(
534    field: &str,
535    field_type: &FieldType,
536    value: &Value,
537    coercion: &CoercionSpec,
538    op: CompareOp,
539) -> Result<(), ValidateError> {
540    if matches!(coercion.id, CoercionId::CollectionElement) {
541        return Err(ValidateError::InvalidCoercion {
542            field: field.to_string(),
543            coercion: coercion.id,
544        });
545    }
546
547    if !field_type.is_orderable() {
548        return Err(ValidateError::InvalidOperator {
549            field: field.to_string(),
550            op: format!("{op:?}"),
551        });
552    }
553
554    if matches!(value, Value::List(_)) {
555        return Err(ValidateError::InvalidLiteral {
556            field: field.to_string(),
557            message: "expected scalar literal".to_string(),
558        });
559    }
560
561    ensure_coercion(field, field_type, value, coercion)
562}
563
564fn validate_in(
565    field: &str,
566    field_type: &FieldType,
567    value: &Value,
568    coercion: &CoercionSpec,
569) -> Result<(), ValidateError> {
570    if field_type.is_collection() {
571        return Err(ValidateError::InvalidOperator {
572            field: field.to_string(),
573            op: format!("{:?}", CompareOp::In),
574        });
575    }
576
577    let Value::List(items) = value else {
578        return Err(ValidateError::InvalidLiteral {
579            field: field.to_string(),
580            message: "expected list literal".to_string(),
581        });
582    };
583
584    for item in items {
585        ensure_coercion(field, field_type, item, coercion)?;
586    }
587
588    Ok(())
589}
590
591fn validate_any_all_in(
592    field: &str,
593    field_type: &FieldType,
594    value: &Value,
595    coercion: &CoercionSpec,
596) -> Result<(), ValidateError> {
597    let element_type = field_type
598        .element_type()
599        .ok_or_else(|| ValidateError::InvalidOperator {
600            field: field.to_string(),
601            op: format!("{:?}", CompareOp::AnyIn),
602        })?;
603
604    let Value::List(items) = value else {
605        return Err(ValidateError::InvalidLiteral {
606            field: field.to_string(),
607            message: "expected list literal".to_string(),
608        });
609    };
610
611    for item in items {
612        ensure_coercion(field, element_type, item, coercion)?;
613    }
614
615    Ok(())
616}
617
618fn validate_contains(
619    field: &str,
620    field_type: &FieldType,
621    value: &Value,
622    coercion: &CoercionSpec,
623) -> Result<(), ValidateError> {
624    if field_type.is_text() {
625        if !matches!(coercion.id, CoercionId::Strict | CoercionId::TextCasefold) {
626            return Err(ValidateError::InvalidCoercion {
627                field: field.to_string(),
628                coercion: coercion.id,
629            });
630        }
631        if !matches!(value, Value::Text(_)) {
632            return Err(ValidateError::InvalidLiteral {
633                field: field.to_string(),
634                message: "expected text literal".to_string(),
635            });
636        }
637
638        return ensure_coercion(field, field_type, value, coercion);
639    }
640
641    let element_type = field_type
642        .element_type()
643        .ok_or_else(|| ValidateError::InvalidOperator {
644            field: field.to_string(),
645            op: format!("{:?}", CompareOp::Contains),
646        })?;
647
648    ensure_coercion(field, element_type, value, coercion)
649}
650
651fn validate_text_op(
652    field: &str,
653    field_type: &FieldType,
654    value: &Value,
655    coercion: &CoercionSpec,
656    op: CompareOp,
657) -> Result<(), ValidateError> {
658    if !field_type.is_text() {
659        return Err(ValidateError::InvalidOperator {
660            field: field.to_string(),
661            op: format!("{op:?}"),
662        });
663    }
664
665    if !matches!(coercion.id, CoercionId::Strict | CoercionId::TextCasefold) {
666        return Err(ValidateError::InvalidCoercion {
667            field: field.to_string(),
668            coercion: coercion.id,
669        });
670    }
671
672    if !matches!(value, Value::Text(_)) {
673        return Err(ValidateError::InvalidLiteral {
674            field: field.to_string(),
675            message: "expected text literal".to_string(),
676        });
677    }
678
679    ensure_coercion(field, field_type, value, coercion)
680}
681
682fn validate_map_key(
683    schema: &SchemaInfo,
684    field: &str,
685    key: &Value,
686    coercion: &CoercionSpec,
687) -> Result<(), ValidateError> {
688    let field_type = ensure_field(schema, field)?;
689    let (key_type, _) = field_type
690        .map_types()
691        .ok_or_else(|| ValidateError::InvalidOperator {
692            field: field.to_string(),
693            op: "map_contains_key".to_string(),
694        })?;
695
696    ensure_coercion(field, key_type, key, coercion)
697}
698
699fn validate_map_value(
700    schema: &SchemaInfo,
701    field: &str,
702    value: &Value,
703    coercion: &CoercionSpec,
704) -> Result<(), ValidateError> {
705    let field_type = ensure_field(schema, field)?;
706    let (_, value_type) = field_type
707        .map_types()
708        .ok_or_else(|| ValidateError::InvalidOperator {
709            field: field.to_string(),
710            op: "map_contains_value".to_string(),
711        })?;
712
713    ensure_coercion(field, value_type, value, coercion)
714}
715
716fn validate_map_entry(
717    schema: &SchemaInfo,
718    field: &str,
719    key: &Value,
720    value: &Value,
721    coercion: &CoercionSpec,
722) -> Result<(), ValidateError> {
723    let field_type = ensure_field(schema, field)?;
724    let (key_type, value_type) =
725        field_type
726            .map_types()
727            .ok_or_else(|| ValidateError::InvalidOperator {
728                field: field.to_string(),
729                op: "map_contains_entry".to_string(),
730            })?;
731
732    ensure_coercion(field, key_type, key, coercion)?;
733    ensure_coercion(field, value_type, value, coercion)?;
734
735    Ok(())
736}
737
738fn ensure_field<'a>(schema: &'a SchemaInfo, field: &str) -> Result<&'a FieldType, ValidateError> {
739    let field_type = schema
740        .field(field)
741        .ok_or_else(|| ValidateError::UnknownField {
742            field: field.to_string(),
743        })?;
744
745    if matches!(field_type, FieldType::Unsupported) {
746        return Err(ValidateError::UnsupportedFieldType {
747            field: field.to_string(),
748        });
749    }
750
751    Ok(field_type)
752}
753
754fn ensure_coercion(
755    field: &str,
756    field_type: &FieldType,
757    literal: &Value,
758    coercion: &CoercionSpec,
759) -> Result<(), ValidateError> {
760    let left_family = field_type
761        .family()
762        .ok_or_else(|| ValidateError::UnsupportedFieldType {
763            field: field.to_string(),
764        })?;
765    let right_family = literal.family();
766
767    if !supports_coercion(left_family, right_family, coercion.id) {
768        return Err(ValidateError::InvalidCoercion {
769            field: field.to_string(),
770            coercion: coercion.id,
771        });
772    }
773
774    if matches!(
775        coercion.id,
776        CoercionId::Strict | CoercionId::CollectionElement
777    ) && !literal_matches_type(literal, field_type)
778    {
779        return Err(ValidateError::InvalidLiteral {
780            field: field.to_string(),
781            message: "literal type does not match field type".to_string(),
782        });
783    }
784
785    Ok(())
786}
787
788fn ensure_list_literal(
789    field: &str,
790    literal: &Value,
791    field_type: &FieldType,
792) -> Result<(), ValidateError> {
793    if !literal_matches_type(literal, field_type) {
794        return Err(ValidateError::InvalidLiteral {
795            field: field.to_string(),
796            message: "list literal does not match field element type".to_string(),
797        });
798    }
799
800    Ok(())
801}
802
803fn ensure_map_literal(
804    field: &str,
805    literal: &Value,
806    field_type: &FieldType,
807) -> Result<(), ValidateError> {
808    if !literal_matches_type(literal, field_type) {
809        return Err(ValidateError::InvalidLiteral {
810            field: field.to_string(),
811            message: "map literal does not match field key/value types".to_string(),
812        });
813    }
814
815    Ok(())
816}
817
818pub(crate) fn literal_matches_type(literal: &Value, field_type: &FieldType) -> bool {
819    match field_type {
820        FieldType::Scalar(inner) => inner.matches_value(literal),
821        FieldType::List(element) | FieldType::Set(element) => match literal {
822            Value::List(items) => items.iter().all(|item| literal_matches_type(item, element)),
823            _ => false,
824        },
825        FieldType::Map { key, value } => match literal {
826            Value::List(entries) => entries.iter().all(|entry| match entry {
827                Value::List(pair) if pair.len() == 2 => {
828                    literal_matches_type(&pair[0], key) && literal_matches_type(&pair[1], value)
829                }
830                _ => false,
831            }),
832            _ => false,
833        },
834        FieldType::Unsupported => false,
835    }
836}
837
838fn field_type_from_value(value: &SValue, schema: &Schema) -> FieldType {
839    let base = field_type_from_item(&value.item, schema);
840
841    match value.cardinality {
842        Cardinality::Many => FieldType::List(Box::new(base)),
843        Cardinality::One | Cardinality::Opt => base,
844    }
845}
846
847fn field_type_from_item(item: &Item, schema: &Schema) -> FieldType {
848    match &item.target {
849        ItemTarget::Primitive(prim) => FieldType::Scalar(scalar_from_primitive(*prim)),
850        ItemTarget::Is(path) => {
851            if schema.cast_node::<Enum>(path).is_ok() {
852                return FieldType::Scalar(ScalarType::Enum);
853            }
854            if let Ok(node) = schema.cast_node::<Newtype>(path) {
855                return field_type_from_item(&node.item, schema);
856            }
857            if let Ok(node) = schema.cast_node::<List>(path) {
858                return FieldType::List(Box::new(field_type_from_item(&node.item, schema)));
859            }
860            if let Ok(node) = schema.cast_node::<Set>(path) {
861                return FieldType::Set(Box::new(field_type_from_item(&node.item, schema)));
862            }
863            if let Ok(node) = schema.cast_node::<Map>(path) {
864                let key = field_type_from_item(&node.key, schema);
865                let value = field_type_from_value(&node.value, schema);
866                return FieldType::Map {
867                    key: Box::new(key),
868                    value: Box::new(value),
869                };
870            }
871            if schema.cast_node::<Record>(path).is_ok() {
872                return FieldType::Unsupported;
873            }
874            if schema.cast_node::<Tuple>(path).is_ok() {
875                return FieldType::Unsupported;
876            }
877
878            FieldType::Unsupported
879        }
880    }
881}
882
883const fn scalar_from_primitive(prim: Primitive) -> ScalarType {
884    match prim {
885        Primitive::Account => ScalarType::Account,
886        Primitive::Blob => ScalarType::Blob,
887        Primitive::Bool => ScalarType::Bool,
888        Primitive::Date => ScalarType::Date,
889        Primitive::Decimal => ScalarType::Decimal,
890        Primitive::Duration => ScalarType::Duration,
891        Primitive::E8s => ScalarType::E8s,
892        Primitive::E18s => ScalarType::E18s,
893        Primitive::Float32 => ScalarType::Float32,
894        Primitive::Float64 => ScalarType::Float64,
895        Primitive::Int => ScalarType::IntBig,
896        Primitive::Int8 | Primitive::Int16 | Primitive::Int32 | Primitive::Int64 => ScalarType::Int,
897        Primitive::Int128 => ScalarType::Int128,
898        Primitive::Nat => ScalarType::UintBig,
899        Primitive::Nat8 | Primitive::Nat16 | Primitive::Nat32 | Primitive::Nat64 => {
900            ScalarType::Uint
901        }
902        Primitive::Nat128 => ScalarType::Uint128,
903        Primitive::Principal => ScalarType::Principal,
904        Primitive::Subaccount => ScalarType::Subaccount,
905        Primitive::Text => ScalarType::Text,
906        Primitive::Timestamp => ScalarType::Timestamp,
907        Primitive::Ulid => ScalarType::Ulid,
908        Primitive::Unit => ScalarType::Unit,
909    }
910}
911
912fn field_type_from_model_kind(kind: &EntityFieldKind) -> FieldType {
913    match kind {
914        EntityFieldKind::Account => FieldType::Scalar(ScalarType::Account),
915        EntityFieldKind::Blob => FieldType::Scalar(ScalarType::Blob),
916        EntityFieldKind::Bool => FieldType::Scalar(ScalarType::Bool),
917        EntityFieldKind::Date => FieldType::Scalar(ScalarType::Date),
918        EntityFieldKind::Decimal => FieldType::Scalar(ScalarType::Decimal),
919        EntityFieldKind::Duration => FieldType::Scalar(ScalarType::Duration),
920        EntityFieldKind::Enum => FieldType::Scalar(ScalarType::Enum),
921        EntityFieldKind::E8s => FieldType::Scalar(ScalarType::E8s),
922        EntityFieldKind::E18s => FieldType::Scalar(ScalarType::E18s),
923        EntityFieldKind::Float32 => FieldType::Scalar(ScalarType::Float32),
924        EntityFieldKind::Float64 => FieldType::Scalar(ScalarType::Float64),
925        EntityFieldKind::Int => FieldType::Scalar(ScalarType::Int),
926        EntityFieldKind::Int128 => FieldType::Scalar(ScalarType::Int128),
927        EntityFieldKind::IntBig => FieldType::Scalar(ScalarType::IntBig),
928        EntityFieldKind::Principal => FieldType::Scalar(ScalarType::Principal),
929        EntityFieldKind::Subaccount => FieldType::Scalar(ScalarType::Subaccount),
930        EntityFieldKind::Text => FieldType::Scalar(ScalarType::Text),
931        EntityFieldKind::Timestamp => FieldType::Scalar(ScalarType::Timestamp),
932        EntityFieldKind::Uint => FieldType::Scalar(ScalarType::Uint),
933        EntityFieldKind::Uint128 => FieldType::Scalar(ScalarType::Uint128),
934        EntityFieldKind::UintBig => FieldType::Scalar(ScalarType::UintBig),
935        EntityFieldKind::Ulid => FieldType::Scalar(ScalarType::Ulid),
936        EntityFieldKind::Unit => FieldType::Scalar(ScalarType::Unit),
937        EntityFieldKind::List(inner) => {
938            FieldType::List(Box::new(field_type_from_model_kind(inner)))
939        }
940        EntityFieldKind::Set(inner) => FieldType::Set(Box::new(field_type_from_model_kind(inner))),
941        EntityFieldKind::Map { key, value } => FieldType::Map {
942            key: Box::new(field_type_from_model_kind(key)),
943            value: Box::new(field_type_from_model_kind(value)),
944        },
945        EntityFieldKind::Unsupported => FieldType::Unsupported,
946    }
947}
948
949impl fmt::Display for FieldType {
950    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
951        match self {
952            Self::Scalar(inner) => write!(f, "{inner:?}"),
953            Self::List(inner) => write!(f, "List<{inner}>"),
954            Self::Set(inner) => write!(f, "Set<{inner}>"),
955            Self::Map { key, value } => write!(f, "Map<{key}, {value}>"),
956            Self::Unsupported => write!(f, "Unsupported"),
957        }
958    }
959}
960
961///
962/// TESTS
963///
964
965#[cfg(test)]
966mod tests {
967    use super::{ValidateError, validate_model};
968    use crate::{
969        db::query::{
970            builder::{eq, eq_ci, is_empty, is_not_empty, lt, map_contains_entry},
971            predicate::{CoercionId, Predicate},
972        },
973        model::{
974            entity::EntityModel,
975            field::{EntityFieldKind, EntityFieldModel},
976            index::IndexModel,
977        },
978        types::Ulid,
979    };
980
981    fn field(name: &'static str, kind: EntityFieldKind) -> EntityFieldModel {
982        EntityFieldModel { name, kind }
983    }
984
985    fn model_with_fields(fields: Vec<EntityFieldModel>, pk_index: usize) -> EntityModel {
986        let fields: &'static [EntityFieldModel] = Box::leak(fields.into_boxed_slice());
987        let primary_key = &fields[pk_index];
988        let indexes: &'static [&'static IndexModel] = &[];
989
990        EntityModel {
991            path: "test::Entity",
992            entity_name: "TestEntity",
993            primary_key,
994            fields,
995            indexes,
996        }
997    }
998
999    #[test]
1000    fn validate_model_accepts_scalars_and_coercions() {
1001        let model = model_with_fields(
1002            vec![
1003                field("id", EntityFieldKind::Ulid),
1004                field("email", EntityFieldKind::Text),
1005                field("age", EntityFieldKind::Uint),
1006                field("created_at", EntityFieldKind::Timestamp),
1007                field("active", EntityFieldKind::Bool),
1008            ],
1009            0,
1010        );
1011
1012        let predicate = Predicate::And(vec![
1013            eq("id", Ulid::nil()),
1014            eq_ci("email", "User@example.com"),
1015            lt("age", 30u32),
1016        ]);
1017
1018        assert!(validate_model(&model, &predicate).is_ok());
1019    }
1020
1021    #[test]
1022    fn validate_model_accepts_collections_and_map_contains() {
1023        let model = model_with_fields(
1024            vec![
1025                field("id", EntityFieldKind::Ulid),
1026                field("tags", EntityFieldKind::List(&EntityFieldKind::Text)),
1027                field(
1028                    "principals",
1029                    EntityFieldKind::Set(&EntityFieldKind::Principal),
1030                ),
1031                field(
1032                    "attributes",
1033                    EntityFieldKind::Map {
1034                        key: &EntityFieldKind::Text,
1035                        value: &EntityFieldKind::Uint,
1036                    },
1037                ),
1038            ],
1039            0,
1040        );
1041
1042        let predicate = Predicate::And(vec![
1043            is_empty("tags"),
1044            is_not_empty("principals"),
1045            map_contains_entry("attributes", "k", 1u64, CoercionId::Strict),
1046        ]);
1047
1048        assert!(validate_model(&model, &predicate).is_ok());
1049
1050        let bad = map_contains_entry("attributes", "k", 1u64, CoercionId::TextCasefold);
1051        assert!(matches!(
1052            validate_model(&model, &bad),
1053            Err(ValidateError::InvalidCoercion { .. })
1054        ));
1055    }
1056
1057    #[test]
1058    fn validate_model_rejects_unsupported_fields() {
1059        let model = model_with_fields(
1060            vec![
1061                field("id", EntityFieldKind::Ulid),
1062                field("broken", EntityFieldKind::Unsupported),
1063            ],
1064            0,
1065        );
1066        let predicate = eq("broken", 1u64);
1067
1068        assert!(matches!(
1069            validate_model(&model, &predicate),
1070            Err(ValidateError::UnsupportedFieldType { field }) if field == "broken"
1071        ));
1072    }
1073}