Skip to main content

icydb_schema/
fragment.rs

1//! Store-free reusable schema fragments.
2
3use std::collections::BTreeSet;
4
5use crate::{
6    ConstraintSourceKey, Decimal, DeclaredEntityVersion, EntitySourceKey, FieldSourceKey,
7    IndexSourceKey, MAX_FRAGMENT_CONSTRAINTS, MAX_FRAGMENT_ENTITIES, MAX_FRAGMENT_FIELDS,
8    MAX_FRAGMENT_INDEXES, MAX_FRAGMENT_RELATIONS, MAX_FRAGMENT_TYPES, MAX_SCHEMA_FIELD_TYPE_DEPTH,
9    RelationSourceKey, RuleSourceKey, ScalarKind, ScalarLiteral, SchemaContractError, SchemaName,
10    SourceCheckExpr, SourceRuleOperation, TypeSourceKey,
11};
12
13/// Logical type reference in a proposal fragment.
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub enum FieldType {
16    /// Exact built-in scalar contract.
17    Scalar(ScalarType),
18    /// Ordered repeated values with one exact element contract.
19    List(Box<Self>),
20    /// Named record, enum, newtype, or collection definition.
21    Named(TypeSourceKey),
22}
23
24/// Exact scalar field contract required by accepted-schema lowering.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum ScalarType {
27    /// ICRC account.
28    Account,
29    /// Binary value with an optional maximum byte length.
30    Blob {
31        /// Maximum bytes, or no field-specific maximum.
32        max_len: Option<u32>,
33    },
34    /// Boolean.
35    Bool,
36    /// Day-precision date.
37    Date,
38    /// Fixed-point decimal with exact accepted scale.
39    Decimal {
40        /// Accepted decimal scale.
41        scale: u32,
42    },
43    /// Millisecond duration.
44    Duration,
45    /// Finite 32-bit float.
46    Float32,
47    /// Finite 64-bit float.
48    Float64,
49    /// Signed 8-bit integer.
50    Int8,
51    /// Signed 16-bit integer.
52    Int16,
53    /// Signed 32-bit integer.
54    Int32,
55    /// Signed 64-bit integer.
56    Int64,
57    /// Signed 128-bit integer.
58    Int128,
59    /// Arbitrary-precision signed integer with an encoded-byte bound.
60    IntBig {
61        /// Maximum canonical encoded bytes.
62        max_bytes: u32,
63    },
64    /// Principal.
65    Principal,
66    /// Fixed-width subaccount.
67    Subaccount,
68    /// Text with an optional maximum Unicode-scalar length.
69    Text {
70        /// Maximum Unicode scalar count, or no field-specific maximum.
71        max_len: Option<u32>,
72    },
73    /// Millisecond timestamp.
74    Timestamp,
75    /// Unsigned 8-bit integer.
76    Nat8,
77    /// Unsigned 16-bit integer.
78    Nat16,
79    /// Unsigned 32-bit integer.
80    Nat32,
81    /// Unsigned 64-bit integer.
82    Nat64,
83    /// Unsigned 128-bit integer.
84    Nat128,
85    /// Arbitrary-precision unsigned integer with an encoded-byte bound.
86    NatBig {
87        /// Maximum canonical encoded bytes.
88        max_bytes: u32,
89    },
90    /// ULID.
91    Ulid,
92    /// Unit.
93    Unit,
94}
95
96impl ScalarType {
97    /// Return the intrinsic scalar capability kind.
98    #[must_use]
99    pub const fn kind(self) -> ScalarKind {
100        match self {
101            Self::Account => ScalarKind::Account,
102            Self::Blob { .. } => ScalarKind::Blob,
103            Self::Bool => ScalarKind::Bool,
104            Self::Date => ScalarKind::Date,
105            Self::Decimal { .. } => ScalarKind::Decimal,
106            Self::Duration => ScalarKind::Duration,
107            Self::Float32 => ScalarKind::Float32,
108            Self::Float64 => ScalarKind::Float64,
109            Self::Int8 | Self::Int16 | Self::Int32 | Self::Int64 => ScalarKind::Int,
110            Self::Int128 => ScalarKind::Int128,
111            Self::IntBig { .. } => ScalarKind::IntBig,
112            Self::Principal => ScalarKind::Principal,
113            Self::Subaccount => ScalarKind::Subaccount,
114            Self::Text { .. } => ScalarKind::Text,
115            Self::Timestamp => ScalarKind::Timestamp,
116            Self::Nat8 | Self::Nat16 | Self::Nat32 | Self::Nat64 => ScalarKind::Nat,
117            Self::Nat128 => ScalarKind::Nat128,
118            Self::NatBig { .. } => ScalarKind::NatBig,
119            Self::Ulid => ScalarKind::Ulid,
120            Self::Unit => ScalarKind::Unit,
121        }
122    }
123
124    pub(crate) const fn validate(self) -> Result<(), SchemaContractError> {
125        match self {
126            Self::Decimal { scale } if scale > Decimal::max_supported_scale() => {
127                Err(SchemaContractError::InvalidFieldType)
128            }
129            Self::IntBig { max_bytes: 0 } | Self::NatBig { max_bytes: 0 } => {
130                Err(SchemaContractError::InvalidFieldType)
131            }
132            _ => Ok(()),
133        }
134    }
135
136    pub(crate) fn accepts_literal(self, literal: &ScalarLiteral) -> bool {
137        match (self, literal) {
138            (Self::Account, ScalarLiteral::Account(_))
139            | (Self::Bool, ScalarLiteral::Bool(_))
140            | (Self::Date, ScalarLiteral::Date(_))
141            | (Self::Duration, ScalarLiteral::Duration(_))
142            | (Self::Float32, ScalarLiteral::Float32(_))
143            | (Self::Float64, ScalarLiteral::Float64(_))
144            | (Self::Int128, ScalarLiteral::Int(_))
145            | (Self::Principal, ScalarLiteral::Principal(_))
146            | (Self::Subaccount, ScalarLiteral::Subaccount(_))
147            | (Self::Timestamp, ScalarLiteral::Timestamp(_))
148            | (Self::Nat128, ScalarLiteral::Nat(_))
149            | (Self::Ulid, ScalarLiteral::Ulid(_))
150            | (Self::Unit, ScalarLiteral::Unit(_)) => true,
151            (Self::Blob { max_len }, ScalarLiteral::Blob(value)) => {
152                max_len.is_none_or(|max| value.len() <= max as usize)
153            }
154            (Self::Text { max_len }, ScalarLiteral::Text(value)) => {
155                max_len.is_none_or(|max| value.chars().count() <= max as usize)
156            }
157            (Self::Int8, ScalarLiteral::Int(value)) => i8::try_from(*value).is_ok(),
158            (Self::Int16, ScalarLiteral::Int(value)) => i16::try_from(*value).is_ok(),
159            (Self::Int32, ScalarLiteral::Int(value)) => i32::try_from(*value).is_ok(),
160            (Self::Int64, ScalarLiteral::Int(value)) => i64::try_from(*value).is_ok(),
161            (Self::IntBig { max_bytes }, ScalarLiteral::IntBig(value)) => {
162                value.to_leb128().len() <= max_bytes as usize
163            }
164            (Self::Nat8, ScalarLiteral::Nat(value)) => u8::try_from(*value).is_ok(),
165            (Self::Nat16, ScalarLiteral::Nat(value)) => u16::try_from(*value).is_ok(),
166            (Self::Nat32, ScalarLiteral::Nat(value)) => u32::try_from(*value).is_ok(),
167            (Self::Nat64, ScalarLiteral::Nat(value)) => u64::try_from(*value).is_ok(),
168            (Self::NatBig { max_bytes }, ScalarLiteral::NatBig(value)) => {
169                value.to_leb128().len() <= max_bytes as usize
170            }
171            (Self::Decimal { scale }, ScalarLiteral::Decimal(value)) => {
172                decimal_fits_scale(*value, scale)
173            }
174            _ => false,
175        }
176    }
177}
178
179impl FieldType {
180    pub(crate) const fn validate(&self) -> Result<(), SchemaContractError> {
181        self.validate_at_depth(0)
182    }
183
184    const fn validate_at_depth(&self, depth: usize) -> Result<(), SchemaContractError> {
185        let Some(depth) = depth.checked_add(1) else {
186            return Err(SchemaContractError::FieldTypeDepthExceeded);
187        };
188        if depth > MAX_SCHEMA_FIELD_TYPE_DEPTH {
189            return Err(SchemaContractError::FieldTypeDepthExceeded);
190        }
191        match self {
192            Self::Scalar(scalar) => scalar.validate(),
193            Self::List(item) => item.validate_at_depth(depth),
194            Self::Named(_) => Ok(()),
195        }
196    }
197}
198
199/// Insert policy authored for one field.
200#[derive(Clone, Debug, Eq, PartialEq)]
201pub enum FieldInsertPolicy {
202    /// Omission rejects.
203    Required,
204    /// Omission resolves to explicit null.
205    Nullable,
206    /// Omission or explicit database `DEFAULT` resolves to this constant.
207    Default(ScalarLiteral),
208    /// IcyDB generates the value.
209    Generated,
210}
211
212/// Accepted database-owned lifecycle policy.
213#[derive(Clone, Copy, Debug, Eq, PartialEq)]
214pub enum FieldManagementPolicy {
215    /// Set once on insert and preserve thereafter.
216    CreatedAt,
217    /// Set on insert and on each logical row change.
218    UpdatedAt,
219}
220
221/// One field definition keyed by its current source name.
222#[derive(Clone, Debug, Eq, PartialEq)]
223pub struct FieldFragment {
224    source_key: FieldSourceKey,
225    name: SchemaName,
226    field_type: FieldType,
227    nullable: bool,
228    insert_policy: FieldInsertPolicy,
229    management: Option<FieldManagementPolicy>,
230}
231
232impl FieldFragment {
233    /// Construct one field definition.
234    #[must_use]
235    pub fn new(
236        name: SchemaName,
237        field_type: FieldType,
238        nullable: bool,
239        insert_policy: FieldInsertPolicy,
240        management: Option<FieldManagementPolicy>,
241    ) -> Self {
242        Self {
243            source_key: FieldSourceKey::from_name(&name),
244            name,
245            field_type,
246            nullable,
247            insert_policy,
248            management,
249        }
250    }
251
252    /// Borrow the typed proposal key derived from the current field name.
253    #[must_use]
254    pub const fn source_key(&self) -> &FieldSourceKey {
255        &self.source_key
256    }
257
258    /// Borrow the current field name.
259    #[must_use]
260    pub const fn name(&self) -> &SchemaName {
261        &self.name
262    }
263
264    /// Borrow the logical field type.
265    #[must_use]
266    pub const fn field_type(&self) -> &FieldType {
267        &self.field_type
268    }
269
270    /// Return whether the field admits authored null.
271    #[must_use]
272    pub const fn nullable(&self) -> bool {
273        self.nullable
274    }
275
276    /// Borrow the future-write insert policy.
277    #[must_use]
278    pub const fn insert_policy(&self) -> &FieldInsertPolicy {
279        &self.insert_policy
280    }
281
282    /// Return the optional accepted management policy.
283    #[must_use]
284    pub const fn management(&self) -> Option<FieldManagementPolicy> {
285        self.management
286    }
287
288    pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
289        ensure_current_name_key(self.source_key.as_str(), &self.name)?;
290        self.field_type.validate()?;
291        if let FieldInsertPolicy::Default(literal) = &self.insert_policy {
292            literal.validate()?;
293            match &self.field_type {
294                FieldType::Scalar(scalar) if scalar.accepts_literal(literal) => {}
295                FieldType::Named(_) if matches!(literal, ScalarLiteral::EnumUnit { .. }) => {}
296                FieldType::Scalar(_) | FieldType::List(_) | FieldType::Named(_) => {
297                    return Err(SchemaContractError::LiteralTypeMismatch);
298                }
299            }
300        }
301        if matches!(self.insert_policy, FieldInsertPolicy::Nullable) && !self.nullable {
302            return Err(SchemaContractError::InvalidFieldPolicy);
303        }
304        if self.management.is_some()
305            && (!matches!(self.field_type, FieldType::Scalar(ScalarType::Timestamp))
306                || self.nullable
307                || !matches!(self.insert_policy, FieldInsertPolicy::Required))
308        {
309            return Err(SchemaContractError::InvalidFieldPolicy);
310        }
311        Ok(())
312    }
313}
314
315/// One ordered index key component.
316#[derive(Clone, Debug, Eq, PartialEq)]
317pub enum IndexKeyFragment {
318    /// Direct field key.
319    Field(FieldSourceKey),
320    /// Lower-cased text expression.
321    Lower(FieldSourceKey),
322    /// Upper-cased text expression.
323    Upper(FieldSourceKey),
324    /// Trimmed text expression.
325    Trim(FieldSourceKey),
326    /// Lower-cased and trimmed text expression.
327    LowerTrim(FieldSourceKey),
328    /// Date extraction expression.
329    Date(FieldSourceKey),
330    /// Year extraction expression.
331    Year(FieldSourceKey),
332    /// Month extraction expression.
333    Month(FieldSourceKey),
334    /// Day extraction expression.
335    Day(FieldSourceKey),
336}
337
338impl IndexKeyFragment {
339    /// Borrow the field source key consumed by this component.
340    #[must_use]
341    pub const fn field(&self) -> &FieldSourceKey {
342        match self {
343            Self::Field(field)
344            | Self::Lower(field)
345            | Self::Upper(field)
346            | Self::Trim(field)
347            | Self::LowerTrim(field)
348            | Self::Date(field)
349            | Self::Year(field)
350            | Self::Month(field)
351            | Self::Day(field) => field,
352        }
353    }
354}
355
356/// One secondary-index proposal definition.
357#[derive(Clone, Debug, Eq, PartialEq)]
358pub struct IndexFragment {
359    source_key: IndexSourceKey,
360    name: SchemaName,
361    key: Vec<IndexKeyFragment>,
362    unique: bool,
363    predicate: Option<SourceCheckExpr>,
364}
365
366impl IndexFragment {
367    /// Construct one bounded index definition.
368    ///
369    /// # Errors
370    ///
371    /// Returns a typed reference-list error when no key component is present.
372    pub fn try_new(
373        name: SchemaName,
374        key: Vec<IndexKeyFragment>,
375        unique: bool,
376        predicate: Option<SourceCheckExpr>,
377    ) -> Result<Self, SchemaContractError> {
378        if key.is_empty() {
379            return Err(SchemaContractError::InvalidReferenceList);
380        }
381        if let Some(predicate) = &predicate {
382            predicate.validate()?;
383        }
384        Ok(Self {
385            source_key: IndexSourceKey::from_name(&name),
386            name,
387            key,
388            unique,
389            predicate,
390        })
391    }
392
393    /// Borrow the typed proposal key derived from the current index name.
394    #[must_use]
395    pub const fn source_key(&self) -> &IndexSourceKey {
396        &self.source_key
397    }
398
399    /// Borrow the current index name.
400    #[must_use]
401    pub const fn name(&self) -> &SchemaName {
402        &self.name
403    }
404
405    /// Borrow ordered index key components.
406    #[must_use]
407    pub fn key(&self) -> &[IndexKeyFragment] {
408        &self.key
409    }
410
411    /// Return whether the index enforces uniqueness.
412    #[must_use]
413    pub const fn unique(&self) -> bool {
414        self.unique
415    }
416
417    /// Borrow the optional source predicate.
418    #[must_use]
419    pub const fn predicate(&self) -> Option<&SourceCheckExpr> {
420        self.predicate.as_ref()
421    }
422
423    fn validate(&self) -> Result<(), SchemaContractError> {
424        let rebuilt = Self::try_new(
425            self.name.clone(),
426            self.key.clone(),
427            self.unique,
428            self.predicate.clone(),
429        )?;
430        ensure_canonical_rebuild(self, &rebuilt)
431    }
432}
433
434/// Maintained referential action in the current proposal contract.
435#[derive(Clone, Copy, Debug, Eq, PartialEq)]
436pub enum RelationDeleteAction {
437    /// Reject deletion while a source row refers to the target.
438    Restrict,
439}
440
441/// One source-owned relation definition.
442#[derive(Clone, Debug, Eq, PartialEq)]
443pub struct RelationFragment {
444    source_key: RelationSourceKey,
445    name: SchemaName,
446    local_fields: Vec<FieldSourceKey>,
447    target_entity: EntitySourceKey,
448    target_fields: Vec<FieldSourceKey>,
449    on_delete: RelationDeleteAction,
450}
451
452impl RelationFragment {
453    /// Construct one relation with ordered source/target components.
454    ///
455    /// # Errors
456    ///
457    /// Returns a typed reference-list error for empty or arity-mismatched
458    /// components.
459    pub fn try_new(
460        name: SchemaName,
461        local_fields: Vec<FieldSourceKey>,
462        target_entity: EntitySourceKey,
463        target_fields: Vec<FieldSourceKey>,
464        on_delete: RelationDeleteAction,
465    ) -> Result<Self, SchemaContractError> {
466        if local_fields.is_empty() || local_fields.len() != target_fields.len() {
467            return Err(SchemaContractError::InvalidReferenceList);
468        }
469        ensure_unique(&local_fields)?;
470        ensure_unique(&target_fields)?;
471        Ok(Self {
472            source_key: RelationSourceKey::from_name(&name),
473            name,
474            local_fields,
475            target_entity,
476            target_fields,
477            on_delete,
478        })
479    }
480
481    /// Borrow the typed proposal key derived from the current relation name.
482    #[must_use]
483    pub const fn source_key(&self) -> &RelationSourceKey {
484        &self.source_key
485    }
486
487    /// Borrow the current relation name.
488    #[must_use]
489    pub const fn name(&self) -> &SchemaName {
490        &self.name
491    }
492
493    /// Borrow ordered source fields.
494    #[must_use]
495    pub fn local_fields(&self) -> &[FieldSourceKey] {
496        &self.local_fields
497    }
498
499    /// Borrow the target entity source key.
500    #[must_use]
501    pub const fn target_entity(&self) -> &EntitySourceKey {
502        &self.target_entity
503    }
504
505    /// Borrow ordered target fields.
506    #[must_use]
507    pub fn target_fields(&self) -> &[FieldSourceKey] {
508        &self.target_fields
509    }
510
511    /// Return the maintained delete action.
512    #[must_use]
513    pub const fn on_delete(&self) -> RelationDeleteAction {
514        self.on_delete
515    }
516
517    fn validate(&self) -> Result<(), SchemaContractError> {
518        let rebuilt = Self::try_new(
519            self.name.clone(),
520            self.local_fields.clone(),
521            self.target_entity.clone(),
522            self.target_fields.clone(),
523            self.on_delete,
524        )?;
525        ensure_canonical_rebuild(self, &rebuilt)
526    }
527}
528
529/// One source constraint kind.
530#[derive(Clone, Debug, Eq, PartialEq)]
531pub enum ConstraintFragmentKind {
532    /// General row check over top-level source fields.
533    Check(SourceCheckExpr),
534    /// Closed durable operation over one nominal target below a persisted root.
535    TargetedRule(TargetedRuleFragment),
536}
537
538impl ConstraintFragmentKind {
539    fn validate(&self) -> Result<(), SchemaContractError> {
540        match self {
541            Self::Check(expression) => expression.validate(),
542            Self::TargetedRule(rule) => rule.validate(),
543        }
544    }
545}
546
547/// One source-bound nominal durable-rule target.
548#[derive(Clone, Debug, Eq, PartialEq)]
549pub struct TargetedRuleFragment {
550    root: FieldSourceKey,
551    target_type: TypeSourceKey,
552    rule: RuleSourceKey,
553    operation: SourceRuleOperation,
554}
555
556impl TargetedRuleFragment {
557    /// Construct one targeted rule below a persisted root field.
558    #[must_use]
559    pub fn new(
560        root: FieldSourceKey,
561        target_type: TypeSourceKey,
562        rule: SchemaName,
563        operation: SourceRuleOperation,
564    ) -> Self {
565        Self {
566            root,
567            target_type,
568            rule: RuleSourceKey::from_name(&rule),
569            operation,
570        }
571    }
572
573    /// Borrow the persisted root-field identity.
574    #[must_use]
575    pub const fn root(&self) -> &FieldSourceKey {
576        &self.root
577    }
578
579    /// Borrow the ruled nominal type identity.
580    #[must_use]
581    pub const fn target_type(&self) -> &TypeSourceKey {
582        &self.target_type
583    }
584
585    /// Borrow the current durable-rule name as a typed proposal key.
586    #[must_use]
587    pub const fn rule(&self) -> &RuleSourceKey {
588        &self.rule
589    }
590
591    /// Borrow the closed durable operation.
592    #[must_use]
593    pub const fn operation(&self) -> &SourceRuleOperation {
594        &self.operation
595    }
596
597    fn validate(&self) -> Result<(), SchemaContractError> {
598        self.operation.validate()
599    }
600}
601
602/// One source constraint declaration.
603#[derive(Clone, Debug, Eq, PartialEq)]
604pub struct ConstraintFragment {
605    source_key: ConstraintSourceKey,
606    name: SchemaName,
607    kind: ConstraintFragmentKind,
608}
609
610impl ConstraintFragment {
611    /// Construct one general source check.
612    #[must_use]
613    pub fn check(name: SchemaName, expression: SourceCheckExpr) -> Self {
614        Self {
615            source_key: ConstraintSourceKey::from_name(&name),
616            name,
617            kind: ConstraintFragmentKind::Check(expression),
618        }
619    }
620
621    /// Construct one source-bound targeted durable rule.
622    #[must_use]
623    pub fn targeted_rule(rule: TargetedRuleFragment) -> Self {
624        let source_key = ConstraintSourceKey::for_targeted_field_rule(
625            rule.root(),
626            rule.target_type(),
627            rule.rule(),
628        );
629        let name = SchemaName::for_targeted_rule(&source_key);
630        Self {
631            source_key,
632            name,
633            kind: ConstraintFragmentKind::TargetedRule(rule),
634        }
635    }
636
637    /// Borrow the typed proposal key derived from the current declaration.
638    #[must_use]
639    pub const fn source_key(&self) -> &ConstraintSourceKey {
640        &self.source_key
641    }
642
643    /// Borrow the current constraint name.
644    #[must_use]
645    pub const fn name(&self) -> &SchemaName {
646        &self.name
647    }
648
649    /// Borrow the exact source constraint kind.
650    #[must_use]
651    pub const fn kind(&self) -> &ConstraintFragmentKind {
652        &self.kind
653    }
654
655    fn validate(&self) -> Result<(), SchemaContractError> {
656        self.kind.validate()?;
657        let rebuilt = match &self.kind {
658            ConstraintFragmentKind::Check(expression) => {
659                Self::check(self.name.clone(), expression.clone())
660            }
661            ConstraintFragmentKind::TargetedRule(rule) => Self::targeted_rule(rule.clone()),
662        };
663        ensure_canonical_rebuild(self, &rebuilt)
664    }
665}
666
667/// Store-free logical entity definition.
668#[derive(Clone, Debug, Eq, PartialEq)]
669pub struct EntityFragment {
670    source_key: EntitySourceKey,
671    name: SchemaName,
672    version: DeclaredEntityVersion,
673    fields: Vec<FieldFragment>,
674    primary_key: Vec<FieldSourceKey>,
675    indexes: Vec<IndexFragment>,
676    relations: Vec<RelationFragment>,
677    constraints: Vec<ConstraintFragment>,
678}
679
680impl EntityFragment {
681    /// Construct and canonicalize one entity definition.
682    ///
683    /// # Errors
684    ///
685    /// Returns a typed contract error for collection overflow, duplicate
686    /// source keys, malformed policy, expression, or primary-key references.
687    pub fn try_new(
688        name: SchemaName,
689        version: DeclaredEntityVersion,
690        mut fields: Vec<FieldFragment>,
691        primary_key: Vec<FieldSourceKey>,
692        mut indexes: Vec<IndexFragment>,
693        mut relations: Vec<RelationFragment>,
694        mut constraints: Vec<ConstraintFragment>,
695    ) -> Result<Self, SchemaContractError> {
696        let source_key = EntitySourceKey::from_name(&name);
697        check_len("entity fields", fields.len(), MAX_FRAGMENT_FIELDS)?;
698        check_len("entity indexes", indexes.len(), MAX_FRAGMENT_INDEXES)?;
699        check_len("entity relations", relations.len(), MAX_FRAGMENT_RELATIONS)?;
700        check_len(
701            "entity constraints",
702            constraints.len(),
703            MAX_FRAGMENT_CONSTRAINTS,
704        )?;
705        if primary_key.is_empty() {
706            return Err(SchemaContractError::InvalidReferenceList);
707        }
708        ensure_unique(&primary_key)?;
709        // Equal source keys are rejected below, so stable tie ordering cannot
710        // be observed and need not retain stable-sort machinery in Wasm.
711        crate::compact_sort_unstable_by(&mut fields, |a, b| a.source_key.cmp(&b.source_key));
712        crate::compact_sort_unstable_by(&mut indexes, |a, b| a.source_key.cmp(&b.source_key));
713        crate::compact_sort_unstable_by(&mut relations, |a, b| a.source_key.cmp(&b.source_key));
714        crate::compact_sort_unstable_by(&mut constraints, |a, b| a.source_key.cmp(&b.source_key));
715        ensure_unique_sorted_by(&fields, FieldFragment::source_key)?;
716        ensure_unique_sorted_by(&indexes, IndexFragment::source_key)?;
717        ensure_unique_sorted_by(&relations, RelationFragment::source_key)?;
718        ensure_unique_sorted_by(&constraints, ConstraintFragment::source_key)?;
719        ensure_unique_names(fields.iter().map(FieldFragment::name))?;
720        ensure_unique_names(indexes.iter().map(IndexFragment::name))?;
721        ensure_unique_names(relations.iter().map(RelationFragment::name))?;
722        ensure_unique_names(constraints.iter().map(ConstraintFragment::name))?;
723        for field in &fields {
724            field.validate()?;
725        }
726        validate_management_cardinality(&fields)?;
727        for index in &indexes {
728            index.validate()?;
729        }
730        for relation in &relations {
731            relation.validate()?;
732        }
733        for constraint in &constraints {
734            constraint.validate()?;
735        }
736        let field_keys = fields
737            .iter()
738            .map(|field| field.source_key.clone())
739            .collect::<BTreeSet<_>>();
740        if primary_key.iter().any(|field| !field_keys.contains(field)) {
741            return Err(SchemaContractError::InvalidLocalReference);
742        }
743        validate_insert_generation(&fields, &primary_key)?;
744        for index in &indexes {
745            if index
746                .key()
747                .iter()
748                .any(|component| !field_keys.contains(component.field()))
749                || index.predicate().is_some_and(|predicate| {
750                    predicate
751                        .dependencies()
752                        .iter()
753                        .any(|field| !field_keys.contains(field))
754                })
755            {
756                return Err(SchemaContractError::InvalidLocalReference);
757            }
758        }
759        for relation in &relations {
760            if relation
761                .local_fields()
762                .iter()
763                .any(|field| !field_keys.contains(field))
764                || (relation.target_entity() == &source_key
765                    && relation
766                        .target_fields()
767                        .iter()
768                        .any(|field| !field_keys.contains(field)))
769            {
770                return Err(SchemaContractError::InvalidLocalReference);
771            }
772        }
773        for constraint in &constraints {
774            let invalid = match constraint.kind() {
775                ConstraintFragmentKind::Check(expression) => expression
776                    .dependencies()
777                    .iter()
778                    .any(|field| !field_keys.contains(field)),
779                ConstraintFragmentKind::TargetedRule(rule) => !field_keys.contains(rule.root()),
780            };
781            if invalid {
782                return Err(SchemaContractError::InvalidLocalReference);
783            }
784        }
785        Ok(Self {
786            source_key,
787            name,
788            version,
789            fields,
790            primary_key,
791            indexes,
792            relations,
793            constraints,
794        })
795    }
796
797    /// Borrow the typed proposal key derived from the current entity name.
798    #[must_use]
799    pub const fn source_key(&self) -> &EntitySourceKey {
800        &self.source_key
801    }
802
803    /// Borrow the current entity name.
804    #[must_use]
805    pub const fn name(&self) -> &SchemaName {
806        &self.name
807    }
808
809    /// Return the application-declared current entity version.
810    #[must_use]
811    pub const fn version(&self) -> DeclaredEntityVersion {
812        self.version
813    }
814
815    /// Borrow canonical field definitions.
816    #[must_use]
817    pub fn fields(&self) -> &[FieldFragment] {
818        &self.fields
819    }
820
821    /// Borrow ordered primary-key fields.
822    #[must_use]
823    pub fn primary_key(&self) -> &[FieldSourceKey] {
824        &self.primary_key
825    }
826
827    /// Borrow canonical secondary-index definitions.
828    #[must_use]
829    pub fn indexes(&self) -> &[IndexFragment] {
830        &self.indexes
831    }
832
833    /// Borrow canonical relation definitions.
834    #[must_use]
835    pub fn relations(&self) -> &[RelationFragment] {
836        &self.relations
837    }
838
839    /// Borrow canonical source constraint definitions.
840    #[must_use]
841    pub fn constraints(&self) -> &[ConstraintFragment] {
842        &self.constraints
843    }
844
845    pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
846        let rebuilt = Self::try_new(
847            self.name.clone(),
848            self.version,
849            self.fields.clone(),
850            self.primary_key.clone(),
851            self.indexes.clone(),
852            self.relations.clone(),
853            self.constraints.clone(),
854        )?;
855        ensure_canonical_rebuild(self, &rebuilt)
856    }
857}
858
859// Keep the public proposal contract exact even when fragments are constructed
860// without the source macro. Numeric generation is unsigned identity
861// generation and therefore belongs only to one non-null scalar primary key.
862fn validate_insert_generation(
863    fields: &[FieldFragment],
864    primary_key: &[FieldSourceKey],
865) -> Result<(), SchemaContractError> {
866    for field in fields {
867        if !matches!(field.insert_policy(), FieldInsertPolicy::Generated) {
868            continue;
869        }
870        if field.nullable() || field.management().is_some() {
871            return Err(SchemaContractError::InvalidFieldPolicy);
872        }
873        match field.field_type() {
874            FieldType::Scalar(ScalarType::Ulid | ScalarType::Timestamp) => {}
875            FieldType::Scalar(
876                ScalarType::Nat8
877                | ScalarType::Nat16
878                | ScalarType::Nat32
879                | ScalarType::Nat64
880                | ScalarType::Nat128,
881            ) if primary_key.len() == 1 && primary_key.first() == Some(field.source_key()) => {}
882            FieldType::Scalar(_) | FieldType::List(_) | FieldType::Named(_) => {
883                return Err(SchemaContractError::InvalidFieldPolicy);
884            }
885        }
886    }
887    Ok(())
888}
889
890/// One structural field in a named record type.
891///
892/// Composite fields carry exact type and nullability facts only. Insert,
893/// generation, and management policies belong to persisted entity fields.
894#[derive(Clone, Debug, Eq, PartialEq)]
895pub struct RecordFieldFragment {
896    source_key: FieldSourceKey,
897    name: SchemaName,
898    field_type: FieldType,
899    nullable: bool,
900}
901
902impl RecordFieldFragment {
903    /// Construct one exact structural record field.
904    #[must_use]
905    pub fn new(name: SchemaName, field_type: FieldType, nullable: bool) -> Self {
906        Self {
907            source_key: FieldSourceKey::from_name(&name),
908            name,
909            field_type,
910            nullable,
911        }
912    }
913
914    /// Borrow the typed proposal key derived from the current field name.
915    #[must_use]
916    pub const fn source_key(&self) -> &FieldSourceKey {
917        &self.source_key
918    }
919
920    /// Borrow the current record-field name.
921    #[must_use]
922    pub const fn name(&self) -> &SchemaName {
923        &self.name
924    }
925
926    /// Borrow the exact logical field type.
927    #[must_use]
928    pub const fn field_type(&self) -> &FieldType {
929        &self.field_type
930    }
931
932    /// Return whether the structural field admits null.
933    #[must_use]
934    pub const fn nullable(&self) -> bool {
935        self.nullable
936    }
937
938    fn validate(&self) -> Result<(), SchemaContractError> {
939        if !current_name_key_matches(self.source_key.as_str(), &self.name) {
940            return Err(SchemaContractError::NonCanonical);
941        }
942        self.field_type.validate()
943    }
944}
945
946/// One positional tuple member and its explicit-null policy.
947
948#[derive(Clone, Debug, Eq, PartialEq)]
949pub struct TupleElementFragment {
950    field_type: FieldType,
951    nullable: bool,
952}
953
954impl TupleElementFragment {
955    /// Construct one exact tuple-member contract.
956    #[must_use]
957    pub const fn new(field_type: FieldType, nullable: bool) -> Self {
958        Self {
959            field_type,
960            nullable,
961        }
962    }
963
964    /// Borrow the exact logical member type.
965    #[must_use]
966    pub const fn field_type(&self) -> &FieldType {
967        &self.field_type
968    }
969
970    /// Return whether this tuple member admits explicit null.
971    #[must_use]
972    pub const fn nullable(&self) -> bool {
973        self.nullable
974    }
975
976    const fn validate(&self) -> Result<(), SchemaContractError> {
977        self.field_type.validate()
978    }
979}
980
981/// Named record-type definition.
982#[derive(Clone, Debug, Eq, PartialEq)]
983pub struct RecordTypeFragment {
984    source_key: TypeSourceKey,
985    name: SchemaName,
986    fields: Vec<RecordFieldFragment>,
987}
988
989impl RecordTypeFragment {
990    /// Construct and canonicalize a record definition.
991    ///
992    /// # Errors
993    ///
994    /// Returns a typed contract error for overflow, duplicates, or malformed
995    /// fields.
996    pub fn try_new(
997        name: SchemaName,
998        mut fields: Vec<RecordFieldFragment>,
999    ) -> Result<Self, SchemaContractError> {
1000        check_len("record fields", fields.len(), MAX_FRAGMENT_FIELDS)?;
1001        crate::compact_sort_unstable_by(&mut fields, |left, right| {
1002            left.source_key.cmp(&right.source_key)
1003        });
1004        ensure_unique_sorted_by(&fields, RecordFieldFragment::source_key)?;
1005        ensure_unique_names(fields.iter().map(RecordFieldFragment::name))?;
1006        for field in &fields {
1007            field.validate()?;
1008        }
1009        Ok(Self {
1010            source_key: TypeSourceKey::from_name(&name),
1011            name,
1012            fields,
1013        })
1014    }
1015
1016    /// Borrow the typed proposal key derived from the current record name.
1017    #[must_use]
1018    pub const fn source_key(&self) -> &TypeSourceKey {
1019        &self.source_key
1020    }
1021
1022    /// Borrow the current record name.
1023    #[must_use]
1024    pub const fn name(&self) -> &SchemaName {
1025        &self.name
1026    }
1027
1028    /// Borrow canonical record fields.
1029    #[must_use]
1030    pub fn fields(&self) -> &[RecordFieldFragment] {
1031        &self.fields
1032    }
1033
1034    fn validate(&self) -> Result<(), SchemaContractError> {
1035        let rebuilt = Self::try_new(self.name.clone(), self.fields.clone())?;
1036        if rebuilt != *self {
1037            return Err(SchemaContractError::NonCanonical);
1038        }
1039        Ok(())
1040    }
1041}
1042
1043/// One named enum variant.
1044#[derive(Clone, Debug, Eq, PartialEq)]
1045pub struct EnumVariantFragment {
1046    source_key: TypeSourceKey,
1047    name: SchemaName,
1048    payload: Option<FieldType>,
1049}
1050
1051impl EnumVariantFragment {
1052    /// Construct one unit variant.
1053    #[must_use]
1054    pub fn new(name: SchemaName) -> Self {
1055        Self {
1056            source_key: TypeSourceKey::from_name(&name),
1057            name,
1058            payload: None,
1059        }
1060    }
1061
1062    /// Construct one payload-bearing variant.
1063    #[must_use]
1064    pub fn with_payload(name: SchemaName, payload: FieldType) -> Self {
1065        Self {
1066            source_key: TypeSourceKey::from_name(&name),
1067            name,
1068            payload: Some(payload),
1069        }
1070    }
1071
1072    /// Borrow the typed proposal key derived from the current variant name.
1073    #[must_use]
1074    pub const fn source_key(&self) -> &TypeSourceKey {
1075        &self.source_key
1076    }
1077
1078    /// Borrow the current variant name.
1079    #[must_use]
1080    pub const fn name(&self) -> &SchemaName {
1081        &self.name
1082    }
1083
1084    /// Borrow the optional exact payload contract.
1085    #[must_use]
1086    pub const fn payload(&self) -> Option<&FieldType> {
1087        self.payload.as_ref()
1088    }
1089
1090    fn validate(&self) -> Result<(), SchemaContractError> {
1091        if !current_name_key_matches(self.source_key.as_str(), &self.name) {
1092            return Err(SchemaContractError::NonCanonical);
1093        }
1094        match &self.payload {
1095            Some(payload) => payload.validate(),
1096            None => Ok(()),
1097        }
1098    }
1099}
1100
1101/// Named enum-type definition.
1102#[derive(Clone, Debug, Eq, PartialEq)]
1103pub struct EnumTypeFragment {
1104    source_key: TypeSourceKey,
1105    name: SchemaName,
1106    variants: Vec<EnumVariantFragment>,
1107}
1108
1109impl EnumTypeFragment {
1110    /// Construct and canonicalize an enum definition.
1111    ///
1112    /// # Errors
1113    ///
1114    /// Returns a typed contract error for empty, oversized, or duplicate
1115    /// variants.
1116    pub fn try_new(
1117        name: SchemaName,
1118        mut variants: Vec<EnumVariantFragment>,
1119    ) -> Result<Self, SchemaContractError> {
1120        if variants.is_empty() {
1121            return Err(SchemaContractError::InvalidReferenceList);
1122        }
1123        check_len("enum variants", variants.len(), MAX_FRAGMENT_FIELDS)?;
1124        crate::compact_sort_unstable_by(&mut variants, |left, right| {
1125            left.source_key.cmp(&right.source_key)
1126        });
1127        ensure_unique_sorted_by(&variants, |variant| &variant.source_key)?;
1128        ensure_unique_names(variants.iter().map(EnumVariantFragment::name))?;
1129        for variant in &variants {
1130            variant.validate()?;
1131        }
1132        Ok(Self {
1133            source_key: TypeSourceKey::from_name(&name),
1134            name,
1135            variants,
1136        })
1137    }
1138
1139    /// Borrow the typed proposal key derived from the current enum name.
1140    #[must_use]
1141    pub const fn source_key(&self) -> &TypeSourceKey {
1142        &self.source_key
1143    }
1144
1145    /// Borrow the current enum name.
1146    #[must_use]
1147    pub const fn name(&self) -> &SchemaName {
1148        &self.name
1149    }
1150
1151    /// Borrow canonical enum variants.
1152    #[must_use]
1153    pub fn variants(&self) -> &[EnumVariantFragment] {
1154        &self.variants
1155    }
1156
1157    fn validate(&self) -> Result<(), SchemaContractError> {
1158        let rebuilt = Self::try_new(self.name.clone(), self.variants.clone())?;
1159        if rebuilt != *self {
1160            return Err(SchemaContractError::NonCanonical);
1161        }
1162        Ok(())
1163    }
1164}
1165
1166/// Named reusable type definition.
1167#[derive(Clone, Debug, Eq, PartialEq)]
1168pub enum NamedTypeFragment {
1169    /// Record type.
1170    Record(RecordTypeFragment),
1171    /// Enum type.
1172    Enum(EnumTypeFragment),
1173    /// Transparent named wrapper.
1174    Newtype {
1175        /// Typed proposal key derived from the current name.
1176        source_key: TypeSourceKey,
1177        /// Current schema name.
1178        name: SchemaName,
1179        /// Wrapped logical type.
1180        inner: FieldType,
1181    },
1182    /// Homogeneous ordered collection.
1183    List {
1184        /// Typed proposal key derived from the current name.
1185        source_key: TypeSourceKey,
1186        /// Current schema name.
1187        name: SchemaName,
1188        /// Element type.
1189        item: FieldType,
1190    },
1191    /// Homogeneous unique collection.
1192    Set {
1193        /// Typed proposal key derived from the current name.
1194        source_key: TypeSourceKey,
1195        /// Current schema name.
1196        name: SchemaName,
1197        /// Element type.
1198        item: FieldType,
1199    },
1200    /// Homogeneous key/value collection.
1201    Map {
1202        /// Typed proposal key derived from the current name.
1203        source_key: TypeSourceKey,
1204        /// Current schema name.
1205        name: SchemaName,
1206        /// Key type.
1207        key: FieldType,
1208        /// Value type.
1209        value: FieldType,
1210    },
1211    /// Ordered heterogeneous product.
1212    Tuple {
1213        /// Typed proposal key derived from the current name.
1214        source_key: TypeSourceKey,
1215        /// Current schema name.
1216        name: SchemaName,
1217        /// Ordered member types.
1218        members: Vec<TupleElementFragment>,
1219    },
1220}
1221
1222impl NamedTypeFragment {
1223    /// Construct one transparent named wrapper from its current name.
1224    #[must_use]
1225    pub fn newtype(name: SchemaName, inner: FieldType) -> Self {
1226        Self::Newtype {
1227            source_key: TypeSourceKey::from_name(&name),
1228            name,
1229            inner,
1230        }
1231    }
1232
1233    /// Construct one named ordered collection from its current name.
1234    #[must_use]
1235    pub fn list(name: SchemaName, item: FieldType) -> Self {
1236        Self::List {
1237            source_key: TypeSourceKey::from_name(&name),
1238            name,
1239            item,
1240        }
1241    }
1242
1243    /// Construct one named unique collection from its current name.
1244    #[must_use]
1245    pub fn set(name: SchemaName, item: FieldType) -> Self {
1246        Self::Set {
1247            source_key: TypeSourceKey::from_name(&name),
1248            name,
1249            item,
1250        }
1251    }
1252
1253    /// Construct one named key/value collection from its current name.
1254    #[must_use]
1255    pub fn map(name: SchemaName, key: FieldType, value: FieldType) -> Self {
1256        Self::Map {
1257            source_key: TypeSourceKey::from_name(&name),
1258            name,
1259            key,
1260            value,
1261        }
1262    }
1263
1264    /// Construct one named heterogeneous product from its current name.
1265    #[must_use]
1266    pub fn tuple(name: SchemaName, members: Vec<TupleElementFragment>) -> Self {
1267        Self::Tuple {
1268            source_key: TypeSourceKey::from_name(&name),
1269            name,
1270            members,
1271        }
1272    }
1273
1274    /// Borrow the typed proposal key derived from the current type name.
1275    #[must_use]
1276    pub const fn source_key(&self) -> &TypeSourceKey {
1277        match self {
1278            Self::Record(record) => record.source_key(),
1279            Self::Enum(r#enum) => r#enum.source_key(),
1280            Self::Newtype { source_key, .. }
1281            | Self::List { source_key, .. }
1282            | Self::Set { source_key, .. }
1283            | Self::Map { source_key, .. }
1284            | Self::Tuple { source_key, .. } => source_key,
1285        }
1286    }
1287
1288    /// Borrow the current type name.
1289    #[must_use]
1290    pub const fn name(&self) -> &SchemaName {
1291        match self {
1292            Self::Record(record) => record.name(),
1293            Self::Enum(r#enum) => r#enum.name(),
1294            Self::Newtype { name, .. }
1295            | Self::List { name, .. }
1296            | Self::Set { name, .. }
1297            | Self::Map { name, .. }
1298            | Self::Tuple { name, .. } => name,
1299        }
1300    }
1301
1302    fn validate(&self) -> Result<(), SchemaContractError> {
1303        ensure_current_name_key(self.source_key().as_str(), self.name())?;
1304        match self {
1305            Self::Record(record) => record.validate(),
1306            Self::Enum(r#enum) => r#enum.validate(),
1307            Self::Newtype { inner, .. }
1308            | Self::List { item: inner, .. }
1309            | Self::Set { item: inner, .. } => inner.validate(),
1310            Self::Map { key, value, .. } => {
1311                key.validate()?;
1312                value.validate()
1313            }
1314            Self::Tuple { members, .. } => {
1315                if members.is_empty() {
1316                    return Err(SchemaContractError::InvalidReferenceList);
1317                }
1318                check_len("tuple members", members.len(), MAX_FRAGMENT_FIELDS)?;
1319                members.iter().try_for_each(TupleElementFragment::validate)
1320            }
1321        }
1322    }
1323}
1324
1325/// Reusable store-free collection of entity and type definitions.
1326#[derive(Clone, Debug, Eq, PartialEq)]
1327pub struct SchemaFragment {
1328    entities: Vec<EntityFragment>,
1329    types: Vec<NamedTypeFragment>,
1330}
1331
1332impl SchemaFragment {
1333    /// Construct and canonicalize one reusable fragment.
1334    ///
1335    /// # Errors
1336    ///
1337    /// Returns a typed contract error for overflow, duplicate definitions, or
1338    /// malformed nested definitions.
1339    pub fn try_new(
1340        mut entities: Vec<EntityFragment>,
1341        mut types: Vec<NamedTypeFragment>,
1342    ) -> Result<Self, SchemaContractError> {
1343        check_len("fragment entities", entities.len(), MAX_FRAGMENT_ENTITIES)?;
1344        check_len("fragment types", types.len(), MAX_FRAGMENT_TYPES)?;
1345        crate::compact_sort_unstable_by(&mut entities, |left, right| {
1346            left.source_key.cmp(&right.source_key)
1347        });
1348        crate::compact_sort_unstable_by(&mut types, |left, right| {
1349            left.source_key().cmp(right.source_key())
1350        });
1351        ensure_unique_sorted_by(&entities, EntityFragment::source_key)?;
1352        ensure_unique_sorted_by(&types, NamedTypeFragment::source_key)?;
1353        ensure_unique_names(entities.iter().map(EntityFragment::name))?;
1354        ensure_unique_names(types.iter().map(NamedTypeFragment::name))?;
1355        for entity in &entities {
1356            entity.validate()?;
1357        }
1358        for r#type in &types {
1359            r#type.validate()?;
1360        }
1361        Ok(Self { entities, types })
1362    }
1363
1364    /// Borrow entity definitions.
1365    #[must_use]
1366    pub fn entities(&self) -> &[EntityFragment] {
1367        &self.entities
1368    }
1369
1370    /// Borrow named type definitions.
1371    #[must_use]
1372    pub fn types(&self) -> &[NamedTypeFragment] {
1373        &self.types
1374    }
1375
1376    pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
1377        for r#type in &self.types {
1378            r#type.validate()?;
1379        }
1380        let rebuilt = Self::try_new(self.entities.clone(), self.types.clone())?;
1381        if rebuilt != *self {
1382            return Err(SchemaContractError::NonCanonical);
1383        }
1384        Ok(())
1385    }
1386}
1387
1388pub(crate) const fn check_len(
1389    kind: &'static str,
1390    len: usize,
1391    max: usize,
1392) -> Result<(), SchemaContractError> {
1393    if len > max {
1394        return Err(SchemaContractError::TooManyItems { kind, len, max });
1395    }
1396    Ok(())
1397}
1398
1399fn current_name_key_matches(source_key: &str, name: &SchemaName) -> bool {
1400    source_key == name.as_str()
1401}
1402
1403fn ensure_current_name_key(source_key: &str, name: &SchemaName) -> Result<(), SchemaContractError> {
1404    if !current_name_key_matches(source_key, name) {
1405        return Err(SchemaContractError::NonCanonical);
1406    }
1407    Ok(())
1408}
1409
1410fn ensure_canonical_rebuild<T: PartialEq>(
1411    current: &T,
1412    rebuilt: &T,
1413) -> Result<(), SchemaContractError> {
1414    if current != rebuilt {
1415        return Err(SchemaContractError::NonCanonical);
1416    }
1417    Ok(())
1418}
1419
1420fn ensure_unique<T>(values: &[T]) -> Result<(), SchemaContractError>
1421where
1422    T: Ord,
1423{
1424    let mut seen = BTreeSet::new();
1425    if values.iter().any(|value| !seen.insert(value)) {
1426        return Err(SchemaContractError::InvalidReferenceList);
1427    }
1428    Ok(())
1429}
1430
1431fn ensure_unique_sorted_by<T, K>(
1432    values: &[T],
1433    key: impl Fn(&T) -> &K,
1434) -> Result<(), SchemaContractError>
1435where
1436    K: Eq,
1437{
1438    if values.windows(2).any(|pair| key(&pair[0]) == key(&pair[1])) {
1439        return Err(SchemaContractError::DuplicateSourceKey);
1440    }
1441    Ok(())
1442}
1443
1444fn ensure_unique_names<'a>(
1445    names: impl IntoIterator<Item = &'a SchemaName>,
1446) -> Result<(), SchemaContractError> {
1447    let mut seen = BTreeSet::new();
1448    if names.into_iter().any(|name| !seen.insert(name)) {
1449        return Err(SchemaContractError::DuplicateName);
1450    }
1451    Ok(())
1452}
1453
1454fn validate_management_cardinality(fields: &[FieldFragment]) -> Result<(), SchemaContractError> {
1455    for policy in [
1456        FieldManagementPolicy::CreatedAt,
1457        FieldManagementPolicy::UpdatedAt,
1458    ] {
1459        if fields
1460            .iter()
1461            .filter(|field| field.management() == Some(policy))
1462            .count()
1463            > 1
1464        {
1465            return Err(SchemaContractError::InvalidFieldPolicy);
1466        }
1467    }
1468    Ok(())
1469}
1470
1471fn decimal_fits_scale(value: Decimal, scale: u32) -> bool {
1472    match value.scale().cmp(&scale) {
1473        std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => true,
1474        std::cmp::Ordering::Less => value.scale_to_integer(scale).is_some(),
1475    }
1476}
1477
1478#[cfg(test)]
1479mod tests {
1480    use super::{
1481        FieldFragment, FieldInsertPolicy, FieldSourceKey, FieldType, ScalarType,
1482        SchemaContractError, SchemaName,
1483    };
1484
1485    #[test]
1486    fn independently_decoded_field_key_and_name_must_match() {
1487        let field = FieldFragment {
1488            source_key: FieldSourceKey::try_new("legacy_name").expect("fixture key should admit"),
1489            name: SchemaName::try_new("current_name").expect("fixture name should admit"),
1490            field_type: FieldType::Scalar(ScalarType::Nat64),
1491            nullable: false,
1492            insert_policy: FieldInsertPolicy::Required,
1493            management: None,
1494        };
1495
1496        assert_eq!(field.validate(), Err(SchemaContractError::NonCanonical));
1497    }
1498}