1use super::{
2 ast::{CompareOp, ComparePredicate, Predicate},
3 coercion::{CoercionId, CoercionSpec, supports_coercion},
4};
5use crate::{
6 db::identity::{EntityName, EntityNameError, IndexName, IndexNameError},
7 model::{entity::EntityModel, field::EntityFieldKind, index::IndexModel},
8 value::{CoercionFamily, CoercionFamilyExt, Value},
9};
10use std::{
11 collections::{BTreeMap, BTreeSet},
12 fmt,
13};
14
15#[derive(Clone, Debug, Eq, PartialEq)]
27pub(crate) enum ScalarType {
28 Account,
29 Blob,
30 Bool,
31 Date,
32 Decimal,
33 Duration,
34 Enum,
35 E8s,
36 E18s,
37 Float32,
38 Float64,
39 Int,
40 Int128,
41 IntBig,
42 Principal,
43 Subaccount,
44 Text,
45 Timestamp,
46 Uint,
47 Uint128,
48 UintBig,
49 Ulid,
50 Unit,
51}
52
53macro_rules! scalar_coercion_family_from_registry {
55 ( @args $self:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
56 match $self {
57 $( ScalarType::$scalar => $coercion_family, )*
58 }
59 };
60}
61
62macro_rules! scalar_matches_value_from_registry {
63 ( @args $self:expr, $value:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
64 matches!(
65 ($self, $value),
66 $( (ScalarType::$scalar, $value_pat) )|*
67 )
68 };
69}
70
71macro_rules! scalar_supports_numeric_coercion_from_registry {
72 ( @args $self:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
73 match $self {
74 $( ScalarType::$scalar => $supports_numeric_coercion, )*
75 }
76 };
77}
78
79#[cfg(test)]
80macro_rules! scalar_supports_arithmetic_from_registry {
81 ( @args $self:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
82 match $self {
83 $( ScalarType::$scalar => $supports_arithmetic, )*
84 }
85 };
86}
87
88macro_rules! scalar_is_keyable_from_registry {
89 ( @args $self:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
90 match $self {
91 $( ScalarType::$scalar => $is_keyable, )*
92 }
93 };
94}
95
96#[cfg(test)]
97macro_rules! scalar_supports_equality_from_registry {
98 ( @args $self:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
99 match $self {
100 $( ScalarType::$scalar => $supports_equality, )*
101 }
102 };
103}
104
105macro_rules! scalar_supports_ordering_from_registry {
106 ( @args $self:expr; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
107 match $self {
108 $( ScalarType::$scalar => $supports_ordering, )*
109 }
110 };
111}
112
113impl ScalarType {
114 #[must_use]
115 pub const fn coercion_family(&self) -> CoercionFamily {
116 scalar_registry!(scalar_coercion_family_from_registry, self)
117 }
118
119 #[must_use]
120 pub const fn is_orderable(&self) -> bool {
121 self.supports_ordering()
124 }
125
126 #[must_use]
127 pub const fn matches_value(&self, value: &Value) -> bool {
128 scalar_registry!(scalar_matches_value_from_registry, self, value)
129 }
130
131 #[must_use]
132 pub const fn supports_numeric_coercion(&self) -> bool {
133 scalar_registry!(scalar_supports_numeric_coercion_from_registry, self)
134 }
135
136 #[must_use]
137 #[cfg(test)]
138 #[expect(dead_code)]
139 pub const fn supports_arithmetic(&self) -> bool {
140 scalar_registry!(scalar_supports_arithmetic_from_registry, self)
141 }
142
143 #[must_use]
144 pub const fn is_keyable(&self) -> bool {
145 scalar_registry!(scalar_is_keyable_from_registry, self)
146 }
147
148 #[must_use]
149 #[cfg(test)]
150 #[expect(dead_code)]
151 pub const fn supports_equality(&self) -> bool {
152 scalar_registry!(scalar_supports_equality_from_registry, self)
153 }
154
155 #[must_use]
156 pub const fn supports_ordering(&self) -> bool {
157 scalar_registry!(scalar_supports_ordering_from_registry, self)
158 }
159}
160
161#[derive(Clone, Debug, Eq, PartialEq)]
172pub(crate) enum FieldType {
173 Scalar(ScalarType),
174 List(Box<Self>),
175 Set(Box<Self>),
176 Map { key: Box<Self>, value: Box<Self> },
177 Unsupported,
178}
179
180impl FieldType {
181 #[must_use]
182 pub const fn coercion_family(&self) -> Option<CoercionFamily> {
183 match self {
184 Self::Scalar(inner) => Some(inner.coercion_family()),
185 Self::List(_) | Self::Set(_) | Self::Map { .. } => Some(CoercionFamily::Collection),
186 Self::Unsupported => None,
187 }
188 }
189
190 #[must_use]
191 pub const fn is_text(&self) -> bool {
192 matches!(self, Self::Scalar(ScalarType::Text))
193 }
194
195 #[must_use]
196 pub const fn is_collection(&self) -> bool {
197 matches!(self, Self::List(_) | Self::Set(_) | Self::Map { .. })
198 }
199
200 #[must_use]
201 pub const fn is_list_like(&self) -> bool {
202 matches!(self, Self::List(_) | Self::Set(_))
203 }
204
205 #[must_use]
206 pub const fn is_map(&self) -> bool {
207 matches!(self, Self::Map { .. })
208 }
209
210 #[must_use]
211 pub fn map_types(&self) -> Option<(&Self, &Self)> {
212 match self {
213 Self::Map { key, value } => Some((key.as_ref(), value.as_ref())),
214 _ => {
215 None
217 }
218 }
219 }
220
221 #[must_use]
222 pub const fn is_orderable(&self) -> bool {
223 match self {
224 Self::Scalar(inner) => inner.is_orderable(),
225 _ => false,
226 }
227 }
228
229 #[must_use]
230 pub const fn is_keyable(&self) -> bool {
231 match self {
232 Self::Scalar(inner) => inner.is_keyable(),
233 _ => false,
234 }
235 }
236
237 #[must_use]
238 pub const fn supports_numeric_coercion(&self) -> bool {
239 match self {
240 Self::Scalar(inner) => inner.supports_numeric_coercion(),
241 _ => false,
242 }
243 }
244}
245
246fn validate_index_fields(
247 fields: &BTreeMap<String, FieldType>,
248 indexes: &[&IndexModel],
249) -> Result<(), ValidateError> {
250 let mut seen_names = BTreeSet::new();
251 for index in indexes {
252 if seen_names.contains(index.name) {
253 return Err(ValidateError::DuplicateIndexName {
254 name: index.name.to_string(),
255 });
256 }
257 seen_names.insert(index.name);
258
259 let mut seen = BTreeSet::new();
260 for field in index.fields {
261 if !fields.contains_key(*field) {
262 return Err(ValidateError::IndexFieldUnknown {
263 index: **index,
264 field: (*field).to_string(),
265 });
266 }
267 if seen.contains(*field) {
268 return Err(ValidateError::IndexFieldDuplicate {
269 index: **index,
270 field: (*field).to_string(),
271 });
272 }
273 seen.insert(*field);
274
275 let field_type = fields
276 .get(*field)
277 .expect("index field existence checked above");
278 if matches!(field_type, FieldType::Map { .. }) {
281 return Err(ValidateError::IndexFieldMapUnsupported {
282 index: **index,
283 field: (*field).to_string(),
284 });
285 }
286 if matches!(field_type, FieldType::Unsupported) {
289 return Err(ValidateError::IndexFieldUnsupported {
290 index: **index,
291 field: (*field).to_string(),
292 });
293 }
294 }
295 }
296
297 Ok(())
298}
299
300#[derive(Clone, Debug)]
308pub struct SchemaInfo {
309 fields: BTreeMap<String, FieldType>,
310}
311
312impl SchemaInfo {
313 #[must_use]
314 pub(crate) fn field(&self, name: &str) -> Option<&FieldType> {
315 self.fields.get(name)
316 }
317
318 pub fn from_entity_model(model: &EntityModel) -> Result<Self, ValidateError> {
319 let entity_name = EntityName::try_from_str(model.entity_name).map_err(|err| {
321 ValidateError::InvalidEntityName {
322 name: model.entity_name.to_string(),
323 source: err,
324 }
325 })?;
326
327 if !model
328 .fields
329 .iter()
330 .any(|field| std::ptr::eq(field, model.primary_key))
331 {
332 return Err(ValidateError::InvalidPrimaryKey {
333 field: model.primary_key.name.to_string(),
334 });
335 }
336
337 let mut fields = BTreeMap::new();
338 for field in model.fields {
339 if fields.contains_key(field.name) {
340 return Err(ValidateError::DuplicateField {
341 field: field.name.to_string(),
342 });
343 }
344 let ty = field_type_from_model_kind(&field.kind);
345 fields.insert(field.name.to_string(), ty);
346 }
347
348 let pk_field_type = fields
349 .get(model.primary_key.name)
350 .expect("primary key verified above");
351 if !pk_field_type.is_keyable() {
352 return Err(ValidateError::InvalidPrimaryKeyType {
353 field: model.primary_key.name.to_string(),
354 });
355 }
356
357 validate_index_fields(&fields, model.indexes)?;
358 for index in model.indexes {
359 IndexName::try_from_parts(&entity_name, index.fields).map_err(|err| {
360 ValidateError::InvalidIndexName {
361 index: **index,
362 source: err,
363 }
364 })?;
365 }
366
367 Ok(Self { fields })
368 }
369}
370
371#[derive(Debug, thiserror::Error)]
373pub enum ValidateError {
374 #[error("invalid entity name '{name}': {source}")]
375 InvalidEntityName {
376 name: String,
377 #[source]
378 source: EntityNameError,
379 },
380
381 #[error("invalid index name for '{index}': {source}")]
382 InvalidIndexName {
383 index: IndexModel,
384 #[source]
385 source: IndexNameError,
386 },
387
388 #[error("unknown field '{field}'")]
389 UnknownField { field: String },
390
391 #[error("unsupported field type for '{field}'")]
392 UnsupportedFieldType { field: String },
393
394 #[error("duplicate field '{field}'")]
395 DuplicateField { field: String },
396
397 #[error("primary key '{field}' not present in entity fields")]
398 InvalidPrimaryKey { field: String },
399
400 #[error("primary key '{field}' has an unsupported type")]
401 InvalidPrimaryKeyType { field: String },
402
403 #[error("index '{index}' references unknown field '{field}'")]
404 IndexFieldUnknown { index: IndexModel, field: String },
405
406 #[error("index '{index}' references unsupported field '{field}'")]
407 IndexFieldUnsupported { index: IndexModel, field: String },
408
409 #[error(
410 "index '{index}' references map field '{field}'; map fields are not queryable in icydb 0.7"
411 )]
412 IndexFieldMapUnsupported { index: IndexModel, field: String },
413
414 #[error("index '{index}' repeats field '{field}'")]
415 IndexFieldDuplicate { index: IndexModel, field: String },
416
417 #[error("duplicate index name '{name}'")]
418 DuplicateIndexName { name: String },
419
420 #[error("operator {op} is not valid for field '{field}'")]
421 InvalidOperator { field: String, op: String },
422
423 #[error("coercion {coercion:?} is not valid for field '{field}'")]
424 InvalidCoercion { field: String, coercion: CoercionId },
425
426 #[error("invalid literal for field '{field}': {message}")]
427 InvalidLiteral { field: String, message: String },
428
429 #[error("Map fields are not queryable in icydb 0.7 (field '{field}')")]
430 MapFieldNotQueryable { field: String },
431}
432
433pub fn validate(schema: &SchemaInfo, predicate: &Predicate) -> Result<(), ValidateError> {
434 match predicate {
435 Predicate::True | Predicate::False => Ok(()),
436 Predicate::And(children) | Predicate::Or(children) => {
437 for child in children {
438 validate(schema, child)?;
439 }
440 Ok(())
441 }
442 Predicate::Not(inner) => validate(schema, inner),
443 Predicate::Compare(cmp) => validate_compare(schema, cmp),
444 Predicate::IsNull { field } | Predicate::IsMissing { field } => {
445 let field_type = ensure_field_exists(schema, field)?;
447 if matches!(field_type, FieldType::Map { .. }) {
448 return Err(ValidateError::MapFieldNotQueryable {
449 field: field.clone(),
450 });
451 }
452
453 Ok(())
454 }
455 Predicate::IsEmpty { 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(invalid_operator(field, "is_empty"))
461 }
462 }
463 Predicate::IsNotEmpty { field } => {
464 let field_type = ensure_field(schema, field)?;
465 if field_type.is_text() || field_type.is_collection() {
466 Ok(())
467 } else {
468 Err(invalid_operator(field, "is_not_empty"))
469 }
470 }
471 Predicate::MapContainsKey {
472 field,
473 key,
474 coercion,
475 } => validate_map_key(schema, field, key, coercion),
476 Predicate::MapContainsValue {
477 field,
478 value,
479 coercion,
480 } => validate_map_value(schema, field, value, coercion),
481 Predicate::MapContainsEntry {
482 field,
483 key,
484 value,
485 coercion,
486 } => validate_map_entry(schema, field, key, value, coercion),
487 Predicate::TextContains { field, value } => {
488 validate_text_contains(schema, field, value, "text_contains")
489 }
490 Predicate::TextContainsCi { field, value } => {
491 validate_text_contains(schema, field, value, "text_contains_ci")
492 }
493 }
494}
495
496pub fn validate_model(model: &EntityModel, predicate: &Predicate) -> Result<(), ValidateError> {
497 let schema = SchemaInfo::from_entity_model(model)?;
498 validate(&schema, predicate)
499}
500
501fn validate_compare(schema: &SchemaInfo, cmp: &ComparePredicate) -> Result<(), ValidateError> {
502 let field_type = ensure_field(schema, &cmp.field)?;
503
504 match cmp.op {
505 CompareOp::Eq | CompareOp::Ne => {
506 validate_eq_ne(&cmp.field, field_type, &cmp.value, &cmp.coercion)
507 }
508 CompareOp::Lt | CompareOp::Lte | CompareOp::Gt | CompareOp::Gte => {
509 validate_ordering(&cmp.field, field_type, &cmp.value, &cmp.coercion, cmp.op)
510 }
511 CompareOp::In | CompareOp::NotIn => {
512 validate_in(&cmp.field, field_type, &cmp.value, &cmp.coercion, cmp.op)
513 }
514 CompareOp::Contains => validate_contains(&cmp.field, field_type, &cmp.value, &cmp.coercion),
515 CompareOp::StartsWith | CompareOp::EndsWith => {
516 validate_text_compare(&cmp.field, field_type, &cmp.value, &cmp.coercion, cmp.op)
517 }
518 }
519}
520
521fn validate_eq_ne(
522 field: &str,
523 field_type: &FieldType,
524 value: &Value,
525 coercion: &CoercionSpec,
526) -> Result<(), ValidateError> {
527 if field_type.is_list_like() {
528 ensure_list_literal(field, value, field_type)?;
529 } else if field_type.is_map() {
530 ensure_map_literal(field, value, field_type)?;
531 } else {
532 ensure_scalar_literal(field, value)?;
533 }
534
535 ensure_coercion(field, field_type, value, coercion)
536}
537
538fn validate_ordering(
539 field: &str,
540 field_type: &FieldType,
541 value: &Value,
542 coercion: &CoercionSpec,
543 op: CompareOp,
544) -> Result<(), ValidateError> {
545 if matches!(coercion.id, CoercionId::CollectionElement) {
546 return Err(ValidateError::InvalidCoercion {
547 field: field.to_string(),
548 coercion: coercion.id,
549 });
550 }
551
552 if !field_type.is_orderable() {
553 return Err(invalid_operator(field, format!("{op:?}")));
554 }
555
556 ensure_scalar_literal(field, value)?;
557
558 ensure_coercion(field, field_type, value, coercion)
559}
560
561fn validate_in(
563 field: &str,
564 field_type: &FieldType,
565 value: &Value,
566 coercion: &CoercionSpec,
567 op: CompareOp,
568) -> Result<(), ValidateError> {
569 if field_type.is_collection() {
570 return Err(invalid_operator(field, format!("{op:?}")));
571 }
572
573 let Value::List(items) = value else {
574 return Err(invalid_literal(field, "expected list literal"));
575 };
576
577 for item in items {
578 ensure_coercion(field, field_type, item, coercion)?;
579 }
580
581 Ok(())
582}
583
584fn validate_contains(
586 field: &str,
587 field_type: &FieldType,
588 value: &Value,
589 coercion: &CoercionSpec,
590) -> Result<(), ValidateError> {
591 if field_type.is_text() {
592 return Err(invalid_operator(
594 field,
595 format!("{:?}", CompareOp::Contains),
596 ));
597 }
598
599 let element_type = match field_type {
600 FieldType::List(inner) | FieldType::Set(inner) => inner.as_ref(),
601 _ => {
602 return Err(invalid_operator(
603 field,
604 format!("{:?}", CompareOp::Contains),
605 ));
606 }
607 };
608
609 if matches!(coercion.id, CoercionId::TextCasefold) {
610 return Err(ValidateError::InvalidCoercion {
612 field: field.to_string(),
613 coercion: coercion.id,
614 });
615 }
616
617 ensure_coercion(field, element_type, value, coercion)
618}
619
620fn validate_text_compare(
622 field: &str,
623 field_type: &FieldType,
624 value: &Value,
625 coercion: &CoercionSpec,
626 op: CompareOp,
627) -> Result<(), ValidateError> {
628 if !field_type.is_text() {
629 return Err(invalid_operator(field, format!("{op:?}")));
630 }
631
632 ensure_text_literal(field, value)?;
633
634 ensure_coercion(field, field_type, value, coercion)
635}
636
637fn ensure_map_types<'a>(
639 schema: &'a SchemaInfo,
640 field: &str,
641 op: &str,
642) -> Result<(&'a FieldType, &'a FieldType), ValidateError> {
643 let field_type = ensure_field(schema, field)?;
644 field_type
645 .map_types()
646 .ok_or_else(|| invalid_operator(field, op))
647}
648
649fn validate_map_key(
650 schema: &SchemaInfo,
651 field: &str,
652 key: &Value,
653 coercion: &CoercionSpec,
654) -> Result<(), ValidateError> {
655 ensure_no_text_casefold(field, coercion)?;
656
657 let (key_type, _) = ensure_map_types(schema, field, "map_contains_key")?;
658
659 ensure_coercion(field, key_type, key, coercion)
660}
661
662fn validate_map_value(
663 schema: &SchemaInfo,
664 field: &str,
665 value: &Value,
666 coercion: &CoercionSpec,
667) -> Result<(), ValidateError> {
668 ensure_no_text_casefold(field, coercion)?;
669
670 let (_, value_type) = ensure_map_types(schema, field, "map_contains_value")?;
671
672 ensure_coercion(field, value_type, value, coercion)
673}
674
675fn validate_map_entry(
676 schema: &SchemaInfo,
677 field: &str,
678 key: &Value,
679 value: &Value,
680 coercion: &CoercionSpec,
681) -> Result<(), ValidateError> {
682 ensure_no_text_casefold(field, coercion)?;
683
684 let (key_type, value_type) = ensure_map_types(schema, field, "map_contains_entry")?;
685
686 ensure_coercion(field, key_type, key, coercion)?;
687 ensure_coercion(field, value_type, value, coercion)?;
688
689 Ok(())
690}
691
692fn validate_text_contains(
694 schema: &SchemaInfo,
695 field: &str,
696 value: &Value,
697 op: &str,
698) -> Result<(), ValidateError> {
699 let field_type = ensure_field(schema, field)?;
700 if !field_type.is_text() {
701 return Err(invalid_operator(field, op));
702 }
703
704 ensure_text_literal(field, value)?;
705
706 Ok(())
707}
708
709fn ensure_field<'a>(schema: &'a SchemaInfo, field: &str) -> Result<&'a FieldType, ValidateError> {
710 let field_type = schema
711 .field(field)
712 .ok_or_else(|| ValidateError::UnknownField {
713 field: field.to_string(),
714 })?;
715
716 if matches!(field_type, FieldType::Map { .. }) {
717 return Err(ValidateError::MapFieldNotQueryable {
718 field: field.to_string(),
719 });
720 }
721
722 if matches!(field_type, FieldType::Unsupported) {
723 return Err(ValidateError::UnsupportedFieldType {
724 field: field.to_string(),
725 });
726 }
727
728 Ok(field_type)
729}
730
731fn ensure_field_exists<'a>(
732 schema: &'a SchemaInfo,
733 field: &str,
734) -> Result<&'a FieldType, ValidateError> {
735 schema
736 .field(field)
737 .ok_or_else(|| ValidateError::UnknownField {
738 field: field.to_string(),
739 })
740}
741
742fn invalid_operator(field: &str, op: impl fmt::Display) -> ValidateError {
743 ValidateError::InvalidOperator {
744 field: field.to_string(),
745 op: op.to_string(),
746 }
747}
748
749fn invalid_literal(field: &str, msg: &str) -> ValidateError {
750 ValidateError::InvalidLiteral {
751 field: field.to_string(),
752 message: msg.to_string(),
753 }
754}
755
756fn ensure_no_text_casefold(field: &str, coercion: &CoercionSpec) -> Result<(), ValidateError> {
758 if matches!(coercion.id, CoercionId::TextCasefold) {
759 return Err(ValidateError::InvalidCoercion {
760 field: field.to_string(),
761 coercion: coercion.id,
762 });
763 }
764
765 Ok(())
766}
767
768fn ensure_text_literal(field: &str, value: &Value) -> Result<(), ValidateError> {
770 if !matches!(value, Value::Text(_)) {
771 return Err(invalid_literal(field, "expected text literal"));
772 }
773
774 Ok(())
775}
776
777fn ensure_scalar_literal(field: &str, value: &Value) -> Result<(), ValidateError> {
779 if matches!(value, Value::List(_)) {
780 return Err(invalid_literal(field, "expected scalar literal"));
781 }
782
783 Ok(())
784}
785
786fn ensure_coercion(
787 field: &str,
788 field_type: &FieldType,
789 literal: &Value,
790 coercion: &CoercionSpec,
791) -> Result<(), ValidateError> {
792 if matches!(coercion.id, CoercionId::TextCasefold) && !field_type.is_text() {
793 return Err(ValidateError::InvalidCoercion {
795 field: field.to_string(),
796 coercion: coercion.id,
797 });
798 }
799
800 if matches!(coercion.id, CoercionId::NumericWiden)
805 && (!field_type.supports_numeric_coercion() || !literal.supports_numeric_coercion())
806 {
807 return Err(ValidateError::InvalidCoercion {
808 field: field.to_string(),
809 coercion: coercion.id,
810 });
811 }
812
813 if !matches!(coercion.id, CoercionId::NumericWiden) {
814 let left_family =
815 field_type
816 .coercion_family()
817 .ok_or_else(|| ValidateError::UnsupportedFieldType {
818 field: field.to_string(),
819 })?;
820 let right_family = literal.coercion_family();
821
822 if !supports_coercion(left_family, right_family, coercion.id) {
823 return Err(ValidateError::InvalidCoercion {
824 field: field.to_string(),
825 coercion: coercion.id,
826 });
827 }
828 }
829
830 if matches!(
831 coercion.id,
832 CoercionId::Strict | CoercionId::CollectionElement
833 ) && !literal_matches_type(literal, field_type)
834 {
835 return Err(invalid_literal(
836 field,
837 "literal type does not match field type",
838 ));
839 }
840
841 Ok(())
842}
843
844fn ensure_list_literal(
845 field: &str,
846 literal: &Value,
847 field_type: &FieldType,
848) -> Result<(), ValidateError> {
849 if !literal_matches_type(literal, field_type) {
850 return Err(invalid_literal(
851 field,
852 "list literal does not match field element type",
853 ));
854 }
855
856 Ok(())
857}
858
859fn ensure_map_literal(
860 field: &str,
861 literal: &Value,
862 field_type: &FieldType,
863) -> Result<(), ValidateError> {
864 if !literal_matches_type(literal, field_type) {
865 return Err(invalid_literal(
866 field,
867 "map literal does not match field key/value types",
868 ));
869 }
870
871 Ok(())
872}
873
874pub(crate) fn literal_matches_type(literal: &Value, field_type: &FieldType) -> bool {
875 match field_type {
876 FieldType::Scalar(inner) => inner.matches_value(literal),
877 FieldType::List(element) | FieldType::Set(element) => match literal {
878 Value::List(items) => items.iter().all(|item| literal_matches_type(item, element)),
879 _ => false,
880 },
881 FieldType::Map { key, value } => match literal {
882 Value::Map(entries) => {
883 if Value::validate_map_entries(entries.as_slice()).is_err() {
884 return false;
885 }
886
887 entries.iter().all(|(entry_key, entry_value)| {
888 literal_matches_type(entry_key, key) && literal_matches_type(entry_value, value)
889 })
890 }
891 _ => false,
892 },
893 FieldType::Unsupported => {
894 false
896 }
897 }
898}
899
900fn field_type_from_model_kind(kind: &EntityFieldKind) -> FieldType {
901 match kind {
902 EntityFieldKind::Account => FieldType::Scalar(ScalarType::Account),
903 EntityFieldKind::Blob => FieldType::Scalar(ScalarType::Blob),
904 EntityFieldKind::Bool => FieldType::Scalar(ScalarType::Bool),
905 EntityFieldKind::Date => FieldType::Scalar(ScalarType::Date),
906 EntityFieldKind::Decimal => FieldType::Scalar(ScalarType::Decimal),
907 EntityFieldKind::Duration => FieldType::Scalar(ScalarType::Duration),
908 EntityFieldKind::Enum => FieldType::Scalar(ScalarType::Enum),
909 EntityFieldKind::E8s => FieldType::Scalar(ScalarType::E8s),
910 EntityFieldKind::E18s => FieldType::Scalar(ScalarType::E18s),
911 EntityFieldKind::Float32 => FieldType::Scalar(ScalarType::Float32),
912 EntityFieldKind::Float64 => FieldType::Scalar(ScalarType::Float64),
913 EntityFieldKind::Int => FieldType::Scalar(ScalarType::Int),
914 EntityFieldKind::Int128 => FieldType::Scalar(ScalarType::Int128),
915 EntityFieldKind::IntBig => FieldType::Scalar(ScalarType::IntBig),
916 EntityFieldKind::Principal => FieldType::Scalar(ScalarType::Principal),
917 EntityFieldKind::Subaccount => FieldType::Scalar(ScalarType::Subaccount),
918 EntityFieldKind::Text => FieldType::Scalar(ScalarType::Text),
919 EntityFieldKind::Timestamp => FieldType::Scalar(ScalarType::Timestamp),
920 EntityFieldKind::Uint => FieldType::Scalar(ScalarType::Uint),
921 EntityFieldKind::Uint128 => FieldType::Scalar(ScalarType::Uint128),
922 EntityFieldKind::UintBig => FieldType::Scalar(ScalarType::UintBig),
923 EntityFieldKind::Ulid => FieldType::Scalar(ScalarType::Ulid),
924 EntityFieldKind::Unit => FieldType::Scalar(ScalarType::Unit),
925 EntityFieldKind::Ref { key_kind, .. } => field_type_from_model_kind(key_kind),
926 EntityFieldKind::List(inner) => {
927 FieldType::List(Box::new(field_type_from_model_kind(inner)))
928 }
929 EntityFieldKind::Set(inner) => FieldType::Set(Box::new(field_type_from_model_kind(inner))),
930 EntityFieldKind::Map { key, value } => FieldType::Map {
931 key: Box::new(field_type_from_model_kind(key)),
932 value: Box::new(field_type_from_model_kind(value)),
933 },
934 EntityFieldKind::Unsupported => FieldType::Unsupported,
935 }
936}
937
938impl fmt::Display for FieldType {
939 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
940 match self {
941 Self::Scalar(inner) => write!(f, "{inner:?}"),
942 Self::List(inner) => write!(f, "List<{inner}>"),
943 Self::Set(inner) => write!(f, "Set<{inner}>"),
944 Self::Map { key, value } => write!(f, "Map<{key}, {value}>"),
945 Self::Unsupported => write!(f, "Unsupported"),
946 }
947 }
948}
949
950#[cfg(test)]
955mod tests {
956 use super::{FieldType, ScalarType, ValidateError, ensure_coercion, validate_model};
958 use crate::{
959 db::query::{
960 FieldRef,
961 predicate::{CoercionId, CoercionSpec, Predicate},
962 },
963 model::field::{EntityFieldKind, EntityFieldModel},
964 test_fixtures::InvalidEntityModelBuilder,
965 traits::EntitySchema,
966 types::{
967 Account, Date, Decimal, Duration, E8s, E18s, Float32, Float64, Int, Int128, Nat,
968 Nat128, Principal, Subaccount, Timestamp, Ulid,
969 },
970 value::{CoercionFamily, Value, ValueEnum},
971 };
972 use std::collections::BTreeSet;
973
974 fn registry_scalars() -> Vec<ScalarType> {
976 macro_rules! collect_scalars {
977 ( @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
978 vec![ $( ScalarType::$scalar ),* ]
979 };
980 ( @args $($ignore:tt)*; @entries $( ($scalar:ident, $coercion_family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
981 vec![ $( ScalarType::$scalar ),* ]
982 };
983 }
984
985 let scalars = scalar_registry!(collect_scalars);
986
987 scalars
988 }
989
990 const SCALAR_TYPE_VARIANT_COUNT: usize = 23;
992
993 fn scalar_index(scalar: ScalarType) -> usize {
995 match scalar {
996 ScalarType::Account => 0,
997 ScalarType::Blob => 1,
998 ScalarType::Bool => 2,
999 ScalarType::Date => 3,
1000 ScalarType::Decimal => 4,
1001 ScalarType::Duration => 5,
1002 ScalarType::Enum => 6,
1003 ScalarType::E8s => 7,
1004 ScalarType::E18s => 8,
1005 ScalarType::Float32 => 9,
1006 ScalarType::Float64 => 10,
1007 ScalarType::Int => 11,
1008 ScalarType::Int128 => 12,
1009 ScalarType::IntBig => 13,
1010 ScalarType::Principal => 14,
1011 ScalarType::Subaccount => 15,
1012 ScalarType::Text => 16,
1013 ScalarType::Timestamp => 17,
1014 ScalarType::Uint => 18,
1015 ScalarType::Uint128 => 19,
1016 ScalarType::UintBig => 20,
1017 ScalarType::Ulid => 21,
1018 ScalarType::Unit => 22,
1019 }
1020 }
1021
1022 fn scalar_from_index(index: usize) -> Option<ScalarType> {
1024 let scalar = match index {
1025 0 => ScalarType::Account,
1026 1 => ScalarType::Blob,
1027 2 => ScalarType::Bool,
1028 3 => ScalarType::Date,
1029 4 => ScalarType::Decimal,
1030 5 => ScalarType::Duration,
1031 6 => ScalarType::Enum,
1032 7 => ScalarType::E8s,
1033 8 => ScalarType::E18s,
1034 9 => ScalarType::Float32,
1035 10 => ScalarType::Float64,
1036 11 => ScalarType::Int,
1037 12 => ScalarType::Int128,
1038 13 => ScalarType::IntBig,
1039 14 => ScalarType::Principal,
1040 15 => ScalarType::Subaccount,
1041 16 => ScalarType::Text,
1042 17 => ScalarType::Timestamp,
1043 18 => ScalarType::Uint,
1044 19 => ScalarType::Uint128,
1045 20 => ScalarType::UintBig,
1046 21 => ScalarType::Ulid,
1047 22 => ScalarType::Unit,
1048 _ => return None,
1049 };
1050
1051 Some(scalar)
1052 }
1053
1054 fn sample_value_for_scalar(scalar: ScalarType) -> Value {
1056 match scalar {
1057 ScalarType::Account => Value::Account(Account::dummy(1)),
1058 ScalarType::Blob => Value::Blob(vec![0u8, 1u8]),
1059 ScalarType::Bool => Value::Bool(true),
1060 ScalarType::Date => Value::Date(Date::EPOCH),
1061 ScalarType::Decimal => Value::Decimal(Decimal::ZERO),
1062 ScalarType::Duration => Value::Duration(Duration::ZERO),
1063 ScalarType::Enum => Value::Enum(ValueEnum::loose("example")),
1064 ScalarType::E8s => Value::E8s(E8s::from_atomic(0)),
1065 ScalarType::E18s => Value::E18s(E18s::from_atomic(0)),
1066 ScalarType::Float32 => {
1067 Value::Float32(Float32::try_new(0.0).expect("Float32 sample should be finite"))
1068 }
1069 ScalarType::Float64 => {
1070 Value::Float64(Float64::try_new(0.0).expect("Float64 sample should be finite"))
1071 }
1072 ScalarType::Int => Value::Int(0),
1073 ScalarType::Int128 => Value::Int128(Int128::from(0i128)),
1074 ScalarType::IntBig => Value::IntBig(Int::from(0i32)),
1075 ScalarType::Principal => Value::Principal(Principal::anonymous()),
1076 ScalarType::Subaccount => Value::Subaccount(Subaccount::dummy(2)),
1077 ScalarType::Text => Value::Text("text".to_string()),
1078 ScalarType::Timestamp => Value::Timestamp(Timestamp::EPOCH),
1079 ScalarType::Uint => Value::Uint(0),
1080 ScalarType::Uint128 => Value::Uint128(Nat128::from(0u128)),
1081 ScalarType::UintBig => Value::UintBig(Nat::from(0u64)),
1082 ScalarType::Ulid => Value::Ulid(Ulid::nil()),
1083 ScalarType::Unit => Value::Unit,
1084 }
1085 }
1086
1087 fn field(name: &'static str, kind: EntityFieldKind) -> EntityFieldModel {
1088 EntityFieldModel { name, kind }
1089 }
1090
1091 crate::test_entity_schema! {
1092 ScalarPredicateEntity,
1093 id = Ulid,
1094 path = "predicate_validate::ScalarEntity",
1095 entity_name = "ScalarEntity",
1096 primary_key = "id",
1097 pk_index = 0,
1098 fields = [
1099 ("id", EntityFieldKind::Ulid),
1100 ("email", EntityFieldKind::Text),
1101 ("age", EntityFieldKind::Uint),
1102 ("created_at", EntityFieldKind::Timestamp),
1103 ("active", EntityFieldKind::Bool),
1104 ],
1105 indexes = [],
1106 }
1107
1108 crate::test_entity_schema! {
1109 CollectionPredicateEntity,
1110 id = Ulid,
1111 path = "predicate_validate::CollectionEntity",
1112 entity_name = "CollectionEntity",
1113 primary_key = "id",
1114 pk_index = 0,
1115 fields = [
1116 ("id", EntityFieldKind::Ulid),
1117 ("tags", EntityFieldKind::List(&EntityFieldKind::Text)),
1118 ("principals", EntityFieldKind::Set(&EntityFieldKind::Principal)),
1119 (
1120 "attributes",
1121 EntityFieldKind::Map {
1122 key: &EntityFieldKind::Text,
1123 value: &EntityFieldKind::Uint,
1124 }
1125 ),
1126 ],
1127 indexes = [],
1128 }
1129
1130 crate::test_entity_schema! {
1131 NumericCoercionPredicateEntity,
1132 id = Ulid,
1133 path = "predicate_validate::NumericCoercionEntity",
1134 entity_name = "NumericCoercionEntity",
1135 primary_key = "id",
1136 pk_index = 0,
1137 fields = [
1138 ("id", EntityFieldKind::Ulid),
1139 ("date", EntityFieldKind::Date),
1140 ("int_big", EntityFieldKind::IntBig),
1141 ("uint_big", EntityFieldKind::UintBig),
1142 ("int_small", EntityFieldKind::Int),
1143 ("uint_small", EntityFieldKind::Uint),
1144 ("decimal", EntityFieldKind::Decimal),
1145 ("e8s", EntityFieldKind::E8s),
1146 ],
1147 indexes = [],
1148 }
1149
1150 #[test]
1151 fn validate_model_accepts_scalars_and_coercions() {
1152 let model = <ScalarPredicateEntity as EntitySchema>::MODEL;
1153
1154 let predicate = Predicate::And(vec![
1155 FieldRef::new("id").eq(Ulid::nil()),
1156 FieldRef::new("email").text_eq_ci("User@example.com"),
1157 FieldRef::new("age").lt(30u32),
1158 ]);
1159
1160 assert!(validate_model(model, &predicate).is_ok());
1161 }
1162
1163 #[test]
1164 fn validate_model_rejects_map_predicates_in_0_7_x() {
1165 let model = <CollectionPredicateEntity as EntitySchema>::MODEL;
1166
1167 let allowed = Predicate::And(vec![
1169 FieldRef::new("tags").is_empty(),
1170 FieldRef::new("principals").is_not_empty(),
1171 ]);
1172 assert!(validate_model(model, &allowed).is_ok());
1173
1174 let map_contains =
1175 FieldRef::new("attributes").map_contains_entry("k", 1u64, CoercionId::Strict);
1176 assert!(matches!(
1177 validate_model(model, &map_contains),
1178 Err(ValidateError::MapFieldNotQueryable { field }) if field == "attributes"
1179 ));
1180
1181 let map_presence = Predicate::IsMissing {
1182 field: "attributes".to_string(),
1183 };
1184 assert!(matches!(
1185 validate_model(model, &map_presence),
1186 Err(ValidateError::MapFieldNotQueryable { field }) if field == "attributes"
1187 ));
1188 }
1189
1190 #[test]
1191 fn validate_model_rejects_unsupported_fields() {
1192 let model = InvalidEntityModelBuilder::from_fields(
1193 vec![
1194 field("id", EntityFieldKind::Ulid),
1195 field("broken", EntityFieldKind::Unsupported),
1196 ],
1197 0,
1198 );
1199
1200 let predicate = FieldRef::new("broken").eq(1u64);
1201
1202 assert!(matches!(
1203 validate_model(&model, &predicate),
1204 Err(ValidateError::UnsupportedFieldType { field }) if field == "broken"
1205 ));
1206 }
1207
1208 #[test]
1209 fn validate_model_accepts_text_contains() {
1210 let model = <ScalarPredicateEntity as EntitySchema>::MODEL;
1211
1212 let predicate = FieldRef::new("email").text_contains("example");
1213 assert!(validate_model(model, &predicate).is_ok());
1214
1215 let predicate = FieldRef::new("email").text_contains_ci("EXAMPLE");
1216 assert!(validate_model(model, &predicate).is_ok());
1217 }
1218
1219 #[test]
1220 fn validate_model_rejects_text_contains_on_non_text() {
1221 let model = <ScalarPredicateEntity as EntitySchema>::MODEL;
1222
1223 let predicate = FieldRef::new("age").text_contains("1");
1224 assert!(matches!(
1225 validate_model(model, &predicate),
1226 Err(ValidateError::InvalidOperator { field, op })
1227 if field == "age" && op == "text_contains"
1228 ));
1229 }
1230
1231 #[test]
1232 fn validate_model_rejects_numeric_widen_for_registry_exclusions() {
1233 let model = <NumericCoercionPredicateEntity as EntitySchema>::MODEL;
1234
1235 let date_pred = FieldRef::new("date").lt(1i64);
1236 assert!(matches!(
1237 validate_model(model, &date_pred),
1238 Err(ValidateError::InvalidCoercion { field, coercion })
1239 if field == "date" && coercion == CoercionId::NumericWiden
1240 ));
1241
1242 let int_big_pred = FieldRef::new("int_big").lt(Int::from(1i32));
1243 assert!(matches!(
1244 validate_model(model, &int_big_pred),
1245 Err(ValidateError::InvalidCoercion { field, coercion })
1246 if field == "int_big" && coercion == CoercionId::NumericWiden
1247 ));
1248
1249 let uint_big_pred = FieldRef::new("uint_big").lt(Nat::from(1u64));
1250 assert!(matches!(
1251 validate_model(model, &uint_big_pred),
1252 Err(ValidateError::InvalidCoercion { field, coercion })
1253 if field == "uint_big" && coercion == CoercionId::NumericWiden
1254 ));
1255 }
1256
1257 #[test]
1258 fn validate_model_accepts_numeric_widen_for_registry_allowed_scalars() {
1259 let model = <NumericCoercionPredicateEntity as EntitySchema>::MODEL;
1260 let predicate = Predicate::And(vec![
1261 FieldRef::new("int_small").lt(9u64),
1262 FieldRef::new("uint_small").lt(9i64),
1263 FieldRef::new("decimal").lt(9u64),
1264 FieldRef::new("e8s").lt(9u64),
1265 ]);
1266
1267 assert!(validate_model(model, &predicate).is_ok());
1268 }
1269
1270 #[test]
1271 fn numeric_widen_authority_tracks_registry_flags() {
1272 for scalar in registry_scalars() {
1273 let field_type = FieldType::Scalar(scalar.clone());
1274 let literal = sample_value_for_scalar(scalar.clone());
1275 let expected = scalar.supports_numeric_coercion();
1276 let actual = ensure_coercion(
1277 "value",
1278 &field_type,
1279 &literal,
1280 &CoercionSpec::new(CoercionId::NumericWiden),
1281 )
1282 .is_ok();
1283
1284 assert_eq!(
1285 actual, expected,
1286 "numeric widen drift for scalar {scalar:?}: expected {expected}, got {actual}"
1287 );
1288 }
1289 }
1290
1291 #[test]
1292 fn numeric_widen_is_not_inferred_from_coercion_family() {
1293 let mut numeric_family_with_no_numeric_widen = 0usize;
1294
1295 for scalar in registry_scalars() {
1296 if scalar.coercion_family() != CoercionFamily::Numeric {
1297 continue;
1298 }
1299
1300 let field_type = FieldType::Scalar(scalar.clone());
1301 let literal = sample_value_for_scalar(scalar.clone());
1302 let numeric_widen_allowed = ensure_coercion(
1303 "value",
1304 &field_type,
1305 &literal,
1306 &CoercionSpec::new(CoercionId::NumericWiden),
1307 )
1308 .is_ok();
1309
1310 assert_eq!(
1311 numeric_widen_allowed,
1312 scalar.supports_numeric_coercion(),
1313 "numeric family must not imply numeric widen for scalar {scalar:?}"
1314 );
1315
1316 if !scalar.supports_numeric_coercion() {
1317 numeric_family_with_no_numeric_widen =
1318 numeric_family_with_no_numeric_widen.saturating_add(1);
1319 }
1320 }
1321
1322 assert!(
1323 numeric_family_with_no_numeric_widen > 0,
1324 "expected at least one numeric-family scalar without numeric widen support"
1325 );
1326 }
1327
1328 #[test]
1329 fn scalar_registry_covers_all_variants_exactly_once() {
1330 let scalars = registry_scalars();
1331 let mut names = BTreeSet::new();
1332 let mut seen = [false; SCALAR_TYPE_VARIANT_COUNT];
1333
1334 for scalar in scalars {
1335 let index = scalar_index(scalar.clone());
1336 assert!(!seen[index], "duplicate scalar entry: {scalar:?}");
1337 seen[index] = true;
1338
1339 let name = format!("{scalar:?}");
1340 assert!(names.insert(name.clone()), "duplicate scalar entry: {name}");
1341 }
1342
1343 let mut missing = Vec::new();
1344 for (index, was_seen) in seen.iter().enumerate() {
1345 if !*was_seen {
1346 let scalar = scalar_from_index(index).expect("index is in range");
1347 missing.push(format!("{scalar:?}"));
1348 }
1349 }
1350
1351 assert!(missing.is_empty(), "missing scalar entries: {missing:?}");
1352 assert_eq!(names.len(), SCALAR_TYPE_VARIANT_COUNT);
1353 }
1354
1355 #[test]
1356 fn scalar_keyability_matches_value_storage_key() {
1357 for scalar in registry_scalars() {
1358 let value = sample_value_for_scalar(scalar.clone());
1359 let scalar_keyable = scalar.is_keyable();
1360 let value_keyable = value.as_storage_key().is_some();
1361
1362 assert_eq!(
1363 value_keyable, scalar_keyable,
1364 "Value::as_storage_key drift for scalar {scalar:?}"
1365 );
1366 }
1367 }
1368}