1use std::{
10 collections::{BTreeMap, BTreeSet},
11 str::FromStr,
12};
13
14use icydb_schema::{
15 Account, Blob, ConstraintFragment, Date, Decimal, DeclaredEntityVersion, Duration,
16 EntityFragment, EntitySourceKey, EnumTypeFragment, EnumVariantFragment, FieldFragment,
17 FieldInsertPolicy, FieldManagementPolicy, FieldSourceKey, FieldType, Float32, Float64,
18 IndexFragment, IndexKeyFragment, NamedTypeFragment, Principal, RecordFieldFragment,
19 RecordTypeFragment, RelationDeleteAction, RelationFragment, RelationPathStepFragment,
20 RelationSourceFragment, RuleSourceKey, ScalarLiteral, ScalarType, SchemaContractError,
21 SchemaFragment, SchemaName, SourceRuleOperation as ProposalSourceRuleOperation, Subaccount,
22 TargetedRuleFragment, Timestamp, TupleElementFragment, TypeSourceKey, U256, Ulid, Unit,
23};
24use thiserror::Error;
25
26use crate::{
27 node::{
28 Arg, ArgNumber, Canister, CheckConstraint, Entity, Enum, Field, FieldWriteManagement,
29 Index, IndexExpression, IndexKeyItem, IndexKeyItemsRef, Item, ItemTarget, List, Map,
30 Record, RelationEdge, RuleNumber, Schema, SchemaNode, Set, SourceRule,
31 SourceRuleAuthoringOperation, Store, Tuple, Value,
32 },
33 types::{Cardinality, Primitive},
34};
35
36#[derive(Debug, Error)]
39pub enum FragmentLoweringError {
40 #[error("schema canister has no registered stores: {0}")]
42 CanisterHasNoStores(String),
43
44 #[error("schema canister path is not registered: {0}")]
46 CanisterNotFound(String),
47
48 #[error(transparent)]
50 Contract(#[from] SchemaContractError),
51
52 #[error("schema graph must be sealed before fragment lowering")]
54 GraphNotSealed,
55
56 #[error("schema field default cannot be lowered: {0}")]
58 InvalidDefault(String),
59
60 #[error("schema fragment reference is invalid: {0}")]
62 InvalidReference(String),
63
64 #[error("schema value cardinality is unsupported at {0}")]
66 UnsupportedCardinality(String),
67}
68
69impl Schema {
74 pub fn schema_fragment_for_canister(
85 &self,
86 canister_path: &str,
87 ) -> Result<SchemaFragment, FragmentLoweringError> {
88 if !self.is_sealed() {
89 return Err(FragmentLoweringError::GraphNotSealed);
90 }
91 self.cast_node::<Canister>(canister_path)
92 .map_err(|_| FragmentLoweringError::CanisterNotFound(canister_path.to_string()))?;
93 let stores = self
94 .filter_nodes::<Store>(|store| store.canister() == canister_path)
95 .map(|(path, _)| path.to_string())
96 .collect::<BTreeSet<_>>();
97 if stores.is_empty() {
98 return Err(FragmentLoweringError::CanisterHasNoStores(
99 canister_path.to_string(),
100 ));
101 }
102
103 let entities = self
104 .get_nodes::<Entity>()
105 .filter(|(_, entity)| stores.contains(entity.store()))
106 .map(|(_, entity)| entity)
107 .collect::<Vec<_>>();
108 let selected_entities = entities
109 .iter()
110 .map(|entity| entity.def().path())
111 .collect::<BTreeSet<_>>();
112 for entity in &entities {
113 ensure_relation_targets_in_database(self, entity, &selected_entities)?;
114 }
115
116 let mut pending_types = Vec::new();
117 let entity_fragments = entities
118 .iter()
119 .map(|entity| lower_entity(self, entity, &mut pending_types))
120 .collect::<Result<Vec<_>, _>>()?;
121 let types = lower_reachable_types(self, pending_types)?;
122
123 SchemaFragment::try_new(entity_fragments, types).map_err(Into::into)
124 }
125}
126
127fn ensure_relation_targets_in_database(
128 schema: &Schema,
129 entity: &Entity,
130 selected_entities: &BTreeSet<String>,
131) -> Result<(), FragmentLoweringError> {
132 let mut targets = entity
133 .relations()
134 .iter()
135 .map(RelationEdge::target)
136 .collect::<Vec<_>>();
137 for field in entity.fields().fields() {
138 collect_relation_targets(
139 schema,
140 field.value().item(),
141 &mut BTreeSet::new(),
142 &mut targets,
143 )?;
144 }
145 for target in targets {
146 schema
147 .cast_node::<Entity>(target)
148 .map_err(|_| FragmentLoweringError::InvalidReference(target.to_string()))?;
149 if !selected_entities.contains(target) {
150 return Err(FragmentLoweringError::InvalidReference(format!(
151 "relation target '{target}' is outside the selected database"
152 )));
153 }
154 }
155 Ok(())
156}
157
158fn collect_relation_targets<'schema>(
159 schema: &'schema Schema,
160 item: &'schema Item,
161 visiting: &mut BTreeSet<String>,
162 targets: &mut Vec<&'schema str>,
163) -> Result<(), FragmentLoweringError> {
164 if let Some(target) = item.relation() {
165 targets.push(target);
166 }
167 let ItemTarget::Is(path) = item.target() else {
168 return Ok(());
169 };
170 if !visiting.insert((*path).to_string()) {
171 return Ok(());
172 }
173 let node = schema
174 .get_node(path)
175 .ok_or_else(|| FragmentLoweringError::InvalidReference((*path).to_string()))?;
176 match node {
177 SchemaNode::Newtype(value) => {
178 collect_relation_targets(schema, value.item(), visiting, targets)?;
179 }
180 SchemaNode::Record(value) => {
181 for field in value.fields().fields() {
182 collect_relation_targets(schema, field.value().item(), visiting, targets)?;
183 }
184 }
185 SchemaNode::Enum(value) => {
186 for variant in value.variants() {
187 if let Some(value) = variant.value() {
188 collect_relation_targets(schema, value.item(), visiting, targets)?;
189 }
190 }
191 }
192 SchemaNode::List(value) => {
193 collect_relation_targets(schema, value.item(), visiting, targets)?;
194 }
195 SchemaNode::Set(value) => {
196 collect_relation_targets(schema, value.item(), visiting, targets)?;
197 }
198 SchemaNode::Map(value) => {
199 collect_relation_targets(schema, value.key(), visiting, targets)?;
200 collect_relation_targets(schema, value.value().item(), visiting, targets)?;
201 }
202 SchemaNode::Tuple(value) => {
203 for member in value.values() {
204 collect_relation_targets(schema, member.item(), visiting, targets)?;
205 }
206 }
207 SchemaNode::Canister(_)
208 | SchemaNode::Entity(_)
209 | SchemaNode::Normalizer(_)
210 | SchemaNode::Store(_)
211 | SchemaNode::Validator(_) => {}
212 }
213 visiting.remove(*path);
214 Ok(())
215}
216
217fn lower_entity(
218 schema: &Schema,
219 entity: &Entity,
220 pending_types: &mut Vec<String>,
221) -> Result<EntityFragment, FragmentLoweringError> {
222 let fields = entity
223 .fields()
224 .fields()
225 .iter()
226 .map(|field| lower_entity_field(schema, field, pending_types))
227 .collect::<Result<Vec<_>, _>>()?;
228 let primary_key = entity
229 .primary_key()
230 .fields()
231 .iter()
232 .map(|name| entity_field_source_key(entity, name))
233 .collect::<Result<Vec<_>, _>>()?;
234 let indexes = entity
235 .indexes()
236 .iter()
237 .map(|index| lower_index(schema, entity, index))
238 .collect::<Result<Vec<_>, _>>()?;
239 let mut relations = entity
240 .fields()
241 .fields()
242 .iter()
243 .filter(|field| field.value().item().relation().is_some())
244 .map(|field| lower_scalar_relation(schema, entity, field))
245 .collect::<Result<Vec<_>, _>>()?;
246 for field in entity.fields().fields() {
247 relations.extend(lower_nested_relations(schema, entity, field)?);
248 }
249 relations.extend(
250 entity
251 .relations()
252 .iter()
253 .map(|relation| lower_composite_relation(schema, entity, relation))
254 .collect::<Result<Vec<_>, _>>()?,
255 );
256 let mut constraints = entity
257 .constraints()
258 .iter()
259 .map(|constraint| lower_constraint(schema, constraint))
260 .collect::<Result<Vec<_>, _>>()?;
261 for field in entity.fields().fields() {
262 constraints.extend(lower_field_rules(schema, field)?);
263 }
264
265 EntityFragment::try_new(
266 SchemaName::try_new(entity.name())?,
267 DeclaredEntityVersion::try_new(entity.schema_version())?,
268 fields,
269 primary_key,
270 indexes,
271 relations,
272 constraints,
273 )
274 .map_err(Into::into)
275}
276
277struct NestedRelationCandidate<'schema> {
278 path_name: String,
279 steps: Vec<RelationPathStepFragment>,
280 target: &'schema str,
281}
282
283fn lower_nested_relations(
284 schema: &Schema,
285 entity: &Entity,
286 root: &Field,
287) -> Result<Vec<RelationFragment>, FragmentLoweringError> {
288 if root.value().item().relation().is_some() {
289 return Ok(Vec::new());
290 }
291 let mut steps = Vec::new();
292 if root.value().cardinality() == Cardinality::Opt {
293 steps.push(RelationPathStepFragment::OptionalSome);
294 } else if root.value().cardinality() == Cardinality::Many {
295 steps.push(RelationPathStepFragment::ListItems);
296 }
297 let mut candidates = Vec::new();
298 collect_nested_relation_candidates(
299 schema,
300 root.value().item(),
301 root.name().to_string(),
302 steps,
303 &mut BTreeSet::new(),
304 &mut candidates,
305 )?;
306 candidates
307 .into_iter()
308 .map(|candidate| {
309 let target = schema.cast_node::<Entity>(candidate.target).map_err(|_| {
310 FragmentLoweringError::InvalidReference(candidate.target.to_string())
311 })?;
312 RelationFragment::try_new(
313 SchemaName::try_new(candidate.path_name)?,
314 RelationSourceFragment::Nested {
315 root: entity_field_source_key(entity, root.name())?,
316 steps: candidate.steps,
317 },
318 EntitySourceKey::try_new(target.name())?,
319 target
320 .primary_key()
321 .fields()
322 .iter()
323 .map(|field| entity_field_source_key(target, field))
324 .collect::<Result<Vec<_>, _>>()?,
325 RelationDeleteAction::Restrict,
326 )
327 .map_err(Into::into)
328 })
329 .collect()
330}
331
332#[expect(
333 clippy::too_many_lines,
334 reason = "one bounded recursive walk keeps every admitted structural node and exclusion adjacent"
335)]
336fn collect_nested_relation_candidates<'schema>(
337 schema: &'schema Schema,
338 item: &'schema Item,
339 path_name: String,
340 steps: Vec<RelationPathStepFragment>,
341 visiting: &mut BTreeSet<String>,
342 candidates: &mut Vec<NestedRelationCandidate<'schema>>,
343) -> Result<(), FragmentLoweringError> {
344 if let Some(target) = item.relation() {
345 let ItemTarget::Primitive(_) = item.target() else {
346 return Err(FragmentLoweringError::InvalidReference(path_name));
347 };
348 candidates.push(NestedRelationCandidate {
349 path_name,
350 steps,
351 target,
352 });
353 return Ok(());
354 }
355 let ItemTarget::Is(path) = item.target() else {
356 return Ok(());
357 };
358 if !visiting.insert((*path).to_string()) {
359 if item_reaches_relation(schema, item, &mut BTreeSet::new())? {
364 return Err(FragmentLoweringError::UnsupportedCardinality(path_name));
365 }
366 return Ok(());
367 }
368 let node = schema
369 .get_node(path)
370 .ok_or_else(|| FragmentLoweringError::InvalidReference((*path).to_string()))?;
371 let type_key = TypeSourceKey::try_new(
372 named_type_name(node)
373 .ok_or_else(|| FragmentLoweringError::InvalidReference((*path).to_string()))?,
374 )?;
375 let mut entered = steps;
376 entered.push(RelationPathStepFragment::EnterNamed {
377 r#type: type_key.clone(),
378 });
379 match node {
380 SchemaNode::Newtype(value) => collect_nested_relation_candidates(
381 schema,
382 value.item(),
383 path_name,
384 entered,
385 visiting,
386 candidates,
387 )?,
388 SchemaNode::Record(value) => {
389 for field in value.fields().fields() {
390 let mut member_steps = entered.clone();
391 member_steps.push(RelationPathStepFragment::RecordMember {
392 record: type_key.clone(),
393 field: FieldSourceKey::try_new(field.name())?,
394 });
395 match field.value().cardinality() {
396 Cardinality::One => {}
397 Cardinality::Opt => {
398 member_steps.push(RelationPathStepFragment::OptionalSome);
399 }
400 Cardinality::Many => {
401 member_steps.push(RelationPathStepFragment::ListItems);
402 }
403 }
404 collect_nested_relation_candidates(
405 schema,
406 field.value().item(),
407 format!("{path_name}.{}", field.name()),
408 member_steps,
409 visiting,
410 candidates,
411 )?;
412 }
413 }
414 SchemaNode::Enum(value) => {
415 for variant in value.variants() {
416 let Some(payload) = variant.value() else {
417 continue;
418 };
419 let mut variant_steps = entered.clone();
420 variant_steps.push(RelationPathStepFragment::EnumVariantPayload {
421 r#enum: type_key.clone(),
422 variant: TypeSourceKey::try_new(variant.name())?,
423 });
424 match payload.cardinality() {
425 Cardinality::One => {}
426 Cardinality::Opt => {
427 variant_steps.push(RelationPathStepFragment::OptionalSome);
428 }
429 Cardinality::Many => {
430 variant_steps.push(RelationPathStepFragment::ListItems);
431 }
432 }
433 collect_nested_relation_candidates(
434 schema,
435 payload.item(),
436 format!("{path_name}.{}", variant.name()),
437 variant_steps,
438 visiting,
439 candidates,
440 )?;
441 }
442 }
443 SchemaNode::List(list) => {
444 let mut item_steps = entered;
445 item_steps.push(RelationPathStepFragment::ListItems);
446 collect_nested_relation_candidates(
447 schema,
448 list.item(),
449 format!("{path_name}.items"),
450 item_steps,
451 visiting,
452 candidates,
453 )?;
454 }
455 SchemaNode::Set(set) => {
456 let mut item_steps = entered;
457 item_steps.push(RelationPathStepFragment::SetItems);
458 collect_nested_relation_candidates(
459 schema,
460 set.item(),
461 format!("{path_name}.items"),
462 item_steps,
463 visiting,
464 candidates,
465 )?;
466 }
467 SchemaNode::Map(map) => {
468 let mut value_steps = entered;
469 value_steps.push(RelationPathStepFragment::MapValues);
470 match map.value().cardinality() {
471 Cardinality::One => {}
472 Cardinality::Opt => {
473 value_steps.push(RelationPathStepFragment::OptionalSome);
474 }
475 Cardinality::Many => {
476 value_steps.push(RelationPathStepFragment::ListItems);
477 }
478 }
479 collect_nested_relation_candidates(
480 schema,
481 map.value().item(),
482 format!("{path_name}.values"),
483 value_steps,
484 visiting,
485 candidates,
486 )?;
487 }
488 SchemaNode::Tuple(_) => {
489 if item_reaches_relation(schema, item, &mut BTreeSet::new())? {
490 return Err(FragmentLoweringError::UnsupportedCardinality(path_name));
491 }
492 }
493 SchemaNode::Canister(_)
494 | SchemaNode::Entity(_)
495 | SchemaNode::Normalizer(_)
496 | SchemaNode::Store(_)
497 | SchemaNode::Validator(_) => {
498 return Err(FragmentLoweringError::InvalidReference((*path).to_string()));
499 }
500 }
501 visiting.remove(*path);
502 Ok(())
503}
504
505fn item_reaches_relation(
506 schema: &Schema,
507 item: &Item,
508 visiting: &mut BTreeSet<String>,
509) -> Result<bool, FragmentLoweringError> {
510 if item.relation().is_some() {
511 return Ok(true);
512 }
513 let mut targets = Vec::new();
514 collect_relation_targets(schema, item, visiting, &mut targets)?;
515 Ok(!targets.is_empty())
516}
517
518fn lower_entity_field(
519 schema: &Schema,
520 field: &Field,
521 pending_types: &mut Vec<String>,
522) -> Result<FieldFragment, FragmentLoweringError> {
523 let field_type = lower_value_type(schema, field.value(), pending_types)?;
524 let nullable = field.value().cardinality() == Cardinality::Opt;
525 let insert_policy = if field.generated().is_some() {
526 FieldInsertPolicy::Generated
527 } else if let Some(default) = field.default() {
528 FieldInsertPolicy::Default(lower_default(schema, field, default)?)
529 } else if nullable {
530 FieldInsertPolicy::Nullable
531 } else {
532 FieldInsertPolicy::Required
533 };
534 let management = match field.write_management() {
535 Some(FieldWriteManagement::CreatedAt) => Some(FieldManagementPolicy::CreatedAt),
536 Some(FieldWriteManagement::UpdatedAt) => Some(FieldManagementPolicy::UpdatedAt),
537 None => None,
538 };
539 Ok(FieldFragment::new(
540 SchemaName::try_new(field.name())?,
541 field_type,
542 nullable,
543 insert_policy,
544 management,
545 ))
546}
547
548fn lower_index(
549 schema: &Schema,
550 entity: &Entity,
551 index: &Index,
552) -> Result<IndexFragment, FragmentLoweringError> {
553 let key = match index.key_items() {
554 IndexKeyItemsRef::Fields(fields) => fields
555 .iter()
556 .map(|field| entity_field_source_key(entity, field).map(IndexKeyFragment::Field))
557 .collect::<Result<Vec<_>, _>>()?,
558 IndexKeyItemsRef::Items(items) => items
559 .iter()
560 .map(|item| lower_index_key(entity, item))
561 .collect::<Result<Vec<_>, _>>()?,
562 };
563 IndexFragment::try_new(
564 SchemaName::try_new(index.name())?,
565 key,
566 index.is_unique(),
567 index.source_predicate(schema)?,
568 )
569 .map_err(Into::into)
570}
571
572fn lower_index_key(
573 entity: &Entity,
574 item: &IndexKeyItem,
575) -> Result<IndexKeyFragment, FragmentLoweringError> {
576 let field = entity_field_source_key(entity, item.field())?;
577 Ok(match item {
578 IndexKeyItem::Field(_) => IndexKeyFragment::Field(field),
579 IndexKeyItem::Expression(IndexExpression::Lower(_)) => IndexKeyFragment::Lower(field),
580 IndexKeyItem::Expression(IndexExpression::Upper(_)) => IndexKeyFragment::Upper(field),
581 IndexKeyItem::Expression(IndexExpression::Trim(_)) => IndexKeyFragment::Trim(field),
582 IndexKeyItem::Expression(IndexExpression::LowerTrim(_)) => {
583 IndexKeyFragment::LowerTrim(field)
584 }
585 IndexKeyItem::Expression(IndexExpression::Date(_)) => IndexKeyFragment::Date(field),
586 IndexKeyItem::Expression(IndexExpression::Year(_)) => IndexKeyFragment::Year(field),
587 IndexKeyItem::Expression(IndexExpression::Month(_)) => IndexKeyFragment::Month(field),
588 IndexKeyItem::Expression(IndexExpression::Day(_)) => IndexKeyFragment::Day(field),
589 })
590}
591
592fn lower_scalar_relation(
593 schema: &Schema,
594 entity: &Entity,
595 field: &Field,
596) -> Result<RelationFragment, FragmentLoweringError> {
597 let target_path = field
598 .value()
599 .item()
600 .relation()
601 .ok_or_else(|| FragmentLoweringError::InvalidReference(field.name().to_string()))?;
602 let target = schema
603 .cast_node::<Entity>(target_path)
604 .map_err(|_| FragmentLoweringError::InvalidReference(target_path.to_string()))?;
605 RelationFragment::try_new(
606 SchemaName::try_new(field.name())?,
607 RelationSourceFragment::direct(vec![entity_field_source_key(entity, field.name())?]),
608 EntitySourceKey::try_new(target.name())?,
609 target
610 .primary_key()
611 .fields()
612 .iter()
613 .map(|field| entity_field_source_key(target, field))
614 .collect::<Result<Vec<_>, _>>()?,
615 RelationDeleteAction::Restrict,
616 )
617 .map_err(Into::into)
618}
619
620fn lower_composite_relation(
621 schema: &Schema,
622 entity: &Entity,
623 relation: &RelationEdge,
624) -> Result<RelationFragment, FragmentLoweringError> {
625 let target = schema
626 .cast_node::<Entity>(relation.target())
627 .map_err(|_| FragmentLoweringError::InvalidReference(relation.target().to_string()))?;
628 RelationFragment::try_new(
629 SchemaName::try_new(relation.name())?,
630 RelationSourceFragment::direct(
631 relation
632 .local_fields()
633 .iter()
634 .map(|field| entity_field_source_key(entity, field))
635 .collect::<Result<Vec<_>, _>>()?,
636 ),
637 EntitySourceKey::try_new(target.name())?,
638 target
639 .primary_key()
640 .fields()
641 .iter()
642 .map(|field| entity_field_source_key(target, field))
643 .collect::<Result<Vec<_>, _>>()?,
644 RelationDeleteAction::Restrict,
645 )
646 .map_err(Into::into)
647}
648
649fn lower_constraint(
650 schema: &Schema,
651 constraint: &CheckConstraint,
652) -> Result<ConstraintFragment, FragmentLoweringError> {
653 Ok(ConstraintFragment::check(
654 SchemaName::try_new(constraint.name())?,
655 constraint.source_expression(schema)?,
656 ))
657}
658
659fn lower_field_rules(
660 schema: &Schema,
661 field: &Field,
662) -> Result<Vec<ConstraintFragment>, FragmentLoweringError> {
663 let field_source = FieldSourceKey::try_new(field.name())?;
664 reachable_source_rules(schema, field.value().item())?
665 .into_iter()
666 .map(|(target_type, rule, target)| {
667 let operation = lower_source_rule_operation(schema, target, rule)?;
668 Ok(ConstraintFragment::targeted_rule(
669 TargetedRuleFragment::new(
670 field_source.clone(),
671 target_type,
672 SchemaName::try_new(rule.name())?,
673 operation,
674 ),
675 ))
676 })
677 .collect()
678}
679
680type ReachableSourceRule<'schema> = (
681 TypeSourceKey,
682 &'schema SourceRule,
683 &'schema crate::node::SchemaNode,
684);
685
686fn reachable_source_rules<'schema>(
687 schema: &'schema Schema,
688 item: &Item,
689) -> Result<Vec<ReachableSourceRule<'schema>>, FragmentLoweringError> {
690 let mut pending = Vec::new();
691 push_item_reference(item, &mut pending);
692 let mut visited = BTreeSet::new();
693 let mut rules = BTreeMap::new();
694 while let Some(path) = pending.pop() {
695 let node = schema
696 .get_node(path.as_str())
697 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.clone()))?;
698 let target_type = TypeSourceKey::try_new(
699 named_type_name(node)
700 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.clone()))?,
701 )?;
702 if !visited.insert(target_type.clone()) {
703 continue;
704 }
705 for rule in schema_node_type(node)?.rules() {
706 let key = (target_type.clone(), RuleSourceKey::try_new(rule.name())?);
707 if rules.insert(key, (rule, node)).is_some() {
708 return Err(FragmentLoweringError::InvalidReference(format!(
709 "duplicate durable rule '{}' on type '{}'",
710 rule.name(),
711 target_type
712 )));
713 }
714 }
715 push_schema_node_references(node, &mut pending);
716 }
717 Ok(rules
718 .into_iter()
719 .map(|((target_type, _), (rule, node))| (target_type, rule, node))
720 .collect())
721}
722
723fn schema_node_type(node: &SchemaNode) -> Result<&crate::node::Type, FragmentLoweringError> {
724 match node {
725 SchemaNode::Newtype(node) => Ok(node.ty()),
726 SchemaNode::Record(node) => Ok(node.ty()),
727 SchemaNode::Enum(node) => Ok(node.ty()),
728 SchemaNode::List(node) => Ok(node.ty()),
729 SchemaNode::Map(node) => Ok(node.ty()),
730 SchemaNode::Set(node) => Ok(node.ty()),
731 SchemaNode::Tuple(node) => Ok(node.ty()),
732 SchemaNode::Canister(_)
733 | SchemaNode::Entity(_)
734 | SchemaNode::Normalizer(_)
735 | SchemaNode::Store(_)
736 | SchemaNode::Validator(_) => Err(FragmentLoweringError::InvalidReference(
737 "durable-rule target is not a named type".to_string(),
738 )),
739 }
740}
741
742fn push_schema_node_references(node: &SchemaNode, pending: &mut Vec<String>) {
743 match node {
744 SchemaNode::Newtype(newtype) => push_item_reference(newtype.item(), pending),
745 SchemaNode::Record(record) => {
746 for field in record.fields().fields() {
747 push_item_reference(field.value().item(), pending);
748 }
749 }
750 SchemaNode::Enum(r#enum) => {
751 for value in r#enum
752 .variants()
753 .iter()
754 .filter_map(crate::node::EnumVariant::value)
755 {
756 push_item_reference(value.item(), pending);
757 }
758 }
759 SchemaNode::List(list) => push_item_reference(list.item(), pending),
760 SchemaNode::Map(map) => {
761 push_item_reference(map.key(), pending);
762 push_item_reference(map.value().item(), pending);
763 }
764 SchemaNode::Set(set) => push_item_reference(set.item(), pending),
765 SchemaNode::Tuple(tuple) => {
766 for value in tuple.values() {
767 push_item_reference(value.item(), pending);
768 }
769 }
770 SchemaNode::Canister(_)
771 | SchemaNode::Entity(_)
772 | SchemaNode::Normalizer(_)
773 | SchemaNode::Store(_)
774 | SchemaNode::Validator(_) => {}
775 }
776}
777
778fn push_item_reference(item: &Item, pending: &mut Vec<String>) {
779 if let ItemTarget::Is(path) = item.target() {
780 pending.push((*path).to_string());
781 }
782}
783
784fn lower_source_rule_operation(
785 schema: &Schema,
786 target: &SchemaNode,
787 rule: &SourceRule,
788) -> Result<ProposalSourceRuleOperation, FragmentLoweringError> {
789 let length_bound = |value: &RuleNumber| {
790 rule_integer_u128(value)
791 .and_then(|value| u64::try_from(value).ok())
792 .ok_or_else(|| {
793 FragmentLoweringError::InvalidReference(format!(
794 "rule '{}' has an invalid length bound",
795 rule.name()
796 ))
797 })
798 };
799
800 let operation = match rule.operation() {
801 SourceRuleAuthoringOperation::NumericMinimumInclusive { value } => {
802 let RuleValueShape::Scalar(primitive, item) = resolve_rule_value_shape(schema, target)?
803 else {
804 return Err(invalid_rule_target(rule));
805 };
806 let value = lower_rule_numeric_literal(primitive, item, value)
807 .ok_or_else(|| invalid_rule_target(rule))?;
808 ProposalSourceRuleOperation::NumericMinimumInclusive { value }
809 }
810 SourceRuleAuthoringOperation::NumericMaximumInclusive { value } => {
811 let RuleValueShape::Scalar(primitive, item) = resolve_rule_value_shape(schema, target)?
812 else {
813 return Err(invalid_rule_target(rule));
814 };
815 let value = lower_rule_numeric_literal(primitive, item, value)
816 .ok_or_else(|| invalid_rule_target(rule))?;
817 ProposalSourceRuleOperation::NumericMaximumInclusive { value }
818 }
819 SourceRuleAuthoringOperation::NumericRangeInclusive { min, max } => {
820 let RuleValueShape::Scalar(primitive, item) = resolve_rule_value_shape(schema, target)?
821 else {
822 return Err(invalid_rule_target(rule));
823 };
824 let literal = |value: &RuleNumber| {
825 lower_rule_numeric_literal(primitive, item, value)
826 .ok_or_else(|| invalid_rule_target(rule))
827 };
828 ProposalSourceRuleOperation::NumericRangeInclusive {
829 min: literal(min)?,
830 max: literal(max)?,
831 }
832 }
833 SourceRuleAuthoringOperation::LengthRangeInclusive { min, max } => {
834 let shape = resolve_rule_value_shape(schema, target)?;
835 if !matches!(
836 shape,
837 RuleValueShape::Collection
838 | RuleValueShape::Scalar(Primitive::Blob | Primitive::Text, _)
839 ) {
840 return Err(invalid_rule_target(rule));
841 }
842 ProposalSourceRuleOperation::LengthRangeInclusive {
843 min: length_bound(min)?,
844 max: length_bound(max)?,
845 }
846 }
847 SourceRuleAuthoringOperation::MultipleOf { divisor } => {
848 let RuleValueShape::Scalar(primitive, item) = resolve_rule_value_shape(schema, target)?
849 else {
850 return Err(invalid_rule_target(rule));
851 };
852 let divisor = lower_rule_numeric_literal(primitive, item, divisor)
853 .ok_or_else(|| invalid_rule_target(rule))?;
854 ProposalSourceRuleOperation::MultipleOf { divisor }
855 }
856 };
857 Ok(operation)
858}
859
860fn lower_rule_numeric_literal(
861 primitive: Primitive,
862 item: &Item,
863 value: &RuleNumber,
864) -> Option<ScalarLiteral> {
865 match primitive {
866 Primitive::Decimal => rule_decimal(value)
867 .and_then(|value| exact_decimal_at_scale(value, item.scale().unwrap_or(0)))
868 .map(ScalarLiteral::Decimal),
869 Primitive::Float32 => match value {
870 RuleNumber::Float32(value) => Float32::try_new(*value).map(ScalarLiteral::Float32),
871 RuleNumber::Integer(_) | RuleNumber::Decimal(_) | RuleNumber::Float64(_) => None,
872 },
873 Primitive::Float64 => match value {
874 RuleNumber::Decimal(value) => value
875 .parse::<f64>()
876 .ok()
877 .and_then(Float64::try_new)
878 .map(ScalarLiteral::Float64),
879 RuleNumber::Float64(value) => Float64::try_new(*value).map(ScalarLiteral::Float64),
880 RuleNumber::Integer(_) | RuleNumber::Float32(_) => None,
881 },
882 Primitive::Int8
883 | Primitive::Int16
884 | Primitive::Int32
885 | Primitive::Int64
886 | Primitive::Int128 => rule_integer_i128(value).map(ScalarLiteral::Int),
887 Primitive::Nat8
888 | Primitive::Nat16
889 | Primitive::Nat32
890 | Primitive::Nat64
891 | Primitive::Nat128 => rule_integer_u128(value).map(ScalarLiteral::Nat),
892 Primitive::U256 => rule_integer_text(value)
893 .and_then(|value| U256::from_str(value).ok())
894 .map(ScalarLiteral::U256),
895 Primitive::Account
896 | Primitive::Blob
897 | Primitive::Bool
898 | Primitive::Date
899 | Primitive::Duration
900 | Primitive::Principal
901 | Primitive::Subaccount
902 | Primitive::Text
903 | Primitive::Timestamp
904 | Primitive::Ulid
905 | Primitive::Unit => None,
906 }
907}
908
909const fn rule_integer_text(value: &RuleNumber) -> Option<&str> {
910 let RuleNumber::Integer(value) = value else {
911 return None;
912 };
913 Some(value)
914}
915
916fn rule_integer_i128(value: &RuleNumber) -> Option<i128> {
917 rule_integer_text(value)?.parse().ok()
918}
919
920fn rule_integer_u128(value: &RuleNumber) -> Option<u128> {
921 rule_integer_text(value)?.parse().ok()
922}
923
924fn rule_decimal(value: &RuleNumber) -> Option<Decimal> {
925 match value {
926 RuleNumber::Integer(value) | RuleNumber::Decimal(value) => Decimal::from_str(value).ok(),
927 RuleNumber::Float32(_) | RuleNumber::Float64(_) => None,
928 }
929}
930
931fn exact_decimal_at_scale(value: Decimal, scale: u32) -> Option<Decimal> {
932 let value = value.normalize();
933 value
934 .scale_to_integer(scale)
935 .and_then(|mantissa| Decimal::try_from_i128_with_scale(mantissa, scale))
936}
937
938#[derive(Clone, Copy)]
939enum RuleValueShape<'schema> {
940 Collection,
941 Scalar(Primitive, &'schema Item),
942}
943
944fn resolve_rule_value_shape<'schema>(
945 schema: &'schema Schema,
946 mut target: &'schema SchemaNode,
947) -> Result<RuleValueShape<'schema>, FragmentLoweringError> {
948 let mut visited = BTreeSet::new();
949 loop {
950 let source = named_type_name(target)
951 .ok_or_else(|| FragmentLoweringError::InvalidReference("non-type rule".to_string()))?;
952 if !visited.insert(source) {
953 return Err(FragmentLoweringError::InvalidReference(format!(
954 "durable-rule target cycle at '{source}'"
955 )));
956 }
957 match target {
958 SchemaNode::List(_) | SchemaNode::Map(_) | SchemaNode::Set(_) => {
959 return Ok(RuleValueShape::Collection);
960 }
961 SchemaNode::Newtype(newtype) => match newtype.item().target() {
962 ItemTarget::Primitive(primitive) => {
963 return Ok(RuleValueShape::Scalar(*primitive, newtype.item()));
964 }
965 ItemTarget::Is(path) => {
966 target = schema
967 .get_node(path)
968 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.to_string()))?;
969 }
970 },
971 SchemaNode::Record(_) | SchemaNode::Enum(_) | SchemaNode::Tuple(_) => {
972 return Err(FragmentLoweringError::InvalidReference(format!(
973 "durable-rule target '{source}' has no supported scalar or collection value"
974 )));
975 }
976 SchemaNode::Canister(_)
977 | SchemaNode::Entity(_)
978 | SchemaNode::Normalizer(_)
979 | SchemaNode::Store(_)
980 | SchemaNode::Validator(_) => {
981 return Err(FragmentLoweringError::InvalidReference(
982 "non-type durable-rule target".to_string(),
983 ));
984 }
985 }
986 }
987}
988
989fn invalid_rule_target(rule: &SourceRule) -> FragmentLoweringError {
990 FragmentLoweringError::InvalidReference(format!(
991 "durable rule '{}' does not match its nominal target",
992 rule.name()
993 ))
994}
995
996fn entity_field_source_key(
997 entity: &Entity,
998 field_name: &str,
999) -> Result<FieldSourceKey, FragmentLoweringError> {
1000 let field = entity
1001 .fields()
1002 .get(field_name)
1003 .ok_or_else(|| FragmentLoweringError::InvalidReference(field_name.to_string()))?;
1004 FieldSourceKey::try_new(field.name()).map_err(Into::into)
1005}
1006
1007fn lower_reachable_types(
1012 schema: &Schema,
1013 mut pending: Vec<String>,
1014) -> Result<Vec<NamedTypeFragment>, FragmentLoweringError> {
1015 let mut lowered = BTreeMap::new();
1016 while let Some(path) = pending.pop() {
1017 let node = schema
1018 .get_node(path.as_str())
1019 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.clone()))?;
1020 let source_key = named_type_name(node)
1021 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.clone()))?;
1022 if lowered.contains_key(source_key) {
1023 continue;
1024 }
1025 let fragment = lower_named_type(schema, node, &mut pending)?;
1026 lowered.insert(source_key.to_string(), fragment);
1027 }
1028 Ok(lowered.into_values().collect())
1029}
1030
1031const fn named_type_name(node: &crate::node::SchemaNode) -> Option<&str> {
1032 match node {
1033 crate::node::SchemaNode::Enum(node) => Some(node.name()),
1034 crate::node::SchemaNode::List(node) => Some(node.name()),
1035 crate::node::SchemaNode::Map(node) => Some(node.name()),
1036 crate::node::SchemaNode::Newtype(node) => Some(node.name()),
1037 crate::node::SchemaNode::Record(node) => Some(node.name()),
1038 crate::node::SchemaNode::Set(node) => Some(node.name()),
1039 crate::node::SchemaNode::Tuple(node) => Some(node.name()),
1040 crate::node::SchemaNode::Canister(_)
1041 | crate::node::SchemaNode::Entity(_)
1042 | crate::node::SchemaNode::Normalizer(_)
1043 | crate::node::SchemaNode::Store(_)
1044 | crate::node::SchemaNode::Validator(_) => None,
1045 }
1046}
1047
1048fn lower_named_type(
1049 schema: &Schema,
1050 node: &crate::node::SchemaNode,
1051 pending: &mut Vec<String>,
1052) -> Result<NamedTypeFragment, FragmentLoweringError> {
1053 match node {
1054 crate::node::SchemaNode::Record(record) => lower_record(schema, record, pending),
1055 crate::node::SchemaNode::Enum(r#enum) => lower_enum(schema, r#enum, pending),
1056 crate::node::SchemaNode::Newtype(newtype) => Ok(NamedTypeFragment::newtype(
1057 SchemaName::try_new(newtype.name())?,
1058 lower_item_type(schema, newtype.item(), pending)?,
1059 )),
1060 crate::node::SchemaNode::List(list) => lower_list(schema, list, pending),
1061 crate::node::SchemaNode::Set(set) => lower_set(schema, set, pending),
1062 crate::node::SchemaNode::Map(map) => lower_map(schema, map, pending),
1063 crate::node::SchemaNode::Tuple(tuple) => lower_tuple(schema, tuple, pending),
1064 crate::node::SchemaNode::Canister(_)
1065 | crate::node::SchemaNode::Entity(_)
1066 | crate::node::SchemaNode::Normalizer(_)
1067 | crate::node::SchemaNode::Store(_)
1068 | crate::node::SchemaNode::Validator(_) => Err(FragmentLoweringError::InvalidReference(
1069 "non-type graph node".to_string(),
1070 )),
1071 }
1072}
1073
1074fn lower_record(
1075 schema: &Schema,
1076 record: &Record,
1077 pending: &mut Vec<String>,
1078) -> Result<NamedTypeFragment, FragmentLoweringError> {
1079 let fields = record
1080 .fields()
1081 .fields()
1082 .iter()
1083 .map(|field| {
1084 Ok(RecordFieldFragment::new(
1085 SchemaName::try_new(field.name())?,
1086 lower_value_type(schema, field.value(), pending)?,
1087 field.value().cardinality() == Cardinality::Opt,
1088 ))
1089 })
1090 .collect::<Result<Vec<_>, FragmentLoweringError>>()?;
1091 Ok(NamedTypeFragment::Record(RecordTypeFragment::try_new(
1092 SchemaName::try_new(record.name())?,
1093 fields,
1094 )?))
1095}
1096
1097fn lower_enum(
1098 schema: &Schema,
1099 r#enum: &Enum,
1100 pending: &mut Vec<String>,
1101) -> Result<NamedTypeFragment, FragmentLoweringError> {
1102 let variants = r#enum
1103 .variants()
1104 .iter()
1105 .map(|variant| {
1106 let name = SchemaName::try_new(variant.name())?;
1107 match variant.value() {
1108 Some(value) if value.cardinality() == Cardinality::Opt => {
1109 Err(FragmentLoweringError::UnsupportedCardinality(format!(
1110 "{}::{}",
1111 r#enum.def().path(),
1112 variant.name()
1113 )))
1114 }
1115 Some(value) => Ok(EnumVariantFragment::with_payload(
1116 name,
1117 lower_value_type(schema, value, pending)?,
1118 )),
1119 None => Ok(EnumVariantFragment::new(name)),
1120 }
1121 })
1122 .collect::<Result<Vec<_>, _>>()?;
1123 Ok(NamedTypeFragment::Enum(EnumTypeFragment::try_new(
1124 SchemaName::try_new(r#enum.name())?,
1125 variants,
1126 )?))
1127}
1128
1129fn lower_list(
1130 schema: &Schema,
1131 list: &List,
1132 pending: &mut Vec<String>,
1133) -> Result<NamedTypeFragment, FragmentLoweringError> {
1134 Ok(NamedTypeFragment::list(
1135 SchemaName::try_new(list.name())?,
1136 lower_item_type(schema, list.item(), pending)?,
1137 ))
1138}
1139
1140fn lower_set(
1141 schema: &Schema,
1142 set: &Set,
1143 pending: &mut Vec<String>,
1144) -> Result<NamedTypeFragment, FragmentLoweringError> {
1145 Ok(NamedTypeFragment::set(
1146 SchemaName::try_new(set.name())?,
1147 lower_item_type(schema, set.item(), pending)?,
1148 ))
1149}
1150
1151fn lower_map(
1152 schema: &Schema,
1153 map: &Map,
1154 pending: &mut Vec<String>,
1155) -> Result<NamedTypeFragment, FragmentLoweringError> {
1156 if map.value().cardinality() == Cardinality::Opt {
1157 return Err(FragmentLoweringError::UnsupportedCardinality(
1158 map.def().path(),
1159 ));
1160 }
1161 Ok(NamedTypeFragment::map(
1162 SchemaName::try_new(map.name())?,
1163 lower_item_type(schema, map.key(), pending)?,
1164 lower_value_type(schema, map.value(), pending)?,
1165 ))
1166}
1167
1168fn lower_tuple(
1169 schema: &Schema,
1170 tuple: &Tuple,
1171 pending: &mut Vec<String>,
1172) -> Result<NamedTypeFragment, FragmentLoweringError> {
1173 let members = tuple
1174 .values()
1175 .iter()
1176 .map(|value| {
1177 Ok::<_, FragmentLoweringError>(TupleElementFragment::new(
1178 lower_value_type(schema, value, pending)?,
1179 value.cardinality() == Cardinality::Opt,
1180 ))
1181 })
1182 .collect::<Result<Vec<_>, _>>()?;
1183 Ok(NamedTypeFragment::tuple(
1184 SchemaName::try_new(tuple.name())?,
1185 members,
1186 ))
1187}
1188
1189fn lower_value_type(
1194 schema: &Schema,
1195 value: &Value,
1196 pending: &mut Vec<String>,
1197) -> Result<FieldType, FragmentLoweringError> {
1198 let item = lower_item_type(schema, value.item(), pending)?;
1199 Ok(if value.cardinality() == Cardinality::Many {
1200 FieldType::List(Box::new(item))
1201 } else {
1202 item
1203 })
1204}
1205
1206fn lower_item_type(
1207 schema: &Schema,
1208 item: &Item,
1209 pending: &mut Vec<String>,
1210) -> Result<FieldType, FragmentLoweringError> {
1211 match item.target() {
1212 ItemTarget::Is(path) => {
1213 pending.push((*path).to_string());
1214 Ok(FieldType::Named(TypeSourceKey::try_new(
1215 type_source_key_for_path(schema, path)?,
1216 )?))
1217 }
1218 ItemTarget::Primitive(primitive) => {
1219 Ok(FieldType::Scalar(lower_scalar_type(*primitive, item)))
1220 }
1221 }
1222}
1223
1224fn type_source_key_for_path<'schema>(
1225 schema: &'schema Schema,
1226 path: &str,
1227) -> Result<&'schema str, FragmentLoweringError> {
1228 let source = schema
1229 .get_node(path)
1230 .and_then(named_type_name)
1231 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.to_string()))?;
1232 Ok(source)
1233}
1234
1235fn lower_scalar_type(primitive: Primitive, item: &Item) -> ScalarType {
1236 match primitive {
1237 Primitive::Account => ScalarType::Account,
1238 Primitive::Blob => ScalarType::Blob {
1239 max_len: item.max_len(),
1240 },
1241 Primitive::Bool => ScalarType::Bool,
1242 Primitive::Date => ScalarType::Date,
1243 Primitive::Decimal => ScalarType::Decimal {
1244 scale: item.scale().unwrap_or(0),
1245 },
1246 Primitive::Duration => ScalarType::Duration,
1247 Primitive::Float32 => ScalarType::Float32,
1248 Primitive::Float64 => ScalarType::Float64,
1249 Primitive::Int8 => ScalarType::Int8,
1250 Primitive::Int16 => ScalarType::Int16,
1251 Primitive::Int32 => ScalarType::Int32,
1252 Primitive::Int64 => ScalarType::Int64,
1253 Primitive::Int128 => ScalarType::Int128,
1254 Primitive::Nat8 => ScalarType::Nat8,
1255 Primitive::Nat16 => ScalarType::Nat16,
1256 Primitive::Nat32 => ScalarType::Nat32,
1257 Primitive::Nat64 => ScalarType::Nat64,
1258 Primitive::Nat128 => ScalarType::Nat128,
1259 Primitive::U256 => ScalarType::U256,
1260 Primitive::Principal => ScalarType::Principal,
1261 Primitive::Subaccount => ScalarType::Subaccount,
1262 Primitive::Text => ScalarType::Text {
1263 max_len: item.max_len(),
1264 },
1265 Primitive::Timestamp => ScalarType::Timestamp,
1266 Primitive::Ulid => ScalarType::Ulid,
1267 Primitive::Unit => ScalarType::Unit,
1268 }
1269}
1270
1271fn lower_default(
1276 schema: &Schema,
1277 field: &Field,
1278 default: &Arg,
1279) -> Result<ScalarLiteral, FragmentLoweringError> {
1280 if let ItemTarget::Is(path) = field.value().item().target() {
1281 let Arg::ConstPath(default_path) = default else {
1282 return Err(FragmentLoweringError::InvalidDefault(
1283 field.name().to_string(),
1284 ));
1285 };
1286 let variant = default_path.rsplit("::").next().unwrap_or(default_path);
1287 return schema
1288 .enum_unit_literal(path, variant)
1289 .map_err(FragmentLoweringError::from);
1290 }
1291 let ItemTarget::Primitive(primitive) = field.value().item().target() else {
1292 return Err(FragmentLoweringError::InvalidDefault(
1293 field.name().to_string(),
1294 ));
1295 };
1296 lower_scalar_default(*primitive, field.value().item(), default)
1297 .ok_or_else(|| FragmentLoweringError::InvalidDefault(field.name().to_string()))
1298}
1299
1300fn lower_scalar_default(primitive: Primitive, item: &Item, default: &Arg) -> Option<ScalarLiteral> {
1301 if default_constructor_is_zero(default) {
1302 return zero_scalar_literal(primitive, item);
1303 }
1304 match (primitive, default) {
1305 (Primitive::Account, Arg::String(value)) => {
1306 Account::from_str(value).ok().map(ScalarLiteral::Account)
1307 }
1308 (Primitive::Blob, Arg::String(value)) => lower_blob_default(value),
1309 (Primitive::Bool, Arg::Bool(value)) => Some(ScalarLiteral::Bool(*value)),
1310 (Primitive::Date, Arg::String(value)) => Date::parse(value).map(ScalarLiteral::Date),
1311 (Primitive::Date, Arg::Number(value)) => arg_i128(value)
1312 .and_then(|value| i32::try_from(value).ok())
1313 .and_then(Date::try_from_days_since_epoch)
1314 .map(ScalarLiteral::Date),
1315 (Primitive::Decimal, Arg::String(value)) => Decimal::from_str(value)
1316 .ok()
1317 .and_then(|value| decimal_at_scale(value, item.scale().unwrap_or(0)))
1318 .map(ScalarLiteral::Decimal),
1319 (Primitive::Decimal, Arg::Number(value)) => arg_decimal(value)
1320 .and_then(|value| decimal_at_scale(value, item.scale().unwrap_or(0)))
1321 .map(ScalarLiteral::Decimal),
1322 (Primitive::Duration, Arg::String(value)) => Duration::parse_flexible(value)
1323 .ok()
1324 .map(ScalarLiteral::Duration),
1325 (Primitive::Duration, Arg::Number(value)) => arg_u128(value)
1326 .and_then(|value| u64::try_from(value).ok())
1327 .map(Duration::from_millis)
1328 .map(ScalarLiteral::Duration),
1329 (Primitive::Float32, Arg::Number(ArgNumber::Float32(value))) => {
1330 Float32::try_new(*value).map(ScalarLiteral::Float32)
1331 }
1332 (Primitive::Float64, Arg::Number(ArgNumber::Float64(value))) => {
1333 Float64::try_new(*value).map(ScalarLiteral::Float64)
1334 }
1335 (
1336 Primitive::Int8
1337 | Primitive::Int16
1338 | Primitive::Int32
1339 | Primitive::Int64
1340 | Primitive::Int128,
1341 Arg::Number(value),
1342 ) => arg_i128(value).map(ScalarLiteral::Int),
1343 (
1344 Primitive::Nat8
1345 | Primitive::Nat16
1346 | Primitive::Nat32
1347 | Primitive::Nat64
1348 | Primitive::Nat128,
1349 Arg::Number(value),
1350 ) => arg_u128(value).map(ScalarLiteral::Nat),
1351 (Primitive::U256, Arg::Number(value)) => {
1352 arg_u128(value).map(U256::from).map(ScalarLiteral::U256)
1353 }
1354 (Primitive::U256, Arg::String(value)) => {
1355 U256::from_str(value).ok().map(ScalarLiteral::U256)
1356 }
1357 (Primitive::Principal, Arg::String(value)) => Principal::from_str(value)
1358 .ok()
1359 .map(ScalarLiteral::Principal),
1360 (Primitive::Subaccount, Arg::String(value)) => Subaccount::from_hex(value)
1361 .ok()
1362 .map(ScalarLiteral::Subaccount),
1363 (Primitive::Text, Arg::String(value)) => Some(ScalarLiteral::Text((*value).to_string())),
1364 (Primitive::Timestamp, Arg::String(value)) => Timestamp::parse_flexible(value)
1365 .ok()
1366 .map(ScalarLiteral::Timestamp),
1367 (Primitive::Timestamp, Arg::Number(value)) => arg_i128(value)
1368 .and_then(|value| i64::try_from(value).ok())
1369 .map(Timestamp::from_millis)
1370 .map(ScalarLiteral::Timestamp),
1371 (Primitive::Ulid, Arg::String(value)) => {
1372 Ulid::from_str(value).ok().map(ScalarLiteral::Ulid)
1373 }
1374 (Primitive::Unit, Arg::ConstPath(path)) if path.ends_with("Unit") => {
1375 Some(ScalarLiteral::Unit(Unit))
1376 }
1377 _ => None,
1378 }
1379}
1380
1381fn default_constructor_is_zero(default: &Arg) -> bool {
1382 let Arg::FuncPath(path) = default else {
1383 return false;
1384 };
1385 path.ends_with("::default")
1386 || path.ends_with("::new")
1387 || path.ends_with("::EPOCH")
1388 || path.ends_with("::nil")
1389}
1390
1391fn zero_scalar_literal(primitive: Primitive, item: &Item) -> Option<ScalarLiteral> {
1392 match primitive {
1393 Primitive::Blob => Some(ScalarLiteral::Blob(Blob::default())),
1394 Primitive::Bool => Some(ScalarLiteral::Bool(false)),
1395 Primitive::Date => Some(ScalarLiteral::Date(Date::EPOCH)),
1396 Primitive::Decimal => Decimal::try_from_i128_with_scale(0, item.scale().unwrap_or(0))
1397 .map(ScalarLiteral::Decimal),
1398 Primitive::Duration => Some(ScalarLiteral::Duration(Duration::ZERO)),
1399 Primitive::Float32 => Float32::try_new(0.0).map(ScalarLiteral::Float32),
1400 Primitive::Float64 => Float64::try_new(0.0).map(ScalarLiteral::Float64),
1401 Primitive::Int8
1402 | Primitive::Int16
1403 | Primitive::Int32
1404 | Primitive::Int64
1405 | Primitive::Int128 => Some(ScalarLiteral::Int(0)),
1406 Primitive::Nat8
1407 | Primitive::Nat16
1408 | Primitive::Nat32
1409 | Primitive::Nat64
1410 | Primitive::Nat128 => Some(ScalarLiteral::Nat(0)),
1411 Primitive::U256 => Some(ScalarLiteral::U256(U256::ZERO)),
1412 Primitive::Text => Some(ScalarLiteral::Text(String::new())),
1413 Primitive::Timestamp => Some(ScalarLiteral::Timestamp(Timestamp::EPOCH)),
1414 Primitive::Ulid => Some(ScalarLiteral::Ulid(Ulid::nil())),
1415 Primitive::Unit => Some(ScalarLiteral::Unit(Unit)),
1416 Primitive::Account | Primitive::Principal | Primitive::Subaccount => None,
1417 }
1418}
1419
1420fn lower_blob_default(value: &str) -> Option<ScalarLiteral> {
1421 Blob::from_hex_literal(value).ok().map(ScalarLiteral::Blob)
1422}
1423
1424fn decimal_at_scale(value: Decimal, scale: u32) -> Option<Decimal> {
1429 match value.scale().cmp(&scale) {
1430 std::cmp::Ordering::Equal => Some(value),
1431 std::cmp::Ordering::Less => value
1432 .scale_to_integer(scale)
1433 .and_then(|mantissa| Decimal::try_from_i128_with_scale(mantissa, scale)),
1434 std::cmp::Ordering::Greater => Some(value.round_dp(scale)),
1435 }
1436}
1437
1438fn arg_i128(value: &ArgNumber) -> Option<i128> {
1439 match value {
1440 ArgNumber::Int8(value) => Some(i128::from(*value)),
1441 ArgNumber::Int16(value) => Some(i128::from(*value)),
1442 ArgNumber::Int32(value) => Some(i128::from(*value)),
1443 ArgNumber::Int64(value) => Some(i128::from(*value)),
1444 ArgNumber::Int128(value) => Some(*value),
1445 ArgNumber::Nat8(value) => Some(i128::from(*value)),
1446 ArgNumber::Nat16(value) => Some(i128::from(*value)),
1447 ArgNumber::Nat32(value) => Some(i128::from(*value)),
1448 ArgNumber::Nat64(value) => Some(i128::from(*value)),
1449 ArgNumber::Nat128(value) => i128::try_from(*value).ok(),
1450 ArgNumber::Float32(_) | ArgNumber::Float64(_) => None,
1451 }
1452}
1453
1454fn arg_u128(value: &ArgNumber) -> Option<u128> {
1455 match value {
1456 ArgNumber::Int8(value) => u128::try_from(*value).ok(),
1457 ArgNumber::Int16(value) => u128::try_from(*value).ok(),
1458 ArgNumber::Int32(value) => u128::try_from(*value).ok(),
1459 ArgNumber::Int64(value) => u128::try_from(*value).ok(),
1460 ArgNumber::Int128(value) => u128::try_from(*value).ok(),
1461 ArgNumber::Nat8(value) => Some(u128::from(*value)),
1462 ArgNumber::Nat16(value) => Some(u128::from(*value)),
1463 ArgNumber::Nat32(value) => Some(u128::from(*value)),
1464 ArgNumber::Nat64(value) => Some(u128::from(*value)),
1465 ArgNumber::Nat128(value) => Some(*value),
1466 ArgNumber::Float32(_) | ArgNumber::Float64(_) => None,
1467 }
1468}
1469
1470fn arg_decimal(value: &ArgNumber) -> Option<Decimal> {
1471 match value {
1472 ArgNumber::Float32(value) => Decimal::from_f32_lossy(*value),
1473 ArgNumber::Float64(value) => Decimal::from_f64_lossy(*value),
1474 _ => arg_i128(value).and_then(Decimal::from_i128),
1475 }
1476}
1477
1478#[cfg(test)]
1479mod tests {
1480 use icydb_schema::{
1481 ConstraintFragmentKind, ConstraintSourceKey, Decimal, FieldSourceKey, FieldType,
1482 MAX_PROPOSAL_LITERAL_BYTES, NamedTypeFragment, RuleSourceKey, ScalarLiteral, ScalarType,
1483 SourceRuleOperation, TypeSourceKey,
1484 };
1485
1486 use super::{
1487 FragmentLoweringError, Schema, lower_blob_default, lower_field_rules,
1488 lower_nested_relations, lower_scalar_default,
1489 };
1490 use crate::{
1491 node::{
1492 Arg, Args, Canister, Def, Entity, Enum, EnumVariant, Field, FieldList, Item,
1493 ItemTarget, Newtype, Normalizer, PrimaryKey, PrimaryKeySource, Record, RuleNumber,
1494 SchemaNode, SourceRule, SourceRuleAuthoringOperation, Store, StoreHeapConfig, Type,
1495 TypeNormalizer, TypeValidator, Validator, Value,
1496 },
1497 types::{Cardinality, Primitive},
1498 };
1499
1500 #[test]
1501 fn blob_defaults_and_migration_fills_share_exact_bytes_and_refusals() {
1502 for text in ["", "00", "deadbeef", "DeAdBeEf", "ff00"] {
1503 let default = lower_blob_default(text).unwrap();
1504 let fill = crate::node::migration_literal_from_text("blob", text).unwrap();
1505 assert_eq!(default, fill);
1506 }
1507 assert_eq!(
1508 lower_blob_default("deadbeef"),
1509 Some(ScalarLiteral::Blob(icydb_schema::Blob::from(vec![
1510 0xde, 0xad, 0xbe, 0xef
1511 ])))
1512 );
1513 for text in ["a", "zz", "+1", "0x00", " 00", "é", "aé0"] {
1514 assert!(lower_blob_default(text).is_none());
1515 assert_eq!(
1516 crate::node::migration_literal_from_text("blob", text),
1517 Err(icydb_schema::SchemaContractError::InvalidLiteral)
1518 );
1519 }
1520 let maximum = "ff".repeat(MAX_PROPOSAL_LITERAL_BYTES);
1521 assert_eq!(
1522 lower_blob_default(&maximum),
1523 Some(crate::node::migration_literal_from_text("blob", &maximum).unwrap())
1524 );
1525 let oversized = format!("{maximum}00");
1526 assert!(lower_blob_default(&oversized).is_none());
1527 assert_eq!(
1528 crate::node::migration_literal_from_text("blob", &oversized),
1529 Err(icydb_schema::SchemaContractError::InvalidLiteral)
1530 );
1531 let subaccount = "aB".repeat(32);
1532 assert!(crate::node::migration_literal_from_text("subaccount", &subaccount).is_ok());
1533 for text in [
1534 format!("0x{subaccount}"),
1535 "00".repeat(31),
1536 "00".repeat(33),
1537 "gg".repeat(32),
1538 "+1".repeat(32),
1539 ] {
1540 assert_eq!(
1541 crate::node::migration_literal_from_text("subaccount", &text),
1542 Err(icydb_schema::SchemaContractError::InvalidLiteral)
1543 );
1544 }
1545 }
1546
1547 #[test]
1548 fn blob_default_lowering_enforces_the_proposal_literal_bound() {
1549 let maximum = "ab".repeat(MAX_PROPOSAL_LITERAL_BYTES);
1550 let oversized = "ab".repeat(MAX_PROPOSAL_LITERAL_BYTES + 1);
1551
1552 assert_eq!(
1553 lower_blob_default(&maximum).and_then(|literal| match literal {
1554 ScalarLiteral::Blob(value) => Some(value.len()),
1555 _ => None,
1556 }),
1557 Some(MAX_PROPOSAL_LITERAL_BYTES),
1558 );
1559 assert_eq!(lower_blob_default(&oversized), None);
1560 }
1561
1562 #[test]
1563 fn duration_default_lowering_rejects_suffixed_overflow() {
1564 let item = Item::new(
1565 ItemTarget::Primitive(Primitive::Duration),
1566 None,
1567 None,
1568 None,
1569 &[],
1570 &[],
1571 false,
1572 );
1573
1574 assert_eq!(
1575 lower_scalar_default(
1576 Primitive::Duration,
1577 &item,
1578 &Arg::String("18446744073709551615ms"),
1579 ),
1580 Some(ScalarLiteral::Duration(icydb_schema::Duration::MAX)),
1581 );
1582 assert_eq!(
1583 lower_scalar_default(
1584 Primitive::Duration,
1585 &item,
1586 &Arg::String("18446744073709552s"),
1587 ),
1588 None,
1589 );
1590 }
1591
1592 static EMPTY_TYPE: Type = Type::new(&[], &[], &[]);
1593 static APPLICATION_FIELDS: [Field; 1] = [Field::new(
1594 "id",
1595 Value::new(
1596 Cardinality::One,
1597 Item::new(
1598 ItemTarget::Primitive(Primitive::Nat64),
1599 None,
1600 None,
1601 None,
1602 &[],
1603 &[],
1604 false,
1605 ),
1606 ),
1607 None,
1608 None,
1609 None,
1610 )];
1611 static APPLICATION_NORMALIZERS_A: [TypeNormalizer; 1] =
1612 [TypeNormalizer::new("test::NormalizeA", Args(&[]))];
1613 static APPLICATION_NORMALIZERS_B: [TypeNormalizer; 1] =
1614 [TypeNormalizer::new("test::NormalizeB", Args(&[]))];
1615 static APPLICATION_VALIDATORS_A: [TypeValidator; 1] =
1616 [TypeValidator::new("test::ValidateA", Args(&[]))];
1617 static APPLICATION_VALIDATORS_B: [TypeValidator; 1] =
1618 [TypeValidator::new("test::ValidateB", Args(&[]))];
1619 static NUMERIC_RULES: [SourceRule; 1] = [SourceRule::new(
1620 "range",
1621 SourceRuleAuthoringOperation::NumericRangeInclusive {
1622 min: RuleNumber::Integer("0"),
1623 max: RuleNumber::Integer("360"),
1624 },
1625 )];
1626 static NUMERIC_RULE_TYPE: Type = Type::new(&[], &[], &NUMERIC_RULES);
1627 static LENGTH_RULES: [SourceRule; 1] = [SourceRule::new(
1628 "length",
1629 SourceRuleAuthoringOperation::LengthRangeInclusive {
1630 min: RuleNumber::Integer("2"),
1631 max: RuleNumber::Integer("40"),
1632 },
1633 )];
1634 static LENGTH_RULE_TYPE: Type = Type::new(&[], &[], &LENGTH_RULES);
1635 static NAT_EXACT_RULES: [SourceRule; 2] = [
1636 SourceRule::new(
1637 "maximum",
1638 SourceRuleAuthoringOperation::NumericMaximumInclusive {
1639 value: RuleNumber::Integer("100"),
1640 },
1641 ),
1642 SourceRule::new(
1643 "step",
1644 SourceRuleAuthoringOperation::MultipleOf {
1645 divisor: RuleNumber::Integer("5"),
1646 },
1647 ),
1648 ];
1649 static NAT_EXACT_RULE_TYPE: Type = Type::new(&[], &[], &NAT_EXACT_RULES);
1650 static DECIMAL_EXACT_RULES: [SourceRule; 1] = [SourceRule::new(
1651 "step",
1652 SourceRuleAuthoringOperation::MultipleOf {
1653 divisor: RuleNumber::Decimal("0.25"),
1654 },
1655 )];
1656 static DECIMAL_EXACT_RULE_TYPE: Type = Type::new(&[], &[], &DECIMAL_EXACT_RULES);
1657 static INEXACT_DECIMAL_RULES: [SourceRule; 1] = [SourceRule::new(
1658 "step",
1659 SourceRuleAuthoringOperation::MultipleOf {
1660 divisor: RuleNumber::Decimal("0.251"),
1661 },
1662 )];
1663 static INEXACT_DECIMAL_RULE_TYPE: Type = Type::new(&[], &[], &INEXACT_DECIMAL_RULES);
1664 static NESTED_RULE_FIELDS: [Field; 1] = [Field::new(
1665 "degrees",
1666 Value::new(
1667 Cardinality::One,
1668 Item::new(
1669 ItemTarget::Is("test::Degrees"),
1670 None,
1671 None,
1672 None,
1673 &[],
1674 &[],
1675 false,
1676 ),
1677 ),
1678 None,
1679 None,
1680 None,
1681 )];
1682 static STATUS_VARIANTS: [EnumVariant; 2] = [
1683 EnumVariant::new("Active", None),
1684 EnumVariant::new(
1685 "Retries",
1686 Some(Value::new(
1687 Cardinality::Many,
1688 Item::new(
1689 ItemTarget::Primitive(Primitive::Nat16),
1690 None,
1691 None,
1692 None,
1693 &[],
1694 &[],
1695 false,
1696 ),
1697 )),
1698 ),
1699 ];
1700 static RECURSIVE_RECORD_FIELDS: [Field; 1] = [Field::new(
1701 "next",
1702 Value::new(
1703 Cardinality::Opt,
1704 Item::new(
1705 ItemTarget::Is("test::Recursive"),
1706 None,
1707 None,
1708 None,
1709 &[],
1710 &[],
1711 false,
1712 ),
1713 ),
1714 None,
1715 None,
1716 None,
1717 )];
1718 static RECURSIVE_ENTITY_FIELDS: [Field; 1] = [Field::new(
1719 "root",
1720 Value::new(
1721 Cardinality::One,
1722 Item::new(
1723 ItemTarget::Is("test::Recursive"),
1724 None,
1725 None,
1726 None,
1727 &[],
1728 &[],
1729 false,
1730 ),
1731 ),
1732 None,
1733 None,
1734 None,
1735 )];
1736
1737 #[test]
1738 fn recursive_structural_type_without_a_finite_relation_path_is_ignored() {
1739 let mut schema = Schema::new();
1740 schema.insert_node(SchemaNode::Record(Record::new(
1741 Def::new("test", "Recursive"),
1742 "Recursive",
1743 FieldList::new(&RECURSIVE_RECORD_FIELDS),
1744 EMPTY_TYPE.clone(),
1745 )));
1746 let entity = Entity::new(
1747 Def::new("test", "RecursiveOwner"),
1748 "test::Store",
1749 1,
1750 PrimaryKey::new(&["root"], PrimaryKeySource::External),
1751 &[],
1752 &[],
1753 &[],
1754 FieldList::new(&RECURSIVE_ENTITY_FIELDS),
1755 EMPTY_TYPE.clone(),
1756 );
1757
1758 assert!(
1759 lower_nested_relations(&schema, &entity, &RECURSIVE_ENTITY_FIELDS[0])
1760 .expect("recursive structural types without finite relations should lower")
1761 .is_empty(),
1762 );
1763 }
1764
1765 #[test]
1766 fn recursive_relation_sources_reject_in_both_sibling_orders() {
1767 for relation_first in [false, true] {
1768 let relation = Field::new(
1769 "owner",
1770 Value::new(
1771 Cardinality::One,
1772 Item::new(
1773 ItemTarget::Primitive(Primitive::Ulid),
1774 Some("test::User"),
1775 None,
1776 None,
1777 &[],
1778 &[],
1779 false,
1780 ),
1781 ),
1782 None,
1783 None,
1784 None,
1785 );
1786 let next = RECURSIVE_RECORD_FIELDS[0].clone();
1787 let fields = if relation_first {
1788 vec![relation, next]
1789 } else {
1790 vec![next, relation]
1791 };
1792 let mut schema = Schema::new();
1793 schema.insert_node(SchemaNode::Record(Record::new(
1794 Def::new("test", "Recursive"),
1795 "Recursive",
1796 FieldList::new(Box::leak(fields.into_boxed_slice())),
1797 EMPTY_TYPE.clone(),
1798 )));
1799 let entity = Entity::new(
1800 Def::new("test", "RecursiveOwner"),
1801 "test::Store",
1802 1,
1803 PrimaryKey::new(&["root"], PrimaryKeySource::External),
1804 &[],
1805 &[],
1806 &[],
1807 FieldList::new(&RECURSIVE_ENTITY_FIELDS),
1808 EMPTY_TYPE.clone(),
1809 );
1810 assert!(matches!(
1811 lower_nested_relations(&schema, &entity, &RECURSIVE_ENTITY_FIELDS[0]),
1812 Err(FragmentLoweringError::UnsupportedCardinality(_))
1813 ));
1814 }
1815 }
1816
1817 fn application_behavior_fragment(
1818 normalizers: &'static [TypeNormalizer],
1819 validators: &'static [TypeValidator],
1820 normalizer_name: &'static str,
1821 validator_name: &'static str,
1822 ) -> icydb_schema::SchemaFragment {
1823 let mut schema = Schema::new();
1824 schema.insert_node(SchemaNode::Canister(Canister::new(
1825 Def::new("test", "Canister"),
1826 "test",
1827 None,
1828 )));
1829 schema.insert_node(SchemaNode::Store(Store::new_heap(
1830 Def::new("test", "Store"),
1831 "test::Canister",
1832 StoreHeapConfig::new(),
1833 )));
1834 schema.insert_node(SchemaNode::Normalizer(Normalizer::new(Def::new(
1835 "test",
1836 normalizer_name,
1837 ))));
1838 schema.insert_node(SchemaNode::Validator(Validator::new(Def::new(
1839 "test",
1840 validator_name,
1841 ))));
1842 schema.insert_node(SchemaNode::Entity(Entity::new(
1843 Def::new("test", "ApplicationOnly"),
1844 "test::Store",
1845 1,
1846 PrimaryKey::new(&["id"], PrimaryKeySource::External),
1847 &[],
1848 &[],
1849 &[],
1850 FieldList::new(&APPLICATION_FIELDS),
1851 Type::new(normalizers, validators, &[]),
1852 )));
1853 schema.seal().expect("application-only fixture should seal");
1854 schema
1855 .schema_fragment_for_canister("test::Canister")
1856 .expect("application-only fixture should lower")
1857 }
1858
1859 #[test]
1860 fn validator_and_normalizer_edits_do_not_change_database_fragment() {
1861 let before = application_behavior_fragment(
1862 &APPLICATION_NORMALIZERS_A,
1863 &APPLICATION_VALIDATORS_A,
1864 "NormalizeA",
1865 "ValidateA",
1866 );
1867 let after = application_behavior_fragment(
1868 &APPLICATION_NORMALIZERS_B,
1869 &APPLICATION_VALIDATORS_B,
1870 "NormalizeB",
1871 "ValidateB",
1872 );
1873
1874 assert_eq!(before, after);
1875 }
1876
1877 #[test]
1878 fn durable_rules_nested_below_structural_fields_lower_to_nominal_targets() {
1879 let mut schema = Schema::new();
1880 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1881 Def::new("test", "Degrees"),
1882 "Degrees",
1883 Item::new(
1884 ItemTarget::Primitive(Primitive::Nat16),
1885 None,
1886 None,
1887 None,
1888 &[],
1889 &[],
1890 false,
1891 ),
1892 None,
1893 NUMERIC_RULE_TYPE.clone(),
1894 )));
1895 schema.insert_node(SchemaNode::Record(Record::new(
1896 Def::new("test", "Nested"),
1897 "Nested",
1898 FieldList::new(&NESTED_RULE_FIELDS),
1899 EMPTY_TYPE.clone(),
1900 )));
1901
1902 let outer = Field::new(
1903 "nested",
1904 Value::new(
1905 Cardinality::One,
1906 Item::new(
1907 ItemTarget::Is("test::Nested"),
1908 None,
1909 None,
1910 None,
1911 &[],
1912 &[],
1913 false,
1914 ),
1915 ),
1916 None,
1917 None,
1918 None,
1919 );
1920 let constraints =
1921 lower_field_rules(&schema, &outer).expect("nested durable rule should lower");
1922 assert_eq!(constraints.len(), 1);
1923 let ConstraintFragmentKind::TargetedRule(rule) = constraints[0].kind() else {
1924 panic!("nested durable rule should use the targeted-rule contract")
1925 };
1926 assert_eq!(rule.root().as_str(), "nested");
1927 assert_eq!(rule.target_type().as_str(), "Degrees");
1928 assert!(matches!(
1929 rule.operation(),
1930 SourceRuleOperation::NumericRangeInclusive { .. }
1931 ));
1932 }
1933
1934 #[test]
1935 fn exact_maximum_and_multiple_of_lower_without_float_reconstruction() {
1936 let mut schema = Schema::new();
1937 for (name, primitive, scale, rules) in [
1938 (
1939 "Counter",
1940 Primitive::Nat64,
1941 None,
1942 NAT_EXACT_RULE_TYPE.clone(),
1943 ),
1944 (
1945 "PriceStep",
1946 Primitive::Decimal,
1947 Some(2),
1948 DECIMAL_EXACT_RULE_TYPE.clone(),
1949 ),
1950 ] {
1951 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1952 Def::new("test", name),
1953 name,
1954 Item::new(
1955 ItemTarget::Primitive(primitive),
1956 None,
1957 scale,
1958 None,
1959 &[],
1960 &[],
1961 false,
1962 ),
1963 None,
1964 rules,
1965 )));
1966 }
1967
1968 let field = |name| {
1969 Field::new(
1970 name,
1971 Value::new(
1972 Cardinality::One,
1973 Item::new(
1974 ItemTarget::Is(if name == "counter" {
1975 "test::Counter"
1976 } else {
1977 "test::PriceStep"
1978 }),
1979 None,
1980 None,
1981 None,
1982 &[],
1983 &[],
1984 false,
1985 ),
1986 ),
1987 None,
1988 None,
1989 None,
1990 )
1991 };
1992 let counter = lower_field_rules(&schema, &field("counter"))
1993 .expect("exact integer rules should lower");
1994 assert!(matches!(
1995 counter[0].kind(),
1996 ConstraintFragmentKind::TargetedRule(rule)
1997 if matches!(
1998 rule.operation(),
1999 SourceRuleOperation::NumericMaximumInclusive {
2000 value: ScalarLiteral::Nat(100)
2001 }
2002 )
2003 ));
2004 assert!(matches!(
2005 counter[1].kind(),
2006 ConstraintFragmentKind::TargetedRule(rule)
2007 if matches!(
2008 rule.operation(),
2009 SourceRuleOperation::MultipleOf {
2010 divisor: ScalarLiteral::Nat(5)
2011 }
2012 )
2013 ));
2014
2015 let decimal = lower_field_rules(&schema, &field("price"))
2016 .expect("exact decimal multiple should lower");
2017 assert!(matches!(
2018 decimal[0].kind(),
2019 ConstraintFragmentKind::TargetedRule(rule)
2020 if matches!(
2021 rule.operation(),
2022 SourceRuleOperation::MultipleOf {
2023 divisor: ScalarLiteral::Decimal(value)
2024 } if *value == Decimal::new(25, 2)
2025 )
2026 ));
2027 }
2028
2029 #[test]
2030 fn inexact_decimal_rule_operand_rejects_before_proposal_composition() {
2031 let mut schema = Schema::new();
2032 schema.insert_node(SchemaNode::Newtype(Newtype::new(
2033 Def::new("test", "InexactStep"),
2034 "InexactStep",
2035 Item::new(
2036 ItemTarget::Primitive(Primitive::Decimal),
2037 None,
2038 Some(2),
2039 None,
2040 &[],
2041 &[],
2042 false,
2043 ),
2044 None,
2045 INEXACT_DECIMAL_RULE_TYPE.clone(),
2046 )));
2047 let field = Field::new(
2048 "price",
2049 Value::new(
2050 Cardinality::One,
2051 Item::new(
2052 ItemTarget::Is("test::InexactStep"),
2053 None,
2054 None,
2055 None,
2056 &[],
2057 &[],
2058 false,
2059 ),
2060 ),
2061 None,
2062 None,
2063 None,
2064 );
2065 assert!(lower_field_rules(&schema, &field).is_err());
2066 }
2067
2068 static ENTITY_FIELDS: [Field; 5] = [
2069 Field::new(
2070 "id",
2071 Value::new(
2072 Cardinality::One,
2073 Item::new(
2074 ItemTarget::Primitive(Primitive::Nat64),
2075 None,
2076 None,
2077 None,
2078 &[],
2079 &[],
2080 false,
2081 ),
2082 ),
2083 None,
2084 None,
2085 None,
2086 ),
2087 Field::new(
2088 "tags",
2089 Value::new(
2090 Cardinality::Many,
2091 Item::new(
2092 ItemTarget::Primitive(Primitive::Text),
2093 None,
2094 None,
2095 Some(32),
2096 &[],
2097 &[],
2098 false,
2099 ),
2100 ),
2101 None,
2102 None,
2103 None,
2104 ),
2105 Field::new(
2106 "status",
2107 Value::new(
2108 Cardinality::One,
2109 Item::new(
2110 ItemTarget::Is("test::Status"),
2111 None,
2112 None,
2113 None,
2114 &[],
2115 &[],
2116 false,
2117 ),
2118 ),
2119 Some(crate::node::Arg::ConstPath("test::Status::Active")),
2120 None,
2121 None,
2122 ),
2123 Field::new(
2124 "degrees",
2125 Value::new(
2126 Cardinality::One,
2127 Item::new(
2128 ItemTarget::Is("test::Degrees"),
2129 None,
2130 None,
2131 None,
2132 &[],
2133 &[],
2134 false,
2135 ),
2136 ),
2137 None,
2138 None,
2139 None,
2140 ),
2141 Field::new(
2142 "label",
2143 Value::new(
2144 Cardinality::One,
2145 Item::new(
2146 ItemTarget::Is("test::Label"),
2147 None,
2148 None,
2149 None,
2150 &[],
2151 &[],
2152 false,
2153 ),
2154 ),
2155 None,
2156 None,
2157 None,
2158 ),
2159 ];
2160
2161 #[test]
2162 #[expect(
2163 clippy::too_many_lines,
2164 reason = "one graph fixture proves the complete field, type, relation, and durable-rule closure"
2165 )]
2166 fn sealed_canister_graph_emits_store_free_database_closure() {
2167 let mut schema = Schema::new();
2168 schema.insert_node(SchemaNode::Canister(Canister::new(
2169 Def::new("test", "Canister"),
2170 "test",
2171 None,
2172 )));
2173 schema.insert_node(SchemaNode::Store(Store::new_heap(
2174 Def::new("test", "Store"),
2175 "test::Canister",
2176 StoreHeapConfig::new(),
2177 )));
2178 schema.insert_node(SchemaNode::Enum(Enum::new(
2179 Def::new("test", "Status"),
2180 "Status",
2181 &STATUS_VARIANTS,
2182 EMPTY_TYPE.clone(),
2183 )));
2184 schema.insert_node(SchemaNode::Newtype(Newtype::new(
2185 Def::new("test", "Degrees"),
2186 "Degrees",
2187 Item::new(
2188 ItemTarget::Primitive(Primitive::Nat16),
2189 None,
2190 None,
2191 None,
2192 &[],
2193 &[],
2194 false,
2195 ),
2196 None,
2197 NUMERIC_RULE_TYPE.clone(),
2198 )));
2199 schema.insert_node(SchemaNode::Newtype(Newtype::new(
2200 Def::new("test", "Label"),
2201 "Label",
2202 Item::new(
2203 ItemTarget::Primitive(Primitive::Text),
2204 None,
2205 None,
2206 None,
2207 &[],
2208 &[],
2209 false,
2210 ),
2211 None,
2212 LENGTH_RULE_TYPE.clone(),
2213 )));
2214 schema.insert_node(SchemaNode::Entity(Entity::new(
2215 Def::new("test", "Task"),
2216 "test::Store",
2217 1,
2218 PrimaryKey::new(&["id"], PrimaryKeySource::External),
2219 &[],
2220 &[],
2221 &[],
2222 FieldList::new(&ENTITY_FIELDS),
2223 EMPTY_TYPE.clone(),
2224 )));
2225 schema.seal().expect("fixture graph should seal");
2226
2227 let fragment = schema
2228 .schema_fragment_for_canister("test::Canister")
2229 .expect("sealed database closure should lower");
2230
2231 assert_eq!(fragment.entities().len(), 1);
2232 assert_eq!(fragment.types().len(), 3);
2233 let fields = fragment.entities()[0].fields();
2234 assert!(matches!(
2235 fields
2236 .iter()
2237 .find(|field| field.name().as_str() == "tags")
2238 .map(icydb_schema::FieldFragment::field_type),
2239 Some(FieldType::List(item))
2240 if matches!(item.as_ref(), FieldType::Scalar(ScalarType::Text { max_len: Some(32) }))
2241 ));
2242 assert!(matches!(
2243 fields
2244 .iter()
2245 .find(|field| field.name().as_str() == "status")
2246 .map(icydb_schema::FieldFragment::insert_policy),
2247 Some(icydb_schema::FieldInsertPolicy::Default(
2248 icydb_schema::ScalarLiteral::EnumUnit { .. }
2249 ))
2250 ));
2251 let NamedTypeFragment::Enum(status) = fragment
2252 .types()
2253 .iter()
2254 .find(|fragment| matches!(fragment, NamedTypeFragment::Enum(_)))
2255 .expect("reachable status type should remain an enum")
2256 else {
2257 panic!("reachable status type should remain an enum")
2258 };
2259 assert!(matches!(
2260 status
2261 .variants()
2262 .iter()
2263 .find(|variant| variant.name().as_str() == "Retries")
2264 .and_then(|variant| variant.payload()),
2265 Some(FieldType::List(item))
2266 if matches!(item.as_ref(), FieldType::Scalar(ScalarType::Nat16))
2267 ));
2268
2269 let constraints = fragment.entities()[0].constraints();
2270 assert_eq!(constraints.len(), 2);
2271 let degrees_source = ConstraintSourceKey::for_targeted_field_rule(
2272 &FieldSourceKey::try_new("degrees").expect("field name"),
2273 &TypeSourceKey::try_new("Degrees").expect("type name"),
2274 &RuleSourceKey::try_new("range").expect("rule name"),
2275 );
2276 let degrees = constraints
2277 .iter()
2278 .find(|constraint| constraint.source_key() == °rees_source)
2279 .expect("numeric rule should become one field-owned constraint");
2280 let ConstraintFragmentKind::TargetedRule(degrees) = degrees.kind() else {
2281 panic!("numeric rule should use the targeted-rule contract")
2282 };
2283 assert_eq!(degrees.root().as_str(), "degrees");
2284 assert_eq!(degrees.target_type().as_str(), "Degrees");
2285 assert!(matches!(
2286 degrees.operation(),
2287 SourceRuleOperation::NumericRangeInclusive { .. }
2288 ));
2289 let label = constraints
2290 .iter()
2291 .find(|constraint| constraint.source_key() != °rees_source)
2292 .expect("length rule should become one field-owned constraint");
2293 let ConstraintFragmentKind::TargetedRule(label) = label.kind() else {
2294 panic!("length rule should use the targeted-rule contract")
2295 };
2296 assert_eq!(label.target_type().as_str(), "Label");
2297 assert!(matches!(
2298 label.operation(),
2299 SourceRuleOperation::LengthRangeInclusive { min: 2, max: 40 }
2300 ));
2301 }
2302}