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, DeclaredEntityVersion, EntitySourceKey, FieldSourceKey,
10    IndexSourceKey, MAX_FRAGMENT_CONSTRAINTS, MAX_FRAGMENT_ENTITIES, MAX_FRAGMENT_FIELDS,
11    MAX_FRAGMENT_INDEXES, MAX_FRAGMENT_RELATIONS, MAX_FRAGMENT_TYPES, MAX_SCHEMA_FIELD_TYPE_DEPTH,
12    RelationSourceKey, RuleSourceKey, ScalarKind, ScalarLiteral, SchemaContractError, SchemaName,
13    SourceCheckExpr, 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    version: DeclaredEntityVersion,
676    fields: Vec<FieldFragment>,
677    primary_key: Vec<FieldSourceKey>,
678    indexes: Vec<IndexFragment>,
679    relations: Vec<RelationFragment>,
680    constraints: Vec<ConstraintFragment>,
681}
682
683impl EntityFragment {
684    /// Construct and canonicalize one entity definition.
685    ///
686    /// # Errors
687    ///
688    /// Returns a typed contract error for collection overflow, duplicate
689    /// source keys, malformed policy, expression, or primary-key references.
690    pub fn try_new(
691        name: SchemaName,
692        version: DeclaredEntityVersion,
693        mut fields: Vec<FieldFragment>,
694        primary_key: Vec<FieldSourceKey>,
695        mut indexes: Vec<IndexFragment>,
696        mut relations: Vec<RelationFragment>,
697        mut constraints: Vec<ConstraintFragment>,
698    ) -> Result<Self, SchemaContractError> {
699        let source_key = EntitySourceKey::from_name(&name);
700        check_len("entity fields", fields.len(), MAX_FRAGMENT_FIELDS)?;
701        check_len("entity indexes", indexes.len(), MAX_FRAGMENT_INDEXES)?;
702        check_len("entity relations", relations.len(), MAX_FRAGMENT_RELATIONS)?;
703        check_len(
704            "entity constraints",
705            constraints.len(),
706            MAX_FRAGMENT_CONSTRAINTS,
707        )?;
708        if primary_key.is_empty() {
709            return Err(SchemaContractError::InvalidReferenceList);
710        }
711        ensure_unique(&primary_key)?;
712        // Equal source keys are rejected below, so stable tie ordering cannot
713        // be observed and need not retain stable-sort machinery in Wasm.
714        fields.sort_unstable_by(|left, right| left.source_key.cmp(&right.source_key));
715        indexes.sort_unstable_by(|left, right| left.source_key.cmp(&right.source_key));
716        relations.sort_unstable_by(|left, right| left.source_key.cmp(&right.source_key));
717        constraints.sort_unstable_by(|left, right| left.source_key.cmp(&right.source_key));
718        ensure_unique_sorted_by(&fields, FieldFragment::source_key)?;
719        ensure_unique_sorted_by(&indexes, IndexFragment::source_key)?;
720        ensure_unique_sorted_by(&relations, RelationFragment::source_key)?;
721        ensure_unique_sorted_by(&constraints, ConstraintFragment::source_key)?;
722        ensure_unique_names(fields.iter().map(FieldFragment::name))?;
723        ensure_unique_names(indexes.iter().map(IndexFragment::name))?;
724        ensure_unique_names(relations.iter().map(RelationFragment::name))?;
725        ensure_unique_names(constraints.iter().map(ConstraintFragment::name))?;
726        for field in &fields {
727            field.validate()?;
728        }
729        validate_management_cardinality(&fields)?;
730        for index in &indexes {
731            index.validate()?;
732        }
733        for relation in &relations {
734            relation.validate()?;
735        }
736        for constraint in &constraints {
737            constraint.validate()?;
738        }
739        let field_keys = fields
740            .iter()
741            .map(|field| field.source_key.clone())
742            .collect::<BTreeSet<_>>();
743        if primary_key.iter().any(|field| !field_keys.contains(field)) {
744            return Err(SchemaContractError::InvalidLocalReference);
745        }
746        validate_insert_generation(&fields, &primary_key)?;
747        for index in &indexes {
748            if index
749                .key()
750                .iter()
751                .any(|component| !field_keys.contains(component.field()))
752                || index.predicate().is_some_and(|predicate| {
753                    predicate
754                        .dependencies()
755                        .iter()
756                        .any(|field| !field_keys.contains(field))
757                })
758            {
759                return Err(SchemaContractError::InvalidLocalReference);
760            }
761        }
762        for relation in &relations {
763            if relation
764                .local_fields()
765                .iter()
766                .any(|field| !field_keys.contains(field))
767                || (relation.target_entity() == &source_key
768                    && relation
769                        .target_fields()
770                        .iter()
771                        .any(|field| !field_keys.contains(field)))
772            {
773                return Err(SchemaContractError::InvalidLocalReference);
774            }
775        }
776        for constraint in &constraints {
777            let invalid = match constraint.kind() {
778                ConstraintFragmentKind::Check(expression) => expression
779                    .dependencies()
780                    .iter()
781                    .any(|field| !field_keys.contains(field)),
782                ConstraintFragmentKind::TargetedRule(rule) => !field_keys.contains(rule.root()),
783            };
784            if invalid {
785                return Err(SchemaContractError::InvalidLocalReference);
786            }
787        }
788        Ok(Self {
789            source_key,
790            name,
791            version,
792            fields,
793            primary_key,
794            indexes,
795            relations,
796            constraints,
797        })
798    }
799
800    /// Borrow the typed proposal key derived from the current entity name.
801    #[must_use]
802    pub const fn source_key(&self) -> &EntitySourceKey {
803        &self.source_key
804    }
805
806    /// Borrow the current entity name.
807    #[must_use]
808    pub const fn name(&self) -> &SchemaName {
809        &self.name
810    }
811
812    /// Return the application-declared current entity version.
813    #[must_use]
814    pub const fn version(&self) -> DeclaredEntityVersion {
815        self.version
816    }
817
818    /// Borrow canonical field definitions.
819    #[must_use]
820    pub fn fields(&self) -> &[FieldFragment] {
821        &self.fields
822    }
823
824    /// Borrow ordered primary-key fields.
825    #[must_use]
826    pub fn primary_key(&self) -> &[FieldSourceKey] {
827        &self.primary_key
828    }
829
830    /// Borrow canonical secondary-index definitions.
831    #[must_use]
832    pub fn indexes(&self) -> &[IndexFragment] {
833        &self.indexes
834    }
835
836    /// Borrow canonical relation definitions.
837    #[must_use]
838    pub fn relations(&self) -> &[RelationFragment] {
839        &self.relations
840    }
841
842    /// Borrow canonical source constraint definitions.
843    #[must_use]
844    pub fn constraints(&self) -> &[ConstraintFragment] {
845        &self.constraints
846    }
847
848    pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
849        let rebuilt = Self::try_new(
850            self.name.clone(),
851            self.version,
852            self.fields.clone(),
853            self.primary_key.clone(),
854            self.indexes.clone(),
855            self.relations.clone(),
856            self.constraints.clone(),
857        )?;
858        ensure_canonical_rebuild(self, &rebuilt)
859    }
860}
861
862// Keep the public proposal contract exact even when fragments are constructed
863// without the source macro. Numeric generation is unsigned identity
864// generation and therefore belongs only to one non-null scalar primary key.
865fn validate_insert_generation(
866    fields: &[FieldFragment],
867    primary_key: &[FieldSourceKey],
868) -> Result<(), SchemaContractError> {
869    for field in fields {
870        if !matches!(field.insert_policy(), FieldInsertPolicy::Generated) {
871            continue;
872        }
873        if field.nullable() || field.management().is_some() {
874            return Err(SchemaContractError::InvalidFieldPolicy);
875        }
876        match field.field_type() {
877            FieldType::Scalar(ScalarType::Ulid | ScalarType::Timestamp) => {}
878            FieldType::Scalar(
879                ScalarType::Nat8
880                | ScalarType::Nat16
881                | ScalarType::Nat32
882                | ScalarType::Nat64
883                | ScalarType::Nat128,
884            ) if primary_key.len() == 1 && primary_key.first() == Some(field.source_key()) => {}
885            FieldType::Scalar(_) | FieldType::List(_) | FieldType::Named(_) => {
886                return Err(SchemaContractError::InvalidFieldPolicy);
887            }
888        }
889    }
890    Ok(())
891}
892
893/// One structural field in a named record type.
894///
895/// Composite fields carry exact type and nullability facts only. Insert,
896/// generation, and management policies belong to persisted entity fields.
897#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
898pub struct RecordFieldFragment {
899    source_key: FieldSourceKey,
900    name: SchemaName,
901    field_type: FieldType,
902    nullable: bool,
903}
904
905impl RecordFieldFragment {
906    /// Construct one exact structural record field.
907    #[must_use]
908    pub fn new(name: SchemaName, field_type: FieldType, nullable: bool) -> Self {
909        Self {
910            source_key: FieldSourceKey::from_name(&name),
911            name,
912            field_type,
913            nullable,
914        }
915    }
916
917    /// Borrow the typed proposal key derived from the current field name.
918    #[must_use]
919    pub const fn source_key(&self) -> &FieldSourceKey {
920        &self.source_key
921    }
922
923    /// Borrow the current record-field name.
924    #[must_use]
925    pub const fn name(&self) -> &SchemaName {
926        &self.name
927    }
928
929    /// Borrow the exact logical field type.
930    #[must_use]
931    pub const fn field_type(&self) -> &FieldType {
932        &self.field_type
933    }
934
935    /// Return whether the structural field admits null.
936    #[must_use]
937    pub const fn nullable(&self) -> bool {
938        self.nullable
939    }
940
941    fn validate(&self) -> Result<(), SchemaContractError> {
942        if !current_name_key_matches(self.source_key.as_str(), &self.name) {
943            return Err(SchemaContractError::NonCanonical);
944        }
945        self.field_type.validate()
946    }
947}
948
949/// One positional tuple member and its explicit-null policy.
950
951#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
952pub struct TupleElementFragment {
953    field_type: FieldType,
954    nullable: bool,
955}
956
957impl TupleElementFragment {
958    /// Construct one exact tuple-member contract.
959    #[must_use]
960    pub const fn new(field_type: FieldType, nullable: bool) -> Self {
961        Self {
962            field_type,
963            nullable,
964        }
965    }
966
967    /// Borrow the exact logical member type.
968    #[must_use]
969    pub const fn field_type(&self) -> &FieldType {
970        &self.field_type
971    }
972
973    /// Return whether this tuple member admits explicit null.
974    #[must_use]
975    pub const fn nullable(&self) -> bool {
976        self.nullable
977    }
978
979    const fn validate(&self) -> Result<(), SchemaContractError> {
980        self.field_type.validate()
981    }
982}
983
984/// Named record-type definition.
985#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
986pub struct RecordTypeFragment {
987    source_key: TypeSourceKey,
988    name: SchemaName,
989    fields: Vec<RecordFieldFragment>,
990}
991
992impl RecordTypeFragment {
993    /// Construct and canonicalize a record definition.
994    ///
995    /// # Errors
996    ///
997    /// Returns a typed contract error for overflow, duplicates, or malformed
998    /// fields.
999    pub fn try_new(
1000        name: SchemaName,
1001        mut fields: Vec<RecordFieldFragment>,
1002    ) -> Result<Self, SchemaContractError> {
1003        check_len("record fields", fields.len(), MAX_FRAGMENT_FIELDS)?;
1004        fields.sort_unstable_by(|left, right| left.source_key.cmp(&right.source_key));
1005        ensure_unique_sorted_by(&fields, RecordFieldFragment::source_key)?;
1006        ensure_unique_names(fields.iter().map(RecordFieldFragment::name))?;
1007        for field in &fields {
1008            field.validate()?;
1009        }
1010        Ok(Self {
1011            source_key: TypeSourceKey::from_name(&name),
1012            name,
1013            fields,
1014        })
1015    }
1016
1017    /// Borrow the typed proposal key derived from the current record name.
1018    #[must_use]
1019    pub const fn source_key(&self) -> &TypeSourceKey {
1020        &self.source_key
1021    }
1022
1023    /// Borrow the current record name.
1024    #[must_use]
1025    pub const fn name(&self) -> &SchemaName {
1026        &self.name
1027    }
1028
1029    /// Borrow canonical record fields.
1030    #[must_use]
1031    pub fn fields(&self) -> &[RecordFieldFragment] {
1032        &self.fields
1033    }
1034
1035    fn validate(&self) -> Result<(), SchemaContractError> {
1036        let rebuilt = Self::try_new(self.name.clone(), self.fields.clone())?;
1037        if rebuilt != *self {
1038            return Err(SchemaContractError::NonCanonical);
1039        }
1040        Ok(())
1041    }
1042}
1043
1044/// One named enum variant.
1045#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1046pub struct EnumVariantFragment {
1047    source_key: TypeSourceKey,
1048    name: SchemaName,
1049    payload: Option<FieldType>,
1050}
1051
1052impl EnumVariantFragment {
1053    /// Construct one unit variant.
1054    #[must_use]
1055    pub fn new(name: SchemaName) -> Self {
1056        Self {
1057            source_key: TypeSourceKey::from_name(&name),
1058            name,
1059            payload: None,
1060        }
1061    }
1062
1063    /// Construct one payload-bearing variant.
1064    #[must_use]
1065    pub fn with_payload(name: SchemaName, payload: FieldType) -> Self {
1066        Self {
1067            source_key: TypeSourceKey::from_name(&name),
1068            name,
1069            payload: Some(payload),
1070        }
1071    }
1072
1073    /// Borrow the typed proposal key derived from the current variant name.
1074    #[must_use]
1075    pub const fn source_key(&self) -> &TypeSourceKey {
1076        &self.source_key
1077    }
1078
1079    /// Borrow the current variant name.
1080    #[must_use]
1081    pub const fn name(&self) -> &SchemaName {
1082        &self.name
1083    }
1084
1085    /// Borrow the optional exact payload contract.
1086    #[must_use]
1087    pub const fn payload(&self) -> Option<&FieldType> {
1088        self.payload.as_ref()
1089    }
1090
1091    fn validate(&self) -> Result<(), SchemaContractError> {
1092        if !current_name_key_matches(self.source_key.as_str(), &self.name) {
1093            return Err(SchemaContractError::NonCanonical);
1094        }
1095        match &self.payload {
1096            Some(payload) => payload.validate(),
1097            None => Ok(()),
1098        }
1099    }
1100}
1101
1102/// Named enum-type definition.
1103#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1104pub struct EnumTypeFragment {
1105    source_key: TypeSourceKey,
1106    name: SchemaName,
1107    variants: Vec<EnumVariantFragment>,
1108}
1109
1110impl EnumTypeFragment {
1111    /// Construct and canonicalize an enum definition.
1112    ///
1113    /// # Errors
1114    ///
1115    /// Returns a typed contract error for empty, oversized, or duplicate
1116    /// variants.
1117    pub fn try_new(
1118        name: SchemaName,
1119        mut variants: Vec<EnumVariantFragment>,
1120    ) -> Result<Self, SchemaContractError> {
1121        if variants.is_empty() {
1122            return Err(SchemaContractError::InvalidReferenceList);
1123        }
1124        check_len("enum variants", variants.len(), MAX_FRAGMENT_FIELDS)?;
1125        variants.sort_unstable_by(|left, right| left.source_key.cmp(&right.source_key));
1126        ensure_unique_sorted_by(&variants, |variant| &variant.source_key)?;
1127        ensure_unique_names(variants.iter().map(EnumVariantFragment::name))?;
1128        for variant in &variants {
1129            variant.validate()?;
1130        }
1131        Ok(Self {
1132            source_key: TypeSourceKey::from_name(&name),
1133            name,
1134            variants,
1135        })
1136    }
1137
1138    /// Borrow the typed proposal key derived from the current enum name.
1139    #[must_use]
1140    pub const fn source_key(&self) -> &TypeSourceKey {
1141        &self.source_key
1142    }
1143
1144    /// Borrow the current enum name.
1145    #[must_use]
1146    pub const fn name(&self) -> &SchemaName {
1147        &self.name
1148    }
1149
1150    /// Borrow canonical enum variants.
1151    #[must_use]
1152    pub fn variants(&self) -> &[EnumVariantFragment] {
1153        &self.variants
1154    }
1155
1156    fn validate(&self) -> Result<(), SchemaContractError> {
1157        let rebuilt = Self::try_new(self.name.clone(), self.variants.clone())?;
1158        if rebuilt != *self {
1159            return Err(SchemaContractError::NonCanonical);
1160        }
1161        Ok(())
1162    }
1163}
1164
1165/// Named reusable type definition.
1166#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1167pub enum NamedTypeFragment {
1168    /// Record type.
1169    Record(RecordTypeFragment),
1170    /// Enum type.
1171    Enum(EnumTypeFragment),
1172    /// Transparent named wrapper.
1173    Newtype {
1174        /// Typed proposal key derived from the current name.
1175        source_key: TypeSourceKey,
1176        /// Current schema name.
1177        name: SchemaName,
1178        /// Wrapped logical type.
1179        inner: FieldType,
1180    },
1181    /// Homogeneous ordered collection.
1182    List {
1183        /// Typed proposal key derived from the current name.
1184        source_key: TypeSourceKey,
1185        /// Current schema name.
1186        name: SchemaName,
1187        /// Element type.
1188        item: FieldType,
1189    },
1190    /// Homogeneous unique collection.
1191    Set {
1192        /// Typed proposal key derived from the current name.
1193        source_key: TypeSourceKey,
1194        /// Current schema name.
1195        name: SchemaName,
1196        /// Element type.
1197        item: FieldType,
1198    },
1199    /// Homogeneous key/value collection.
1200    Map {
1201        /// Typed proposal key derived from the current name.
1202        source_key: TypeSourceKey,
1203        /// Current schema name.
1204        name: SchemaName,
1205        /// Key type.
1206        key: FieldType,
1207        /// Value type.
1208        value: FieldType,
1209    },
1210    /// Ordered heterogeneous product.
1211    Tuple {
1212        /// Typed proposal key derived from the current name.
1213        source_key: TypeSourceKey,
1214        /// Current schema name.
1215        name: SchemaName,
1216        /// Ordered member types.
1217        members: Vec<TupleElementFragment>,
1218    },
1219}
1220
1221impl NamedTypeFragment {
1222    /// Construct one transparent named wrapper from its current name.
1223    #[must_use]
1224    pub fn newtype(name: SchemaName, inner: FieldType) -> Self {
1225        Self::Newtype {
1226            source_key: TypeSourceKey::from_name(&name),
1227            name,
1228            inner,
1229        }
1230    }
1231
1232    /// Construct one named ordered collection from its current name.
1233    #[must_use]
1234    pub fn list(name: SchemaName, item: FieldType) -> Self {
1235        Self::List {
1236            source_key: TypeSourceKey::from_name(&name),
1237            name,
1238            item,
1239        }
1240    }
1241
1242    /// Construct one named unique collection from its current name.
1243    #[must_use]
1244    pub fn set(name: SchemaName, item: FieldType) -> Self {
1245        Self::Set {
1246            source_key: TypeSourceKey::from_name(&name),
1247            name,
1248            item,
1249        }
1250    }
1251
1252    /// Construct one named key/value collection from its current name.
1253    #[must_use]
1254    pub fn map(name: SchemaName, key: FieldType, value: FieldType) -> Self {
1255        Self::Map {
1256            source_key: TypeSourceKey::from_name(&name),
1257            name,
1258            key,
1259            value,
1260        }
1261    }
1262
1263    /// Construct one named heterogeneous product from its current name.
1264    #[must_use]
1265    pub fn tuple(name: SchemaName, members: Vec<TupleElementFragment>) -> Self {
1266        Self::Tuple {
1267            source_key: TypeSourceKey::from_name(&name),
1268            name,
1269            members,
1270        }
1271    }
1272
1273    /// Borrow the typed proposal key derived from the current type name.
1274    #[must_use]
1275    pub const fn source_key(&self) -> &TypeSourceKey {
1276        match self {
1277            Self::Record(record) => record.source_key(),
1278            Self::Enum(r#enum) => r#enum.source_key(),
1279            Self::Newtype { source_key, .. }
1280            | Self::List { source_key, .. }
1281            | Self::Set { source_key, .. }
1282            | Self::Map { source_key, .. }
1283            | Self::Tuple { source_key, .. } => source_key,
1284        }
1285    }
1286
1287    /// Borrow the current type name.
1288    #[must_use]
1289    pub const fn name(&self) -> &SchemaName {
1290        match self {
1291            Self::Record(record) => record.name(),
1292            Self::Enum(r#enum) => r#enum.name(),
1293            Self::Newtype { name, .. }
1294            | Self::List { name, .. }
1295            | Self::Set { name, .. }
1296            | Self::Map { name, .. }
1297            | Self::Tuple { name, .. } => name,
1298        }
1299    }
1300
1301    fn validate(&self) -> Result<(), SchemaContractError> {
1302        ensure_current_name_key(self.source_key().as_str(), self.name())?;
1303        match self {
1304            Self::Record(record) => record.validate(),
1305            Self::Enum(r#enum) => r#enum.validate(),
1306            Self::Newtype { inner, .. }
1307            | Self::List { item: inner, .. }
1308            | Self::Set { item: inner, .. } => inner.validate(),
1309            Self::Map { key, value, .. } => {
1310                key.validate()?;
1311                value.validate()
1312            }
1313            Self::Tuple { members, .. } => {
1314                if members.is_empty() {
1315                    return Err(SchemaContractError::InvalidReferenceList);
1316                }
1317                check_len("tuple members", members.len(), MAX_FRAGMENT_FIELDS)?;
1318                members.iter().try_for_each(TupleElementFragment::validate)
1319            }
1320        }
1321    }
1322}
1323
1324/// Reusable store-free collection of entity and type definitions.
1325#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1326pub struct SchemaFragment {
1327    entities: Vec<EntityFragment>,
1328    types: Vec<NamedTypeFragment>,
1329}
1330
1331impl SchemaFragment {
1332    /// Construct and canonicalize one reusable fragment.
1333    ///
1334    /// # Errors
1335    ///
1336    /// Returns a typed contract error for overflow, duplicate definitions, or
1337    /// malformed nested definitions.
1338    pub fn try_new(
1339        mut entities: Vec<EntityFragment>,
1340        mut types: Vec<NamedTypeFragment>,
1341    ) -> Result<Self, SchemaContractError> {
1342        check_len("fragment entities", entities.len(), MAX_FRAGMENT_ENTITIES)?;
1343        check_len("fragment types", types.len(), MAX_FRAGMENT_TYPES)?;
1344        entities.sort_unstable_by(|left, right| left.source_key.cmp(&right.source_key));
1345        types.sort_unstable_by(|left, right| left.source_key().cmp(right.source_key()));
1346        ensure_unique_sorted_by(&entities, EntityFragment::source_key)?;
1347        ensure_unique_sorted_by(&types, NamedTypeFragment::source_key)?;
1348        ensure_unique_names(entities.iter().map(EntityFragment::name))?;
1349        ensure_unique_names(types.iter().map(NamedTypeFragment::name))?;
1350        for entity in &entities {
1351            entity.validate()?;
1352        }
1353        for r#type in &types {
1354            r#type.validate()?;
1355        }
1356        Ok(Self { entities, types })
1357    }
1358
1359    /// Borrow entity definitions.
1360    #[must_use]
1361    pub fn entities(&self) -> &[EntityFragment] {
1362        &self.entities
1363    }
1364
1365    /// Borrow named type definitions.
1366    #[must_use]
1367    pub fn types(&self) -> &[NamedTypeFragment] {
1368        &self.types
1369    }
1370
1371    pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
1372        for r#type in &self.types {
1373            r#type.validate()?;
1374        }
1375        let rebuilt = Self::try_new(self.entities.clone(), self.types.clone())?;
1376        if rebuilt != *self {
1377            return Err(SchemaContractError::NonCanonical);
1378        }
1379        Ok(())
1380    }
1381}
1382
1383pub(crate) const fn check_len(
1384    kind: &'static str,
1385    len: usize,
1386    max: usize,
1387) -> Result<(), SchemaContractError> {
1388    if len > max {
1389        return Err(SchemaContractError::TooManyItems { kind, len, max });
1390    }
1391    Ok(())
1392}
1393
1394fn current_name_key_matches(source_key: &str, name: &SchemaName) -> bool {
1395    source_key == name.as_str()
1396}
1397
1398fn ensure_current_name_key(source_key: &str, name: &SchemaName) -> Result<(), SchemaContractError> {
1399    if !current_name_key_matches(source_key, name) {
1400        return Err(SchemaContractError::NonCanonical);
1401    }
1402    Ok(())
1403}
1404
1405fn ensure_canonical_rebuild<T: PartialEq>(
1406    current: &T,
1407    rebuilt: &T,
1408) -> Result<(), SchemaContractError> {
1409    if current != rebuilt {
1410        return Err(SchemaContractError::NonCanonical);
1411    }
1412    Ok(())
1413}
1414
1415fn ensure_unique<T>(values: &[T]) -> Result<(), SchemaContractError>
1416where
1417    T: Ord,
1418{
1419    let mut seen = BTreeSet::new();
1420    if values.iter().any(|value| !seen.insert(value)) {
1421        return Err(SchemaContractError::InvalidReferenceList);
1422    }
1423    Ok(())
1424}
1425
1426fn ensure_unique_sorted_by<T, K>(
1427    values: &[T],
1428    key: impl Fn(&T) -> &K,
1429) -> Result<(), SchemaContractError>
1430where
1431    K: Eq,
1432{
1433    if values.windows(2).any(|pair| key(&pair[0]) == key(&pair[1])) {
1434        return Err(SchemaContractError::DuplicateSourceKey);
1435    }
1436    Ok(())
1437}
1438
1439fn ensure_unique_names<'a>(
1440    names: impl IntoIterator<Item = &'a SchemaName>,
1441) -> Result<(), SchemaContractError> {
1442    let mut seen = BTreeSet::new();
1443    if names.into_iter().any(|name| !seen.insert(name)) {
1444        return Err(SchemaContractError::DuplicateName);
1445    }
1446    Ok(())
1447}
1448
1449fn validate_management_cardinality(fields: &[FieldFragment]) -> Result<(), SchemaContractError> {
1450    for policy in [
1451        FieldManagementPolicy::CreatedAt,
1452        FieldManagementPolicy::UpdatedAt,
1453    ] {
1454        if fields
1455            .iter()
1456            .filter(|field| field.management() == Some(policy))
1457            .count()
1458            > 1
1459        {
1460            return Err(SchemaContractError::InvalidFieldPolicy);
1461        }
1462    }
1463    Ok(())
1464}
1465
1466fn decimal_fits_scale(value: Decimal, scale: u32) -> bool {
1467    match value.scale().cmp(&scale) {
1468        std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => true,
1469        std::cmp::Ordering::Less => value.scale_to_integer(scale).is_some(),
1470    }
1471}
1472
1473#[cfg(test)]
1474mod tests {
1475    use super::{
1476        FieldFragment, FieldInsertPolicy, FieldSourceKey, FieldType, ScalarType,
1477        SchemaContractError, SchemaName,
1478    };
1479
1480    #[test]
1481    fn independently_decoded_field_key_and_name_must_match() {
1482        let field = FieldFragment {
1483            source_key: FieldSourceKey::try_new("legacy_name").expect("fixture key should admit"),
1484            name: SchemaName::try_new("current_name").expect("fixture name should admit"),
1485            field_type: FieldType::Scalar(ScalarType::Nat64),
1486            nullable: false,
1487            insert_policy: FieldInsertPolicy::Required,
1488            management: None,
1489        };
1490
1491        assert_eq!(field.validate(), Err(SchemaContractError::NonCanonical));
1492    }
1493}