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 contract error for an engine-reserved name or an empty key.
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 crate::is_engine_reserved_schema_name(name.as_str()) {
376            return Err(SchemaContractError::InvalidSourceKey);
377        }
378        if key.is_empty() {
379            return Err(SchemaContractError::InvalidReferenceList);
380        }
381        if let Some(predicate) = &predicate {
382            predicate.validate()?;
383        }
384        Ok(Self {
385            source_key: IndexSourceKey::from_name(&name),
386            name,
387            key,
388            unique,
389            predicate,
390        })
391    }
392
393    /// Borrow the typed proposal key derived from the current index name.
394    #[must_use]
395    pub const fn source_key(&self) -> &IndexSourceKey {
396        &self.source_key
397    }
398
399    /// Borrow the current index name.
400    #[must_use]
401    pub const fn name(&self) -> &SchemaName {
402        &self.name
403    }
404
405    /// Borrow ordered index key components.
406    #[must_use]
407    pub fn key(&self) -> &[IndexKeyFragment] {
408        &self.key
409    }
410
411    /// Return whether the index enforces uniqueness.
412    #[must_use]
413    pub const fn unique(&self) -> bool {
414        self.unique
415    }
416
417    /// Borrow the optional source predicate.
418    #[must_use]
419    pub const fn predicate(&self) -> Option<&SourceCheckExpr> {
420        self.predicate.as_ref()
421    }
422
423    fn validate(&self) -> Result<(), SchemaContractError> {
424        let rebuilt = Self::try_new(
425            self.name.clone(),
426            self.key.clone(),
427            self.unique,
428            self.predicate.clone(),
429        )?;
430        ensure_canonical_rebuild(self, &rebuilt)
431    }
432}
433
434/// Maintained referential action in the current proposal contract.
435#[derive(Clone, Copy, Debug, Eq, PartialEq)]
436pub enum RelationDeleteAction {
437    /// Reject deletion while a source row refers to the target.
438    Restrict,
439}
440
441/// One deterministic step below an entity-root relation field.
442#[derive(Clone, Debug, Eq, PartialEq)]
443pub enum RelationPathStepFragment {
444    /// Enter one accepted named record, enum, or transparent newtype.
445    EnterNamed { r#type: TypeSourceKey },
446    /// Continue through a present optional value; `NULL` means no target.
447    OptionalSome,
448    /// Select one accepted member of an accepted record.
449    RecordMember {
450        /// Record declaration containing the member.
451        record: TypeSourceKey,
452        /// Stable proposal key for the member.
453        field: FieldSourceKey,
454    },
455    /// Select one payload-bearing accepted enum variant.
456    EnumVariantPayload {
457        /// Enum declaration containing the variant.
458        r#enum: TypeSourceKey,
459        /// Stable proposal key for the variant.
460        variant: TypeSourceKey,
461    },
462    /// Traverse every item of an accepted ordered collection.
463    ListItems,
464    /// Traverse every item of an accepted unique collection.
465    SetItems,
466    /// Traverse every value of an accepted map in canonical key order.
467    MapValues,
468}
469
470/// Current source program for one relation edge.
471#[derive(Clone, Debug, Eq, PartialEq)]
472pub enum RelationSourceFragment {
473    /// Ordered entity-root fields for the direct relation fast path.
474    Direct { fields: Vec<FieldSourceKey> },
475    /// One deterministic path from an entity-root field to scalar targets.
476    Nested {
477        /// Entity-root field that owns the finite nested value.
478        root: FieldSourceKey,
479        /// Complete ordered traversal program below `root`.
480        steps: Vec<RelationPathStepFragment>,
481    },
482}
483
484impl RelationSourceFragment {
485    /// Construct a direct relation source from ordered root fields.
486    #[must_use]
487    pub const fn direct(fields: Vec<FieldSourceKey>) -> Self {
488        Self::Direct { fields }
489    }
490
491    /// Borrow every entity-root field owned by this relation source.
492    pub fn root_fields(&self) -> impl Iterator<Item = &FieldSourceKey> {
493        let direct = match self {
494            Self::Direct { fields } => fields.as_slice(),
495            Self::Nested { .. } => &[],
496        };
497        let nested = match self {
498            Self::Nested { root, .. } => Some(root),
499            Self::Direct { .. } => None,
500        };
501        direct.iter().chain(nested)
502    }
503
504    fn validate(&self, target_field_count: usize) -> Result<(), SchemaContractError> {
505        match self {
506            Self::Direct { fields } => {
507                if fields.is_empty() || fields.len() != target_field_count {
508                    return Err(SchemaContractError::InvalidReferenceList);
509                }
510                ensure_unique(fields)
511            }
512            Self::Nested { steps, .. } => {
513                if steps.is_empty()
514                    || steps.len() > MAX_RELATION_PATH_STEPS
515                    || target_field_count != 1
516                {
517                    return Err(SchemaContractError::InvalidReferenceList);
518                }
519                Ok(())
520            }
521        }
522    }
523}
524
525/// One source-owned relation definition.
526#[derive(Clone, Debug, Eq, PartialEq)]
527pub struct RelationFragment {
528    source_key: RelationSourceKey,
529    name: SchemaName,
530    source: RelationSourceFragment,
531    target_entity: EntitySourceKey,
532    target_fields: Vec<FieldSourceKey>,
533    on_delete: RelationDeleteAction,
534}
535
536impl RelationFragment {
537    /// Construct one relation with one bounded source program and ordered target components.
538    ///
539    /// # Errors
540    ///
541    /// Returns a typed reference-list error for empty or arity-mismatched
542    /// components.
543    pub fn try_new(
544        name: SchemaName,
545        source: RelationSourceFragment,
546        target_entity: EntitySourceKey,
547        target_fields: Vec<FieldSourceKey>,
548        on_delete: RelationDeleteAction,
549    ) -> Result<Self, SchemaContractError> {
550        if target_fields.is_empty() {
551            return Err(SchemaContractError::InvalidReferenceList);
552        }
553        source.validate(target_fields.len())?;
554        ensure_unique(&target_fields)?;
555        Ok(Self {
556            source_key: RelationSourceKey::from_name(&name),
557            name,
558            source,
559            target_entity,
560            target_fields,
561            on_delete,
562        })
563    }
564
565    /// Borrow the typed proposal key derived from the current relation name.
566    #[must_use]
567    pub const fn source_key(&self) -> &RelationSourceKey {
568        &self.source_key
569    }
570
571    /// Borrow the current relation name.
572    #[must_use]
573    pub const fn name(&self) -> &SchemaName {
574        &self.name
575    }
576
577    /// Borrow the complete current relation source program.
578    #[must_use]
579    pub const fn source(&self) -> &RelationSourceFragment {
580        &self.source
581    }
582
583    /// Borrow the target entity source key.
584    #[must_use]
585    pub const fn target_entity(&self) -> &EntitySourceKey {
586        &self.target_entity
587    }
588
589    /// Borrow ordered target fields.
590    #[must_use]
591    pub fn target_fields(&self) -> &[FieldSourceKey] {
592        &self.target_fields
593    }
594
595    /// Return the maintained delete action.
596    #[must_use]
597    pub const fn on_delete(&self) -> RelationDeleteAction {
598        self.on_delete
599    }
600
601    fn validate(&self) -> Result<(), SchemaContractError> {
602        let rebuilt = Self::try_new(
603            self.name.clone(),
604            self.source.clone(),
605            self.target_entity.clone(),
606            self.target_fields.clone(),
607            self.on_delete,
608        )?;
609        ensure_canonical_rebuild(self, &rebuilt)
610    }
611}
612
613/// One source constraint kind.
614#[derive(Clone, Debug, Eq, PartialEq)]
615pub enum ConstraintFragmentKind {
616    /// General row check over top-level source fields.
617    Check(SourceCheckExpr),
618    /// Closed durable operation over one nominal target below a persisted root.
619    TargetedRule(TargetedRuleFragment),
620}
621
622impl ConstraintFragmentKind {
623    fn validate(&self) -> Result<(), SchemaContractError> {
624        match self {
625            Self::Check(expression) => expression.validate(),
626            Self::TargetedRule(rule) => rule.validate(),
627        }
628    }
629}
630
631/// One source-bound nominal durable-rule target.
632#[derive(Clone, Debug, Eq, PartialEq)]
633pub struct TargetedRuleFragment {
634    root: FieldSourceKey,
635    target_type: TypeSourceKey,
636    rule: RuleSourceKey,
637    operation: SourceRuleOperation,
638}
639
640impl TargetedRuleFragment {
641    /// Construct one targeted rule below a persisted root field.
642    #[must_use]
643    pub fn new(
644        root: FieldSourceKey,
645        target_type: TypeSourceKey,
646        rule: SchemaName,
647        operation: SourceRuleOperation,
648    ) -> Self {
649        Self {
650            root,
651            target_type,
652            rule: RuleSourceKey::from_name(&rule),
653            operation,
654        }
655    }
656
657    /// Borrow the persisted root-field identity.
658    #[must_use]
659    pub const fn root(&self) -> &FieldSourceKey {
660        &self.root
661    }
662
663    /// Borrow the ruled nominal type identity.
664    #[must_use]
665    pub const fn target_type(&self) -> &TypeSourceKey {
666        &self.target_type
667    }
668
669    /// Borrow the current durable-rule name as a typed proposal key.
670    #[must_use]
671    pub const fn rule(&self) -> &RuleSourceKey {
672        &self.rule
673    }
674
675    /// Borrow the closed durable operation.
676    #[must_use]
677    pub const fn operation(&self) -> &SourceRuleOperation {
678        &self.operation
679    }
680
681    fn validate(&self) -> Result<(), SchemaContractError> {
682        self.operation.validate()
683    }
684}
685
686/// One source constraint declaration.
687#[derive(Clone, Debug, Eq, PartialEq)]
688pub struct ConstraintFragment {
689    source_key: ConstraintSourceKey,
690    name: SchemaName,
691    kind: ConstraintFragmentKind,
692}
693
694impl ConstraintFragment {
695    /// Construct one general source check.
696    #[must_use]
697    pub fn check(name: SchemaName, expression: SourceCheckExpr) -> Self {
698        Self {
699            source_key: ConstraintSourceKey::from_name(&name),
700            name,
701            kind: ConstraintFragmentKind::Check(expression),
702        }
703    }
704
705    /// Construct one source-bound targeted durable rule.
706    #[must_use]
707    pub fn targeted_rule(rule: TargetedRuleFragment) -> Self {
708        let source_key = ConstraintSourceKey::for_targeted_field_rule(
709            rule.root(),
710            rule.target_type(),
711            rule.rule(),
712        );
713        let name = SchemaName::for_targeted_rule(&source_key);
714        Self {
715            source_key,
716            name,
717            kind: ConstraintFragmentKind::TargetedRule(rule),
718        }
719    }
720
721    /// Borrow the typed proposal key derived from the current declaration.
722    #[must_use]
723    pub const fn source_key(&self) -> &ConstraintSourceKey {
724        &self.source_key
725    }
726
727    /// Borrow the current constraint name.
728    #[must_use]
729    pub const fn name(&self) -> &SchemaName {
730        &self.name
731    }
732
733    /// Borrow the exact source constraint kind.
734    #[must_use]
735    pub const fn kind(&self) -> &ConstraintFragmentKind {
736        &self.kind
737    }
738
739    fn validate(&self) -> Result<(), SchemaContractError> {
740        self.kind.validate()?;
741        let rebuilt = match &self.kind {
742            ConstraintFragmentKind::Check(expression) => {
743                if crate::is_engine_reserved_schema_name(self.name.as_str()) {
744                    return Err(SchemaContractError::InvalidSourceKey);
745                }
746                Self::check(self.name.clone(), expression.clone())
747            }
748            ConstraintFragmentKind::TargetedRule(rule) => Self::targeted_rule(rule.clone()),
749        };
750        ensure_canonical_rebuild(self, &rebuilt)
751    }
752}
753
754/// Store-free logical entity definition.
755#[derive(Clone, Debug, Eq, PartialEq)]
756pub struct EntityFragment {
757    source_key: EntitySourceKey,
758    name: SchemaName,
759    version: DeclaredEntityVersion,
760    fields: Vec<FieldFragment>,
761    primary_key: Vec<FieldSourceKey>,
762    indexes: Vec<IndexFragment>,
763    relations: Vec<RelationFragment>,
764    constraints: Vec<ConstraintFragment>,
765}
766
767impl EntityFragment {
768    /// Construct and canonicalize one entity definition.
769    ///
770    /// # Errors
771    ///
772    /// Returns a typed contract error for collection overflow, duplicate
773    /// source keys, malformed policy, expression, or primary-key references.
774    /// Primary keys require non-null scalar key fields; Unit is valid only as
775    /// the sole primary-key field.
776    pub fn try_new(
777        name: SchemaName,
778        version: DeclaredEntityVersion,
779        mut fields: Vec<FieldFragment>,
780        primary_key: Vec<FieldSourceKey>,
781        mut indexes: Vec<IndexFragment>,
782        mut relations: Vec<RelationFragment>,
783        mut constraints: Vec<ConstraintFragment>,
784    ) -> Result<Self, SchemaContractError> {
785        let source_key = EntitySourceKey::from_name(&name);
786        check_len("entity fields", fields.len(), MAX_FRAGMENT_FIELDS)?;
787        check_len("entity indexes", indexes.len(), MAX_FRAGMENT_INDEXES)?;
788        check_len("entity relations", relations.len(), MAX_FRAGMENT_RELATIONS)?;
789        check_len(
790            "entity constraints",
791            constraints.len(),
792            MAX_FRAGMENT_CONSTRAINTS,
793        )?;
794        if primary_key.is_empty() {
795            return Err(SchemaContractError::InvalidReferenceList);
796        }
797        ensure_unique(&primary_key)?;
798        // Equal source keys are rejected below, so stable tie ordering cannot
799        // be observed and need not retain stable-sort machinery in Wasm.
800        crate::compact_sort_unstable_by(&mut fields, |a, b| a.source_key.cmp(&b.source_key));
801        crate::compact_sort_unstable_by(&mut indexes, |a, b| a.source_key.cmp(&b.source_key));
802        crate::compact_sort_unstable_by(&mut relations, |a, b| a.source_key.cmp(&b.source_key));
803        crate::compact_sort_unstable_by(&mut constraints, |a, b| a.source_key.cmp(&b.source_key));
804        ensure_unique_sorted_by(&fields, FieldFragment::source_key)?;
805        ensure_unique_sorted_by(&indexes, IndexFragment::source_key)?;
806        ensure_unique_sorted_by(&relations, RelationFragment::source_key)?;
807        ensure_unique_sorted_by(&constraints, ConstraintFragment::source_key)?;
808        ensure_unique_names(fields.iter().map(FieldFragment::name))?;
809        ensure_unique_names(indexes.iter().map(IndexFragment::name))?;
810        ensure_unique_names(relations.iter().map(RelationFragment::name))?;
811        ensure_unique_names(constraints.iter().map(ConstraintFragment::name))?;
812        for field in &fields {
813            field.validate()?;
814        }
815        validate_management_cardinality(&fields)?;
816        for index in &indexes {
817            index.validate()?;
818        }
819        for relation in &relations {
820            relation.validate()?;
821        }
822        for constraint in &constraints {
823            constraint.validate()?;
824        }
825        let field_keys = fields
826            .iter()
827            .map(|field| field.source_key.clone())
828            .collect::<BTreeSet<_>>();
829        if primary_key.iter().any(|field| !field_keys.contains(field)) {
830            return Err(SchemaContractError::InvalidLocalReference);
831        }
832        validate_insert_generation(&fields, &primary_key)?;
833        validate_primary_key_field_shapes(&fields, &primary_key)?;
834        for index in &indexes {
835            if index
836                .key()
837                .iter()
838                .any(|component| !field_keys.contains(component.field()))
839                || index.predicate().is_some_and(|predicate| {
840                    predicate
841                        .dependencies()
842                        .iter()
843                        .any(|field| !field_keys.contains(field))
844                })
845            {
846                return Err(SchemaContractError::InvalidLocalReference);
847            }
848        }
849        for relation in &relations {
850            if relation
851                .source()
852                .root_fields()
853                .any(|field| !field_keys.contains(field))
854                || (relation.target_entity() == &source_key
855                    && relation
856                        .target_fields()
857                        .iter()
858                        .any(|field| !field_keys.contains(field)))
859            {
860                return Err(SchemaContractError::InvalidLocalReference);
861            }
862        }
863        for constraint in &constraints {
864            let invalid = match constraint.kind() {
865                ConstraintFragmentKind::Check(expression) => expression
866                    .dependencies()
867                    .iter()
868                    .any(|field| !field_keys.contains(field)),
869                ConstraintFragmentKind::TargetedRule(rule) => !field_keys.contains(rule.root()),
870            };
871            if invalid {
872                return Err(SchemaContractError::InvalidLocalReference);
873            }
874        }
875        Ok(Self {
876            source_key,
877            name,
878            version,
879            fields,
880            primary_key,
881            indexes,
882            relations,
883            constraints,
884        })
885    }
886
887    /// Borrow the typed proposal key derived from the current entity name.
888    #[must_use]
889    pub const fn source_key(&self) -> &EntitySourceKey {
890        &self.source_key
891    }
892
893    /// Borrow the current entity name.
894    #[must_use]
895    pub const fn name(&self) -> &SchemaName {
896        &self.name
897    }
898
899    /// Return the application-declared current entity version.
900    #[must_use]
901    pub const fn version(&self) -> DeclaredEntityVersion {
902        self.version
903    }
904
905    /// Borrow canonical field definitions.
906    #[must_use]
907    pub fn fields(&self) -> &[FieldFragment] {
908        &self.fields
909    }
910
911    /// Borrow ordered primary-key fields.
912    #[must_use]
913    pub fn primary_key(&self) -> &[FieldSourceKey] {
914        &self.primary_key
915    }
916
917    /// Borrow canonical secondary-index definitions.
918    #[must_use]
919    pub fn indexes(&self) -> &[IndexFragment] {
920        &self.indexes
921    }
922
923    /// Borrow canonical relation definitions.
924    #[must_use]
925    pub fn relations(&self) -> &[RelationFragment] {
926        &self.relations
927    }
928
929    /// Borrow canonical source constraint definitions.
930    #[must_use]
931    pub fn constraints(&self) -> &[ConstraintFragment] {
932        &self.constraints
933    }
934
935    pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
936        let rebuilt = Self::try_new(
937            self.name.clone(),
938            self.version,
939            self.fields.clone(),
940            self.primary_key.clone(),
941            self.indexes.clone(),
942            self.relations.clone(),
943            self.constraints.clone(),
944        )?;
945        ensure_canonical_rebuild(self, &rebuilt)
946    }
947}
948
949// Share intrinsic key capabilities with generated model admission. A named
950// type or collection cannot substitute for an exact scalar key.
951fn validate_primary_key_field_shapes(
952    fields: &[FieldFragment],
953    primary_key: &[FieldSourceKey],
954) -> Result<(), SchemaContractError> {
955    if fields.iter().any(|field| {
956        primary_key.contains(&field.source_key)
957            && (field.nullable
958                || !matches!(field.field_type, FieldType::Scalar(scalar)
959                    if scalar.kind().is_primary_key_component_encodable()
960                        && (primary_key.len() == 1 || scalar != ScalarType::Unit)))
961    }) {
962        return Err(SchemaContractError::InvalidFieldPolicy);
963    }
964    Ok(())
965}
966
967// Keep the public proposal contract exact even when fragments are constructed
968// without the source macro. Numeric generation is unsigned identity
969// generation and therefore belongs only to one non-null scalar primary key.
970fn validate_insert_generation(
971    fields: &[FieldFragment],
972    primary_key: &[FieldSourceKey],
973) -> Result<(), SchemaContractError> {
974    for field in fields {
975        if !matches!(field.insert_policy(), FieldInsertPolicy::Generated) {
976            continue;
977        }
978        if field.nullable() || field.management().is_some() {
979            return Err(SchemaContractError::InvalidFieldPolicy);
980        }
981        match field.field_type() {
982            FieldType::Scalar(ScalarType::Ulid | ScalarType::Timestamp) => {}
983            FieldType::Scalar(
984                ScalarType::Nat8
985                | ScalarType::Nat16
986                | ScalarType::Nat32
987                | ScalarType::Nat64
988                | ScalarType::Nat128,
989            ) if primary_key.len() == 1 && primary_key.first() == Some(field.source_key()) => {}
990            FieldType::Scalar(_) | FieldType::List(_) | FieldType::Named(_) => {
991                return Err(SchemaContractError::InvalidFieldPolicy);
992            }
993        }
994    }
995    Ok(())
996}
997
998/// One structural field in a named record type.
999///
1000/// Composite fields carry exact type and nullability facts only. Insert,
1001/// generation, and management policies belong to persisted entity fields.
1002#[derive(Clone, Debug, Eq, PartialEq)]
1003pub struct RecordFieldFragment {
1004    source_key: FieldSourceKey,
1005    name: SchemaName,
1006    field_type: FieldType,
1007    nullable: bool,
1008}
1009
1010impl RecordFieldFragment {
1011    /// Construct one exact structural record field.
1012    #[must_use]
1013    pub fn new(name: SchemaName, field_type: FieldType, nullable: bool) -> Self {
1014        Self {
1015            source_key: FieldSourceKey::from_name(&name),
1016            name,
1017            field_type,
1018            nullable,
1019        }
1020    }
1021
1022    /// Borrow the typed proposal key derived from the current field name.
1023    #[must_use]
1024    pub const fn source_key(&self) -> &FieldSourceKey {
1025        &self.source_key
1026    }
1027
1028    /// Borrow the current record-field name.
1029    #[must_use]
1030    pub const fn name(&self) -> &SchemaName {
1031        &self.name
1032    }
1033
1034    /// Borrow the exact logical field type.
1035    #[must_use]
1036    pub const fn field_type(&self) -> &FieldType {
1037        &self.field_type
1038    }
1039
1040    /// Return whether the structural field admits null.
1041    #[must_use]
1042    pub const fn nullable(&self) -> bool {
1043        self.nullable
1044    }
1045
1046    fn validate(&self) -> Result<(), SchemaContractError> {
1047        if !current_name_key_matches(self.source_key.as_str(), &self.name) {
1048            return Err(SchemaContractError::NonCanonical);
1049        }
1050        self.field_type.validate()
1051    }
1052}
1053
1054/// One positional tuple member and its explicit-null policy.
1055
1056#[derive(Clone, Debug, Eq, PartialEq)]
1057pub struct TupleElementFragment {
1058    field_type: FieldType,
1059    nullable: bool,
1060}
1061
1062impl TupleElementFragment {
1063    /// Construct one exact tuple-member contract.
1064    #[must_use]
1065    pub const fn new(field_type: FieldType, nullable: bool) -> Self {
1066        Self {
1067            field_type,
1068            nullable,
1069        }
1070    }
1071
1072    /// Borrow the exact logical member type.
1073    #[must_use]
1074    pub const fn field_type(&self) -> &FieldType {
1075        &self.field_type
1076    }
1077
1078    /// Return whether this tuple member admits explicit null.
1079    #[must_use]
1080    pub const fn nullable(&self) -> bool {
1081        self.nullable
1082    }
1083
1084    const fn validate(&self) -> Result<(), SchemaContractError> {
1085        self.field_type.validate()
1086    }
1087}
1088
1089/// Named record-type definition.
1090#[derive(Clone, Debug, Eq, PartialEq)]
1091pub struct RecordTypeFragment {
1092    source_key: TypeSourceKey,
1093    name: SchemaName,
1094    fields: Vec<RecordFieldFragment>,
1095}
1096
1097impl RecordTypeFragment {
1098    /// Construct and canonicalize a record definition.
1099    ///
1100    /// # Errors
1101    ///
1102    /// Returns a typed contract error for overflow, duplicates, or malformed
1103    /// fields.
1104    pub fn try_new(
1105        name: SchemaName,
1106        mut fields: Vec<RecordFieldFragment>,
1107    ) -> Result<Self, SchemaContractError> {
1108        check_len("record fields", fields.len(), MAX_FRAGMENT_FIELDS)?;
1109        crate::compact_sort_unstable_by(&mut fields, |left, right| {
1110            left.source_key.cmp(&right.source_key)
1111        });
1112        ensure_unique_sorted_by(&fields, RecordFieldFragment::source_key)?;
1113        ensure_unique_names(fields.iter().map(RecordFieldFragment::name))?;
1114        for field in &fields {
1115            field.validate()?;
1116        }
1117        Ok(Self {
1118            source_key: TypeSourceKey::from_name(&name),
1119            name,
1120            fields,
1121        })
1122    }
1123
1124    /// Borrow the typed proposal key derived from the current record name.
1125    #[must_use]
1126    pub const fn source_key(&self) -> &TypeSourceKey {
1127        &self.source_key
1128    }
1129
1130    /// Borrow the current record name.
1131    #[must_use]
1132    pub const fn name(&self) -> &SchemaName {
1133        &self.name
1134    }
1135
1136    /// Borrow canonical record fields.
1137    #[must_use]
1138    pub fn fields(&self) -> &[RecordFieldFragment] {
1139        &self.fields
1140    }
1141
1142    fn validate(&self) -> Result<(), SchemaContractError> {
1143        let rebuilt = Self::try_new(self.name.clone(), self.fields.clone())?;
1144        if rebuilt != *self {
1145            return Err(SchemaContractError::NonCanonical);
1146        }
1147        Ok(())
1148    }
1149}
1150
1151/// One named enum variant.
1152#[derive(Clone, Debug, Eq, PartialEq)]
1153pub struct EnumVariantFragment {
1154    source_key: TypeSourceKey,
1155    name: SchemaName,
1156    payload: Option<FieldType>,
1157}
1158
1159impl EnumVariantFragment {
1160    /// Construct one unit variant.
1161    #[must_use]
1162    pub fn new(name: SchemaName) -> Self {
1163        Self {
1164            source_key: TypeSourceKey::from_name(&name),
1165            name,
1166            payload: None,
1167        }
1168    }
1169
1170    /// Construct one payload-bearing variant.
1171    #[must_use]
1172    pub fn with_payload(name: SchemaName, payload: FieldType) -> Self {
1173        Self {
1174            source_key: TypeSourceKey::from_name(&name),
1175            name,
1176            payload: Some(payload),
1177        }
1178    }
1179
1180    /// Borrow the typed proposal key derived from the current variant name.
1181    #[must_use]
1182    pub const fn source_key(&self) -> &TypeSourceKey {
1183        &self.source_key
1184    }
1185
1186    /// Borrow the current variant name.
1187    #[must_use]
1188    pub const fn name(&self) -> &SchemaName {
1189        &self.name
1190    }
1191
1192    /// Borrow the optional exact payload contract.
1193    #[must_use]
1194    pub const fn payload(&self) -> Option<&FieldType> {
1195        self.payload.as_ref()
1196    }
1197
1198    fn validate(&self) -> Result<(), SchemaContractError> {
1199        if !current_name_key_matches(self.source_key.as_str(), &self.name) {
1200            return Err(SchemaContractError::NonCanonical);
1201        }
1202        match &self.payload {
1203            Some(payload) => payload.validate(),
1204            None => Ok(()),
1205        }
1206    }
1207}
1208
1209/// Named enum-type definition.
1210#[derive(Clone, Debug, Eq, PartialEq)]
1211pub struct EnumTypeFragment {
1212    source_key: TypeSourceKey,
1213    name: SchemaName,
1214    variants: Vec<EnumVariantFragment>,
1215}
1216
1217impl EnumTypeFragment {
1218    /// Construct and canonicalize an enum definition.
1219    ///
1220    /// # Errors
1221    ///
1222    /// Returns a typed contract error for empty, oversized, or duplicate
1223    /// variants.
1224    pub fn try_new(
1225        name: SchemaName,
1226        mut variants: Vec<EnumVariantFragment>,
1227    ) -> Result<Self, SchemaContractError> {
1228        if variants.is_empty() {
1229            return Err(SchemaContractError::InvalidReferenceList);
1230        }
1231        check_len("enum variants", variants.len(), MAX_FRAGMENT_FIELDS)?;
1232        crate::compact_sort_unstable_by(&mut variants, |left, right| {
1233            left.source_key.cmp(&right.source_key)
1234        });
1235        ensure_unique_sorted_by(&variants, |variant| &variant.source_key)?;
1236        ensure_unique_names(variants.iter().map(EnumVariantFragment::name))?;
1237        for variant in &variants {
1238            variant.validate()?;
1239        }
1240        Ok(Self {
1241            source_key: TypeSourceKey::from_name(&name),
1242            name,
1243            variants,
1244        })
1245    }
1246
1247    /// Borrow the typed proposal key derived from the current enum name.
1248    #[must_use]
1249    pub const fn source_key(&self) -> &TypeSourceKey {
1250        &self.source_key
1251    }
1252
1253    /// Borrow the current enum name.
1254    #[must_use]
1255    pub const fn name(&self) -> &SchemaName {
1256        &self.name
1257    }
1258
1259    /// Borrow canonical enum variants.
1260    #[must_use]
1261    pub fn variants(&self) -> &[EnumVariantFragment] {
1262        &self.variants
1263    }
1264
1265    fn validate(&self) -> Result<(), SchemaContractError> {
1266        let rebuilt = Self::try_new(self.name.clone(), self.variants.clone())?;
1267        if rebuilt != *self {
1268            return Err(SchemaContractError::NonCanonical);
1269        }
1270        Ok(())
1271    }
1272}
1273
1274/// Named reusable type definition.
1275#[derive(Clone, Debug, Eq, PartialEq)]
1276pub enum NamedTypeFragment {
1277    /// Record type.
1278    Record(RecordTypeFragment),
1279    /// Enum type.
1280    Enum(EnumTypeFragment),
1281    /// Transparent named wrapper.
1282    Newtype {
1283        /// Typed proposal key derived from the current name.
1284        source_key: TypeSourceKey,
1285        /// Current schema name.
1286        name: SchemaName,
1287        /// Wrapped logical type.
1288        inner: FieldType,
1289    },
1290    /// Homogeneous ordered collection.
1291    List {
1292        /// Typed proposal key derived from the current name.
1293        source_key: TypeSourceKey,
1294        /// Current schema name.
1295        name: SchemaName,
1296        /// Element type.
1297        item: FieldType,
1298    },
1299    /// Homogeneous unique collection.
1300    Set {
1301        /// Typed proposal key derived from the current name.
1302        source_key: TypeSourceKey,
1303        /// Current schema name.
1304        name: SchemaName,
1305        /// Element type.
1306        item: FieldType,
1307    },
1308    /// Homogeneous key/value collection.
1309    Map {
1310        /// Typed proposal key derived from the current name.
1311        source_key: TypeSourceKey,
1312        /// Current schema name.
1313        name: SchemaName,
1314        /// Key type.
1315        key: FieldType,
1316        /// Value type.
1317        value: FieldType,
1318    },
1319    /// Ordered heterogeneous product.
1320    Tuple {
1321        /// Typed proposal key derived from the current name.
1322        source_key: TypeSourceKey,
1323        /// Current schema name.
1324        name: SchemaName,
1325        /// Ordered member types.
1326        members: Vec<TupleElementFragment>,
1327    },
1328}
1329
1330impl NamedTypeFragment {
1331    /// Append direct named-type dependencies in fragment order.
1332    ///
1333    /// Inline list layers are traversed, but referenced definitions are not.
1334    /// Existing entries and duplicate references are preserved so callers own
1335    /// reachability, canonicalization and accepted-catalog validation.
1336    pub fn append_named_type_dependencies(&self, pending: &mut Vec<TypeSourceKey>) {
1337        match self {
1338            Self::Record(record) => {
1339                for field in record.fields() {
1340                    field.field_type().append_named_type_dependencies(pending);
1341                }
1342            }
1343            Self::Enum(r#enum) => {
1344                for variant in r#enum.variants() {
1345                    if let Some(payload) = variant.payload() {
1346                        payload.append_named_type_dependencies(pending);
1347                    }
1348                }
1349            }
1350            Self::Newtype { inner, .. }
1351            | Self::List { item: inner, .. }
1352            | Self::Set { item: inner, .. } => {
1353                inner.append_named_type_dependencies(pending);
1354            }
1355            Self::Map { key, value, .. } => {
1356                key.append_named_type_dependencies(pending);
1357                value.append_named_type_dependencies(pending);
1358            }
1359            Self::Tuple { members, .. } => {
1360                for member in members {
1361                    member.field_type().append_named_type_dependencies(pending);
1362                }
1363            }
1364        }
1365    }
1366
1367    /// Construct one transparent named wrapper from its current name.
1368    #[must_use]
1369    pub fn newtype(name: SchemaName, inner: FieldType) -> Self {
1370        Self::Newtype {
1371            source_key: TypeSourceKey::from_name(&name),
1372            name,
1373            inner,
1374        }
1375    }
1376
1377    /// Construct one named ordered collection from its current name.
1378    #[must_use]
1379    pub fn list(name: SchemaName, item: FieldType) -> Self {
1380        Self::List {
1381            source_key: TypeSourceKey::from_name(&name),
1382            name,
1383            item,
1384        }
1385    }
1386
1387    /// Construct one named unique collection from its current name.
1388    #[must_use]
1389    pub fn set(name: SchemaName, item: FieldType) -> Self {
1390        Self::Set {
1391            source_key: TypeSourceKey::from_name(&name),
1392            name,
1393            item,
1394        }
1395    }
1396
1397    /// Construct one named key/value collection from its current name.
1398    #[must_use]
1399    pub fn map(name: SchemaName, key: FieldType, value: FieldType) -> Self {
1400        Self::Map {
1401            source_key: TypeSourceKey::from_name(&name),
1402            name,
1403            key,
1404            value,
1405        }
1406    }
1407
1408    /// Construct one named heterogeneous product from its current name.
1409    #[must_use]
1410    pub fn tuple(name: SchemaName, members: Vec<TupleElementFragment>) -> Self {
1411        Self::Tuple {
1412            source_key: TypeSourceKey::from_name(&name),
1413            name,
1414            members,
1415        }
1416    }
1417
1418    /// Borrow the typed proposal key derived from the current type name.
1419    #[must_use]
1420    pub const fn source_key(&self) -> &TypeSourceKey {
1421        match self {
1422            Self::Record(record) => record.source_key(),
1423            Self::Enum(r#enum) => r#enum.source_key(),
1424            Self::Newtype { source_key, .. }
1425            | Self::List { source_key, .. }
1426            | Self::Set { source_key, .. }
1427            | Self::Map { source_key, .. }
1428            | Self::Tuple { source_key, .. } => source_key,
1429        }
1430    }
1431
1432    /// Borrow the current type name.
1433    #[must_use]
1434    pub const fn name(&self) -> &SchemaName {
1435        match self {
1436            Self::Record(record) => record.name(),
1437            Self::Enum(r#enum) => r#enum.name(),
1438            Self::Newtype { name, .. }
1439            | Self::List { name, .. }
1440            | Self::Set { name, .. }
1441            | Self::Map { name, .. }
1442            | Self::Tuple { name, .. } => name,
1443        }
1444    }
1445
1446    fn validate(&self) -> Result<(), SchemaContractError> {
1447        ensure_current_name_key(self.source_key().as_str(), self.name())?;
1448        match self {
1449            Self::Record(record) => record.validate(),
1450            Self::Enum(r#enum) => r#enum.validate(),
1451            Self::Newtype { inner, .. }
1452            | Self::List { item: inner, .. }
1453            | Self::Set { item: inner, .. } => inner.validate(),
1454            Self::Map { key, value, .. } => {
1455                key.validate()?;
1456                value.validate()
1457            }
1458            Self::Tuple { members, .. } => {
1459                if members.is_empty() {
1460                    return Err(SchemaContractError::InvalidReferenceList);
1461                }
1462                check_len("tuple members", members.len(), MAX_FRAGMENT_FIELDS)?;
1463                members.iter().try_for_each(TupleElementFragment::validate)
1464            }
1465        }
1466    }
1467}
1468
1469/// Reusable store-free collection of entity and type definitions.
1470#[derive(Clone, Debug, Eq, PartialEq)]
1471pub struct SchemaFragment {
1472    entities: Vec<EntityFragment>,
1473    types: Vec<NamedTypeFragment>,
1474}
1475
1476impl SchemaFragment {
1477    /// Construct and canonicalize one reusable fragment.
1478    ///
1479    /// # Errors
1480    ///
1481    /// Returns a typed contract error for overflow, duplicate definitions, or
1482    /// malformed nested definitions.
1483    pub fn try_new(
1484        mut entities: Vec<EntityFragment>,
1485        mut types: Vec<NamedTypeFragment>,
1486    ) -> Result<Self, SchemaContractError> {
1487        check_len("fragment entities", entities.len(), MAX_FRAGMENT_ENTITIES)?;
1488        check_len("fragment types", types.len(), MAX_FRAGMENT_TYPES)?;
1489        crate::compact_sort_unstable_by(&mut entities, |left, right| {
1490            left.source_key.cmp(&right.source_key)
1491        });
1492        crate::compact_sort_unstable_by(&mut types, |left, right| {
1493            left.source_key().cmp(right.source_key())
1494        });
1495        ensure_unique_sorted_by(&entities, EntityFragment::source_key)?;
1496        ensure_unique_sorted_by(&types, NamedTypeFragment::source_key)?;
1497        ensure_unique_names(entities.iter().map(EntityFragment::name))?;
1498        ensure_unique_names(types.iter().map(NamedTypeFragment::name))?;
1499        for entity in &entities {
1500            entity.validate()?;
1501        }
1502        for r#type in &types {
1503            r#type.validate()?;
1504        }
1505        Ok(Self { entities, types })
1506    }
1507
1508    /// Borrow entity definitions.
1509    #[must_use]
1510    pub fn entities(&self) -> &[EntityFragment] {
1511        &self.entities
1512    }
1513
1514    /// Borrow named type definitions.
1515    #[must_use]
1516    pub fn types(&self) -> &[NamedTypeFragment] {
1517        &self.types
1518    }
1519
1520    pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
1521        for r#type in &self.types {
1522            r#type.validate()?;
1523        }
1524        let rebuilt = Self::try_new(self.entities.clone(), self.types.clone())?;
1525        if rebuilt != *self {
1526            return Err(SchemaContractError::NonCanonical);
1527        }
1528        Ok(())
1529    }
1530}
1531
1532pub(crate) const fn check_len(
1533    kind: &'static str,
1534    len: usize,
1535    max: usize,
1536) -> Result<(), SchemaContractError> {
1537    if len > max {
1538        return Err(SchemaContractError::TooManyItems { kind, len, max });
1539    }
1540    Ok(())
1541}
1542
1543fn current_name_key_matches(source_key: &str, name: &SchemaName) -> bool {
1544    source_key == name.as_str()
1545}
1546
1547fn ensure_current_name_key(source_key: &str, name: &SchemaName) -> Result<(), SchemaContractError> {
1548    if !current_name_key_matches(source_key, name) {
1549        return Err(SchemaContractError::NonCanonical);
1550    }
1551    Ok(())
1552}
1553
1554fn ensure_canonical_rebuild<T: PartialEq>(
1555    current: &T,
1556    rebuilt: &T,
1557) -> Result<(), SchemaContractError> {
1558    if current != rebuilt {
1559        return Err(SchemaContractError::NonCanonical);
1560    }
1561    Ok(())
1562}
1563
1564fn ensure_unique<T>(values: &[T]) -> Result<(), SchemaContractError>
1565where
1566    T: Ord,
1567{
1568    let mut seen = BTreeSet::new();
1569    if values.iter().any(|value| !seen.insert(value)) {
1570        return Err(SchemaContractError::InvalidReferenceList);
1571    }
1572    Ok(())
1573}
1574
1575fn ensure_unique_sorted_by<T, K>(
1576    values: &[T],
1577    key: impl Fn(&T) -> &K,
1578) -> Result<(), SchemaContractError>
1579where
1580    K: Eq,
1581{
1582    if values.windows(2).any(|pair| key(&pair[0]) == key(&pair[1])) {
1583        return Err(SchemaContractError::DuplicateSourceKey);
1584    }
1585    Ok(())
1586}
1587
1588fn ensure_unique_names<'a>(
1589    names: impl IntoIterator<Item = &'a SchemaName>,
1590) -> Result<(), SchemaContractError> {
1591    let mut seen = BTreeSet::new();
1592    if names.into_iter().any(|name| !seen.insert(name)) {
1593        return Err(SchemaContractError::DuplicateName);
1594    }
1595    Ok(())
1596}
1597
1598fn validate_management_cardinality(fields: &[FieldFragment]) -> Result<(), SchemaContractError> {
1599    for policy in [
1600        FieldManagementPolicy::CreatedAt,
1601        FieldManagementPolicy::UpdatedAt,
1602    ] {
1603        if fields
1604            .iter()
1605            .filter(|field| field.management() == Some(policy))
1606            .count()
1607            > 1
1608        {
1609            return Err(SchemaContractError::InvalidFieldPolicy);
1610        }
1611    }
1612    Ok(())
1613}
1614
1615fn decimal_fits_scale(value: Decimal, scale: u32) -> bool {
1616    match value.scale().cmp(&scale) {
1617        std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => true,
1618        std::cmp::Ordering::Less => value.scale_to_integer(scale).is_some(),
1619    }
1620}
1621
1622#[cfg(test)]
1623mod tests {
1624    use super::{
1625        FieldFragment, FieldInsertPolicy, FieldSourceKey, FieldType, ScalarType,
1626        SchemaContractError, SchemaName,
1627    };
1628
1629    #[test]
1630    fn independently_decoded_field_key_and_name_must_match() {
1631        let field = FieldFragment {
1632            source_key: FieldSourceKey::try_new("legacy_name").expect("fixture key should admit"),
1633            name: SchemaName::try_new("current_name").expect("fixture name should admit"),
1634            field_type: FieldType::Scalar(ScalarType::Nat64),
1635            nullable: false,
1636            insert_policy: FieldInsertPolicy::Required,
1637            management: None,
1638        };
1639
1640        assert_eq!(field.validate(), Err(SchemaContractError::NonCanonical));
1641    }
1642}