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, MAX_PROPOSAL_LITERAL_BYTES, NamedTypeFragment, Principal,
19 RecordFieldFragment, RecordTypeFragment, RelationDeleteAction, RelationFragment,
20 RelationPathStepFragment, RelationSourceFragment, RuleSourceKey, ScalarLiteral, ScalarType,
21 SchemaContractError, SchemaFragment, SchemaName,
22 SourceRuleOperation as ProposalSourceRuleOperation, Subaccount, TargetedRuleFragment,
23 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::Nat8
886 | Primitive::Nat16
887 | Primitive::Nat32
888 | Primitive::Nat64
889 | Primitive::Nat128 => rule_integer_u128(value).map(ScalarLiteral::Nat),
890 Primitive::U256 => rule_integer_text(value)
891 .and_then(|value| U256::from_str(value).ok())
892 .map(ScalarLiteral::U256),
893 Primitive::Account
894 | Primitive::Blob
895 | Primitive::Bool
896 | Primitive::Date
897 | Primitive::Duration
898 | Primitive::Principal
899 | Primitive::Subaccount
900 | Primitive::Text
901 | Primitive::Timestamp
902 | Primitive::Ulid
903 | Primitive::Unit => None,
904 }
905}
906
907const fn rule_integer_text(value: &RuleNumber) -> Option<&str> {
908 let RuleNumber::Integer(value) = value else {
909 return None;
910 };
911 Some(value)
912}
913
914fn rule_integer_i128(value: &RuleNumber) -> Option<i128> {
915 rule_integer_text(value)?.parse().ok()
916}
917
918fn rule_integer_u128(value: &RuleNumber) -> Option<u128> {
919 rule_integer_text(value)?.parse().ok()
920}
921
922fn rule_decimal(value: &RuleNumber) -> Option<Decimal> {
923 match value {
924 RuleNumber::Integer(value) | RuleNumber::Decimal(value) => Decimal::from_str(value).ok(),
925 RuleNumber::Float32(_) | RuleNumber::Float64(_) => None,
926 }
927}
928
929fn exact_decimal_at_scale(value: Decimal, scale: u32) -> Option<Decimal> {
930 let value = value.normalize();
931 value
932 .scale_to_integer(scale)
933 .and_then(|mantissa| Decimal::try_from_i128_with_scale(mantissa, scale))
934}
935
936#[derive(Clone, Copy)]
937enum RuleValueShape<'schema> {
938 Collection,
939 Scalar(Primitive, &'schema Item),
940}
941
942fn resolve_rule_value_shape<'schema>(
943 schema: &'schema Schema,
944 mut target: &'schema SchemaNode,
945) -> Result<RuleValueShape<'schema>, FragmentLoweringError> {
946 let mut visited = BTreeSet::new();
947 loop {
948 let source = named_type_name(target)
949 .ok_or_else(|| FragmentLoweringError::InvalidReference("non-type rule".to_string()))?;
950 if !visited.insert(source) {
951 return Err(FragmentLoweringError::InvalidReference(format!(
952 "durable-rule target cycle at '{source}'"
953 )));
954 }
955 match target {
956 SchemaNode::List(_) | SchemaNode::Map(_) | SchemaNode::Set(_) => {
957 return Ok(RuleValueShape::Collection);
958 }
959 SchemaNode::Newtype(newtype) => match newtype.item().target() {
960 ItemTarget::Primitive(primitive) => {
961 return Ok(RuleValueShape::Scalar(*primitive, newtype.item()));
962 }
963 ItemTarget::Is(path) => {
964 target = schema
965 .get_node(path)
966 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.to_string()))?;
967 }
968 },
969 SchemaNode::Record(_) | SchemaNode::Enum(_) | SchemaNode::Tuple(_) => {
970 return Err(FragmentLoweringError::InvalidReference(format!(
971 "durable-rule target '{source}' has no supported scalar or collection value"
972 )));
973 }
974 SchemaNode::Canister(_)
975 | SchemaNode::Entity(_)
976 | SchemaNode::Normalizer(_)
977 | SchemaNode::Store(_)
978 | SchemaNode::Validator(_) => {
979 return Err(FragmentLoweringError::InvalidReference(
980 "non-type durable-rule target".to_string(),
981 ));
982 }
983 }
984 }
985}
986
987fn invalid_rule_target(rule: &SourceRule) -> FragmentLoweringError {
988 FragmentLoweringError::InvalidReference(format!(
989 "durable rule '{}' does not match its nominal target",
990 rule.name()
991 ))
992}
993
994fn entity_field_source_key(
995 entity: &Entity,
996 field_name: &str,
997) -> Result<FieldSourceKey, FragmentLoweringError> {
998 let field = entity
999 .fields()
1000 .get(field_name)
1001 .ok_or_else(|| FragmentLoweringError::InvalidReference(field_name.to_string()))?;
1002 FieldSourceKey::try_new(field.name()).map_err(Into::into)
1003}
1004
1005fn lower_reachable_types(
1010 schema: &Schema,
1011 mut pending: Vec<String>,
1012) -> Result<Vec<NamedTypeFragment>, FragmentLoweringError> {
1013 let mut lowered = BTreeMap::new();
1014 while let Some(path) = pending.pop() {
1015 let node = schema
1016 .get_node(path.as_str())
1017 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.clone()))?;
1018 let source_key = named_type_name(node)
1019 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.clone()))?;
1020 if lowered.contains_key(source_key) {
1021 continue;
1022 }
1023 let fragment = lower_named_type(schema, node, &mut pending)?;
1024 lowered.insert(source_key.to_string(), fragment);
1025 }
1026 Ok(lowered.into_values().collect())
1027}
1028
1029const fn named_type_name(node: &crate::node::SchemaNode) -> Option<&str> {
1030 match node {
1031 crate::node::SchemaNode::Enum(node) => Some(node.name()),
1032 crate::node::SchemaNode::List(node) => Some(node.name()),
1033 crate::node::SchemaNode::Map(node) => Some(node.name()),
1034 crate::node::SchemaNode::Newtype(node) => Some(node.name()),
1035 crate::node::SchemaNode::Record(node) => Some(node.name()),
1036 crate::node::SchemaNode::Set(node) => Some(node.name()),
1037 crate::node::SchemaNode::Tuple(node) => Some(node.name()),
1038 crate::node::SchemaNode::Canister(_)
1039 | crate::node::SchemaNode::Entity(_)
1040 | crate::node::SchemaNode::Normalizer(_)
1041 | crate::node::SchemaNode::Store(_)
1042 | crate::node::SchemaNode::Validator(_) => None,
1043 }
1044}
1045
1046fn lower_named_type(
1047 schema: &Schema,
1048 node: &crate::node::SchemaNode,
1049 pending: &mut Vec<String>,
1050) -> Result<NamedTypeFragment, FragmentLoweringError> {
1051 match node {
1052 crate::node::SchemaNode::Record(record) => lower_record(schema, record, pending),
1053 crate::node::SchemaNode::Enum(r#enum) => lower_enum(schema, r#enum, pending),
1054 crate::node::SchemaNode::Newtype(newtype) => Ok(NamedTypeFragment::newtype(
1055 SchemaName::try_new(newtype.name())?,
1056 lower_item_type(schema, newtype.item(), pending)?,
1057 )),
1058 crate::node::SchemaNode::List(list) => lower_list(schema, list, pending),
1059 crate::node::SchemaNode::Set(set) => lower_set(schema, set, pending),
1060 crate::node::SchemaNode::Map(map) => lower_map(schema, map, pending),
1061 crate::node::SchemaNode::Tuple(tuple) => lower_tuple(schema, tuple, pending),
1062 crate::node::SchemaNode::Canister(_)
1063 | crate::node::SchemaNode::Entity(_)
1064 | crate::node::SchemaNode::Normalizer(_)
1065 | crate::node::SchemaNode::Store(_)
1066 | crate::node::SchemaNode::Validator(_) => Err(FragmentLoweringError::InvalidReference(
1067 "non-type graph node".to_string(),
1068 )),
1069 }
1070}
1071
1072fn lower_record(
1073 schema: &Schema,
1074 record: &Record,
1075 pending: &mut Vec<String>,
1076) -> Result<NamedTypeFragment, FragmentLoweringError> {
1077 let fields = record
1078 .fields()
1079 .fields()
1080 .iter()
1081 .map(|field| {
1082 Ok(RecordFieldFragment::new(
1083 SchemaName::try_new(field.name())?,
1084 lower_value_type(schema, field.value(), pending)?,
1085 field.value().cardinality() == Cardinality::Opt,
1086 ))
1087 })
1088 .collect::<Result<Vec<_>, FragmentLoweringError>>()?;
1089 Ok(NamedTypeFragment::Record(RecordTypeFragment::try_new(
1090 SchemaName::try_new(record.name())?,
1091 fields,
1092 )?))
1093}
1094
1095fn lower_enum(
1096 schema: &Schema,
1097 r#enum: &Enum,
1098 pending: &mut Vec<String>,
1099) -> Result<NamedTypeFragment, FragmentLoweringError> {
1100 let variants = r#enum
1101 .variants()
1102 .iter()
1103 .map(|variant| {
1104 let name = SchemaName::try_new(variant.name())?;
1105 match variant.value() {
1106 Some(value) if value.cardinality() == Cardinality::Opt => {
1107 Err(FragmentLoweringError::UnsupportedCardinality(format!(
1108 "{}::{}",
1109 r#enum.def().path(),
1110 variant.name()
1111 )))
1112 }
1113 Some(value) => Ok(EnumVariantFragment::with_payload(
1114 name,
1115 lower_value_type(schema, value, pending)?,
1116 )),
1117 None => Ok(EnumVariantFragment::new(name)),
1118 }
1119 })
1120 .collect::<Result<Vec<_>, _>>()?;
1121 Ok(NamedTypeFragment::Enum(EnumTypeFragment::try_new(
1122 SchemaName::try_new(r#enum.name())?,
1123 variants,
1124 )?))
1125}
1126
1127fn lower_list(
1128 schema: &Schema,
1129 list: &List,
1130 pending: &mut Vec<String>,
1131) -> Result<NamedTypeFragment, FragmentLoweringError> {
1132 Ok(NamedTypeFragment::list(
1133 SchemaName::try_new(list.name())?,
1134 lower_item_type(schema, list.item(), pending)?,
1135 ))
1136}
1137
1138fn lower_set(
1139 schema: &Schema,
1140 set: &Set,
1141 pending: &mut Vec<String>,
1142) -> Result<NamedTypeFragment, FragmentLoweringError> {
1143 Ok(NamedTypeFragment::set(
1144 SchemaName::try_new(set.name())?,
1145 lower_item_type(schema, set.item(), pending)?,
1146 ))
1147}
1148
1149fn lower_map(
1150 schema: &Schema,
1151 map: &Map,
1152 pending: &mut Vec<String>,
1153) -> Result<NamedTypeFragment, FragmentLoweringError> {
1154 if map.value().cardinality() == Cardinality::Opt {
1155 return Err(FragmentLoweringError::UnsupportedCardinality(
1156 map.def().path(),
1157 ));
1158 }
1159 Ok(NamedTypeFragment::map(
1160 SchemaName::try_new(map.name())?,
1161 lower_item_type(schema, map.key(), pending)?,
1162 lower_value_type(schema, map.value(), pending)?,
1163 ))
1164}
1165
1166fn lower_tuple(
1167 schema: &Schema,
1168 tuple: &Tuple,
1169 pending: &mut Vec<String>,
1170) -> Result<NamedTypeFragment, FragmentLoweringError> {
1171 let members = tuple
1172 .values()
1173 .iter()
1174 .map(|value| {
1175 Ok::<_, FragmentLoweringError>(TupleElementFragment::new(
1176 lower_value_type(schema, value, pending)?,
1177 value.cardinality() == Cardinality::Opt,
1178 ))
1179 })
1180 .collect::<Result<Vec<_>, _>>()?;
1181 Ok(NamedTypeFragment::tuple(
1182 SchemaName::try_new(tuple.name())?,
1183 members,
1184 ))
1185}
1186
1187fn lower_value_type(
1192 schema: &Schema,
1193 value: &Value,
1194 pending: &mut Vec<String>,
1195) -> Result<FieldType, FragmentLoweringError> {
1196 let item = lower_item_type(schema, value.item(), pending)?;
1197 Ok(if value.cardinality() == Cardinality::Many {
1198 FieldType::List(Box::new(item))
1199 } else {
1200 item
1201 })
1202}
1203
1204fn lower_item_type(
1205 schema: &Schema,
1206 item: &Item,
1207 pending: &mut Vec<String>,
1208) -> Result<FieldType, FragmentLoweringError> {
1209 match item.target() {
1210 ItemTarget::Is(path) => {
1211 pending.push((*path).to_string());
1212 Ok(FieldType::Named(TypeSourceKey::try_new(
1213 type_source_key_for_path(schema, path)?,
1214 )?))
1215 }
1216 ItemTarget::Primitive(primitive) => {
1217 Ok(FieldType::Scalar(lower_scalar_type(*primitive, item)))
1218 }
1219 }
1220}
1221
1222fn type_source_key_for_path<'schema>(
1223 schema: &'schema Schema,
1224 path: &str,
1225) -> Result<&'schema str, FragmentLoweringError> {
1226 let source = schema
1227 .get_node(path)
1228 .and_then(named_type_name)
1229 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.to_string()))?;
1230 Ok(source)
1231}
1232
1233fn lower_scalar_type(primitive: Primitive, item: &Item) -> ScalarType {
1234 match primitive {
1235 Primitive::Account => ScalarType::Account,
1236 Primitive::Blob => ScalarType::Blob {
1237 max_len: item.max_len(),
1238 },
1239 Primitive::Bool => ScalarType::Bool,
1240 Primitive::Date => ScalarType::Date,
1241 Primitive::Decimal => ScalarType::Decimal {
1242 scale: item.scale().unwrap_or(0),
1243 },
1244 Primitive::Duration => ScalarType::Duration,
1245 Primitive::Float32 => ScalarType::Float32,
1246 Primitive::Float64 => ScalarType::Float64,
1247 Primitive::Int8 => ScalarType::Int8,
1248 Primitive::Int16 => ScalarType::Int16,
1249 Primitive::Int32 => ScalarType::Int32,
1250 Primitive::Int64 => ScalarType::Int64,
1251 Primitive::Int128 => ScalarType::Int128,
1252 Primitive::Nat8 => ScalarType::Nat8,
1253 Primitive::Nat16 => ScalarType::Nat16,
1254 Primitive::Nat32 => ScalarType::Nat32,
1255 Primitive::Nat64 => ScalarType::Nat64,
1256 Primitive::Nat128 => ScalarType::Nat128,
1257 Primitive::U256 => ScalarType::U256,
1258 Primitive::Principal => ScalarType::Principal,
1259 Primitive::Subaccount => ScalarType::Subaccount,
1260 Primitive::Text => ScalarType::Text {
1261 max_len: item.max_len(),
1262 },
1263 Primitive::Timestamp => ScalarType::Timestamp,
1264 Primitive::Ulid => ScalarType::Ulid,
1265 Primitive::Unit => ScalarType::Unit,
1266 }
1267}
1268
1269fn lower_default(
1274 schema: &Schema,
1275 field: &Field,
1276 default: &Arg,
1277) -> Result<ScalarLiteral, FragmentLoweringError> {
1278 if let ItemTarget::Is(path) = field.value().item().target() {
1279 let Arg::ConstPath(default_path) = default else {
1280 return Err(FragmentLoweringError::InvalidDefault(
1281 field.name().to_string(),
1282 ));
1283 };
1284 let variant = default_path.rsplit("::").next().unwrap_or(default_path);
1285 return schema
1286 .enum_unit_literal(path, variant)
1287 .map_err(FragmentLoweringError::from);
1288 }
1289 let ItemTarget::Primitive(primitive) = field.value().item().target() else {
1290 return Err(FragmentLoweringError::InvalidDefault(
1291 field.name().to_string(),
1292 ));
1293 };
1294 lower_scalar_default(*primitive, field.value().item(), default)
1295 .ok_or_else(|| FragmentLoweringError::InvalidDefault(field.name().to_string()))
1296}
1297
1298fn lower_scalar_default(primitive: Primitive, item: &Item, default: &Arg) -> Option<ScalarLiteral> {
1299 if default_constructor_is_zero(default) {
1300 return zero_scalar_literal(primitive, item);
1301 }
1302 match (primitive, default) {
1303 (Primitive::Account, Arg::String(value)) => {
1304 Account::from_str(value).ok().map(ScalarLiteral::Account)
1305 }
1306 (Primitive::Blob, Arg::String(value)) => lower_blob_default(value),
1307 (Primitive::Bool, Arg::Bool(value)) => Some(ScalarLiteral::Bool(*value)),
1308 (Primitive::Date, Arg::String(value)) => Date::parse(value).map(ScalarLiteral::Date),
1309 (Primitive::Date, Arg::Number(value)) => arg_i128(value)
1310 .and_then(|value| i32::try_from(value).ok())
1311 .and_then(Date::try_from_days_since_epoch)
1312 .map(ScalarLiteral::Date),
1313 (Primitive::Decimal, Arg::String(value)) => Decimal::from_str(value)
1314 .ok()
1315 .and_then(|value| decimal_at_scale(value, item.scale().unwrap_or(0)))
1316 .map(ScalarLiteral::Decimal),
1317 (Primitive::Decimal, Arg::Number(value)) => arg_decimal(value)
1318 .and_then(|value| decimal_at_scale(value, item.scale().unwrap_or(0)))
1319 .map(ScalarLiteral::Decimal),
1320 (Primitive::Duration, Arg::String(value)) => Duration::parse_flexible(value)
1321 .ok()
1322 .map(ScalarLiteral::Duration),
1323 (Primitive::Duration, Arg::Number(value)) => arg_u128(value)
1324 .and_then(|value| u64::try_from(value).ok())
1325 .map(Duration::from_millis)
1326 .map(ScalarLiteral::Duration),
1327 (Primitive::Float32, Arg::Number(ArgNumber::Float32(value))) => {
1328 Float32::try_new(*value).map(ScalarLiteral::Float32)
1329 }
1330 (Primitive::Float64, Arg::Number(ArgNumber::Float64(value))) => {
1331 Float64::try_new(*value).map(ScalarLiteral::Float64)
1332 }
1333 (
1334 Primitive::Int8
1335 | Primitive::Int16
1336 | Primitive::Int32
1337 | Primitive::Int64
1338 | Primitive::Int128,
1339 Arg::Number(value),
1340 ) => arg_i128(value).map(ScalarLiteral::Int),
1341 (
1342 Primitive::Nat8
1343 | Primitive::Nat16
1344 | Primitive::Nat32
1345 | Primitive::Nat64
1346 | Primitive::Nat128,
1347 Arg::Number(value),
1348 ) => arg_u128(value).map(ScalarLiteral::Nat),
1349 (Primitive::U256, Arg::Number(value)) => {
1350 arg_u128(value).map(U256::from).map(ScalarLiteral::U256)
1351 }
1352 (Primitive::U256, Arg::String(value)) => {
1353 U256::from_str(value).ok().map(ScalarLiteral::U256)
1354 }
1355 (Primitive::Principal, Arg::String(value)) => Principal::from_str(value)
1356 .ok()
1357 .map(ScalarLiteral::Principal),
1358 (Primitive::Subaccount, Arg::String(value)) => Subaccount::from_hex(value)
1359 .ok()
1360 .map(ScalarLiteral::Subaccount),
1361 (Primitive::Text, Arg::String(value)) => Some(ScalarLiteral::Text((*value).to_string())),
1362 (Primitive::Timestamp, Arg::String(value)) => Timestamp::parse_flexible(value)
1363 .ok()
1364 .map(ScalarLiteral::Timestamp),
1365 (Primitive::Timestamp, Arg::Number(value)) => arg_i128(value)
1366 .and_then(|value| i64::try_from(value).ok())
1367 .map(Timestamp::from_millis)
1368 .map(ScalarLiteral::Timestamp),
1369 (Primitive::Ulid, Arg::String(value)) => {
1370 Ulid::from_str(value).ok().map(ScalarLiteral::Ulid)
1371 }
1372 (Primitive::Unit, Arg::ConstPath(path)) if path.ends_with("Unit") => {
1373 Some(ScalarLiteral::Unit(Unit))
1374 }
1375 _ => None,
1376 }
1377}
1378
1379fn default_constructor_is_zero(default: &Arg) -> bool {
1380 let Arg::FuncPath(path) = default else {
1381 return false;
1382 };
1383 path.ends_with("::default")
1384 || path.ends_with("::new")
1385 || path.ends_with("::EPOCH")
1386 || path.ends_with("::nil")
1387}
1388
1389fn zero_scalar_literal(primitive: Primitive, item: &Item) -> Option<ScalarLiteral> {
1390 match primitive {
1391 Primitive::Blob => Some(ScalarLiteral::Blob(Blob::default())),
1392 Primitive::Bool => Some(ScalarLiteral::Bool(false)),
1393 Primitive::Date => Some(ScalarLiteral::Date(Date::EPOCH)),
1394 Primitive::Decimal => Decimal::try_from_i128_with_scale(0, item.scale().unwrap_or(0))
1395 .map(ScalarLiteral::Decimal),
1396 Primitive::Duration => Some(ScalarLiteral::Duration(Duration::ZERO)),
1397 Primitive::Float32 => Float32::try_new(0.0).map(ScalarLiteral::Float32),
1398 Primitive::Float64 => Float64::try_new(0.0).map(ScalarLiteral::Float64),
1399 Primitive::Int8
1400 | Primitive::Int16
1401 | Primitive::Int32
1402 | Primitive::Int64
1403 | Primitive::Int128 => Some(ScalarLiteral::Int(0)),
1404 Primitive::Nat8
1405 | Primitive::Nat16
1406 | Primitive::Nat32
1407 | Primitive::Nat64
1408 | Primitive::Nat128 => Some(ScalarLiteral::Nat(0)),
1409 Primitive::U256 => Some(ScalarLiteral::U256(U256::ZERO)),
1410 Primitive::Text => Some(ScalarLiteral::Text(String::new())),
1411 Primitive::Timestamp => Some(ScalarLiteral::Timestamp(Timestamp::EPOCH)),
1412 Primitive::Ulid => Some(ScalarLiteral::Ulid(Ulid::nil())),
1413 Primitive::Unit => Some(ScalarLiteral::Unit(Unit)),
1414 Primitive::Account | Primitive::Principal | Primitive::Subaccount => None,
1415 }
1416}
1417
1418fn lower_blob_default(value: &str) -> Option<ScalarLiteral> {
1419 if value.len() > MAX_PROPOSAL_LITERAL_BYTES {
1420 return None;
1421 }
1422 Some(ScalarLiteral::Blob(Blob::from(value.as_bytes())))
1423}
1424
1425fn decimal_at_scale(value: Decimal, scale: u32) -> Option<Decimal> {
1430 match value.scale().cmp(&scale) {
1431 std::cmp::Ordering::Equal => Some(value),
1432 std::cmp::Ordering::Less => value
1433 .scale_to_integer(scale)
1434 .and_then(|mantissa| Decimal::try_from_i128_with_scale(mantissa, scale)),
1435 std::cmp::Ordering::Greater => Some(value.round_dp(scale)),
1436 }
1437}
1438
1439fn arg_i128(value: &ArgNumber) -> Option<i128> {
1440 match value {
1441 ArgNumber::Int8(value) => Some(i128::from(*value)),
1442 ArgNumber::Int16(value) => Some(i128::from(*value)),
1443 ArgNumber::Int32(value) => Some(i128::from(*value)),
1444 ArgNumber::Int64(value) => Some(i128::from(*value)),
1445 ArgNumber::Int128(value) => Some(*value),
1446 ArgNumber::Nat8(value) => Some(i128::from(*value)),
1447 ArgNumber::Nat16(value) => Some(i128::from(*value)),
1448 ArgNumber::Nat32(value) => Some(i128::from(*value)),
1449 ArgNumber::Nat64(value) => Some(i128::from(*value)),
1450 ArgNumber::Nat128(value) => i128::try_from(*value).ok(),
1451 ArgNumber::Float32(_) | ArgNumber::Float64(_) => None,
1452 }
1453}
1454
1455fn arg_u128(value: &ArgNumber) -> Option<u128> {
1456 match value {
1457 ArgNumber::Int8(value) => u128::try_from(*value).ok(),
1458 ArgNumber::Int16(value) => u128::try_from(*value).ok(),
1459 ArgNumber::Int32(value) => u128::try_from(*value).ok(),
1460 ArgNumber::Int64(value) => u128::try_from(*value).ok(),
1461 ArgNumber::Int128(value) => u128::try_from(*value).ok(),
1462 ArgNumber::Nat8(value) => Some(u128::from(*value)),
1463 ArgNumber::Nat16(value) => Some(u128::from(*value)),
1464 ArgNumber::Nat32(value) => Some(u128::from(*value)),
1465 ArgNumber::Nat64(value) => Some(u128::from(*value)),
1466 ArgNumber::Nat128(value) => Some(*value),
1467 ArgNumber::Float32(_) | ArgNumber::Float64(_) => None,
1468 }
1469}
1470
1471fn arg_decimal(value: &ArgNumber) -> Option<Decimal> {
1472 match value {
1473 ArgNumber::Float32(value) => Decimal::from_f32_lossy(*value),
1474 ArgNumber::Float64(value) => Decimal::from_f64_lossy(*value),
1475 _ => arg_i128(value).and_then(Decimal::from_i128),
1476 }
1477}
1478
1479#[cfg(test)]
1480mod tests {
1481 use icydb_schema::{
1482 ConstraintFragmentKind, ConstraintSourceKey, Decimal, FieldSourceKey, FieldType,
1483 MAX_PROPOSAL_LITERAL_BYTES, NamedTypeFragment, RuleSourceKey, ScalarLiteral, ScalarType,
1484 SourceRuleOperation, TypeSourceKey,
1485 };
1486
1487 use super::{
1488 Schema, lower_blob_default, lower_field_rules, lower_nested_relations, lower_scalar_default,
1489 };
1490 use crate::{
1491 node::{
1492 Arg, Args, Canister, Def, Entity, Enum, EnumVariant, Field, FieldList, Item,
1493 ItemTarget, Newtype, Normalizer, PrimaryKey, PrimaryKeySource, Record, RuleNumber,
1494 SchemaNode, SourceRule, SourceRuleAuthoringOperation, Store, StoreHeapConfig, Type,
1495 TypeNormalizer, TypeValidator, Validator, Value,
1496 },
1497 types::{Cardinality, Primitive},
1498 };
1499
1500 #[test]
1501 fn blob_default_lowering_enforces_the_proposal_literal_bound() {
1502 let maximum = "a".repeat(MAX_PROPOSAL_LITERAL_BYTES);
1503 let oversized = "a".repeat(MAX_PROPOSAL_LITERAL_BYTES + 1);
1504
1505 assert_eq!(
1506 lower_blob_default(&maximum).and_then(|literal| match literal {
1507 ScalarLiteral::Blob(value) => Some(value.len()),
1508 _ => None,
1509 }),
1510 Some(MAX_PROPOSAL_LITERAL_BYTES),
1511 );
1512 assert_eq!(lower_blob_default(&oversized), None);
1513 }
1514
1515 #[test]
1516 fn duration_default_lowering_rejects_suffixed_overflow() {
1517 let item = Item::new(
1518 ItemTarget::Primitive(Primitive::Duration),
1519 None,
1520 None,
1521 None,
1522 &[],
1523 &[],
1524 false,
1525 );
1526
1527 assert_eq!(
1528 lower_scalar_default(
1529 Primitive::Duration,
1530 &item,
1531 &Arg::String("18446744073709551615ms"),
1532 ),
1533 Some(ScalarLiteral::Duration(icydb_schema::Duration::MAX)),
1534 );
1535 assert_eq!(
1536 lower_scalar_default(
1537 Primitive::Duration,
1538 &item,
1539 &Arg::String("18446744073709552s"),
1540 ),
1541 None,
1542 );
1543 }
1544
1545 static EMPTY_TYPE: Type = Type::new(&[], &[], &[]);
1546 static APPLICATION_FIELDS: [Field; 1] = [Field::new(
1547 "id",
1548 Value::new(
1549 Cardinality::One,
1550 Item::new(
1551 ItemTarget::Primitive(Primitive::Nat64),
1552 None,
1553 None,
1554 None,
1555 &[],
1556 &[],
1557 false,
1558 ),
1559 ),
1560 None,
1561 None,
1562 None,
1563 )];
1564 static APPLICATION_NORMALIZERS_A: [TypeNormalizer; 1] =
1565 [TypeNormalizer::new("test::NormalizeA", Args(&[]))];
1566 static APPLICATION_NORMALIZERS_B: [TypeNormalizer; 1] =
1567 [TypeNormalizer::new("test::NormalizeB", Args(&[]))];
1568 static APPLICATION_VALIDATORS_A: [TypeValidator; 1] =
1569 [TypeValidator::new("test::ValidateA", Args(&[]))];
1570 static APPLICATION_VALIDATORS_B: [TypeValidator; 1] =
1571 [TypeValidator::new("test::ValidateB", Args(&[]))];
1572 static NUMERIC_RULES: [SourceRule; 1] = [SourceRule::new(
1573 "range",
1574 SourceRuleAuthoringOperation::NumericRangeInclusive {
1575 min: RuleNumber::Integer("0"),
1576 max: RuleNumber::Integer("360"),
1577 },
1578 )];
1579 static NUMERIC_RULE_TYPE: Type = Type::new(&[], &[], &NUMERIC_RULES);
1580 static LENGTH_RULES: [SourceRule; 1] = [SourceRule::new(
1581 "length",
1582 SourceRuleAuthoringOperation::LengthRangeInclusive {
1583 min: RuleNumber::Integer("2"),
1584 max: RuleNumber::Integer("40"),
1585 },
1586 )];
1587 static LENGTH_RULE_TYPE: Type = Type::new(&[], &[], &LENGTH_RULES);
1588 static NAT_EXACT_RULES: [SourceRule; 2] = [
1589 SourceRule::new(
1590 "maximum",
1591 SourceRuleAuthoringOperation::NumericMaximumInclusive {
1592 value: RuleNumber::Integer("100"),
1593 },
1594 ),
1595 SourceRule::new(
1596 "step",
1597 SourceRuleAuthoringOperation::MultipleOf {
1598 divisor: RuleNumber::Integer("5"),
1599 },
1600 ),
1601 ];
1602 static NAT_EXACT_RULE_TYPE: Type = Type::new(&[], &[], &NAT_EXACT_RULES);
1603 static DECIMAL_EXACT_RULES: [SourceRule; 1] = [SourceRule::new(
1604 "step",
1605 SourceRuleAuthoringOperation::MultipleOf {
1606 divisor: RuleNumber::Decimal("0.25"),
1607 },
1608 )];
1609 static DECIMAL_EXACT_RULE_TYPE: Type = Type::new(&[], &[], &DECIMAL_EXACT_RULES);
1610 static INEXACT_DECIMAL_RULES: [SourceRule; 1] = [SourceRule::new(
1611 "step",
1612 SourceRuleAuthoringOperation::MultipleOf {
1613 divisor: RuleNumber::Decimal("0.251"),
1614 },
1615 )];
1616 static INEXACT_DECIMAL_RULE_TYPE: Type = Type::new(&[], &[], &INEXACT_DECIMAL_RULES);
1617 static NESTED_RULE_FIELDS: [Field; 1] = [Field::new(
1618 "degrees",
1619 Value::new(
1620 Cardinality::One,
1621 Item::new(
1622 ItemTarget::Is("test::Degrees"),
1623 None,
1624 None,
1625 None,
1626 &[],
1627 &[],
1628 false,
1629 ),
1630 ),
1631 None,
1632 None,
1633 None,
1634 )];
1635 static STATUS_VARIANTS: [EnumVariant; 2] = [
1636 EnumVariant::new("Active", None),
1637 EnumVariant::new(
1638 "Retries",
1639 Some(Value::new(
1640 Cardinality::Many,
1641 Item::new(
1642 ItemTarget::Primitive(Primitive::Nat16),
1643 None,
1644 None,
1645 None,
1646 &[],
1647 &[],
1648 false,
1649 ),
1650 )),
1651 ),
1652 ];
1653 static RECURSIVE_RECORD_FIELDS: [Field; 1] = [Field::new(
1654 "next",
1655 Value::new(
1656 Cardinality::Opt,
1657 Item::new(
1658 ItemTarget::Is("test::Recursive"),
1659 None,
1660 None,
1661 None,
1662 &[],
1663 &[],
1664 false,
1665 ),
1666 ),
1667 None,
1668 None,
1669 None,
1670 )];
1671 static RECURSIVE_ENTITY_FIELDS: [Field; 1] = [Field::new(
1672 "root",
1673 Value::new(
1674 Cardinality::One,
1675 Item::new(
1676 ItemTarget::Is("test::Recursive"),
1677 None,
1678 None,
1679 None,
1680 &[],
1681 &[],
1682 false,
1683 ),
1684 ),
1685 None,
1686 None,
1687 None,
1688 )];
1689
1690 #[test]
1691 fn recursive_structural_type_without_a_finite_relation_path_is_ignored() {
1692 let mut schema = Schema::new();
1693 schema.insert_node(SchemaNode::Record(Record::new(
1694 Def::new("test", "Recursive"),
1695 "Recursive",
1696 FieldList::new(&RECURSIVE_RECORD_FIELDS),
1697 EMPTY_TYPE.clone(),
1698 )));
1699 let entity = Entity::new(
1700 Def::new("test", "RecursiveOwner"),
1701 "test::Store",
1702 1,
1703 PrimaryKey::new(&["root"], PrimaryKeySource::External),
1704 &[],
1705 &[],
1706 &[],
1707 FieldList::new(&RECURSIVE_ENTITY_FIELDS),
1708 EMPTY_TYPE.clone(),
1709 );
1710
1711 assert!(
1712 lower_nested_relations(&schema, &entity, &RECURSIVE_ENTITY_FIELDS[0])
1713 .expect("recursive structural types without finite relations should lower")
1714 .is_empty(),
1715 );
1716 }
1717
1718 fn application_behavior_fragment(
1719 normalizers: &'static [TypeNormalizer],
1720 validators: &'static [TypeValidator],
1721 normalizer_name: &'static str,
1722 validator_name: &'static str,
1723 ) -> icydb_schema::SchemaFragment {
1724 let mut schema = Schema::new();
1725 schema.insert_node(SchemaNode::Canister(Canister::new(
1726 Def::new("test", "Canister"),
1727 "test",
1728 None,
1729 )));
1730 schema.insert_node(SchemaNode::Store(Store::new_heap(
1731 Def::new("test", "Store"),
1732 "test::Canister",
1733 StoreHeapConfig::new(),
1734 )));
1735 schema.insert_node(SchemaNode::Normalizer(Normalizer::new(Def::new(
1736 "test",
1737 normalizer_name,
1738 ))));
1739 schema.insert_node(SchemaNode::Validator(Validator::new(Def::new(
1740 "test",
1741 validator_name,
1742 ))));
1743 schema.insert_node(SchemaNode::Entity(Entity::new(
1744 Def::new("test", "ApplicationOnly"),
1745 "test::Store",
1746 1,
1747 PrimaryKey::new(&["id"], PrimaryKeySource::External),
1748 &[],
1749 &[],
1750 &[],
1751 FieldList::new(&APPLICATION_FIELDS),
1752 Type::new(normalizers, validators, &[]),
1753 )));
1754 schema.seal().expect("application-only fixture should seal");
1755 schema
1756 .schema_fragment_for_canister("test::Canister")
1757 .expect("application-only fixture should lower")
1758 }
1759
1760 #[test]
1761 fn validator_and_normalizer_edits_do_not_change_database_fragment() {
1762 let before = application_behavior_fragment(
1763 &APPLICATION_NORMALIZERS_A,
1764 &APPLICATION_VALIDATORS_A,
1765 "NormalizeA",
1766 "ValidateA",
1767 );
1768 let after = application_behavior_fragment(
1769 &APPLICATION_NORMALIZERS_B,
1770 &APPLICATION_VALIDATORS_B,
1771 "NormalizeB",
1772 "ValidateB",
1773 );
1774
1775 assert_eq!(before, after);
1776 }
1777
1778 #[test]
1779 fn durable_rules_nested_below_structural_fields_lower_to_nominal_targets() {
1780 let mut schema = Schema::new();
1781 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1782 Def::new("test", "Degrees"),
1783 "Degrees",
1784 Item::new(
1785 ItemTarget::Primitive(Primitive::Nat16),
1786 None,
1787 None,
1788 None,
1789 &[],
1790 &[],
1791 false,
1792 ),
1793 None,
1794 NUMERIC_RULE_TYPE.clone(),
1795 )));
1796 schema.insert_node(SchemaNode::Record(Record::new(
1797 Def::new("test", "Nested"),
1798 "Nested",
1799 FieldList::new(&NESTED_RULE_FIELDS),
1800 EMPTY_TYPE.clone(),
1801 )));
1802
1803 let outer = Field::new(
1804 "nested",
1805 Value::new(
1806 Cardinality::One,
1807 Item::new(
1808 ItemTarget::Is("test::Nested"),
1809 None,
1810 None,
1811 None,
1812 &[],
1813 &[],
1814 false,
1815 ),
1816 ),
1817 None,
1818 None,
1819 None,
1820 );
1821 let constraints =
1822 lower_field_rules(&schema, &outer).expect("nested durable rule should lower");
1823 assert_eq!(constraints.len(), 1);
1824 let ConstraintFragmentKind::TargetedRule(rule) = constraints[0].kind() else {
1825 panic!("nested durable rule should use the targeted-rule contract")
1826 };
1827 assert_eq!(rule.root().as_str(), "nested");
1828 assert_eq!(rule.target_type().as_str(), "Degrees");
1829 assert!(matches!(
1830 rule.operation(),
1831 SourceRuleOperation::NumericRangeInclusive { .. }
1832 ));
1833 }
1834
1835 #[test]
1836 fn exact_maximum_and_multiple_of_lower_without_float_reconstruction() {
1837 let mut schema = Schema::new();
1838 for (name, primitive, scale, rules) in [
1839 (
1840 "Counter",
1841 Primitive::Nat64,
1842 None,
1843 NAT_EXACT_RULE_TYPE.clone(),
1844 ),
1845 (
1846 "PriceStep",
1847 Primitive::Decimal,
1848 Some(2),
1849 DECIMAL_EXACT_RULE_TYPE.clone(),
1850 ),
1851 ] {
1852 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1853 Def::new("test", name),
1854 name,
1855 Item::new(
1856 ItemTarget::Primitive(primitive),
1857 None,
1858 scale,
1859 None,
1860 &[],
1861 &[],
1862 false,
1863 ),
1864 None,
1865 rules,
1866 )));
1867 }
1868
1869 let field = |name| {
1870 Field::new(
1871 name,
1872 Value::new(
1873 Cardinality::One,
1874 Item::new(
1875 ItemTarget::Is(if name == "counter" {
1876 "test::Counter"
1877 } else {
1878 "test::PriceStep"
1879 }),
1880 None,
1881 None,
1882 None,
1883 &[],
1884 &[],
1885 false,
1886 ),
1887 ),
1888 None,
1889 None,
1890 None,
1891 )
1892 };
1893 let counter = lower_field_rules(&schema, &field("counter"))
1894 .expect("exact integer rules should lower");
1895 assert!(matches!(
1896 counter[0].kind(),
1897 ConstraintFragmentKind::TargetedRule(rule)
1898 if matches!(
1899 rule.operation(),
1900 SourceRuleOperation::NumericMaximumInclusive {
1901 value: ScalarLiteral::Nat(100)
1902 }
1903 )
1904 ));
1905 assert!(matches!(
1906 counter[1].kind(),
1907 ConstraintFragmentKind::TargetedRule(rule)
1908 if matches!(
1909 rule.operation(),
1910 SourceRuleOperation::MultipleOf {
1911 divisor: ScalarLiteral::Nat(5)
1912 }
1913 )
1914 ));
1915
1916 let decimal = lower_field_rules(&schema, &field("price"))
1917 .expect("exact decimal multiple should lower");
1918 assert!(matches!(
1919 decimal[0].kind(),
1920 ConstraintFragmentKind::TargetedRule(rule)
1921 if matches!(
1922 rule.operation(),
1923 SourceRuleOperation::MultipleOf {
1924 divisor: ScalarLiteral::Decimal(value)
1925 } if *value == Decimal::new(25, 2)
1926 )
1927 ));
1928 }
1929
1930 #[test]
1931 fn inexact_decimal_rule_operand_rejects_before_proposal_composition() {
1932 let mut schema = Schema::new();
1933 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1934 Def::new("test", "InexactStep"),
1935 "InexactStep",
1936 Item::new(
1937 ItemTarget::Primitive(Primitive::Decimal),
1938 None,
1939 Some(2),
1940 None,
1941 &[],
1942 &[],
1943 false,
1944 ),
1945 None,
1946 INEXACT_DECIMAL_RULE_TYPE.clone(),
1947 )));
1948 let field = Field::new(
1949 "price",
1950 Value::new(
1951 Cardinality::One,
1952 Item::new(
1953 ItemTarget::Is("test::InexactStep"),
1954 None,
1955 None,
1956 None,
1957 &[],
1958 &[],
1959 false,
1960 ),
1961 ),
1962 None,
1963 None,
1964 None,
1965 );
1966 assert!(lower_field_rules(&schema, &field).is_err());
1967 }
1968
1969 static ENTITY_FIELDS: [Field; 5] = [
1970 Field::new(
1971 "id",
1972 Value::new(
1973 Cardinality::One,
1974 Item::new(
1975 ItemTarget::Primitive(Primitive::Nat64),
1976 None,
1977 None,
1978 None,
1979 &[],
1980 &[],
1981 false,
1982 ),
1983 ),
1984 None,
1985 None,
1986 None,
1987 ),
1988 Field::new(
1989 "tags",
1990 Value::new(
1991 Cardinality::Many,
1992 Item::new(
1993 ItemTarget::Primitive(Primitive::Text),
1994 None,
1995 None,
1996 Some(32),
1997 &[],
1998 &[],
1999 false,
2000 ),
2001 ),
2002 None,
2003 None,
2004 None,
2005 ),
2006 Field::new(
2007 "status",
2008 Value::new(
2009 Cardinality::One,
2010 Item::new(
2011 ItemTarget::Is("test::Status"),
2012 None,
2013 None,
2014 None,
2015 &[],
2016 &[],
2017 false,
2018 ),
2019 ),
2020 Some(crate::node::Arg::ConstPath("test::Status::Active")),
2021 None,
2022 None,
2023 ),
2024 Field::new(
2025 "degrees",
2026 Value::new(
2027 Cardinality::One,
2028 Item::new(
2029 ItemTarget::Is("test::Degrees"),
2030 None,
2031 None,
2032 None,
2033 &[],
2034 &[],
2035 false,
2036 ),
2037 ),
2038 None,
2039 None,
2040 None,
2041 ),
2042 Field::new(
2043 "label",
2044 Value::new(
2045 Cardinality::One,
2046 Item::new(
2047 ItemTarget::Is("test::Label"),
2048 None,
2049 None,
2050 None,
2051 &[],
2052 &[],
2053 false,
2054 ),
2055 ),
2056 None,
2057 None,
2058 None,
2059 ),
2060 ];
2061
2062 #[test]
2063 #[expect(
2064 clippy::too_many_lines,
2065 reason = "one graph fixture proves the complete field, type, relation, and durable-rule closure"
2066 )]
2067 fn sealed_canister_graph_emits_store_free_database_closure() {
2068 let mut schema = Schema::new();
2069 schema.insert_node(SchemaNode::Canister(Canister::new(
2070 Def::new("test", "Canister"),
2071 "test",
2072 None,
2073 )));
2074 schema.insert_node(SchemaNode::Store(Store::new_heap(
2075 Def::new("test", "Store"),
2076 "test::Canister",
2077 StoreHeapConfig::new(),
2078 )));
2079 schema.insert_node(SchemaNode::Enum(Enum::new(
2080 Def::new("test", "Status"),
2081 "Status",
2082 &STATUS_VARIANTS,
2083 EMPTY_TYPE.clone(),
2084 )));
2085 schema.insert_node(SchemaNode::Newtype(Newtype::new(
2086 Def::new("test", "Degrees"),
2087 "Degrees",
2088 Item::new(
2089 ItemTarget::Primitive(Primitive::Nat16),
2090 None,
2091 None,
2092 None,
2093 &[],
2094 &[],
2095 false,
2096 ),
2097 None,
2098 NUMERIC_RULE_TYPE.clone(),
2099 )));
2100 schema.insert_node(SchemaNode::Newtype(Newtype::new(
2101 Def::new("test", "Label"),
2102 "Label",
2103 Item::new(
2104 ItemTarget::Primitive(Primitive::Text),
2105 None,
2106 None,
2107 None,
2108 &[],
2109 &[],
2110 false,
2111 ),
2112 None,
2113 LENGTH_RULE_TYPE.clone(),
2114 )));
2115 schema.insert_node(SchemaNode::Entity(Entity::new(
2116 Def::new("test", "Task"),
2117 "test::Store",
2118 1,
2119 PrimaryKey::new(&["id"], PrimaryKeySource::External),
2120 &[],
2121 &[],
2122 &[],
2123 FieldList::new(&ENTITY_FIELDS),
2124 EMPTY_TYPE.clone(),
2125 )));
2126 schema.seal().expect("fixture graph should seal");
2127
2128 let fragment = schema
2129 .schema_fragment_for_canister("test::Canister")
2130 .expect("sealed database closure should lower");
2131
2132 assert_eq!(fragment.entities().len(), 1);
2133 assert_eq!(fragment.types().len(), 3);
2134 let fields = fragment.entities()[0].fields();
2135 assert!(matches!(
2136 fields
2137 .iter()
2138 .find(|field| field.name().as_str() == "tags")
2139 .map(icydb_schema::FieldFragment::field_type),
2140 Some(FieldType::List(item))
2141 if matches!(item.as_ref(), FieldType::Scalar(ScalarType::Text { max_len: Some(32) }))
2142 ));
2143 assert!(matches!(
2144 fields
2145 .iter()
2146 .find(|field| field.name().as_str() == "status")
2147 .map(icydb_schema::FieldFragment::insert_policy),
2148 Some(icydb_schema::FieldInsertPolicy::Default(
2149 icydb_schema::ScalarLiteral::EnumUnit { .. }
2150 ))
2151 ));
2152 let NamedTypeFragment::Enum(status) = fragment
2153 .types()
2154 .iter()
2155 .find(|fragment| matches!(fragment, NamedTypeFragment::Enum(_)))
2156 .expect("reachable status type should remain an enum")
2157 else {
2158 panic!("reachable status type should remain an enum")
2159 };
2160 assert!(matches!(
2161 status
2162 .variants()
2163 .iter()
2164 .find(|variant| variant.name().as_str() == "Retries")
2165 .and_then(|variant| variant.payload()),
2166 Some(FieldType::List(item))
2167 if matches!(item.as_ref(), FieldType::Scalar(ScalarType::Nat16))
2168 ));
2169
2170 let constraints = fragment.entities()[0].constraints();
2171 assert_eq!(constraints.len(), 2);
2172 let degrees_source = ConstraintSourceKey::for_targeted_field_rule(
2173 &FieldSourceKey::try_new("degrees").expect("field name"),
2174 &TypeSourceKey::try_new("Degrees").expect("type name"),
2175 &RuleSourceKey::try_new("range").expect("rule name"),
2176 );
2177 let degrees = constraints
2178 .iter()
2179 .find(|constraint| constraint.source_key() == °rees_source)
2180 .expect("numeric rule should become one field-owned constraint");
2181 let ConstraintFragmentKind::TargetedRule(degrees) = degrees.kind() else {
2182 panic!("numeric rule should use the targeted-rule contract")
2183 };
2184 assert_eq!(degrees.root().as_str(), "degrees");
2185 assert_eq!(degrees.target_type().as_str(), "Degrees");
2186 assert!(matches!(
2187 degrees.operation(),
2188 SourceRuleOperation::NumericRangeInclusive { .. }
2189 ));
2190 let label = constraints
2191 .iter()
2192 .find(|constraint| constraint.source_key() != °rees_source)
2193 .expect("length rule should become one field-owned constraint");
2194 let ConstraintFragmentKind::TargetedRule(label) = label.kind() else {
2195 panic!("length rule should use the targeted-rule contract")
2196 };
2197 assert_eq!(label.target_type().as_str(), "Label");
2198 assert!(matches!(
2199 label.operation(),
2200 SourceRuleOperation::LengthRangeInclusive { min: 2, max: 40 }
2201 ));
2202 }
2203}