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