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