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