Skip to main content

icydb_schema/
proposal.rs

1//! Canonical database-scoped proposal envelope.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use candid::CandidType;
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8
9use crate::{
10    ConstraintFragmentKind, ConstraintSourceKey, EntityFragment, EntitySourceKey,
11    EnumVariantFragment, FieldFragment, FieldSourceKey, FieldType, IndexSourceKey,
12    MAX_SCHEMA_ASSIGNMENTS, MAX_SCHEMA_CAPABILITIES, MAX_SCHEMA_PROPOSAL_FRAGMENTS,
13    MAX_SCHEMA_REMOVALS, NamedTypeFragment, RelationFragment, RelationPathStepFragment,
14    RelationSourceFragment, RelationSourceKey, ScalarLiteral, ScalarType, SchemaContractError,
15    SchemaFragment, SchemaMigrationPlan, SchemaMigrationRename, SchemaMigrationTransform,
16    SchemaProposalDigest, SchemaSubmissionKey, SourceCheckExpr, SourceCheckInstruction,
17    SourceRuleOperation, TargetDatabaseIdentity, TargetStoreIdentity, TargetedRuleFragment,
18    TypeSourceKey, check_len, encode_schema_fragment, encode_schema_proposal,
19};
20
21/// Sole maintained proposal contract version.
22#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
23#[repr(transparent)]
24pub struct ProposalContractVersion(u16);
25
26impl ProposalContractVersion {
27    /// Current pre-1.0 hard-cut proposal contract version.
28    pub const CURRENT: Self = Self(1);
29
30    /// Construct a version token for decoding and incompatibility tests.
31    #[must_use]
32    pub const fn from_raw(value: u16) -> Self {
33        Self(value)
34    }
35
36    /// Return the raw version value.
37    #[must_use]
38    pub const fn get(self) -> u16 {
39        self.0
40    }
41}
42
43/// Feature required by one proposal.
44#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
45#[repr(transparent)]
46pub struct SchemaCapability(u16);
47
48impl SchemaCapability {
49    /// Exact composite record and enum contracts.
50    pub const EXACT_COMPOSITE_TYPES: Self = Self(1);
51    /// Accepted row-local constraints.
52    pub const ACCEPTED_CHECKS: Self = Self(2);
53    /// Secondary indexes.
54    pub const SECONDARY_INDEXES: Self = Self(3);
55    /// Restrictive relations.
56    pub const RESTRICTIVE_RELATIONS: Self = Self(4);
57    /// Accepted database defaults.
58    pub const INSERT_DEFAULTS: Self = Self(5);
59    /// Generated values.
60    pub const GENERATED_VALUES: Self = Self(6);
61    /// Managed created/updated timestamps.
62    pub const MANAGED_TIMESTAMPS: Self = Self(7);
63    /// Explicit versioned source migration declarations.
64    pub const VERSIONED_MIGRATIONS: Self = Self(8);
65
66    /// Construct a raw token for incompatibility testing and transport.
67    #[must_use]
68    pub const fn from_raw(value: u16) -> Self {
69        Self(value)
70    }
71
72    /// Return the raw capability number.
73    #[must_use]
74    pub const fn get(self) -> u16 {
75        self.0
76    }
77
78    const fn is_supported(self) -> bool {
79        matches!(self.0, 1..=8)
80    }
81}
82
83/// Expected accepted-schema head used for optimistic application.
84#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
85pub enum ExpectedAcceptedHead {
86    /// The target database has no accepted schema.
87    Empty,
88    /// The target must match this exact accepted head.
89    Exact {
90        /// Nonzero accepted-schema revision.
91        revision: u64,
92        /// Opaque accepted-schema fingerprint.
93        fingerprint: crate::ExpectedSchemaFingerprint,
94    },
95}
96
97impl ExpectedAcceptedHead {
98    const fn validate(&self) -> Result<(), SchemaContractError> {
99        match self {
100            Self::Exact { revision: 0, .. } => Err(SchemaContractError::InvalidReferenceList),
101            Self::Empty | Self::Exact { .. } => Ok(()),
102        }
103    }
104}
105
106/// Explicit entity-to-store routing in the target database.
107#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct EntityStoreAssignment {
109    entity: EntitySourceKey,
110    store: TargetStoreIdentity,
111}
112
113impl EntityStoreAssignment {
114    /// Construct one opaque routing assignment.
115    #[must_use]
116    pub const fn new(entity: EntitySourceKey, store: TargetStoreIdentity) -> Self {
117        Self { entity, store }
118    }
119
120    /// Borrow the routed entity source key.
121    #[must_use]
122    pub const fn entity(&self) -> &EntitySourceKey {
123        &self.entity
124    }
125
126    /// Return the opaque target-store identity.
127    #[must_use]
128    pub const fn store(&self) -> TargetStoreIdentity {
129        self.store
130    }
131}
132
133/// Explicit hard-cut removal operation.
134#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
135pub enum SchemaRemoval {
136    /// Remove an entity.
137    Entity(EntitySourceKey),
138    /// Remove one field from an entity.
139    Field {
140        /// Owning entity.
141        entity: EntitySourceKey,
142        /// Field identity.
143        field: FieldSourceKey,
144    },
145    /// Remove a named type.
146    Type(TypeSourceKey),
147    /// Remove one accepted constraint.
148    Constraint {
149        /// Owning entity.
150        entity: EntitySourceKey,
151        /// Constraint identity.
152        constraint: ConstraintSourceKey,
153    },
154    /// Remove one index.
155    Index {
156        /// Owning entity.
157        entity: EntitySourceKey,
158        /// Index identity.
159        index: IndexSourceKey,
160    },
161    /// Remove one relation.
162    Relation {
163        /// Owning entity.
164        entity: EntitySourceKey,
165        /// Relation identity.
166        relation: RelationSourceKey,
167    },
168}
169
170/// Canonical current-form database-scoped proposal.
171#[derive(Clone, Debug, Eq, PartialEq)]
172pub struct SchemaProposal {
173    version: ProposalContractVersion,
174    capabilities: Vec<SchemaCapability>,
175    target_database: TargetDatabaseIdentity,
176    submission_key: SchemaSubmissionKey,
177    expected_head: ExpectedAcceptedHead,
178    fragments: Vec<SchemaFragment>,
179    assignments: Vec<EntityStoreAssignment>,
180    removals: Vec<SchemaRemoval>,
181    migration: Option<SchemaMigrationPlan>,
182}
183
184impl SchemaProposal {
185    /// Compose the public transport form from already selected fragments.
186    ///
187    /// This validates contract-local closure only. IcyDB still treats the
188    /// result as untrusted and resolves target ownership, accepted references,
189    /// capabilities, and catalog-native mutation semantics during application.
190    ///
191    /// # Errors
192    ///
193    /// Returns a typed contract error for bounds, duplicate definitions,
194    /// ambiguous routing, removal conflicts, or malformed nested data.
195    #[expect(
196        clippy::too_many_arguments,
197        clippy::too_many_lines,
198        reason = "composition receives and validates one complete atomic public envelope"
199    )]
200    pub fn try_compose(
201        mut capabilities: Vec<SchemaCapability>,
202        target_database: TargetDatabaseIdentity,
203        submission_key: SchemaSubmissionKey,
204        expected_head: ExpectedAcceptedHead,
205        mut fragments: Vec<SchemaFragment>,
206        mut assignments: Vec<EntityStoreAssignment>,
207        mut removals: Vec<SchemaRemoval>,
208        migration: Option<SchemaMigrationPlan>,
209    ) -> Result<Self, SchemaContractError> {
210        check_len(
211            "proposal capabilities",
212            capabilities.len(),
213            MAX_SCHEMA_CAPABILITIES,
214        )?;
215        check_len(
216            "proposal fragments",
217            fragments.len(),
218            MAX_SCHEMA_PROPOSAL_FRAGMENTS,
219        )?;
220        check_len(
221            "proposal assignments",
222            assignments.len(),
223            MAX_SCHEMA_ASSIGNMENTS,
224        )?;
225        check_len("proposal removals", removals.len(), MAX_SCHEMA_REMOVALS)?;
226        expected_head.validate()?;
227        crate::compact_sort_unstable_by(&mut capabilities, Ord::cmp);
228        ensure_no_adjacent_duplicates(&capabilities)?;
229        if capabilities
230            .iter()
231            .any(|capability| !capability.is_supported())
232        {
233            return Err(SchemaContractError::UnsupportedCapability);
234        }
235        let declares_migration_capability = capabilities
236            .binary_search(&SchemaCapability::VERSIONED_MIGRATIONS)
237            .is_ok();
238        if declares_migration_capability != migration.is_some() {
239            return Err(SchemaContractError::InvalidMigrationPlan);
240        }
241        if let Some(plan) = &migration {
242            plan.validate()?;
243        }
244        for fragment in &fragments {
245            fragment.validate()?;
246        }
247        let mut keyed_fragments = fragments
248            .into_iter()
249            .map(|fragment| encode_schema_fragment(&fragment).map(|bytes| (bytes, fragment)))
250            .collect::<Result<Vec<_>, _>>()?;
251        // Canonically equal fragment bytes describe equal fragments, while
252        // duplicate assignment/removal keys reject below; no stable tie is observable.
253        crate::compact_sort_unstable_by(&mut keyed_fragments, |left, right| left.0.cmp(&right.0));
254        fragments = keyed_fragments
255            .into_iter()
256            .map(|(_, fragment)| fragment)
257            .collect();
258        crate::compact_sort_unstable_by(&mut assignments, |left, right| {
259            left.entity.cmp(&right.entity)
260        });
261        ensure_no_adjacent_duplicates_by(&assignments, |assignment| &assignment.entity)?;
262        crate::compact_sort_unstable_by(&mut removals, Ord::cmp);
263        ensure_no_adjacent_duplicates(&removals)?;
264
265        let mut entity_definitions = BTreeMap::new();
266        let mut type_definitions = BTreeMap::new();
267        let mut field_definitions = BTreeSet::new();
268        let mut constraint_definitions = BTreeSet::new();
269        let mut index_definitions = BTreeSet::new();
270        let mut relation_definitions = BTreeSet::new();
271        let mut entity_names = BTreeSet::new();
272        let mut type_names = BTreeSet::new();
273        for fragment in &fragments {
274            for entity in fragment.entities() {
275                if entity_definitions
276                    .insert(entity.source_key().clone(), entity)
277                    .is_some()
278                {
279                    return Err(SchemaContractError::DuplicateSourceKey);
280                }
281                if !entity_names.insert(entity.name()) {
282                    return Err(SchemaContractError::DuplicateName);
283                }
284                for field in entity.fields() {
285                    field_definitions
286                        .insert((entity.source_key().clone(), field.source_key().clone()));
287                }
288                for constraint in entity.constraints() {
289                    constraint_definitions
290                        .insert((entity.source_key().clone(), constraint.source_key().clone()));
291                }
292                for index in entity.indexes() {
293                    index_definitions
294                        .insert((entity.source_key().clone(), index.source_key().clone()));
295                }
296                for relation in entity.relations() {
297                    relation_definitions
298                        .insert((entity.source_key().clone(), relation.source_key().clone()));
299                }
300            }
301            for r#type in fragment.types() {
302                if type_definitions
303                    .insert(r#type.source_key().clone(), r#type)
304                    .is_some()
305                {
306                    return Err(SchemaContractError::DuplicateSourceKey);
307                }
308                if !type_names.insert(r#type.name()) {
309                    return Err(SchemaContractError::DuplicateName);
310                }
311            }
312        }
313        for assignment in &assignments {
314            if !entity_definitions.contains_key(assignment.entity()) {
315                return Err(SchemaContractError::InvalidReferenceList);
316            }
317        }
318        if assignments.len() != entity_definitions.len() {
319            return Err(SchemaContractError::MissingEntityStoreAssignment);
320        }
321        for removal in &removals {
322            let collides = match removal {
323                SchemaRemoval::Entity(entity) => entity_definitions.contains_key(entity),
324                SchemaRemoval::Field { entity, field } => {
325                    field_definitions.contains(&(entity.clone(), field.clone()))
326                }
327                SchemaRemoval::Type(r#type) => type_definitions.contains_key(r#type),
328                SchemaRemoval::Constraint { entity, constraint } => {
329                    constraint_definitions.contains(&(entity.clone(), constraint.clone()))
330                }
331                SchemaRemoval::Index { entity, index } => {
332                    index_definitions.contains(&(entity.clone(), index.clone()))
333                }
334                SchemaRemoval::Relation { entity, relation } => {
335                    relation_definitions.contains(&(entity.clone(), relation.clone()))
336                }
337            };
338            if collides {
339                return Err(SchemaContractError::DefinitionRemovalConflict);
340            }
341        }
342        validate_proposal_closure(
343            &expected_head,
344            &entity_definitions,
345            &type_definitions,
346            &removals,
347        )?;
348        if let Some(plan) = &migration {
349            validate_migration_plan(plan, &entity_definitions, &type_definitions)?;
350        }
351
352        Ok(Self {
353            version: ProposalContractVersion::CURRENT,
354            capabilities,
355            target_database,
356            submission_key,
357            expected_head,
358            fragments,
359            assignments,
360            removals,
361            migration,
362        })
363    }
364
365    /// Return the contract version.
366    #[must_use]
367    pub const fn version(&self) -> ProposalContractVersion {
368        self.version
369    }
370
371    /// Borrow required capabilities in canonical order.
372    #[must_use]
373    pub fn capabilities(&self) -> &[SchemaCapability] {
374        &self.capabilities
375    }
376
377    /// Return the target database identity.
378    #[must_use]
379    pub const fn target_database(&self) -> TargetDatabaseIdentity {
380        self.target_database
381    }
382
383    /// Borrow the submission key.
384    #[must_use]
385    pub const fn submission_key(&self) -> &SchemaSubmissionKey {
386        &self.submission_key
387    }
388
389    /// Borrow the optimistic accepted-head condition.
390    #[must_use]
391    pub const fn expected_head(&self) -> &ExpectedAcceptedHead {
392        &self.expected_head
393    }
394
395    /// Borrow reusable fragments.
396    #[must_use]
397    pub fn fragments(&self) -> &[SchemaFragment] {
398        &self.fragments
399    }
400
401    /// Borrow canonical entity-to-store assignments.
402    #[must_use]
403    pub fn assignments(&self) -> &[EntityStoreAssignment] {
404        &self.assignments
405    }
406
407    /// Borrow explicit removals.
408    #[must_use]
409    pub fn removals(&self) -> &[SchemaRemoval] {
410        &self.removals
411    }
412
413    /// Borrow the optional coordinated source migration plan.
414    #[must_use]
415    pub const fn migration(&self) -> Option<&SchemaMigrationPlan> {
416        self.migration.as_ref()
417    }
418
419    /// Compute the canonical proposal digest.
420    ///
421    /// # Errors
422    ///
423    /// Returns a typed encoding error if the proposal no longer satisfies the
424    /// current bounded contract.
425    pub fn digest(&self) -> Result<SchemaProposalDigest, SchemaContractError> {
426        let bytes = encode_schema_proposal(self)?;
427        let digest: [u8; 32] = Sha256::digest(bytes).into();
428        Ok(SchemaProposalDigest::from_bytes(digest))
429    }
430
431    pub(crate) fn validate_current(&self) -> Result<(), SchemaContractError> {
432        if self.version != ProposalContractVersion::CURRENT {
433            return Err(SchemaContractError::UnsupportedVersion {
434                found: self.version.get(),
435                supported: ProposalContractVersion::CURRENT.get(),
436            });
437        }
438        let rebuilt = Self::try_compose(
439            self.capabilities.clone(),
440            self.target_database,
441            self.submission_key.clone(),
442            self.expected_head.clone(),
443            self.fragments.clone(),
444            self.assignments.clone(),
445            self.removals.clone(),
446            self.migration.clone(),
447        )?;
448        if rebuilt != *self {
449            return Err(SchemaContractError::NonCanonical);
450        }
451        Ok(())
452    }
453}
454
455fn validate_migration_plan(
456    plan: &SchemaMigrationPlan,
457    entities: &BTreeMap<EntitySourceKey, &EntityFragment>,
458    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
459) -> Result<(), SchemaContractError> {
460    for transition in plan.transitions() {
461        let entity = entities
462            .get(transition.entity())
463            .copied()
464            .ok_or(SchemaContractError::InvalidMigrationReference)?;
465        if transition.from().get().checked_add(1) != Some(entity.version().get()) {
466            return Err(SchemaContractError::MigrationVersionGap);
467        }
468        for rename in transition.renames() {
469            validate_migration_rename_target(rename, transition, entity, types)?;
470        }
471        for transform in transition.transforms() {
472            validate_migration_transform_target(transform, entity, types)?;
473        }
474    }
475    validate_shared_type_transition_closure(plan, entities, types)?;
476    Ok(())
477}
478
479fn validate_shared_type_transition_closure(
480    plan: &SchemaMigrationPlan,
481    entities: &BTreeMap<EntitySourceKey, &EntityFragment>,
482    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
483) -> Result<(), SchemaContractError> {
484    for declaring_transition in plan.transitions() {
485        for rename in declaring_transition.renames() {
486            let Some(target_type) = shared_rename_target_type(declaring_transition, rename) else {
487                continue;
488            };
489            for (entity_source, entity) in entities {
490                if !entity_reaches_named_type(entity, &target_type, types) {
491                    continue;
492                }
493                let repeats_rename = plan.transitions().iter().any(|transition| {
494                    transition.entity() == entity_source && transition.renames().contains(rename)
495                });
496                if !repeats_rename {
497                    return Err(SchemaContractError::InvalidMigrationReference);
498                }
499            }
500        }
501    }
502    Ok(())
503}
504
505fn shared_rename_target_type(
506    transition: &crate::EntityMigration,
507    rename: &SchemaMigrationRename,
508) -> Option<TypeSourceKey> {
509    match rename {
510        SchemaMigrationRename::NamedType { to, .. } => Some(to.clone()),
511        SchemaMigrationRename::EnumVariant { named_type, .. }
512        | SchemaMigrationRename::RecordField { named_type, .. } => {
513            Some(migration_target_type(transition, named_type))
514        }
515        SchemaMigrationRename::Field { .. }
516        | SchemaMigrationRename::Relation { .. }
517        | SchemaMigrationRename::Constraint { .. }
518        | SchemaMigrationRename::Rule { .. } => None,
519    }
520}
521
522fn validate_migration_rename_target(
523    rename: &SchemaMigrationRename,
524    transition: &crate::EntityMigration,
525    entity: &EntityFragment,
526    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
527) -> Result<(), SchemaContractError> {
528    let valid = match rename {
529        SchemaMigrationRename::Field { to, .. } => {
530            entity.fields().iter().any(|field| field.source_key() == to)
531        }
532        SchemaMigrationRename::NamedType { to, .. } => entity_reaches_named_type(entity, to, types),
533        SchemaMigrationRename::EnumVariant { named_type, to, .. } => {
534            let target_type = migration_target_type(transition, named_type);
535            entity_reaches_named_type(entity, &target_type, types)
536                && matches!(
537                    types.get(&target_type),
538                    Some(NamedTypeFragment::Enum(target))
539                        if target.variants().iter().any(|variant| variant.source_key() == to)
540                )
541        }
542        SchemaMigrationRename::RecordField { named_type, to, .. } => {
543            let target_type = migration_target_type(transition, named_type);
544            entity_reaches_named_type(entity, &target_type, types)
545                && matches!(
546                    types.get(&target_type),
547                    Some(NamedTypeFragment::Record(target))
548                        if target.fields().iter().any(|field| field.source_key() == to)
549                )
550        }
551        SchemaMigrationRename::Relation { to, .. } => entity
552            .relations()
553            .iter()
554            .any(|relation| relation.source_key() == to),
555        SchemaMigrationRename::Constraint { to, .. } => entity
556            .constraints()
557            .iter()
558            .any(|constraint| constraint.source_key() == to),
559        SchemaMigrationRename::Rule { named_type, to, .. } => {
560            let target_type = migration_target_type(transition, named_type);
561            entity.constraints().iter().any(|constraint| {
562                matches!(
563                    constraint.kind(),
564                    ConstraintFragmentKind::TargetedRule(rule)
565                        if rule.target_type() == &target_type && rule.rule() == to
566                )
567            })
568        }
569    };
570    if valid {
571        Ok(())
572    } else {
573        Err(SchemaContractError::InvalidMigrationReference)
574    }
575}
576
577fn entity_reaches_named_type(
578    entity: &EntityFragment,
579    target: &TypeSourceKey,
580    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
581) -> bool {
582    let mut seen = BTreeSet::new();
583    entity
584        .fields()
585        .iter()
586        .any(|field| field_type_reaches_named_type(field.field_type(), target, types, &mut seen))
587        || entity.constraints().iter().any(|constraint| {
588            matches!(
589                constraint.kind(),
590                ConstraintFragmentKind::TargetedRule(rule) if rule.target_type() == target
591            )
592        })
593}
594
595fn field_type_reaches_named_type(
596    field_type: &FieldType,
597    target: &TypeSourceKey,
598    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
599    seen: &mut BTreeSet<TypeSourceKey>,
600) -> bool {
601    match field_type {
602        FieldType::Scalar(_) => false,
603        FieldType::List(inner) => field_type_reaches_named_type(inner, target, types, seen),
604        FieldType::Named(source) if source == target => true,
605        FieldType::Named(source) if !seen.insert(source.clone()) => false,
606        FieldType::Named(source) => match types.get(source) {
607            Some(NamedTypeFragment::Record(record)) => record.fields().iter().any(|field| {
608                field_type_reaches_named_type(field.field_type(), target, types, seen)
609            }),
610            Some(NamedTypeFragment::Enum(r#enum)) => r#enum.variants().iter().any(|variant| {
611                variant.payload().is_some_and(|payload| {
612                    field_type_reaches_named_type(payload, target, types, seen)
613                })
614            }),
615            Some(
616                NamedTypeFragment::Newtype { inner, .. }
617                | NamedTypeFragment::List { item: inner, .. }
618                | NamedTypeFragment::Set { item: inner, .. },
619            ) => field_type_reaches_named_type(inner, target, types, seen),
620            Some(NamedTypeFragment::Map { key, value, .. }) => {
621                field_type_reaches_named_type(key, target, types, seen)
622                    || field_type_reaches_named_type(value, target, types, seen)
623            }
624            Some(NamedTypeFragment::Tuple { members, .. }) => members.iter().any(|member| {
625                field_type_reaches_named_type(member.field_type(), target, types, seen)
626            }),
627            None => false,
628        },
629    }
630}
631
632fn migration_target_type(
633    transition: &crate::EntityMigration,
634    accepted_before: &TypeSourceKey,
635) -> TypeSourceKey {
636    transition
637        .renames()
638        .iter()
639        .find_map(|rename| match rename {
640            SchemaMigrationRename::NamedType { from, to } if from == accepted_before => {
641                Some(to.clone())
642            }
643            _ => None,
644        })
645        .unwrap_or_else(|| accepted_before.clone())
646}
647
648fn validate_migration_transform_target(
649    transform: &SchemaMigrationTransform,
650    entity: &EntityFragment,
651    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
652) -> Result<(), SchemaContractError> {
653    let target = entity
654        .fields()
655        .iter()
656        .find(|field| field.source_key() == transform.target())
657        .ok_or(SchemaContractError::InvalidMigrationReference)?;
658    match transform {
659        SchemaMigrationTransform::Fill { literal, .. }
660        | SchemaMigrationTransform::Coalesce { literal, .. } => {
661            validate_migration_literal_target(literal, target, types)
662        }
663        SchemaMigrationTransform::CheckedCast {
664            target: target_scalar,
665            ..
666        } if target.field_type() == &FieldType::Scalar(*target_scalar) => Ok(()),
667        SchemaMigrationTransform::Copy { .. } => Ok(()),
668        SchemaMigrationTransform::CheckedCast { .. } => {
669            Err(SchemaContractError::InvalidMigrationTransform)
670        }
671    }
672}
673
674fn validate_migration_literal_target(
675    literal: &ScalarLiteral,
676    target: &FieldFragment,
677    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
678) -> Result<(), SchemaContractError> {
679    let valid = match (target.field_type(), literal) {
680        (FieldType::Scalar(scalar), literal) => scalar.accepts_literal(literal),
681        (FieldType::Named(target_type), ScalarLiteral::EnumUnit { enum_type, variant })
682            if target_type == enum_type =>
683        {
684            matches!(
685                types.get(enum_type),
686                Some(NamedTypeFragment::Enum(target_enum))
687                    if target_enum
688                        .variants()
689                        .iter()
690                        .any(|candidate| candidate.source_key() == variant)
691            )
692        }
693        (FieldType::List(_) | FieldType::Named(_), _) => false,
694    };
695    if valid {
696        Ok(())
697    } else {
698        Err(SchemaContractError::LiteralTypeMismatch)
699    }
700}
701
702#[derive(Default)]
703struct ProposalReferences {
704    types: BTreeSet<TypeSourceKey>,
705    relation_entities: BTreeSet<EntitySourceKey>,
706    relation_fields: BTreeSet<(EntitySourceKey, FieldSourceKey)>,
707}
708
709fn validate_proposal_closure(
710    expected_head: &ExpectedAcceptedHead,
711    entities: &BTreeMap<EntitySourceKey, &EntityFragment>,
712    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
713    removals: &[SchemaRemoval],
714) -> Result<(), SchemaContractError> {
715    let mut references = ProposalReferences::default();
716    for entity in entities.values() {
717        collect_entity_references(entity, types, &mut references)?;
718        validate_local_relation_targets(entity, entities, types)?;
719    }
720    for r#type in types.values() {
721        collect_named_type_references(r#type, &mut references);
722    }
723    for removal in removals {
724        let removes_reference = match removal {
725            SchemaRemoval::Entity(entity) => references.relation_entities.contains(entity),
726            SchemaRemoval::Field { entity, field } => references
727                .relation_fields
728                .contains(&(entity.clone(), field.clone())),
729            SchemaRemoval::Type(r#type) => references.types.contains(r#type),
730            SchemaRemoval::Constraint { .. }
731            | SchemaRemoval::Index { .. }
732            | SchemaRemoval::Relation { .. } => false,
733        };
734        if removes_reference {
735            return Err(SchemaContractError::RemovedReference);
736        }
737    }
738    if matches!(expected_head, ExpectedAcceptedHead::Empty)
739        && (references
740            .types
741            .iter()
742            .any(|reference| !types.contains_key(reference))
743            || references
744                .relation_entities
745                .iter()
746                .any(|reference| !entities.contains_key(reference)))
747    {
748        return Err(SchemaContractError::InvalidLocalReference);
749    }
750    Ok(())
751}
752
753fn collect_entity_references(
754    entity: &EntityFragment,
755    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
756    references: &mut ProposalReferences,
757) -> Result<(), SchemaContractError> {
758    for field in entity.fields() {
759        collect_field_references(field, types, references)?;
760    }
761    for relation in entity.relations() {
762        references
763            .relation_entities
764            .insert(relation.target_entity().clone());
765        references.relation_fields.extend(
766            relation
767                .target_fields()
768                .iter()
769                .cloned()
770                .map(|field| (relation.target_entity().clone(), field)),
771        );
772        if let RelationSourceFragment::Nested { steps, .. } = relation.source() {
773            references
774                .types
775                .extend(steps.iter().filter_map(|step| match step {
776                    RelationPathStepFragment::EnterNamed { r#type } => Some(r#type.clone()),
777                    RelationPathStepFragment::RecordMember { record, .. } => Some(record.clone()),
778                    RelationPathStepFragment::EnumVariantPayload { r#enum, .. } => {
779                        Some(r#enum.clone())
780                    }
781                    RelationPathStepFragment::OptionalSome
782                    | RelationPathStepFragment::ListItems
783                    | RelationPathStepFragment::SetItems
784                    | RelationPathStepFragment::MapValues => None,
785                }));
786        }
787    }
788    for index in entity.indexes() {
789        if let Some(predicate) = index.predicate() {
790            collect_expression_enum_references(predicate, types, references)?;
791        }
792    }
793    for constraint in entity.constraints() {
794        match constraint.kind() {
795            ConstraintFragmentKind::Check(expression) => {
796                collect_expression_enum_references(expression, types, references)?;
797            }
798            ConstraintFragmentKind::TargetedRule(rule) => {
799                references.types.insert(rule.target_type().clone());
800                validate_targeted_rule(entity, rule, types)?;
801            }
802        }
803    }
804    Ok(())
805}
806
807fn validate_targeted_rule(
808    entity: &EntityFragment,
809    rule: &TargetedRuleFragment,
810    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
811) -> Result<(), SchemaContractError> {
812    let root = entity
813        .fields()
814        .iter()
815        .find(|field| field.source_key() == rule.root())
816        .ok_or(SchemaContractError::InvalidLocalReference)?;
817    if types.contains_key(rule.target_type())
818        && !field_type_reaches_target(root.field_type(), rule.target_type(), types)
819    {
820        return Err(SchemaContractError::InvalidRuleTarget);
821    }
822    let Some(target) = types.get(rule.target_type()) else {
823        return Ok(());
824    };
825    let shape = resolve_rule_target_shape(target, types)?;
826    if operation_matches_target(rule.operation(), shape) {
827        Ok(())
828    } else {
829        Err(SchemaContractError::InvalidRuleTarget)
830    }
831}
832
833fn field_type_reaches_target(
834    root: &FieldType,
835    target: &TypeSourceKey,
836    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
837) -> bool {
838    let mut pending = vec![root];
839    let mut visited = BTreeSet::new();
840    while let Some(field_type) = pending.pop() {
841        match field_type {
842            FieldType::Scalar(_) => {}
843            FieldType::List(item) => pending.push(item),
844            FieldType::Named(source) => {
845                if source == target {
846                    return true;
847                }
848                if !visited.insert(source) {
849                    continue;
850                }
851                if let Some(definition) = types.get(source) {
852                    push_named_type_field_types(definition, &mut pending);
853                }
854            }
855        }
856    }
857    false
858}
859
860fn push_named_type_field_types<'types>(
861    r#type: &'types NamedTypeFragment,
862    pending: &mut Vec<&'types FieldType>,
863) {
864    match r#type {
865        NamedTypeFragment::Record(record) => {
866            pending.extend(
867                record
868                    .fields()
869                    .iter()
870                    .map(crate::RecordFieldFragment::field_type),
871            );
872        }
873        NamedTypeFragment::Enum(r#enum) => {
874            pending.extend(
875                r#enum
876                    .variants()
877                    .iter()
878                    .filter_map(|variant| variant.payload()),
879            );
880        }
881        NamedTypeFragment::Newtype { inner, .. }
882        | NamedTypeFragment::List { item: inner, .. }
883        | NamedTypeFragment::Set { item: inner, .. } => pending.push(inner),
884        NamedTypeFragment::Map { key, value, .. } => {
885            pending.push(key);
886            pending.push(value);
887        }
888        NamedTypeFragment::Tuple { members, .. } => {
889            pending.extend(members.iter().map(crate::TupleElementFragment::field_type));
890        }
891    }
892}
893
894#[derive(Clone, Copy)]
895enum RuleTargetShape {
896    Collection,
897    Scalar(ScalarType),
898}
899
900fn resolve_rule_target_shape(
901    target: &NamedTypeFragment,
902    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
903) -> Result<RuleTargetShape, SchemaContractError> {
904    let mut current = target;
905    let mut visited = BTreeSet::new();
906    loop {
907        if !visited.insert(current.source_key()) {
908            return Err(SchemaContractError::InvalidRuleTarget);
909        }
910        match current {
911            NamedTypeFragment::List { .. }
912            | NamedTypeFragment::Set { .. }
913            | NamedTypeFragment::Map { .. } => return Ok(RuleTargetShape::Collection),
914            NamedTypeFragment::Newtype { inner, .. } => match inner {
915                FieldType::Scalar(scalar) => return Ok(RuleTargetShape::Scalar(*scalar)),
916                FieldType::List(_) => return Ok(RuleTargetShape::Collection),
917                FieldType::Named(source) => {
918                    current = types
919                        .get(source)
920                        .copied()
921                        .ok_or(SchemaContractError::InvalidRuleTarget)?;
922                }
923            },
924            NamedTypeFragment::Record(_)
925            | NamedTypeFragment::Enum(_)
926            | NamedTypeFragment::Tuple { .. } => {
927                return Err(SchemaContractError::InvalidRuleTarget);
928            }
929        }
930    }
931}
932
933fn operation_matches_target(operation: &SourceRuleOperation, shape: RuleTargetShape) -> bool {
934    match (operation, shape) {
935        (
936            SourceRuleOperation::LengthRangeInclusive { .. },
937            RuleTargetShape::Collection
938            | RuleTargetShape::Scalar(ScalarType::Blob { .. } | ScalarType::Text { .. }),
939        ) => true,
940        (
941            SourceRuleOperation::NumericMaximumInclusive { value }
942            | SourceRuleOperation::NumericMinimumInclusive { value },
943            RuleTargetShape::Scalar(scalar),
944        ) => numeric_scalar(scalar) && numeric_rule_literal_matches(scalar, value),
945        (
946            SourceRuleOperation::NumericRangeInclusive { min, max },
947            RuleTargetShape::Scalar(scalar),
948        ) => {
949            numeric_scalar(scalar)
950                && numeric_rule_literal_matches(scalar, min)
951                && numeric_rule_literal_matches(scalar, max)
952        }
953        (SourceRuleOperation::MultipleOf { divisor }, RuleTargetShape::Scalar(scalar)) => {
954            exact_numeric_scalar(scalar) && numeric_rule_literal_matches(scalar, divisor)
955        }
956        _ => false,
957    }
958}
959
960fn numeric_rule_literal_matches(scalar: ScalarType, literal: &crate::ScalarLiteral) -> bool {
961    if let (ScalarType::Decimal { scale }, crate::ScalarLiteral::Decimal(value)) = (scalar, literal)
962    {
963        let value = value.normalize();
964        return value.scale() <= scale && value.scale_to_integer(scale).is_some();
965    }
966    scalar.accepts_literal(literal)
967}
968
969const fn exact_numeric_scalar(scalar: ScalarType) -> bool {
970    matches!(
971        scalar,
972        ScalarType::Decimal { .. }
973            | ScalarType::Int8
974            | ScalarType::Int16
975            | ScalarType::Int32
976            | ScalarType::Int64
977            | ScalarType::Int128
978            | ScalarType::IntBig { .. }
979            | ScalarType::Nat8
980            | ScalarType::Nat16
981            | ScalarType::Nat32
982            | ScalarType::Nat64
983            | ScalarType::Nat128
984            | ScalarType::NatBig { .. }
985    )
986}
987
988const fn numeric_scalar(scalar: ScalarType) -> bool {
989    matches!(
990        scalar,
991        ScalarType::Decimal { .. }
992            | ScalarType::Float32
993            | ScalarType::Float64
994            | ScalarType::Int8
995            | ScalarType::Int16
996            | ScalarType::Int32
997            | ScalarType::Int64
998            | ScalarType::Int128
999            | ScalarType::IntBig { .. }
1000            | ScalarType::Nat8
1001            | ScalarType::Nat16
1002            | ScalarType::Nat32
1003            | ScalarType::Nat64
1004            | ScalarType::Nat128
1005            | ScalarType::NatBig { .. }
1006            | ScalarType::U256
1007    )
1008}
1009
1010fn collect_named_type_references(r#type: &NamedTypeFragment, references: &mut ProposalReferences) {
1011    match r#type {
1012        NamedTypeFragment::Record(record) => {
1013            for field in record.fields() {
1014                collect_field_type_reference(field.field_type(), references);
1015            }
1016        }
1017        NamedTypeFragment::Enum(r#enum) => {
1018            for variant in r#enum.variants() {
1019                if let Some(payload) = variant.payload() {
1020                    collect_field_type_reference(payload, references);
1021                }
1022            }
1023        }
1024        NamedTypeFragment::Newtype { inner, .. }
1025        | NamedTypeFragment::List { item: inner, .. }
1026        | NamedTypeFragment::Set { item: inner, .. } => {
1027            collect_field_type_reference(inner, references);
1028        }
1029        NamedTypeFragment::Map { key, value, .. } => {
1030            collect_field_type_reference(key, references);
1031            collect_field_type_reference(value, references);
1032        }
1033        NamedTypeFragment::Tuple { members, .. } => {
1034            for member in members {
1035                collect_field_type_reference(member.field_type(), references);
1036            }
1037        }
1038    }
1039}
1040
1041#[cfg(test)]
1042mod u256_tests {
1043    use super::*;
1044    use crate::{ScalarLiteral, U256};
1045
1046    #[test]
1047    fn u256_targeted_ranges_admit_while_multiple_of_remains_deferred() {
1048        let shape = RuleTargetShape::Scalar(ScalarType::U256);
1049        assert!(operation_matches_target(
1050            &SourceRuleOperation::NumericRangeInclusive {
1051                min: ScalarLiteral::U256(U256::ZERO),
1052                max: ScalarLiteral::U256(U256::MAX),
1053            },
1054            shape,
1055        ));
1056        assert!(!operation_matches_target(
1057            &SourceRuleOperation::MultipleOf {
1058                divisor: ScalarLiteral::U256(U256::ONE),
1059            },
1060            shape,
1061        ));
1062    }
1063}
1064
1065fn collect_field_references(
1066    field: &FieldFragment,
1067    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
1068    references: &mut ProposalReferences,
1069) -> Result<(), SchemaContractError> {
1070    collect_field_type_reference(field.field_type(), references);
1071    if let crate::FieldInsertPolicy::Default(ScalarLiteral::EnumUnit { enum_type, variant }) =
1072        field.insert_policy()
1073    {
1074        let FieldType::Named(field_type) = field.field_type() else {
1075            return Err(SchemaContractError::LiteralTypeMismatch);
1076        };
1077        if field_type != enum_type {
1078            return Err(SchemaContractError::LiteralTypeMismatch);
1079        }
1080        collect_enum_literal_reference(enum_type, variant, types, references)?;
1081    }
1082    Ok(())
1083}
1084
1085fn collect_field_type_reference(field_type: &FieldType, references: &mut ProposalReferences) {
1086    match field_type {
1087        FieldType::List(item) => collect_field_type_reference(item, references),
1088        FieldType::Named(reference) => {
1089            references.types.insert(reference.clone());
1090        }
1091        FieldType::Scalar(_) => {}
1092    }
1093}
1094
1095fn collect_expression_enum_references(
1096    expression: &SourceCheckExpr,
1097    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
1098    references: &mut ProposalReferences,
1099) -> Result<(), SchemaContractError> {
1100    for instruction in expression.instructions() {
1101        if let SourceCheckInstruction::Literal(ScalarLiteral::EnumUnit { enum_type, variant }) =
1102            instruction
1103        {
1104            collect_enum_literal_reference(enum_type, variant, types, references)?;
1105        }
1106    }
1107    Ok(())
1108}
1109
1110fn collect_enum_literal_reference(
1111    enum_type: &TypeSourceKey,
1112    variant: &TypeSourceKey,
1113    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
1114    references: &mut ProposalReferences,
1115) -> Result<(), SchemaContractError> {
1116    references.types.insert(enum_type.clone());
1117    let Some(local) = types.get(enum_type) else {
1118        return Ok(());
1119    };
1120    let NamedTypeFragment::Enum(local) = local else {
1121        return Err(SchemaContractError::InvalidEnumLiteral);
1122    };
1123    if local
1124        .variants()
1125        .iter()
1126        .all(|candidate| candidate.source_key() != variant)
1127    {
1128        return Err(SchemaContractError::InvalidEnumLiteral);
1129    }
1130    Ok(())
1131}
1132
1133fn validate_local_relation_targets(
1134    source: &EntityFragment,
1135    entities: &BTreeMap<EntitySourceKey, &EntityFragment>,
1136    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
1137) -> Result<(), SchemaContractError> {
1138    for relation in source.relations() {
1139        let Some(target) = entities.get(relation.target_entity()) else {
1140            continue;
1141        };
1142        match relation.source() {
1143            RelationSourceFragment::Direct { fields } => {
1144                for (source_key, target_key) in fields.iter().zip(relation.target_fields()) {
1145                    let source_field = source
1146                        .fields()
1147                        .iter()
1148                        .find(|field| field.source_key() == source_key)
1149                        .ok_or(SchemaContractError::InvalidLocalReference)?;
1150                    let target_field = target
1151                        .fields()
1152                        .iter()
1153                        .find(|field| field.source_key() == target_key)
1154                        .ok_or(SchemaContractError::InvalidLocalReference)?;
1155                    let source_type = match source_field.field_type() {
1156                        FieldType::List(item) => item.as_ref(),
1157                        field_type => field_type,
1158                    };
1159                    if source_type != target_field.field_type() {
1160                        return Err(SchemaContractError::RelationTypeMismatch);
1161                    }
1162                }
1163            }
1164            RelationSourceFragment::Nested { .. } => {
1165                let terminal = validate_nested_relation_path(source, relation, types)?;
1166                let target_key = relation
1167                    .target_fields()
1168                    .first()
1169                    .ok_or(SchemaContractError::InvalidReferenceList)?;
1170                let target_field = target
1171                    .fields()
1172                    .iter()
1173                    .find(|field| field.source_key() == target_key)
1174                    .ok_or(SchemaContractError::InvalidLocalReference)?;
1175                if terminal != target_field.field_type() {
1176                    return Err(SchemaContractError::RelationTypeMismatch);
1177                }
1178            }
1179        }
1180    }
1181    Ok(())
1182}
1183
1184fn validate_nested_relation_path<'schema>(
1185    source: &'schema EntityFragment,
1186    relation: &RelationFragment,
1187    types: &BTreeMap<TypeSourceKey, &'schema NamedTypeFragment>,
1188) -> Result<&'schema FieldType, SchemaContractError> {
1189    let RelationSourceFragment::Nested { root, steps } = relation.source() else {
1190        return Err(SchemaContractError::RelationTypeMismatch);
1191    };
1192    let root = source
1193        .fields()
1194        .iter()
1195        .find(|field| field.source_key() == root)
1196        .ok_or(SchemaContractError::InvalidLocalReference)?;
1197    let mut kind = root.field_type();
1198    let mut nullable = root.nullable();
1199    let mut entered = None;
1200    for step in steps {
1201        match step {
1202            RelationPathStepFragment::OptionalSome => {
1203                if !nullable || entered.is_some() {
1204                    return Err(SchemaContractError::RelationTypeMismatch);
1205                }
1206                nullable = false;
1207            }
1208            RelationPathStepFragment::EnterNamed { r#type } => {
1209                let FieldType::Named(named) = kind else {
1210                    return Err(SchemaContractError::RelationTypeMismatch);
1211                };
1212                if nullable || entered.is_some() || named != r#type {
1213                    return Err(SchemaContractError::RelationTypeMismatch);
1214                }
1215                match types.get(named).copied() {
1216                    Some(
1217                        NamedTypeFragment::Record(_)
1218                        | NamedTypeFragment::Enum(_)
1219                        | NamedTypeFragment::List { .. }
1220                        | NamedTypeFragment::Set { .. }
1221                        | NamedTypeFragment::Map { .. },
1222                    ) => {
1223                        entered = Some(named);
1224                    }
1225                    Some(NamedTypeFragment::Newtype { inner, .. }) => {
1226                        kind = inner;
1227                    }
1228                    Some(NamedTypeFragment::Tuple { .. }) | None => {
1229                        return Err(SchemaContractError::RelationTypeMismatch);
1230                    }
1231                }
1232            }
1233            RelationPathStepFragment::RecordMember { record, field } => {
1234                if nullable || entered != Some(record) {
1235                    return Err(SchemaContractError::RelationTypeMismatch);
1236                }
1237                let Some(NamedTypeFragment::Record(definition)) = types.get(record).copied() else {
1238                    return Err(SchemaContractError::RelationTypeMismatch);
1239                };
1240                let member = definition
1241                    .fields()
1242                    .iter()
1243                    .find(|member| member.source_key() == field)
1244                    .ok_or(SchemaContractError::InvalidLocalReference)?;
1245                kind = member.field_type();
1246                nullable = member.nullable();
1247                entered = None;
1248            }
1249            RelationPathStepFragment::EnumVariantPayload { r#enum, variant } => {
1250                if nullable || entered != Some(r#enum) {
1251                    return Err(SchemaContractError::RelationTypeMismatch);
1252                }
1253                let Some(NamedTypeFragment::Enum(definition)) = types.get(r#enum).copied() else {
1254                    return Err(SchemaContractError::RelationTypeMismatch);
1255                };
1256                let payload = definition
1257                    .variants()
1258                    .iter()
1259                    .find(|candidate| candidate.source_key() == variant)
1260                    .and_then(EnumVariantFragment::payload)
1261                    .ok_or(SchemaContractError::InvalidLocalReference)?;
1262                kind = payload;
1263                nullable = false;
1264                entered = None;
1265            }
1266            RelationPathStepFragment::ListItems
1267            | RelationPathStepFragment::SetItems
1268            | RelationPathStepFragment::MapValues => {
1269                if nullable {
1270                    return Err(SchemaContractError::RelationTypeMismatch);
1271                }
1272                kind = relation_collection_step_output(kind, entered.take(), step, types)?;
1273                nullable = false;
1274            }
1275        }
1276    }
1277    if nullable || entered.is_some() || !matches!(kind, FieldType::Scalar(_)) {
1278        return Err(SchemaContractError::RelationTypeMismatch);
1279    }
1280    Ok(kind)
1281}
1282
1283fn relation_collection_step_output<'schema>(
1284    kind: &'schema FieldType,
1285    entered: Option<&TypeSourceKey>,
1286    step: &RelationPathStepFragment,
1287    types: &BTreeMap<TypeSourceKey, &'schema NamedTypeFragment>,
1288) -> Result<&'schema FieldType, SchemaContractError> {
1289    match (step, entered, kind) {
1290        (RelationPathStepFragment::ListItems, None, FieldType::List(item)) => Ok(item),
1291        (RelationPathStepFragment::ListItems, Some(named), FieldType::Named(current))
1292            if named == current =>
1293        {
1294            let Some(NamedTypeFragment::List { item, .. }) = types.get(current).copied() else {
1295                return Err(SchemaContractError::RelationTypeMismatch);
1296            };
1297            Ok(item)
1298        }
1299        (RelationPathStepFragment::SetItems, Some(named), FieldType::Named(current))
1300            if named == current =>
1301        {
1302            let Some(NamedTypeFragment::Set { item, .. }) = types.get(current).copied() else {
1303                return Err(SchemaContractError::RelationTypeMismatch);
1304            };
1305            Ok(item)
1306        }
1307        (RelationPathStepFragment::MapValues, Some(named), FieldType::Named(current))
1308            if named == current =>
1309        {
1310            let Some(NamedTypeFragment::Map { value, .. }) = types.get(current).copied() else {
1311                return Err(SchemaContractError::RelationTypeMismatch);
1312            };
1313            Ok(value)
1314        }
1315        _ => Err(SchemaContractError::RelationTypeMismatch),
1316    }
1317}
1318
1319fn ensure_no_adjacent_duplicates<T>(values: &[T]) -> Result<(), SchemaContractError>
1320where
1321    T: Eq,
1322{
1323    if values.windows(2).any(|pair| pair[0] == pair[1]) {
1324        return Err(SchemaContractError::DuplicateSourceKey);
1325    }
1326    Ok(())
1327}
1328
1329fn ensure_no_adjacent_duplicates_by<T, K>(
1330    values: &[T],
1331    key: impl Fn(&T) -> &K,
1332) -> Result<(), SchemaContractError>
1333where
1334    K: Eq,
1335{
1336    if values.windows(2).any(|pair| key(&pair[0]) == key(&pair[1])) {
1337        return Err(SchemaContractError::DuplicateSourceKey);
1338    }
1339    Ok(())
1340}
1341
1342#[cfg(test)]
1343mod tests {
1344    use super::*;
1345    use crate::decode_schema_proposal;
1346    use std::str::FromStr;
1347
1348    fn empty_proposal_with(capabilities: Vec<SchemaCapability>) -> SchemaProposal {
1349        SchemaProposal::try_compose(
1350            capabilities,
1351            TargetDatabaseIdentity::from_bytes([1; 32]),
1352            SchemaSubmissionKey::try_new("proposal-version-test")
1353                .expect("submission key should admit"),
1354            ExpectedAcceptedHead::Empty,
1355            Vec::new(),
1356            Vec::new(),
1357            Vec::new(),
1358            None,
1359        )
1360        .expect("empty proposal should compose")
1361    }
1362
1363    #[test]
1364    fn decoded_future_contract_version_fails_typed() {
1365        let proposal = empty_proposal_with(Vec::new());
1366        let mut bytes = crate::encode_schema_proposal(&proposal).expect("proposal should encode");
1367        bytes[5..7].copy_from_slice(&2_u16.to_be_bytes());
1368
1369        assert_eq!(
1370            decode_schema_proposal(&bytes),
1371            Err(SchemaContractError::UnsupportedVersion {
1372                found: 2,
1373                supported: 1,
1374            }),
1375        );
1376    }
1377
1378    #[test]
1379    fn decoded_unknown_capability_fails_typed() {
1380        let proposal = empty_proposal_with(vec![SchemaCapability::EXACT_COMPOSITE_TYPES]);
1381        let mut bytes = crate::encode_schema_proposal(&proposal).expect("proposal should encode");
1382        bytes[11..13].copy_from_slice(&u16::MAX.to_be_bytes());
1383
1384        assert_eq!(
1385            decode_schema_proposal(&bytes),
1386            Err(SchemaContractError::UnsupportedCapability),
1387        );
1388    }
1389
1390    #[test]
1391    fn targeted_numeric_operations_require_exact_target_literal_admission() {
1392        let decimal = |value| {
1393            crate::ScalarLiteral::Decimal(
1394                crate::Decimal::from_str(value).expect("decimal fixture should parse"),
1395            )
1396        };
1397        assert!(operation_matches_target(
1398            &SourceRuleOperation::MultipleOf {
1399                divisor: decimal("0.25"),
1400            },
1401            RuleTargetShape::Scalar(ScalarType::Decimal { scale: 2 }),
1402        ));
1403        assert!(!operation_matches_target(
1404            &SourceRuleOperation::MultipleOf {
1405                divisor: decimal("0.251"),
1406            },
1407            RuleTargetShape::Scalar(ScalarType::Decimal { scale: 2 }),
1408        ));
1409        assert!(!operation_matches_target(
1410            &SourceRuleOperation::MultipleOf {
1411                divisor: crate::ScalarLiteral::Float64(
1412                    crate::Float64::try_new(2.0).expect("finite float"),
1413                ),
1414            },
1415            RuleTargetShape::Scalar(ScalarType::Float64),
1416        ));
1417        assert!(operation_matches_target(
1418            &SourceRuleOperation::NumericMaximumInclusive {
1419                value: crate::ScalarLiteral::Float64(
1420                    crate::Float64::try_new(2.0).expect("finite float"),
1421                ),
1422            },
1423            RuleTargetShape::Scalar(ScalarType::Float64),
1424        ));
1425    }
1426}