Skip to main content

icydb_schema/
fragment.rs

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