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)) => parse_subaccount(value)
1385 .map(Subaccount::from_array)
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
1507fn parse_subaccount(value: &str) -> Option<[u8; 32]> {
1508 if value.len() != 64 {
1509 return None;
1510 }
1511 let mut bytes = [0; 32];
1512 for (index, chunk) in value.as_bytes().chunks_exact(2).enumerate() {
1513 let text = std::str::from_utf8(chunk).ok()?;
1514 bytes[index] = u8::from_str_radix(text, 16).ok()?;
1515 }
1516 Some(bytes)
1517}
1518
1519#[cfg(test)]
1520mod tests {
1521 use icydb_schema::{
1522 ConstraintFragmentKind, ConstraintSourceKey, Decimal, FieldSourceKey, FieldType,
1523 MAX_PROPOSAL_LITERAL_BYTES, NamedTypeFragment, RuleSourceKey, ScalarLiteral, ScalarType,
1524 SourceRuleOperation, TypeSourceKey,
1525 };
1526
1527 use super::{
1528 Schema, lower_blob_default, lower_field_rules, lower_nested_relations, lower_scalar_default,
1529 };
1530 use crate::{
1531 node::{
1532 Arg, Args, Canister, Def, Entity, Enum, EnumVariant, Field, FieldList, Item,
1533 ItemTarget, Newtype, Normalizer, PrimaryKey, PrimaryKeySource, Record, RuleNumber,
1534 SchemaNode, SourceRule, SourceRuleAuthoringOperation, Store, StoreHeapConfig, Type,
1535 TypeNormalizer, TypeValidator, Validator, Value,
1536 },
1537 types::{Cardinality, Primitive},
1538 };
1539
1540 #[test]
1541 fn blob_default_lowering_enforces_the_proposal_literal_bound() {
1542 let maximum = "a".repeat(MAX_PROPOSAL_LITERAL_BYTES);
1543 let oversized = "a".repeat(MAX_PROPOSAL_LITERAL_BYTES + 1);
1544
1545 assert_eq!(
1546 lower_blob_default(&maximum).and_then(|literal| match literal {
1547 ScalarLiteral::Blob(value) => Some(value.len()),
1548 _ => None,
1549 }),
1550 Some(MAX_PROPOSAL_LITERAL_BYTES),
1551 );
1552 assert_eq!(lower_blob_default(&oversized), None);
1553 }
1554
1555 #[test]
1556 fn duration_default_lowering_rejects_suffixed_overflow() {
1557 let item = Item::new(
1558 ItemTarget::Primitive(Primitive::Duration),
1559 None,
1560 None,
1561 None,
1562 None,
1563 &[],
1564 &[],
1565 false,
1566 );
1567
1568 assert_eq!(
1569 lower_scalar_default(
1570 Primitive::Duration,
1571 &item,
1572 &Arg::String("18446744073709551615ms"),
1573 ),
1574 Some(ScalarLiteral::Duration(icydb_schema::Duration::MAX)),
1575 );
1576 assert_eq!(
1577 lower_scalar_default(
1578 Primitive::Duration,
1579 &item,
1580 &Arg::String("18446744073709552s"),
1581 ),
1582 None,
1583 );
1584 }
1585
1586 static EMPTY_TYPE: Type = Type::new(&[], &[], &[]);
1587 static APPLICATION_FIELDS: [Field; 1] = [Field::new(
1588 "id",
1589 Value::new(
1590 Cardinality::One,
1591 Item::new(
1592 ItemTarget::Primitive(Primitive::Nat64),
1593 None,
1594 None,
1595 None,
1596 None,
1597 &[],
1598 &[],
1599 false,
1600 ),
1601 ),
1602 None,
1603 None,
1604 None,
1605 )];
1606 static APPLICATION_NORMALIZERS_A: [TypeNormalizer; 1] =
1607 [TypeNormalizer::new("test::NormalizeA", Args(&[]))];
1608 static APPLICATION_NORMALIZERS_B: [TypeNormalizer; 1] =
1609 [TypeNormalizer::new("test::NormalizeB", Args(&[]))];
1610 static APPLICATION_VALIDATORS_A: [TypeValidator; 1] =
1611 [TypeValidator::new("test::ValidateA", Args(&[]))];
1612 static APPLICATION_VALIDATORS_B: [TypeValidator; 1] =
1613 [TypeValidator::new("test::ValidateB", Args(&[]))];
1614 static NUMERIC_RULES: [SourceRule; 1] = [SourceRule::new(
1615 "range",
1616 SourceRuleAuthoringOperation::NumericRangeInclusive {
1617 min: RuleNumber::Integer("0"),
1618 max: RuleNumber::Integer("360"),
1619 },
1620 )];
1621 static NUMERIC_RULE_TYPE: Type = Type::new(&[], &[], &NUMERIC_RULES);
1622 static LENGTH_RULES: [SourceRule; 1] = [SourceRule::new(
1623 "length",
1624 SourceRuleAuthoringOperation::LengthRangeInclusive {
1625 min: RuleNumber::Integer("2"),
1626 max: RuleNumber::Integer("40"),
1627 },
1628 )];
1629 static LENGTH_RULE_TYPE: Type = Type::new(&[], &[], &LENGTH_RULES);
1630 static NAT_EXACT_RULES: [SourceRule; 2] = [
1631 SourceRule::new(
1632 "maximum",
1633 SourceRuleAuthoringOperation::NumericMaximumInclusive {
1634 value: RuleNumber::Integer("100"),
1635 },
1636 ),
1637 SourceRule::new(
1638 "step",
1639 SourceRuleAuthoringOperation::MultipleOf {
1640 divisor: RuleNumber::Integer("5"),
1641 },
1642 ),
1643 ];
1644 static NAT_EXACT_RULE_TYPE: Type = Type::new(&[], &[], &NAT_EXACT_RULES);
1645 static DECIMAL_EXACT_RULES: [SourceRule; 1] = [SourceRule::new(
1646 "step",
1647 SourceRuleAuthoringOperation::MultipleOf {
1648 divisor: RuleNumber::Decimal("0.25"),
1649 },
1650 )];
1651 static DECIMAL_EXACT_RULE_TYPE: Type = Type::new(&[], &[], &DECIMAL_EXACT_RULES);
1652 static INEXACT_DECIMAL_RULES: [SourceRule; 1] = [SourceRule::new(
1653 "step",
1654 SourceRuleAuthoringOperation::MultipleOf {
1655 divisor: RuleNumber::Decimal("0.251"),
1656 },
1657 )];
1658 static INEXACT_DECIMAL_RULE_TYPE: Type = Type::new(&[], &[], &INEXACT_DECIMAL_RULES);
1659 static NESTED_RULE_FIELDS: [Field; 1] = [Field::new(
1660 "degrees",
1661 Value::new(
1662 Cardinality::One,
1663 Item::new(
1664 ItemTarget::Is("test::Degrees"),
1665 None,
1666 None,
1667 None,
1668 None,
1669 &[],
1670 &[],
1671 false,
1672 ),
1673 ),
1674 None,
1675 None,
1676 None,
1677 )];
1678 static STATUS_VARIANTS: [EnumVariant; 2] = [
1679 EnumVariant::new("Active", None),
1680 EnumVariant::new(
1681 "Retries",
1682 Some(Value::new(
1683 Cardinality::Many,
1684 Item::new(
1685 ItemTarget::Primitive(Primitive::Nat16),
1686 None,
1687 None,
1688 None,
1689 None,
1690 &[],
1691 &[],
1692 false,
1693 ),
1694 )),
1695 ),
1696 ];
1697 static RECURSIVE_RECORD_FIELDS: [Field; 1] = [Field::new(
1698 "next",
1699 Value::new(
1700 Cardinality::Opt,
1701 Item::new(
1702 ItemTarget::Is("test::Recursive"),
1703 None,
1704 None,
1705 None,
1706 None,
1707 &[],
1708 &[],
1709 false,
1710 ),
1711 ),
1712 None,
1713 None,
1714 None,
1715 )];
1716 static RECURSIVE_ENTITY_FIELDS: [Field; 1] = [Field::new(
1717 "root",
1718 Value::new(
1719 Cardinality::One,
1720 Item::new(
1721 ItemTarget::Is("test::Recursive"),
1722 None,
1723 None,
1724 None,
1725 None,
1726 &[],
1727 &[],
1728 false,
1729 ),
1730 ),
1731 None,
1732 None,
1733 None,
1734 )];
1735
1736 #[test]
1737 fn recursive_structural_type_without_a_finite_relation_path_is_ignored() {
1738 let mut schema = Schema::new();
1739 schema.insert_node(SchemaNode::Record(Record::new(
1740 Def::new("test", "Recursive"),
1741 "Recursive",
1742 FieldList::new(&RECURSIVE_RECORD_FIELDS),
1743 EMPTY_TYPE.clone(),
1744 )));
1745 let entity = Entity::new(
1746 Def::new("test", "RecursiveOwner"),
1747 "test::Store",
1748 1,
1749 PrimaryKey::new(&["root"], PrimaryKeySource::External),
1750 &[],
1751 &[],
1752 &[],
1753 FieldList::new(&RECURSIVE_ENTITY_FIELDS),
1754 EMPTY_TYPE.clone(),
1755 );
1756
1757 assert!(
1758 lower_nested_relations(&schema, &entity, &RECURSIVE_ENTITY_FIELDS[0])
1759 .expect("recursive structural types without finite relations should lower")
1760 .is_empty(),
1761 );
1762 }
1763
1764 fn application_behavior_fragment(
1765 normalizers: &'static [TypeNormalizer],
1766 validators: &'static [TypeValidator],
1767 normalizer_name: &'static str,
1768 validator_name: &'static str,
1769 ) -> icydb_schema::SchemaFragment {
1770 let mut schema = Schema::new();
1771 schema.insert_node(SchemaNode::Canister(Canister::new(
1772 Def::new("test", "Canister"),
1773 "test",
1774 0,
1775 10,
1776 9,
1777 7,
1778 8,
1779 None,
1780 )));
1781 schema.insert_node(SchemaNode::Store(Store::new_heap(
1782 Def::new("test", "Store"),
1783 "test::Canister",
1784 StoreHeapConfig::new(),
1785 )));
1786 schema.insert_node(SchemaNode::Normalizer(Normalizer::new(Def::new(
1787 "test",
1788 normalizer_name,
1789 ))));
1790 schema.insert_node(SchemaNode::Validator(Validator::new(Def::new(
1791 "test",
1792 validator_name,
1793 ))));
1794 schema.insert_node(SchemaNode::Entity(Entity::new(
1795 Def::new("test", "ApplicationOnly"),
1796 "test::Store",
1797 1,
1798 PrimaryKey::new(&["id"], PrimaryKeySource::External),
1799 &[],
1800 &[],
1801 &[],
1802 FieldList::new(&APPLICATION_FIELDS),
1803 Type::new(normalizers, validators, &[]),
1804 )));
1805 schema.seal().expect("application-only fixture should seal");
1806 schema
1807 .schema_fragment_for_canister("test::Canister")
1808 .expect("application-only fixture should lower")
1809 }
1810
1811 #[test]
1812 fn validator_and_normalizer_edits_do_not_change_database_fragment() {
1813 let before = application_behavior_fragment(
1814 &APPLICATION_NORMALIZERS_A,
1815 &APPLICATION_VALIDATORS_A,
1816 "NormalizeA",
1817 "ValidateA",
1818 );
1819 let after = application_behavior_fragment(
1820 &APPLICATION_NORMALIZERS_B,
1821 &APPLICATION_VALIDATORS_B,
1822 "NormalizeB",
1823 "ValidateB",
1824 );
1825
1826 assert_eq!(before, after);
1827 }
1828
1829 #[test]
1830 fn durable_rules_nested_below_structural_fields_lower_to_nominal_targets() {
1831 let mut schema = Schema::new();
1832 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1833 Def::new("test", "Degrees"),
1834 "Degrees",
1835 Item::new(
1836 ItemTarget::Primitive(Primitive::Nat16),
1837 None,
1838 None,
1839 None,
1840 None,
1841 &[],
1842 &[],
1843 false,
1844 ),
1845 None,
1846 NUMERIC_RULE_TYPE.clone(),
1847 )));
1848 schema.insert_node(SchemaNode::Record(Record::new(
1849 Def::new("test", "Nested"),
1850 "Nested",
1851 FieldList::new(&NESTED_RULE_FIELDS),
1852 EMPTY_TYPE.clone(),
1853 )));
1854
1855 let outer = Field::new(
1856 "nested",
1857 Value::new(
1858 Cardinality::One,
1859 Item::new(
1860 ItemTarget::Is("test::Nested"),
1861 None,
1862 None,
1863 None,
1864 None,
1865 &[],
1866 &[],
1867 false,
1868 ),
1869 ),
1870 None,
1871 None,
1872 None,
1873 );
1874 let constraints =
1875 lower_field_rules(&schema, &outer).expect("nested durable rule should lower");
1876 assert_eq!(constraints.len(), 1);
1877 let ConstraintFragmentKind::TargetedRule(rule) = constraints[0].kind() else {
1878 panic!("nested durable rule should use the targeted-rule contract")
1879 };
1880 assert_eq!(rule.root().as_str(), "nested");
1881 assert_eq!(rule.target_type().as_str(), "Degrees");
1882 assert!(matches!(
1883 rule.operation(),
1884 SourceRuleOperation::NumericRangeInclusive { .. }
1885 ));
1886 }
1887
1888 #[test]
1889 fn exact_maximum_and_multiple_of_lower_without_float_reconstruction() {
1890 let mut schema = Schema::new();
1891 for (name, primitive, scale, rules) in [
1892 (
1893 "Counter",
1894 Primitive::Nat64,
1895 None,
1896 NAT_EXACT_RULE_TYPE.clone(),
1897 ),
1898 (
1899 "PriceStep",
1900 Primitive::Decimal,
1901 Some(2),
1902 DECIMAL_EXACT_RULE_TYPE.clone(),
1903 ),
1904 ] {
1905 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1906 Def::new("test", name),
1907 name,
1908 Item::new(
1909 ItemTarget::Primitive(primitive),
1910 None,
1911 scale,
1912 None,
1913 None,
1914 &[],
1915 &[],
1916 false,
1917 ),
1918 None,
1919 rules,
1920 )));
1921 }
1922
1923 let field = |name| {
1924 Field::new(
1925 name,
1926 Value::new(
1927 Cardinality::One,
1928 Item::new(
1929 ItemTarget::Is(if name == "counter" {
1930 "test::Counter"
1931 } else {
1932 "test::PriceStep"
1933 }),
1934 None,
1935 None,
1936 None,
1937 None,
1938 &[],
1939 &[],
1940 false,
1941 ),
1942 ),
1943 None,
1944 None,
1945 None,
1946 )
1947 };
1948 let counter = lower_field_rules(&schema, &field("counter"))
1949 .expect("exact integer rules should lower");
1950 assert!(matches!(
1951 counter[0].kind(),
1952 ConstraintFragmentKind::TargetedRule(rule)
1953 if matches!(
1954 rule.operation(),
1955 SourceRuleOperation::NumericMaximumInclusive {
1956 value: ScalarLiteral::Nat(100)
1957 }
1958 )
1959 ));
1960 assert!(matches!(
1961 counter[1].kind(),
1962 ConstraintFragmentKind::TargetedRule(rule)
1963 if matches!(
1964 rule.operation(),
1965 SourceRuleOperation::MultipleOf {
1966 divisor: ScalarLiteral::Nat(5)
1967 }
1968 )
1969 ));
1970
1971 let decimal = lower_field_rules(&schema, &field("price"))
1972 .expect("exact decimal multiple should lower");
1973 assert!(matches!(
1974 decimal[0].kind(),
1975 ConstraintFragmentKind::TargetedRule(rule)
1976 if matches!(
1977 rule.operation(),
1978 SourceRuleOperation::MultipleOf {
1979 divisor: ScalarLiteral::Decimal(value)
1980 } if *value == Decimal::new(25, 2)
1981 )
1982 ));
1983 }
1984
1985 #[test]
1986 fn inexact_decimal_rule_operand_rejects_before_proposal_composition() {
1987 let mut schema = Schema::new();
1988 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1989 Def::new("test", "InexactStep"),
1990 "InexactStep",
1991 Item::new(
1992 ItemTarget::Primitive(Primitive::Decimal),
1993 None,
1994 Some(2),
1995 None,
1996 None,
1997 &[],
1998 &[],
1999 false,
2000 ),
2001 None,
2002 INEXACT_DECIMAL_RULE_TYPE.clone(),
2003 )));
2004 let field = Field::new(
2005 "price",
2006 Value::new(
2007 Cardinality::One,
2008 Item::new(
2009 ItemTarget::Is("test::InexactStep"),
2010 None,
2011 None,
2012 None,
2013 None,
2014 &[],
2015 &[],
2016 false,
2017 ),
2018 ),
2019 None,
2020 None,
2021 None,
2022 );
2023 assert!(lower_field_rules(&schema, &field).is_err());
2024 }
2025
2026 static ENTITY_FIELDS: [Field; 5] = [
2027 Field::new(
2028 "id",
2029 Value::new(
2030 Cardinality::One,
2031 Item::new(
2032 ItemTarget::Primitive(Primitive::Nat64),
2033 None,
2034 None,
2035 None,
2036 None,
2037 &[],
2038 &[],
2039 false,
2040 ),
2041 ),
2042 None,
2043 None,
2044 None,
2045 ),
2046 Field::new(
2047 "tags",
2048 Value::new(
2049 Cardinality::Many,
2050 Item::new(
2051 ItemTarget::Primitive(Primitive::Text),
2052 None,
2053 None,
2054 Some(32),
2055 None,
2056 &[],
2057 &[],
2058 false,
2059 ),
2060 ),
2061 None,
2062 None,
2063 None,
2064 ),
2065 Field::new(
2066 "status",
2067 Value::new(
2068 Cardinality::One,
2069 Item::new(
2070 ItemTarget::Is("test::Status"),
2071 None,
2072 None,
2073 None,
2074 None,
2075 &[],
2076 &[],
2077 false,
2078 ),
2079 ),
2080 Some(crate::node::Arg::ConstPath("test::Status::Active")),
2081 None,
2082 None,
2083 ),
2084 Field::new(
2085 "degrees",
2086 Value::new(
2087 Cardinality::One,
2088 Item::new(
2089 ItemTarget::Is("test::Degrees"),
2090 None,
2091 None,
2092 None,
2093 None,
2094 &[],
2095 &[],
2096 false,
2097 ),
2098 ),
2099 None,
2100 None,
2101 None,
2102 ),
2103 Field::new(
2104 "label",
2105 Value::new(
2106 Cardinality::One,
2107 Item::new(
2108 ItemTarget::Is("test::Label"),
2109 None,
2110 None,
2111 None,
2112 None,
2113 &[],
2114 &[],
2115 false,
2116 ),
2117 ),
2118 None,
2119 None,
2120 None,
2121 ),
2122 ];
2123
2124 #[test]
2125 #[expect(
2126 clippy::too_many_lines,
2127 reason = "one graph fixture proves the complete field, type, relation, and durable-rule closure"
2128 )]
2129 fn sealed_canister_graph_emits_store_free_database_closure() {
2130 let mut schema = Schema::new();
2131 schema.insert_node(SchemaNode::Canister(Canister::new(
2132 Def::new("test", "Canister"),
2133 "test",
2134 0,
2135 10,
2136 9,
2137 7,
2138 8,
2139 None,
2140 )));
2141 schema.insert_node(SchemaNode::Store(Store::new_heap(
2142 Def::new("test", "Store"),
2143 "test::Canister",
2144 StoreHeapConfig::new(),
2145 )));
2146 schema.insert_node(SchemaNode::Enum(Enum::new(
2147 Def::new("test", "Status"),
2148 "Status",
2149 &STATUS_VARIANTS,
2150 EMPTY_TYPE.clone(),
2151 )));
2152 schema.insert_node(SchemaNode::Newtype(Newtype::new(
2153 Def::new("test", "Degrees"),
2154 "Degrees",
2155 Item::new(
2156 ItemTarget::Primitive(Primitive::Nat16),
2157 None,
2158 None,
2159 None,
2160 None,
2161 &[],
2162 &[],
2163 false,
2164 ),
2165 None,
2166 NUMERIC_RULE_TYPE.clone(),
2167 )));
2168 schema.insert_node(SchemaNode::Newtype(Newtype::new(
2169 Def::new("test", "Label"),
2170 "Label",
2171 Item::new(
2172 ItemTarget::Primitive(Primitive::Text),
2173 None,
2174 None,
2175 None,
2176 None,
2177 &[],
2178 &[],
2179 false,
2180 ),
2181 None,
2182 LENGTH_RULE_TYPE.clone(),
2183 )));
2184 schema.insert_node(SchemaNode::Entity(Entity::new(
2185 Def::new("test", "Task"),
2186 "test::Store",
2187 1,
2188 PrimaryKey::new(&["id"], PrimaryKeySource::External),
2189 &[],
2190 &[],
2191 &[],
2192 FieldList::new(&ENTITY_FIELDS),
2193 EMPTY_TYPE.clone(),
2194 )));
2195 schema.seal().expect("fixture graph should seal");
2196
2197 let fragment = schema
2198 .schema_fragment_for_canister("test::Canister")
2199 .expect("sealed database closure should lower");
2200
2201 assert_eq!(fragment.entities().len(), 1);
2202 assert_eq!(fragment.types().len(), 3);
2203 let fields = fragment.entities()[0].fields();
2204 assert!(matches!(
2205 fields
2206 .iter()
2207 .find(|field| field.name().as_str() == "tags")
2208 .map(icydb_schema::FieldFragment::field_type),
2209 Some(FieldType::List(item))
2210 if matches!(item.as_ref(), FieldType::Scalar(ScalarType::Text { max_len: Some(32) }))
2211 ));
2212 assert!(matches!(
2213 fields
2214 .iter()
2215 .find(|field| field.name().as_str() == "status")
2216 .map(icydb_schema::FieldFragment::insert_policy),
2217 Some(icydb_schema::FieldInsertPolicy::Default(
2218 icydb_schema::ScalarLiteral::EnumUnit { .. }
2219 ))
2220 ));
2221 let NamedTypeFragment::Enum(status) = fragment
2222 .types()
2223 .iter()
2224 .find(|fragment| matches!(fragment, NamedTypeFragment::Enum(_)))
2225 .expect("reachable status type should remain an enum")
2226 else {
2227 panic!("reachable status type should remain an enum")
2228 };
2229 assert!(matches!(
2230 status
2231 .variants()
2232 .iter()
2233 .find(|variant| variant.name().as_str() == "Retries")
2234 .and_then(|variant| variant.payload()),
2235 Some(FieldType::List(item))
2236 if matches!(item.as_ref(), FieldType::Scalar(ScalarType::Nat16))
2237 ));
2238
2239 let constraints = fragment.entities()[0].constraints();
2240 assert_eq!(constraints.len(), 2);
2241 let degrees_source = ConstraintSourceKey::for_targeted_field_rule(
2242 &FieldSourceKey::try_new("degrees").expect("field name"),
2243 &TypeSourceKey::try_new("Degrees").expect("type name"),
2244 &RuleSourceKey::try_new("range").expect("rule name"),
2245 );
2246 let degrees = constraints
2247 .iter()
2248 .find(|constraint| constraint.source_key() == °rees_source)
2249 .expect("numeric rule should become one field-owned constraint");
2250 let ConstraintFragmentKind::TargetedRule(degrees) = degrees.kind() else {
2251 panic!("numeric rule should use the targeted-rule contract")
2252 };
2253 assert_eq!(degrees.root().as_str(), "degrees");
2254 assert_eq!(degrees.target_type().as_str(), "Degrees");
2255 assert!(matches!(
2256 degrees.operation(),
2257 SourceRuleOperation::NumericRangeInclusive { .. }
2258 ));
2259 let label = constraints
2260 .iter()
2261 .find(|constraint| constraint.source_key() != °rees_source)
2262 .expect("length rule should become one field-owned constraint");
2263 let ConstraintFragmentKind::TargetedRule(label) = label.kind() else {
2264 panic!("length rule should use the targeted-rule contract")
2265 };
2266 assert_eq!(label.target_type().as_str(), "Label");
2267 assert!(matches!(
2268 label.operation(),
2269 SourceRuleOperation::LengthRangeInclusive { min: 2, max: 40 }
2270 ));
2271 }
2272}