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