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