Skip to main content

icydb_schema/
proposal.rs

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