1use 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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
22#[repr(transparent)]
23pub struct ProposalContractVersion(u16);
24
25impl ProposalContractVersion {
26 pub const CURRENT: Self = Self(1);
28
29 #[must_use]
31 pub const fn from_raw(value: u16) -> Self {
32 Self(value)
33 }
34
35 #[must_use]
37 pub const fn get(self) -> u16 {
38 self.0
39 }
40}
41
42#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
44#[repr(transparent)]
45pub struct SchemaCapability(u16);
46
47impl SchemaCapability {
48 pub const EXACT_COMPOSITE_TYPES: Self = Self(1);
50 pub const ACCEPTED_CHECKS: Self = Self(2);
52 pub const SECONDARY_INDEXES: Self = Self(3);
54 pub const RESTRICTIVE_RELATIONS: Self = Self(4);
56 pub const INSERT_DEFAULTS: Self = Self(5);
58 pub const GENERATED_VALUES: Self = Self(6);
60 pub const MANAGED_TIMESTAMPS: Self = Self(7);
62 pub const VERSIONED_MIGRATIONS: Self = Self(8);
64
65 #[must_use]
67 pub const fn from_raw(value: u16) -> Self {
68 Self(value)
69 }
70
71 #[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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
84pub enum ExpectedAcceptedHead {
85 Empty,
87 Exact {
89 revision: u64,
91 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#[derive(Clone, Debug, Eq, PartialEq)]
107pub struct EntityStoreAssignment {
108 entity: EntitySourceKey,
109 store: TargetStoreIdentity,
110}
111
112impl EntityStoreAssignment {
113 #[must_use]
115 pub const fn new(entity: EntitySourceKey, store: TargetStoreIdentity) -> Self {
116 Self { entity, store }
117 }
118
119 #[must_use]
121 pub const fn entity(&self) -> &EntitySourceKey {
122 &self.entity
123 }
124
125 #[must_use]
127 pub const fn store(&self) -> TargetStoreIdentity {
128 self.store
129 }
130}
131
132#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
134pub enum SchemaRemoval {
135 Entity(EntitySourceKey),
137 Field {
139 entity: EntitySourceKey,
141 field: FieldSourceKey,
143 },
144 Type(TypeSourceKey),
146 Constraint {
148 entity: EntitySourceKey,
150 constraint: ConstraintSourceKey,
152 },
153 Index {
155 entity: EntitySourceKey,
157 index: IndexSourceKey,
159 },
160 Relation {
162 entity: EntitySourceKey,
164 relation: RelationSourceKey,
166 },
167}
168
169#[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 #[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 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 #[must_use]
366 pub const fn version(&self) -> ProposalContractVersion {
367 self.version
368 }
369
370 #[must_use]
372 pub fn capabilities(&self) -> &[SchemaCapability] {
373 &self.capabilities
374 }
375
376 #[must_use]
378 pub const fn target_database(&self) -> TargetDatabaseIdentity {
379 self.target_database
380 }
381
382 #[must_use]
384 pub const fn submission_key(&self) -> &SchemaSubmissionKey {
385 &self.submission_key
386 }
387
388 #[must_use]
390 pub const fn expected_head(&self) -> &ExpectedAcceptedHead {
391 &self.expected_head
392 }
393
394 #[must_use]
396 pub fn fragments(&self) -> &[SchemaFragment] {
397 &self.fragments
398 }
399
400 #[must_use]
402 pub fn assignments(&self) -> &[EntityStoreAssignment] {
403 &self.assignments
404 }
405
406 #[must_use]
408 pub fn removals(&self) -> &[SchemaRemoval] {
409 &self.removals
410 }
411
412 #[must_use]
414 pub const fn migration(&self) -> Option<&SchemaMigrationPlan> {
415 self.migration.as_ref()
416 }
417
418 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 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 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 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}