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