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        let reachable = if transition.renames().iter().any(|rename| {
468            matches!(
469                rename,
470                SchemaMigrationRename::NamedType { .. }
471                    | SchemaMigrationRename::EnumVariant { .. }
472                    | SchemaMigrationRename::RecordField { .. }
473            )
474        }) {
475            entity_named_types(entity, types)
476        } else {
477            BTreeSet::new()
478        };
479        for rename in transition.renames() {
480            validate_migration_rename_target(rename, transition, entity, types, &reachable)?;
481        }
482        for transform in transition.transforms() {
483            validate_migration_transform_target(transform, entity, types)?;
484        }
485    }
486    validate_shared_type_transition_closure(plan, entities, types)?;
487    Ok(())
488}
489
490fn validate_shared_type_transition_closure(
491    plan: &SchemaMigrationPlan,
492    entities: &BTreeMap<EntitySourceKey, &EntityFragment>,
493    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
494) -> Result<(), SchemaContractError> {
495    // Deduplicate shared requirements by the same exact key used to canonicalize
496    // declarations. Keep no cross-entity reachability cache: one large shared
497    // graph must not allocate one retained copy per entity.
498    let mut required = BTreeMap::<_, BTreeSet<_>>::new();
499    for transition in plan.transitions() {
500        for rename in transition.renames() {
501            if let Some(target) = shared_rename_target_type(transition, rename) {
502                required
503                    .entry(target)
504                    .or_default()
505                    .insert(rename.sort_key());
506            }
507        }
508    }
509    if required.is_empty() {
510        return Ok(());
511    }
512    for (source, entity) in entities {
513        let transition = plan
514            .transitions()
515            .binary_search_by(|transition| transition.entity().cmp(source))
516            .ok()
517            .map(|index| &plan.transitions()[index]);
518        // Traverse each consumer once, independent of the number of declarations.
519        for target in entity_named_types(entity, types) {
520            let Some(renames) = required.get(target) else {
521                continue;
522            };
523            let transition = transition.ok_or(SchemaContractError::InvalidMigrationReference)?;
524            for rename in renames {
525                if transition
526                    .renames()
527                    .binary_search_by(|candidate| candidate.sort_key().cmp(rename))
528                    .is_err()
529                {
530                    return Err(SchemaContractError::InvalidMigrationReference);
531                }
532            }
533        }
534    }
535    Ok(())
536}
537
538fn shared_rename_target_type(
539    transition: &crate::EntityMigration,
540    rename: &SchemaMigrationRename,
541) -> Option<TypeSourceKey> {
542    match rename {
543        SchemaMigrationRename::NamedType { to, .. } => Some(to.clone()),
544        SchemaMigrationRename::EnumVariant { named_type, .. }
545        | SchemaMigrationRename::RecordField { named_type, .. } => {
546            Some(migration_target_type(transition, named_type))
547        }
548        SchemaMigrationRename::Field { .. }
549        | SchemaMigrationRename::Relation { .. }
550        | SchemaMigrationRename::Constraint { .. }
551        | SchemaMigrationRename::Rule { .. } => None,
552    }
553}
554
555fn validate_migration_rename_target(
556    rename: &SchemaMigrationRename,
557    transition: &crate::EntityMigration,
558    entity: &EntityFragment,
559    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
560    reachable: &BTreeSet<&TypeSourceKey>,
561) -> Result<(), SchemaContractError> {
562    let valid = match rename {
563        SchemaMigrationRename::Field { to, .. } => {
564            entity.fields().iter().any(|field| field.source_key() == to)
565        }
566        SchemaMigrationRename::NamedType { to, .. } => reachable.contains(to),
567        SchemaMigrationRename::EnumVariant { named_type, to, .. } => {
568            let target_type = migration_target_type(transition, named_type);
569            reachable.contains(&target_type)
570                && matches!(
571                    types.get(&target_type),
572                    Some(NamedTypeFragment::Enum(target))
573                        if target.variants().iter().any(|variant| variant.source_key() == to)
574                )
575        }
576        SchemaMigrationRename::RecordField { named_type, to, .. } => {
577            let target_type = migration_target_type(transition, named_type);
578            reachable.contains(&target_type)
579                && matches!(
580                    types.get(&target_type),
581                    Some(NamedTypeFragment::Record(target))
582                        if target.fields().iter().any(|field| field.source_key() == to)
583                )
584        }
585        SchemaMigrationRename::Relation { to, .. } => entity
586            .relations()
587            .iter()
588            .any(|relation| relation.source_key() == to),
589        SchemaMigrationRename::Constraint { to, .. } => entity
590            .constraints()
591            .iter()
592            .any(|constraint| constraint.source_key() == to),
593        SchemaMigrationRename::Rule { named_type, to, .. } => {
594            let target_type = migration_target_type(transition, named_type);
595            entity.constraints().iter().any(|constraint| {
596                matches!(
597                    constraint.kind(),
598                    ConstraintFragmentKind::TargetedRule(rule)
599                        if rule.target_type() == &target_type && rule.rule() == to
600                )
601            })
602        }
603    };
604    if valid {
605        Ok(())
606    } else {
607        Err(SchemaContractError::InvalidMigrationReference)
608    }
609}
610
611fn entity_named_types<'schema>(
612    entity: &'schema EntityFragment,
613    types: &BTreeMap<TypeSourceKey, &'schema NamedTypeFragment>,
614) -> BTreeSet<&'schema TypeSourceKey> {
615    let mut pending: Vec<_> = entity
616        .fields()
617        .iter()
618        .map(FieldFragment::field_type)
619        .collect();
620    let mut reachable = BTreeSet::new();
621    while let Some(field_type) = pending.pop() {
622        match field_type {
623            FieldType::Scalar(_) => {}
624            FieldType::List(item) => pending.push(item),
625            FieldType::Named(source) => {
626                if reachable.insert(source)
627                    && let Some(definition) = types.get(source)
628                {
629                    push_named_type_field_types(definition, &mut pending);
630                }
631            }
632        }
633    }
634    // Targeted rules contribute their exact target, not its dependency closure.
635    // This preserves migration ownership independently of field-local rule checks.
636    for constraint in entity.constraints() {
637        if let ConstraintFragmentKind::TargetedRule(rule) = constraint.kind() {
638            reachable.insert(rule.target_type());
639        }
640    }
641    reachable
642}
643
644fn migration_target_type(
645    transition: &crate::EntityMigration,
646    accepted_before: &TypeSourceKey,
647) -> TypeSourceKey {
648    transition
649        .renames()
650        .iter()
651        .find_map(|rename| match rename {
652            SchemaMigrationRename::NamedType { from, to } if from == accepted_before => {
653                Some(to.clone())
654            }
655            _ => None,
656        })
657        .unwrap_or_else(|| accepted_before.clone())
658}
659
660fn validate_migration_transform_target(
661    transform: &SchemaMigrationTransform,
662    entity: &EntityFragment,
663    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
664) -> Result<(), SchemaContractError> {
665    let target = entity
666        .fields()
667        .iter()
668        .find(|field| field.source_key() == transform.target())
669        .ok_or(SchemaContractError::InvalidMigrationReference)?;
670    match transform {
671        SchemaMigrationTransform::Fill { literal, .. }
672        | SchemaMigrationTransform::Coalesce { literal, .. } => {
673            validate_migration_literal_target(literal, target, types)
674        }
675        SchemaMigrationTransform::CheckedCast {
676            target: target_scalar,
677            ..
678        } if target.field_type() == &FieldType::Scalar(*target_scalar) => Ok(()),
679        SchemaMigrationTransform::Copy { .. } => Ok(()),
680        SchemaMigrationTransform::CheckedCast { .. } => {
681            Err(SchemaContractError::InvalidMigrationTransform)
682        }
683    }
684}
685
686fn validate_migration_literal_target(
687    literal: &ScalarLiteral,
688    target: &FieldFragment,
689    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
690) -> Result<(), SchemaContractError> {
691    let valid = match (target.field_type(), literal) {
692        (FieldType::Scalar(scalar), literal) => scalar.accepts_literal(literal),
693        (FieldType::Named(target_type), ScalarLiteral::EnumUnit { enum_type, variant })
694            if target_type == enum_type =>
695        {
696            matches!(
697                types.get(enum_type),
698                Some(NamedTypeFragment::Enum(target_enum))
699                    if target_enum
700                        .variants()
701                        .iter()
702                        .any(|candidate| candidate.source_key() == variant)
703            )
704        }
705        (FieldType::List(_) | FieldType::Named(_), _) => false,
706    };
707    if valid {
708        Ok(())
709    } else {
710        Err(SchemaContractError::LiteralTypeMismatch)
711    }
712}
713
714#[derive(Default)]
715struct ProposalReferences {
716    types: BTreeSet<TypeSourceKey>,
717    relation_entities: BTreeSet<EntitySourceKey>,
718    relation_fields: BTreeSet<(EntitySourceKey, FieldSourceKey)>,
719}
720
721fn validate_proposal_closure(
722    expected_head: &ExpectedAcceptedHead,
723    entities: &BTreeMap<EntitySourceKey, &EntityFragment>,
724    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
725    removals: &[SchemaRemoval],
726) -> Result<(), SchemaContractError> {
727    let mut references = ProposalReferences::default();
728    for entity in entities.values() {
729        collect_entity_references(entity, types, &mut references)?;
730        validate_local_relation_targets(entity, entities, types)?;
731    }
732    for r#type in types.values() {
733        collect_named_type_references(r#type, &mut references);
734    }
735    for removal in removals {
736        let removes_reference = match removal {
737            SchemaRemoval::Entity(entity) => references.relation_entities.contains(entity),
738            SchemaRemoval::Field { entity, field } => references
739                .relation_fields
740                .contains(&(entity.clone(), field.clone())),
741            SchemaRemoval::Type(r#type) => references.types.contains(r#type),
742            SchemaRemoval::Constraint { .. }
743            | SchemaRemoval::Index { .. }
744            | SchemaRemoval::Relation { .. } => false,
745        };
746        if removes_reference {
747            return Err(SchemaContractError::RemovedReference);
748        }
749    }
750    if matches!(expected_head, ExpectedAcceptedHead::Empty)
751        && (references
752            .types
753            .iter()
754            .any(|reference| !types.contains_key(reference))
755            || references
756                .relation_entities
757                .iter()
758                .any(|reference| !entities.contains_key(reference)))
759    {
760        return Err(SchemaContractError::InvalidLocalReference);
761    }
762    Ok(())
763}
764
765fn collect_entity_references(
766    entity: &EntityFragment,
767    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
768    references: &mut ProposalReferences,
769) -> Result<(), SchemaContractError> {
770    for field in entity.fields() {
771        collect_field_references(field, types, references)?;
772    }
773    for relation in entity.relations() {
774        references
775            .relation_entities
776            .insert(relation.target_entity().clone());
777        references.relation_fields.extend(
778            relation
779                .target_fields()
780                .iter()
781                .cloned()
782                .map(|field| (relation.target_entity().clone(), field)),
783        );
784        if let RelationSourceFragment::Nested { steps, .. } = relation.source() {
785            references
786                .types
787                .extend(steps.iter().filter_map(|step| match step {
788                    RelationPathStepFragment::EnterNamed { r#type } => Some(r#type.clone()),
789                    RelationPathStepFragment::RecordMember { record, .. } => Some(record.clone()),
790                    RelationPathStepFragment::EnumVariantPayload { r#enum, .. } => {
791                        Some(r#enum.clone())
792                    }
793                    RelationPathStepFragment::OptionalSome
794                    | RelationPathStepFragment::ListItems
795                    | RelationPathStepFragment::SetItems
796                    | RelationPathStepFragment::MapValues => None,
797                }));
798        }
799    }
800    for index in entity.indexes() {
801        if let Some(predicate) = index.predicate() {
802            collect_expression_enum_references(predicate, types, references)?;
803        }
804    }
805    for constraint in entity.constraints() {
806        match constraint.kind() {
807            ConstraintFragmentKind::Check(expression) => {
808                collect_expression_enum_references(expression, types, references)?;
809            }
810            ConstraintFragmentKind::TargetedRule(rule) => {
811                references.types.insert(rule.target_type().clone());
812                validate_targeted_rule(entity, rule, types)?;
813            }
814        }
815    }
816    Ok(())
817}
818
819fn validate_targeted_rule(
820    entity: &EntityFragment,
821    rule: &TargetedRuleFragment,
822    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
823) -> Result<(), SchemaContractError> {
824    let root = entity
825        .fields()
826        .iter()
827        .find(|field| field.source_key() == rule.root())
828        .ok_or(SchemaContractError::InvalidLocalReference)?;
829    if types.contains_key(rule.target_type())
830        && !field_type_reaches_target(root.field_type(), rule.target_type(), types)
831    {
832        return Err(SchemaContractError::InvalidRuleTarget);
833    }
834    let Some(target) = types.get(rule.target_type()) else {
835        return Ok(());
836    };
837    let shape = resolve_rule_target_shape(target, types)?;
838    if operation_matches_target(rule.operation(), shape) {
839        Ok(())
840    } else {
841        Err(SchemaContractError::InvalidRuleTarget)
842    }
843}
844
845fn field_type_reaches_target(
846    root: &FieldType,
847    target: &TypeSourceKey,
848    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
849) -> bool {
850    let mut pending = vec![root];
851    let mut visited = BTreeSet::new();
852    while let Some(field_type) = pending.pop() {
853        match field_type {
854            FieldType::Scalar(_) => {}
855            FieldType::List(item) => pending.push(item),
856            FieldType::Named(source) => {
857                if source == target {
858                    return true;
859                }
860                if !visited.insert(source) {
861                    continue;
862                }
863                if let Some(definition) = types.get(source) {
864                    push_named_type_field_types(definition, &mut pending);
865                }
866            }
867        }
868    }
869    false
870}
871
872fn push_named_type_field_types<'types>(
873    r#type: &'types NamedTypeFragment,
874    pending: &mut Vec<&'types FieldType>,
875) {
876    match r#type {
877        NamedTypeFragment::Record(record) => {
878            pending.extend(
879                record
880                    .fields()
881                    .iter()
882                    .map(crate::RecordFieldFragment::field_type),
883            );
884        }
885        NamedTypeFragment::Enum(r#enum) => {
886            pending.extend(
887                r#enum
888                    .variants()
889                    .iter()
890                    .filter_map(|variant| variant.payload()),
891            );
892        }
893        NamedTypeFragment::Newtype { inner, .. }
894        | NamedTypeFragment::List { item: inner, .. }
895        | NamedTypeFragment::Set { item: inner, .. } => pending.push(inner),
896        NamedTypeFragment::Map { key, value, .. } => {
897            pending.push(key);
898            pending.push(value);
899        }
900        NamedTypeFragment::Tuple { members, .. } => {
901            pending.extend(members.iter().map(crate::TupleElementFragment::field_type));
902        }
903    }
904}
905
906#[derive(Clone, Copy)]
907enum RuleTargetShape {
908    Collection,
909    Scalar(ScalarType),
910}
911
912fn resolve_rule_target_shape(
913    target: &NamedTypeFragment,
914    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
915) -> Result<RuleTargetShape, SchemaContractError> {
916    let mut current = target;
917    let mut visited = BTreeSet::new();
918    loop {
919        if !visited.insert(current.source_key()) {
920            return Err(SchemaContractError::InvalidRuleTarget);
921        }
922        match current {
923            NamedTypeFragment::List { .. }
924            | NamedTypeFragment::Set { .. }
925            | NamedTypeFragment::Map { .. } => return Ok(RuleTargetShape::Collection),
926            NamedTypeFragment::Newtype { inner, .. } => match inner {
927                FieldType::Scalar(scalar) => return Ok(RuleTargetShape::Scalar(*scalar)),
928                FieldType::List(_) => return Ok(RuleTargetShape::Collection),
929                FieldType::Named(source) => {
930                    current = types
931                        .get(source)
932                        .copied()
933                        .ok_or(SchemaContractError::InvalidRuleTarget)?;
934                }
935            },
936            NamedTypeFragment::Record(_)
937            | NamedTypeFragment::Enum(_)
938            | NamedTypeFragment::Tuple { .. } => {
939                return Err(SchemaContractError::InvalidRuleTarget);
940            }
941        }
942    }
943}
944
945fn operation_matches_target(operation: &SourceRuleOperation, shape: RuleTargetShape) -> bool {
946    match (operation, shape) {
947        (
948            SourceRuleOperation::LengthRangeInclusive { .. },
949            RuleTargetShape::Collection
950            | RuleTargetShape::Scalar(ScalarType::Blob { .. } | ScalarType::Text { .. }),
951        ) => true,
952        (
953            SourceRuleOperation::NumericMaximumInclusive { value }
954            | SourceRuleOperation::NumericMinimumInclusive { value },
955            RuleTargetShape::Scalar(scalar),
956        ) => numeric_scalar(scalar) && numeric_rule_literal_matches(scalar, value),
957        (
958            SourceRuleOperation::NumericRangeInclusive { min, max },
959            RuleTargetShape::Scalar(scalar),
960        ) => {
961            numeric_scalar(scalar)
962                && numeric_rule_literal_matches(scalar, min)
963                && numeric_rule_literal_matches(scalar, max)
964        }
965        (SourceRuleOperation::MultipleOf { divisor }, RuleTargetShape::Scalar(scalar)) => {
966            exact_numeric_scalar(scalar) && numeric_rule_literal_matches(scalar, divisor)
967        }
968        _ => false,
969    }
970}
971
972fn numeric_rule_literal_matches(scalar: ScalarType, literal: &crate::ScalarLiteral) -> bool {
973    if let (ScalarType::Decimal { scale }, crate::ScalarLiteral::Decimal(value)) = (scalar, literal)
974    {
975        let value = value.normalize();
976        return value.scale() <= scale && value.scale_to_integer(scale).is_some();
977    }
978    scalar.accepts_literal(literal)
979}
980
981const fn exact_numeric_scalar(scalar: ScalarType) -> bool {
982    matches!(
983        scalar,
984        ScalarType::Decimal { .. }
985            | ScalarType::Int8
986            | ScalarType::Int16
987            | ScalarType::Int32
988            | ScalarType::Int64
989            | ScalarType::Int128
990            | ScalarType::Nat8
991            | ScalarType::Nat16
992            | ScalarType::Nat32
993            | ScalarType::Nat64
994            | ScalarType::Nat128
995    )
996}
997
998const fn numeric_scalar(scalar: ScalarType) -> bool {
999    matches!(
1000        scalar,
1001        ScalarType::Decimal { .. }
1002            | ScalarType::Float32
1003            | ScalarType::Float64
1004            | ScalarType::Int8
1005            | ScalarType::Int16
1006            | ScalarType::Int32
1007            | ScalarType::Int64
1008            | ScalarType::Int128
1009            | ScalarType::Nat8
1010            | ScalarType::Nat16
1011            | ScalarType::Nat32
1012            | ScalarType::Nat64
1013            | ScalarType::Nat128
1014            | ScalarType::U256
1015    )
1016}
1017
1018fn collect_named_type_references(r#type: &NamedTypeFragment, references: &mut ProposalReferences) {
1019    match r#type {
1020        NamedTypeFragment::Record(record) => {
1021            for field in record.fields() {
1022                collect_field_type_reference(field.field_type(), references);
1023            }
1024        }
1025        NamedTypeFragment::Enum(r#enum) => {
1026            for variant in r#enum.variants() {
1027                if let Some(payload) = variant.payload() {
1028                    collect_field_type_reference(payload, references);
1029                }
1030            }
1031        }
1032        NamedTypeFragment::Newtype { inner, .. }
1033        | NamedTypeFragment::List { item: inner, .. }
1034        | NamedTypeFragment::Set { item: inner, .. } => {
1035            collect_field_type_reference(inner, references);
1036        }
1037        NamedTypeFragment::Map { key, value, .. } => {
1038            collect_field_type_reference(key, references);
1039            collect_field_type_reference(value, references);
1040        }
1041        NamedTypeFragment::Tuple { members, .. } => {
1042            for member in members {
1043                collect_field_type_reference(member.field_type(), references);
1044            }
1045        }
1046    }
1047}
1048
1049#[cfg(test)]
1050mod u256_tests {
1051    use super::*;
1052    use crate::{ScalarLiteral, U256};
1053
1054    #[test]
1055    fn u256_targeted_ranges_admit_while_multiple_of_remains_deferred() {
1056        let shape = RuleTargetShape::Scalar(ScalarType::U256);
1057        assert!(operation_matches_target(
1058            &SourceRuleOperation::NumericRangeInclusive {
1059                min: ScalarLiteral::U256(U256::ZERO),
1060                max: ScalarLiteral::U256(U256::MAX),
1061            },
1062            shape,
1063        ));
1064        assert!(!operation_matches_target(
1065            &SourceRuleOperation::MultipleOf {
1066                divisor: ScalarLiteral::U256(U256::ONE),
1067            },
1068            shape,
1069        ));
1070    }
1071}
1072
1073fn collect_field_references(
1074    field: &FieldFragment,
1075    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
1076    references: &mut ProposalReferences,
1077) -> Result<(), SchemaContractError> {
1078    collect_field_type_reference(field.field_type(), references);
1079    if let crate::FieldInsertPolicy::Default(ScalarLiteral::EnumUnit { enum_type, variant }) =
1080        field.insert_policy()
1081    {
1082        let FieldType::Named(field_type) = field.field_type() else {
1083            return Err(SchemaContractError::LiteralTypeMismatch);
1084        };
1085        if field_type != enum_type {
1086            return Err(SchemaContractError::LiteralTypeMismatch);
1087        }
1088        collect_enum_literal_reference(enum_type, variant, types, references)?;
1089    }
1090    Ok(())
1091}
1092
1093fn collect_field_type_reference(field_type: &FieldType, references: &mut ProposalReferences) {
1094    match field_type {
1095        FieldType::List(item) => collect_field_type_reference(item, references),
1096        FieldType::Named(reference) => {
1097            references.types.insert(reference.clone());
1098        }
1099        FieldType::Scalar(_) => {}
1100    }
1101}
1102
1103fn collect_expression_enum_references(
1104    expression: &SourceCheckExpr,
1105    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
1106    references: &mut ProposalReferences,
1107) -> Result<(), SchemaContractError> {
1108    for instruction in expression.instructions() {
1109        if let SourceCheckInstruction::Literal(ScalarLiteral::EnumUnit { enum_type, variant }) =
1110            instruction
1111        {
1112            collect_enum_literal_reference(enum_type, variant, types, references)?;
1113        }
1114    }
1115    Ok(())
1116}
1117
1118fn collect_enum_literal_reference(
1119    enum_type: &TypeSourceKey,
1120    variant: &TypeSourceKey,
1121    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
1122    references: &mut ProposalReferences,
1123) -> Result<(), SchemaContractError> {
1124    references.types.insert(enum_type.clone());
1125    let Some(local) = types.get(enum_type) else {
1126        return Ok(());
1127    };
1128    let NamedTypeFragment::Enum(local) = local else {
1129        return Err(SchemaContractError::InvalidEnumLiteral);
1130    };
1131    if local
1132        .variants()
1133        .iter()
1134        .all(|candidate| candidate.source_key() != variant)
1135    {
1136        return Err(SchemaContractError::InvalidEnumLiteral);
1137    }
1138    Ok(())
1139}
1140
1141fn validate_local_relation_targets(
1142    source: &EntityFragment,
1143    entities: &BTreeMap<EntitySourceKey, &EntityFragment>,
1144    types: &BTreeMap<TypeSourceKey, &NamedTypeFragment>,
1145) -> Result<(), SchemaContractError> {
1146    for relation in source.relations() {
1147        let Some(target) = entities.get(relation.target_entity()) else {
1148            continue;
1149        };
1150        match relation.source() {
1151            RelationSourceFragment::Direct { fields } => {
1152                for (source_key, target_key) in fields.iter().zip(relation.target_fields()) {
1153                    let source_field = source
1154                        .fields()
1155                        .iter()
1156                        .find(|field| field.source_key() == source_key)
1157                        .ok_or(SchemaContractError::InvalidLocalReference)?;
1158                    let target_field = target
1159                        .fields()
1160                        .iter()
1161                        .find(|field| field.source_key() == target_key)
1162                        .ok_or(SchemaContractError::InvalidLocalReference)?;
1163                    let source_type = match source_field.field_type() {
1164                        FieldType::List(item) => item.as_ref(),
1165                        field_type => field_type,
1166                    };
1167                    if source_type != target_field.field_type() {
1168                        return Err(SchemaContractError::RelationTypeMismatch);
1169                    }
1170                }
1171            }
1172            RelationSourceFragment::Nested { .. } => {
1173                let terminal = validate_nested_relation_path(source, relation, types)?;
1174                let target_key = relation
1175                    .target_fields()
1176                    .first()
1177                    .ok_or(SchemaContractError::InvalidReferenceList)?;
1178                let target_field = target
1179                    .fields()
1180                    .iter()
1181                    .find(|field| field.source_key() == target_key)
1182                    .ok_or(SchemaContractError::InvalidLocalReference)?;
1183                if terminal != target_field.field_type() {
1184                    return Err(SchemaContractError::RelationTypeMismatch);
1185                }
1186            }
1187        }
1188    }
1189    Ok(())
1190}
1191
1192fn validate_nested_relation_path<'schema>(
1193    source: &'schema EntityFragment,
1194    relation: &RelationFragment,
1195    types: &BTreeMap<TypeSourceKey, &'schema NamedTypeFragment>,
1196) -> Result<&'schema FieldType, SchemaContractError> {
1197    let RelationSourceFragment::Nested { root, steps } = relation.source() else {
1198        return Err(SchemaContractError::RelationTypeMismatch);
1199    };
1200    let root = source
1201        .fields()
1202        .iter()
1203        .find(|field| field.source_key() == root)
1204        .ok_or(SchemaContractError::InvalidLocalReference)?;
1205    let mut kind = root.field_type();
1206    let mut nullable = root.nullable();
1207    let mut entered = None;
1208    for step in steps {
1209        match step {
1210            RelationPathStepFragment::OptionalSome => {
1211                if !nullable || entered.is_some() {
1212                    return Err(SchemaContractError::RelationTypeMismatch);
1213                }
1214                nullable = false;
1215            }
1216            RelationPathStepFragment::EnterNamed { r#type } => {
1217                let FieldType::Named(named) = kind else {
1218                    return Err(SchemaContractError::RelationTypeMismatch);
1219                };
1220                if nullable || entered.is_some() || named != r#type {
1221                    return Err(SchemaContractError::RelationTypeMismatch);
1222                }
1223                match types.get(named).copied() {
1224                    Some(
1225                        NamedTypeFragment::Record(_)
1226                        | NamedTypeFragment::Enum(_)
1227                        | NamedTypeFragment::List { .. }
1228                        | NamedTypeFragment::Set { .. }
1229                        | NamedTypeFragment::Map { .. },
1230                    ) => {
1231                        entered = Some(named);
1232                    }
1233                    Some(NamedTypeFragment::Newtype { inner, .. }) => {
1234                        kind = inner;
1235                    }
1236                    Some(NamedTypeFragment::Tuple { .. }) | None => {
1237                        return Err(SchemaContractError::RelationTypeMismatch);
1238                    }
1239                }
1240            }
1241            RelationPathStepFragment::RecordMember { record, field } => {
1242                if nullable || entered != Some(record) {
1243                    return Err(SchemaContractError::RelationTypeMismatch);
1244                }
1245                let Some(NamedTypeFragment::Record(definition)) = types.get(record).copied() else {
1246                    return Err(SchemaContractError::RelationTypeMismatch);
1247                };
1248                let member = definition
1249                    .fields()
1250                    .iter()
1251                    .find(|member| member.source_key() == field)
1252                    .ok_or(SchemaContractError::InvalidLocalReference)?;
1253                kind = member.field_type();
1254                nullable = member.nullable();
1255                entered = None;
1256            }
1257            RelationPathStepFragment::EnumVariantPayload { r#enum, variant } => {
1258                if nullable || entered != Some(r#enum) {
1259                    return Err(SchemaContractError::RelationTypeMismatch);
1260                }
1261                let Some(NamedTypeFragment::Enum(definition)) = types.get(r#enum).copied() else {
1262                    return Err(SchemaContractError::RelationTypeMismatch);
1263                };
1264                let payload = definition
1265                    .variants()
1266                    .iter()
1267                    .find(|candidate| candidate.source_key() == variant)
1268                    .and_then(EnumVariantFragment::payload)
1269                    .ok_or(SchemaContractError::InvalidLocalReference)?;
1270                kind = payload;
1271                nullable = false;
1272                entered = None;
1273            }
1274            RelationPathStepFragment::ListItems
1275            | RelationPathStepFragment::SetItems
1276            | RelationPathStepFragment::MapValues => {
1277                if nullable {
1278                    return Err(SchemaContractError::RelationTypeMismatch);
1279                }
1280                kind = relation_collection_step_output(kind, entered.take(), step, types)?;
1281                nullable = false;
1282            }
1283        }
1284    }
1285    if nullable || entered.is_some() || !matches!(kind, FieldType::Scalar(_)) {
1286        return Err(SchemaContractError::RelationTypeMismatch);
1287    }
1288    Ok(kind)
1289}
1290
1291fn relation_collection_step_output<'schema>(
1292    kind: &'schema FieldType,
1293    entered: Option<&TypeSourceKey>,
1294    step: &RelationPathStepFragment,
1295    types: &BTreeMap<TypeSourceKey, &'schema NamedTypeFragment>,
1296) -> Result<&'schema FieldType, SchemaContractError> {
1297    match (step, entered, kind) {
1298        (RelationPathStepFragment::ListItems, None, FieldType::List(item)) => Ok(item),
1299        (RelationPathStepFragment::ListItems, Some(named), FieldType::Named(current))
1300            if named == current =>
1301        {
1302            let Some(NamedTypeFragment::List { item, .. }) = types.get(current).copied() else {
1303                return Err(SchemaContractError::RelationTypeMismatch);
1304            };
1305            Ok(item)
1306        }
1307        (RelationPathStepFragment::SetItems, Some(named), FieldType::Named(current))
1308            if named == current =>
1309        {
1310            let Some(NamedTypeFragment::Set { item, .. }) = types.get(current).copied() else {
1311                return Err(SchemaContractError::RelationTypeMismatch);
1312            };
1313            Ok(item)
1314        }
1315        (RelationPathStepFragment::MapValues, Some(named), FieldType::Named(current))
1316            if named == current =>
1317        {
1318            let Some(NamedTypeFragment::Map { value, .. }) = types.get(current).copied() else {
1319                return Err(SchemaContractError::RelationTypeMismatch);
1320            };
1321            Ok(value)
1322        }
1323        _ => Err(SchemaContractError::RelationTypeMismatch),
1324    }
1325}
1326
1327fn ensure_no_adjacent_duplicates<T>(values: &[T]) -> Result<(), SchemaContractError>
1328where
1329    T: Eq,
1330{
1331    if values.windows(2).any(|pair| pair[0] == pair[1]) {
1332        return Err(SchemaContractError::DuplicateSourceKey);
1333    }
1334    Ok(())
1335}
1336
1337fn ensure_no_adjacent_duplicates_by<T, K>(
1338    values: &[T],
1339    key: impl Fn(&T) -> &K,
1340) -> Result<(), SchemaContractError>
1341where
1342    K: Eq,
1343{
1344    if values.windows(2).any(|pair| key(&pair[0]) == key(&pair[1])) {
1345        return Err(SchemaContractError::DuplicateSourceKey);
1346    }
1347    Ok(())
1348}
1349
1350#[cfg(test)]
1351mod tests {
1352    use super::*;
1353    use crate::decode_schema_proposal;
1354    use std::str::FromStr;
1355
1356    fn empty_proposal_with(capabilities: Vec<SchemaCapability>) -> SchemaProposal {
1357        SchemaProposal::try_compose(
1358            capabilities,
1359            TargetDatabaseIdentity::from_bytes([1; 32]),
1360            SchemaSubmissionKey::try_new("proposal-version-test")
1361                .expect("submission key should admit"),
1362            ExpectedAcceptedHead::Empty,
1363            Vec::new(),
1364            Vec::new(),
1365            Vec::new(),
1366            None,
1367        )
1368        .expect("empty proposal should compose")
1369    }
1370
1371    #[test]
1372    fn decoded_future_contract_version_fails_typed() {
1373        let proposal = empty_proposal_with(Vec::new());
1374        let mut bytes = crate::encode_schema_proposal(&proposal).expect("proposal should encode");
1375        bytes[5..7].copy_from_slice(&2_u16.to_be_bytes());
1376
1377        assert_eq!(
1378            decode_schema_proposal(&bytes),
1379            Err(SchemaContractError::UnsupportedVersion {
1380                found: 2,
1381                supported: 1,
1382            }),
1383        );
1384    }
1385
1386    #[test]
1387    fn decoded_unknown_capability_fails_typed() {
1388        let proposal = empty_proposal_with(vec![SchemaCapability::EXACT_COMPOSITE_TYPES]);
1389        let mut bytes = crate::encode_schema_proposal(&proposal).expect("proposal should encode");
1390        bytes[11..13].copy_from_slice(&u16::MAX.to_be_bytes());
1391
1392        assert_eq!(
1393            decode_schema_proposal(&bytes),
1394            Err(SchemaContractError::UnsupportedCapability),
1395        );
1396    }
1397
1398    #[test]
1399    fn targeted_numeric_operations_require_exact_target_literal_admission() {
1400        let decimal = |value| {
1401            crate::ScalarLiteral::Decimal(
1402                crate::Decimal::from_str(value).expect("decimal fixture should parse"),
1403            )
1404        };
1405        assert!(operation_matches_target(
1406            &SourceRuleOperation::MultipleOf {
1407                divisor: decimal("0.25"),
1408            },
1409            RuleTargetShape::Scalar(ScalarType::Decimal { scale: 2 }),
1410        ));
1411        assert!(!operation_matches_target(
1412            &SourceRuleOperation::MultipleOf {
1413                divisor: decimal("0.251"),
1414            },
1415            RuleTargetShape::Scalar(ScalarType::Decimal { scale: 2 }),
1416        ));
1417        assert!(!operation_matches_target(
1418            &SourceRuleOperation::MultipleOf {
1419                divisor: crate::ScalarLiteral::Float64(
1420                    crate::Float64::try_new(2.0).expect("finite float"),
1421                ),
1422            },
1423            RuleTargetShape::Scalar(ScalarType::Float64),
1424        ));
1425        assert!(operation_matches_target(
1426            &SourceRuleOperation::NumericMaximumInclusive {
1427                value: crate::ScalarLiteral::Float64(
1428                    crate::Float64::try_new(2.0).expect("finite float"),
1429                ),
1430            },
1431            RuleTargetShape::Scalar(ScalarType::Float64),
1432        ));
1433    }
1434}