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        fields.sort_unstable_by(|left, right| left.source_key.cmp(&right.source_key));
712        indexes.sort_unstable_by(|left, right| left.source_key.cmp(&right.source_key));
713        relations.sort_unstable_by(|left, right| left.source_key.cmp(&right.source_key));
714        constraints.sort_unstable_by(|left, right| left.source_key.cmp(&right.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        fields.sort_unstable_by(|left, right| left.source_key.cmp(&right.source_key));
1002        ensure_unique_sorted_by(&fields, RecordFieldFragment::source_key)?;
1003        ensure_unique_names(fields.iter().map(RecordFieldFragment::name))?;
1004        for field in &fields {
1005            field.validate()?;
1006        }
1007        Ok(Self {
1008            source_key: TypeSourceKey::from_name(&name),
1009            name,
1010            fields,
1011        })
1012    }
1013
1014    /// Borrow the typed proposal key derived from the current record name.
1015    #[must_use]
1016    pub const fn source_key(&self) -> &TypeSourceKey {
1017        &self.source_key
1018    }
1019
1020    /// Borrow the current record name.
1021    #[must_use]
1022    pub const fn name(&self) -> &SchemaName {
1023        &self.name
1024    }
1025
1026    /// Borrow canonical record fields.
1027    #[must_use]
1028    pub fn fields(&self) -> &[RecordFieldFragment] {
1029        &self.fields
1030    }
1031
1032    fn validate(&self) -> Result<(), SchemaContractError> {
1033        let rebuilt = Self::try_new(self.name.clone(), self.fields.clone())?;
1034        if rebuilt != *self {
1035            return Err(SchemaContractError::NonCanonical);
1036        }
1037        Ok(())
1038    }
1039}
1040
1041/// One named enum variant.
1042#[derive(Clone, Debug, Eq, PartialEq)]
1043pub struct EnumVariantFragment {
1044    source_key: TypeSourceKey,
1045    name: SchemaName,
1046    payload: Option<FieldType>,
1047}
1048
1049impl EnumVariantFragment {
1050    /// Construct one unit variant.
1051    #[must_use]
1052    pub fn new(name: SchemaName) -> Self {
1053        Self {
1054            source_key: TypeSourceKey::from_name(&name),
1055            name,
1056            payload: None,
1057        }
1058    }
1059
1060    /// Construct one payload-bearing variant.
1061    #[must_use]
1062    pub fn with_payload(name: SchemaName, payload: FieldType) -> Self {
1063        Self {
1064            source_key: TypeSourceKey::from_name(&name),
1065            name,
1066            payload: Some(payload),
1067        }
1068    }
1069
1070    /// Borrow the typed proposal key derived from the current variant name.
1071    #[must_use]
1072    pub const fn source_key(&self) -> &TypeSourceKey {
1073        &self.source_key
1074    }
1075
1076    /// Borrow the current variant name.
1077    #[must_use]
1078    pub const fn name(&self) -> &SchemaName {
1079        &self.name
1080    }
1081
1082    /// Borrow the optional exact payload contract.
1083    #[must_use]
1084    pub const fn payload(&self) -> Option<&FieldType> {
1085        self.payload.as_ref()
1086    }
1087
1088    fn validate(&self) -> Result<(), SchemaContractError> {
1089        if !current_name_key_matches(self.source_key.as_str(), &self.name) {
1090            return Err(SchemaContractError::NonCanonical);
1091        }
1092        match &self.payload {
1093            Some(payload) => payload.validate(),
1094            None => Ok(()),
1095        }
1096    }
1097}
1098
1099/// Named enum-type definition.
1100#[derive(Clone, Debug, Eq, PartialEq)]
1101pub struct EnumTypeFragment {
1102    source_key: TypeSourceKey,
1103    name: SchemaName,
1104    variants: Vec<EnumVariantFragment>,
1105}
1106
1107impl EnumTypeFragment {
1108    /// Construct and canonicalize an enum definition.
1109    ///
1110    /// # Errors
1111    ///
1112    /// Returns a typed contract error for empty, oversized, or duplicate
1113    /// variants.
1114    pub fn try_new(
1115        name: SchemaName,
1116        mut variants: Vec<EnumVariantFragment>,
1117    ) -> Result<Self, SchemaContractError> {
1118        if variants.is_empty() {
1119            return Err(SchemaContractError::InvalidReferenceList);
1120        }
1121        check_len("enum variants", variants.len(), MAX_FRAGMENT_FIELDS)?;
1122        variants.sort_unstable_by(|left, right| left.source_key.cmp(&right.source_key));
1123        ensure_unique_sorted_by(&variants, |variant| &variant.source_key)?;
1124        ensure_unique_names(variants.iter().map(EnumVariantFragment::name))?;
1125        for variant in &variants {
1126            variant.validate()?;
1127        }
1128        Ok(Self {
1129            source_key: TypeSourceKey::from_name(&name),
1130            name,
1131            variants,
1132        })
1133    }
1134
1135    /// Borrow the typed proposal key derived from the current enum name.
1136    #[must_use]
1137    pub const fn source_key(&self) -> &TypeSourceKey {
1138        &self.source_key
1139    }
1140
1141    /// Borrow the current enum name.
1142    #[must_use]
1143    pub const fn name(&self) -> &SchemaName {
1144        &self.name
1145    }
1146
1147    /// Borrow canonical enum variants.
1148    #[must_use]
1149    pub fn variants(&self) -> &[EnumVariantFragment] {
1150        &self.variants
1151    }
1152
1153    fn validate(&self) -> Result<(), SchemaContractError> {
1154        let rebuilt = Self::try_new(self.name.clone(), self.variants.clone())?;
1155        if rebuilt != *self {
1156            return Err(SchemaContractError::NonCanonical);
1157        }
1158        Ok(())
1159    }
1160}
1161
1162/// Named reusable type definition.
1163#[derive(Clone, Debug, Eq, PartialEq)]
1164pub enum NamedTypeFragment {
1165    /// Record type.
1166    Record(RecordTypeFragment),
1167    /// Enum type.
1168    Enum(EnumTypeFragment),
1169    /// Transparent named wrapper.
1170    Newtype {
1171        /// Typed proposal key derived from the current name.
1172        source_key: TypeSourceKey,
1173        /// Current schema name.
1174        name: SchemaName,
1175        /// Wrapped logical type.
1176        inner: FieldType,
1177    },
1178    /// Homogeneous ordered collection.
1179    List {
1180        /// Typed proposal key derived from the current name.
1181        source_key: TypeSourceKey,
1182        /// Current schema name.
1183        name: SchemaName,
1184        /// Element type.
1185        item: FieldType,
1186    },
1187    /// Homogeneous unique collection.
1188    Set {
1189        /// Typed proposal key derived from the current name.
1190        source_key: TypeSourceKey,
1191        /// Current schema name.
1192        name: SchemaName,
1193        /// Element type.
1194        item: FieldType,
1195    },
1196    /// Homogeneous key/value collection.
1197    Map {
1198        /// Typed proposal key derived from the current name.
1199        source_key: TypeSourceKey,
1200        /// Current schema name.
1201        name: SchemaName,
1202        /// Key type.
1203        key: FieldType,
1204        /// Value type.
1205        value: FieldType,
1206    },
1207    /// Ordered heterogeneous product.
1208    Tuple {
1209        /// Typed proposal key derived from the current name.
1210        source_key: TypeSourceKey,
1211        /// Current schema name.
1212        name: SchemaName,
1213        /// Ordered member types.
1214        members: Vec<TupleElementFragment>,
1215    },
1216}
1217
1218impl NamedTypeFragment {
1219    /// Construct one transparent named wrapper from its current name.
1220    #[must_use]
1221    pub fn newtype(name: SchemaName, inner: FieldType) -> Self {
1222        Self::Newtype {
1223            source_key: TypeSourceKey::from_name(&name),
1224            name,
1225            inner,
1226        }
1227    }
1228
1229    /// Construct one named ordered collection from its current name.
1230    #[must_use]
1231    pub fn list(name: SchemaName, item: FieldType) -> Self {
1232        Self::List {
1233            source_key: TypeSourceKey::from_name(&name),
1234            name,
1235            item,
1236        }
1237    }
1238
1239    /// Construct one named unique collection from its current name.
1240    #[must_use]
1241    pub fn set(name: SchemaName, item: FieldType) -> Self {
1242        Self::Set {
1243            source_key: TypeSourceKey::from_name(&name),
1244            name,
1245            item,
1246        }
1247    }
1248
1249    /// Construct one named key/value collection from its current name.
1250    #[must_use]
1251    pub fn map(name: SchemaName, key: FieldType, value: FieldType) -> Self {
1252        Self::Map {
1253            source_key: TypeSourceKey::from_name(&name),
1254            name,
1255            key,
1256            value,
1257        }
1258    }
1259
1260    /// Construct one named heterogeneous product from its current name.
1261    #[must_use]
1262    pub fn tuple(name: SchemaName, members: Vec<TupleElementFragment>) -> Self {
1263        Self::Tuple {
1264            source_key: TypeSourceKey::from_name(&name),
1265            name,
1266            members,
1267        }
1268    }
1269
1270    /// Borrow the typed proposal key derived from the current type name.
1271    #[must_use]
1272    pub const fn source_key(&self) -> &TypeSourceKey {
1273        match self {
1274            Self::Record(record) => record.source_key(),
1275            Self::Enum(r#enum) => r#enum.source_key(),
1276            Self::Newtype { source_key, .. }
1277            | Self::List { source_key, .. }
1278            | Self::Set { source_key, .. }
1279            | Self::Map { source_key, .. }
1280            | Self::Tuple { source_key, .. } => source_key,
1281        }
1282    }
1283
1284    /// Borrow the current type name.
1285    #[must_use]
1286    pub const fn name(&self) -> &SchemaName {
1287        match self {
1288            Self::Record(record) => record.name(),
1289            Self::Enum(r#enum) => r#enum.name(),
1290            Self::Newtype { name, .. }
1291            | Self::List { name, .. }
1292            | Self::Set { name, .. }
1293            | Self::Map { name, .. }
1294            | Self::Tuple { name, .. } => name,
1295        }
1296    }
1297
1298    fn validate(&self) -> Result<(), SchemaContractError> {
1299        ensure_current_name_key(self.source_key().as_str(), self.name())?;
1300        match self {
1301            Self::Record(record) => record.validate(),
1302            Self::Enum(r#enum) => r#enum.validate(),
1303            Self::Newtype { inner, .. }
1304            | Self::List { item: inner, .. }
1305            | Self::Set { item: inner, .. } => inner.validate(),
1306            Self::Map { key, value, .. } => {
1307                key.validate()?;
1308                value.validate()
1309            }
1310            Self::Tuple { members, .. } => {
1311                if members.is_empty() {
1312                    return Err(SchemaContractError::InvalidReferenceList);
1313                }
1314                check_len("tuple members", members.len(), MAX_FRAGMENT_FIELDS)?;
1315                members.iter().try_for_each(TupleElementFragment::validate)
1316            }
1317        }
1318    }
1319}
1320
1321/// Reusable store-free collection of entity and type definitions.
1322#[derive(Clone, Debug, Eq, PartialEq)]
1323pub struct SchemaFragment {
1324    entities: Vec<EntityFragment>,
1325    types: Vec<NamedTypeFragment>,
1326}
1327
1328impl SchemaFragment {
1329    /// Construct and canonicalize one reusable fragment.
1330    ///
1331    /// # Errors
1332    ///
1333    /// Returns a typed contract error for overflow, duplicate definitions, or
1334    /// malformed nested definitions.
1335    pub fn try_new(
1336        mut entities: Vec<EntityFragment>,
1337        mut types: Vec<NamedTypeFragment>,
1338    ) -> Result<Self, SchemaContractError> {
1339        check_len("fragment entities", entities.len(), MAX_FRAGMENT_ENTITIES)?;
1340        check_len("fragment types", types.len(), MAX_FRAGMENT_TYPES)?;
1341        entities.sort_unstable_by(|left, right| left.source_key.cmp(&right.source_key));
1342        types.sort_unstable_by(|left, right| left.source_key().cmp(right.source_key()));
1343        ensure_unique_sorted_by(&entities, EntityFragment::source_key)?;
1344        ensure_unique_sorted_by(&types, NamedTypeFragment::source_key)?;
1345        ensure_unique_names(entities.iter().map(EntityFragment::name))?;
1346        ensure_unique_names(types.iter().map(NamedTypeFragment::name))?;
1347        for entity in &entities {
1348            entity.validate()?;
1349        }
1350        for r#type in &types {
1351            r#type.validate()?;
1352        }
1353        Ok(Self { entities, types })
1354    }
1355
1356    /// Borrow entity definitions.
1357    #[must_use]
1358    pub fn entities(&self) -> &[EntityFragment] {
1359        &self.entities
1360    }
1361
1362    /// Borrow named type definitions.
1363    #[must_use]
1364    pub fn types(&self) -> &[NamedTypeFragment] {
1365        &self.types
1366    }
1367
1368    pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
1369        for r#type in &self.types {
1370            r#type.validate()?;
1371        }
1372        let rebuilt = Self::try_new(self.entities.clone(), self.types.clone())?;
1373        if rebuilt != *self {
1374            return Err(SchemaContractError::NonCanonical);
1375        }
1376        Ok(())
1377    }
1378}
1379
1380pub(crate) const fn check_len(
1381    kind: &'static str,
1382    len: usize,
1383    max: usize,
1384) -> Result<(), SchemaContractError> {
1385    if len > max {
1386        return Err(SchemaContractError::TooManyItems { kind, len, max });
1387    }
1388    Ok(())
1389}
1390
1391fn current_name_key_matches(source_key: &str, name: &SchemaName) -> bool {
1392    source_key == name.as_str()
1393}
1394
1395fn ensure_current_name_key(source_key: &str, name: &SchemaName) -> Result<(), SchemaContractError> {
1396    if !current_name_key_matches(source_key, name) {
1397        return Err(SchemaContractError::NonCanonical);
1398    }
1399    Ok(())
1400}
1401
1402fn ensure_canonical_rebuild<T: PartialEq>(
1403    current: &T,
1404    rebuilt: &T,
1405) -> Result<(), SchemaContractError> {
1406    if current != rebuilt {
1407        return Err(SchemaContractError::NonCanonical);
1408    }
1409    Ok(())
1410}
1411
1412fn ensure_unique<T>(values: &[T]) -> Result<(), SchemaContractError>
1413where
1414    T: Ord,
1415{
1416    let mut seen = BTreeSet::new();
1417    if values.iter().any(|value| !seen.insert(value)) {
1418        return Err(SchemaContractError::InvalidReferenceList);
1419    }
1420    Ok(())
1421}
1422
1423fn ensure_unique_sorted_by<T, K>(
1424    values: &[T],
1425    key: impl Fn(&T) -> &K,
1426) -> Result<(), SchemaContractError>
1427where
1428    K: Eq,
1429{
1430    if values.windows(2).any(|pair| key(&pair[0]) == key(&pair[1])) {
1431        return Err(SchemaContractError::DuplicateSourceKey);
1432    }
1433    Ok(())
1434}
1435
1436fn ensure_unique_names<'a>(
1437    names: impl IntoIterator<Item = &'a SchemaName>,
1438) -> Result<(), SchemaContractError> {
1439    let mut seen = BTreeSet::new();
1440    if names.into_iter().any(|name| !seen.insert(name)) {
1441        return Err(SchemaContractError::DuplicateName);
1442    }
1443    Ok(())
1444}
1445
1446fn validate_management_cardinality(fields: &[FieldFragment]) -> Result<(), SchemaContractError> {
1447    for policy in [
1448        FieldManagementPolicy::CreatedAt,
1449        FieldManagementPolicy::UpdatedAt,
1450    ] {
1451        if fields
1452            .iter()
1453            .filter(|field| field.management() == Some(policy))
1454            .count()
1455            > 1
1456        {
1457            return Err(SchemaContractError::InvalidFieldPolicy);
1458        }
1459    }
1460    Ok(())
1461}
1462
1463fn decimal_fits_scale(value: Decimal, scale: u32) -> bool {
1464    match value.scale().cmp(&scale) {
1465        std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => true,
1466        std::cmp::Ordering::Less => value.scale_to_integer(scale).is_some(),
1467    }
1468}
1469
1470#[cfg(test)]
1471mod tests {
1472    use super::{
1473        FieldFragment, FieldInsertPolicy, FieldSourceKey, FieldType, ScalarType,
1474        SchemaContractError, SchemaName,
1475    };
1476
1477    #[test]
1478    fn independently_decoded_field_key_and_name_must_match() {
1479        let field = FieldFragment {
1480            source_key: FieldSourceKey::try_new("legacy_name").expect("fixture key should admit"),
1481            name: SchemaName::try_new("current_name").expect("fixture name should admit"),
1482            field_type: FieldType::Scalar(ScalarType::Nat64),
1483            nullable: false,
1484            insert_policy: FieldInsertPolicy::Required,
1485            management: None,
1486        };
1487
1488        assert_eq!(field.validate(), Err(SchemaContractError::NonCanonical));
1489    }
1490}