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, Duration,
16 EntityFragment, EntitySourceKey, EnumTypeFragment, EnumVariantFragment, FieldFragment,
17 FieldInsertPolicy, FieldManagementPolicy, FieldSourceKey, FieldType, Float32, Float64,
18 IndexFragment, IndexKeyFragment, IntBig, NamedTypeFragment, NatBig, Principal,
19 RecordFieldFragment, RecordTypeFragment, RelationDeleteAction, RelationFragment, RuleSourceKey,
20 ScalarLiteral, ScalarType, SchemaContractError, SchemaFragment, SchemaName,
21 SourceRuleOperation, Subaccount, TargetedRuleFragment, Timestamp, TupleElementFragment,
22 TypeSourceKey, Ulid, Unit,
23};
24use thiserror::Error;
25
26use crate::{
27 node::{
28 Arg, ArgNumber, Canister, CheckConstraint, Entity, Enum, Field, FieldWriteManagement,
29 Index, IndexExpression, IndexKeyItem, IndexKeyItemsRef, Item, ItemTarget, List, Map,
30 Record, RelationEdge, Schema, SchemaNode, Set, SourceRule, SourceRuleKind, Store, Tuple,
31 Value,
32 },
33 types::{Cardinality, Primitive},
34};
35
36#[derive(Debug, Error)]
39pub enum FragmentLoweringError {
40 #[error("schema canister has no registered stores: {0}")]
42 CanisterHasNoStores(String),
43
44 #[error("schema canister path is not registered: {0}")]
46 CanisterNotFound(String),
47
48 #[error(transparent)]
50 Contract(#[from] SchemaContractError),
51
52 #[error("schema graph must be sealed before fragment lowering")]
54 GraphNotSealed,
55
56 #[error("schema field default cannot be lowered: {0}")]
58 InvalidDefault(String),
59
60 #[error("schema fragment reference is invalid: {0}")]
62 InvalidReference(String),
63
64 #[error("schema value cardinality is unsupported at {0}")]
66 UnsupportedCardinality(String),
67}
68
69impl Schema {
74 pub fn schema_fragment_for_canister(
85 &self,
86 canister_path: &str,
87 ) -> Result<SchemaFragment, FragmentLoweringError> {
88 if !self.is_sealed() {
89 return Err(FragmentLoweringError::GraphNotSealed);
90 }
91 self.cast_node::<Canister>(canister_path)
92 .map_err(|_| FragmentLoweringError::CanisterNotFound(canister_path.to_string()))?;
93 let stores = self
94 .filter_nodes::<Store>(|store| store.canister() == canister_path)
95 .map(|(path, _)| path.to_string())
96 .collect::<BTreeSet<_>>();
97 if stores.is_empty() {
98 return Err(FragmentLoweringError::CanisterHasNoStores(
99 canister_path.to_string(),
100 ));
101 }
102
103 let entities = self
104 .get_nodes::<Entity>()
105 .filter(|(_, entity)| stores.contains(entity.store()))
106 .map(|(_, entity)| entity)
107 .collect::<Vec<_>>();
108 let selected_entities = entities
109 .iter()
110 .map(|entity| entity.def().path())
111 .collect::<BTreeSet<_>>();
112 for entity in &entities {
113 ensure_relation_targets_in_database(self, entity, &selected_entities)?;
114 }
115
116 let mut pending_types = Vec::new();
117 let entity_fragments = entities
118 .iter()
119 .map(|entity| lower_entity(self, entity, &mut pending_types))
120 .collect::<Result<Vec<_>, _>>()?;
121 let types = lower_reachable_types(self, pending_types)?;
122
123 SchemaFragment::try_new(entity_fragments, types).map_err(Into::into)
124 }
125}
126
127fn ensure_relation_targets_in_database(
128 schema: &Schema,
129 entity: &Entity,
130 selected_entities: &BTreeSet<String>,
131) -> Result<(), FragmentLoweringError> {
132 for target in entity
133 .fields()
134 .fields()
135 .iter()
136 .filter_map(|field| field.value().item().relation())
137 .chain(entity.relations().iter().map(RelationEdge::target))
138 {
139 schema
140 .cast_node::<Entity>(target)
141 .map_err(|_| FragmentLoweringError::InvalidReference(target.to_string()))?;
142 if !selected_entities.contains(target) {
143 return Err(FragmentLoweringError::InvalidReference(format!(
144 "relation target '{target}' is outside the selected database"
145 )));
146 }
147 }
148 Ok(())
149}
150
151fn lower_entity(
152 schema: &Schema,
153 entity: &Entity,
154 pending_types: &mut Vec<String>,
155) -> Result<EntityFragment, FragmentLoweringError> {
156 let fields = entity
157 .fields()
158 .fields()
159 .iter()
160 .map(|field| lower_entity_field(schema, field, pending_types))
161 .collect::<Result<Vec<_>, _>>()?;
162 let primary_key = entity
163 .primary_key()
164 .fields()
165 .iter()
166 .map(|name| entity_field_source_key(entity, name))
167 .collect::<Result<Vec<_>, _>>()?;
168 let indexes = entity
169 .indexes()
170 .iter()
171 .map(|index| lower_index(schema, entity, index))
172 .collect::<Result<Vec<_>, _>>()?;
173 let mut relations = entity
174 .fields()
175 .fields()
176 .iter()
177 .filter(|field| field.value().item().relation().is_some())
178 .map(|field| lower_scalar_relation(schema, entity, field))
179 .collect::<Result<Vec<_>, _>>()?;
180 relations.extend(
181 entity
182 .relations()
183 .iter()
184 .map(|relation| lower_composite_relation(schema, entity, relation))
185 .collect::<Result<Vec<_>, _>>()?,
186 );
187 let mut constraints = entity
188 .constraints()
189 .iter()
190 .map(|constraint| lower_constraint(schema, constraint))
191 .collect::<Result<Vec<_>, _>>()?;
192 for field in entity.fields().fields() {
193 constraints.extend(lower_field_rules(schema, field)?);
194 }
195
196 EntityFragment::try_new(
197 SchemaName::try_new(entity.name())?,
198 fields,
199 primary_key,
200 indexes,
201 relations,
202 constraints,
203 )
204 .map_err(Into::into)
205}
206
207fn lower_entity_field(
208 schema: &Schema,
209 field: &Field,
210 pending_types: &mut Vec<String>,
211) -> Result<FieldFragment, FragmentLoweringError> {
212 let field_type = lower_value_type(schema, field.value(), pending_types)?;
213 let nullable = field.value().cardinality() == Cardinality::Opt;
214 let insert_policy = if field.generated().is_some() {
215 FieldInsertPolicy::Generated
216 } else if let Some(default) = field.default() {
217 FieldInsertPolicy::Default(lower_default(schema, field, default)?)
218 } else if nullable {
219 FieldInsertPolicy::Nullable
220 } else {
221 FieldInsertPolicy::Required
222 };
223 let management = match field.write_management() {
224 Some(FieldWriteManagement::CreatedAt) => Some(FieldManagementPolicy::CreatedAt),
225 Some(FieldWriteManagement::UpdatedAt) => Some(FieldManagementPolicy::UpdatedAt),
226 None => None,
227 };
228 Ok(FieldFragment::new(
229 SchemaName::try_new(field.name())?,
230 field_type,
231 nullable,
232 insert_policy,
233 management,
234 ))
235}
236
237fn lower_index(
238 schema: &Schema,
239 entity: &Entity,
240 index: &Index,
241) -> Result<IndexFragment, FragmentLoweringError> {
242 let key = match index.key_items() {
243 IndexKeyItemsRef::Fields(fields) => fields
244 .iter()
245 .map(|field| entity_field_source_key(entity, field).map(IndexKeyFragment::Field))
246 .collect::<Result<Vec<_>, _>>()?,
247 IndexKeyItemsRef::Items(items) => items
248 .iter()
249 .map(|item| lower_index_key(entity, item))
250 .collect::<Result<Vec<_>, _>>()?,
251 };
252 IndexFragment::try_new(
253 SchemaName::try_new(index.name())?,
254 key,
255 index.is_unique(),
256 index.source_predicate(schema)?,
257 )
258 .map_err(Into::into)
259}
260
261fn lower_index_key(
262 entity: &Entity,
263 item: &IndexKeyItem,
264) -> Result<IndexKeyFragment, FragmentLoweringError> {
265 let field = entity_field_source_key(entity, item.field())?;
266 Ok(match item {
267 IndexKeyItem::Field(_) => IndexKeyFragment::Field(field),
268 IndexKeyItem::Expression(IndexExpression::Lower(_)) => IndexKeyFragment::Lower(field),
269 IndexKeyItem::Expression(IndexExpression::Upper(_)) => IndexKeyFragment::Upper(field),
270 IndexKeyItem::Expression(IndexExpression::Trim(_)) => IndexKeyFragment::Trim(field),
271 IndexKeyItem::Expression(IndexExpression::LowerTrim(_)) => {
272 IndexKeyFragment::LowerTrim(field)
273 }
274 IndexKeyItem::Expression(IndexExpression::Date(_)) => IndexKeyFragment::Date(field),
275 IndexKeyItem::Expression(IndexExpression::Year(_)) => IndexKeyFragment::Year(field),
276 IndexKeyItem::Expression(IndexExpression::Month(_)) => IndexKeyFragment::Month(field),
277 IndexKeyItem::Expression(IndexExpression::Day(_)) => IndexKeyFragment::Day(field),
278 })
279}
280
281fn lower_scalar_relation(
282 schema: &Schema,
283 entity: &Entity,
284 field: &Field,
285) -> Result<RelationFragment, FragmentLoweringError> {
286 let target_path = field
287 .value()
288 .item()
289 .relation()
290 .ok_or_else(|| FragmentLoweringError::InvalidReference(field.name().to_string()))?;
291 let target = schema
292 .cast_node::<Entity>(target_path)
293 .map_err(|_| FragmentLoweringError::InvalidReference(target_path.to_string()))?;
294 RelationFragment::try_new(
295 SchemaName::try_new(field.name())?,
296 vec![entity_field_source_key(entity, field.name())?],
297 EntitySourceKey::try_new(target.name())?,
298 target
299 .primary_key()
300 .fields()
301 .iter()
302 .map(|field| entity_field_source_key(target, field))
303 .collect::<Result<Vec<_>, _>>()?,
304 RelationDeleteAction::Restrict,
305 )
306 .map_err(Into::into)
307}
308
309fn lower_composite_relation(
310 schema: &Schema,
311 entity: &Entity,
312 relation: &RelationEdge,
313) -> Result<RelationFragment, FragmentLoweringError> {
314 let target = schema
315 .cast_node::<Entity>(relation.target())
316 .map_err(|_| FragmentLoweringError::InvalidReference(relation.target().to_string()))?;
317 RelationFragment::try_new(
318 SchemaName::try_new(relation.name())?,
319 relation
320 .local_fields()
321 .iter()
322 .map(|field| entity_field_source_key(entity, field))
323 .collect::<Result<Vec<_>, _>>()?,
324 EntitySourceKey::try_new(target.name())?,
325 target
326 .primary_key()
327 .fields()
328 .iter()
329 .map(|field| entity_field_source_key(target, field))
330 .collect::<Result<Vec<_>, _>>()?,
331 RelationDeleteAction::Restrict,
332 )
333 .map_err(Into::into)
334}
335
336fn lower_constraint(
337 schema: &Schema,
338 constraint: &CheckConstraint,
339) -> Result<ConstraintFragment, FragmentLoweringError> {
340 Ok(ConstraintFragment::check(
341 SchemaName::try_new(constraint.name())?,
342 constraint.source_expression(schema)?,
343 ))
344}
345
346fn lower_field_rules(
347 schema: &Schema,
348 field: &Field,
349) -> Result<Vec<ConstraintFragment>, FragmentLoweringError> {
350 let field_source = FieldSourceKey::try_new(field.name())?;
351 reachable_source_rules(schema, field.value().item())?
352 .into_iter()
353 .map(|(target_type, rule, target)| {
354 let operation = lower_source_rule_operation(schema, target, rule)?;
355 Ok(ConstraintFragment::targeted_rule(
356 TargetedRuleFragment::new(
357 field_source.clone(),
358 target_type,
359 SchemaName::try_new(rule.name())?,
360 operation,
361 ),
362 ))
363 })
364 .collect()
365}
366
367type ReachableSourceRule<'schema> = (
368 TypeSourceKey,
369 &'schema SourceRule,
370 &'schema crate::node::SchemaNode,
371);
372
373fn reachable_source_rules<'schema>(
374 schema: &'schema Schema,
375 item: &Item,
376) -> Result<Vec<ReachableSourceRule<'schema>>, FragmentLoweringError> {
377 let mut pending = Vec::new();
378 push_item_reference(item, &mut pending);
379 let mut visited = BTreeSet::new();
380 let mut rules = BTreeMap::new();
381 while let Some(path) = pending.pop() {
382 let node = schema
383 .get_node(path.as_str())
384 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.clone()))?;
385 let target_type = TypeSourceKey::try_new(
386 named_type_name(node)
387 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.clone()))?,
388 )?;
389 if !visited.insert(target_type.clone()) {
390 continue;
391 }
392 for rule in schema_node_type(node)?.rules() {
393 let key = (target_type.clone(), RuleSourceKey::try_new(rule.name())?);
394 if rules.insert(key, (rule, node)).is_some() {
395 return Err(FragmentLoweringError::InvalidReference(format!(
396 "duplicate durable rule '{}' on type '{}'",
397 rule.name(),
398 target_type
399 )));
400 }
401 }
402 push_schema_node_references(node, &mut pending);
403 }
404 Ok(rules
405 .into_iter()
406 .map(|((target_type, _), (rule, node))| (target_type, rule, node))
407 .collect())
408}
409
410fn schema_node_type(node: &SchemaNode) -> Result<&crate::node::Type, FragmentLoweringError> {
411 match node {
412 SchemaNode::Newtype(node) => Ok(node.ty()),
413 SchemaNode::Record(node) => Ok(node.ty()),
414 SchemaNode::Enum(node) => Ok(node.ty()),
415 SchemaNode::List(node) => Ok(node.ty()),
416 SchemaNode::Map(node) => Ok(node.ty()),
417 SchemaNode::Set(node) => Ok(node.ty()),
418 SchemaNode::Tuple(node) => Ok(node.ty()),
419 SchemaNode::Canister(_)
420 | SchemaNode::Entity(_)
421 | SchemaNode::Normalizer(_)
422 | SchemaNode::Store(_)
423 | SchemaNode::Validator(_) => Err(FragmentLoweringError::InvalidReference(
424 "durable-rule target is not a named type".to_string(),
425 )),
426 }
427}
428
429fn push_schema_node_references(node: &SchemaNode, pending: &mut Vec<String>) {
430 match node {
431 SchemaNode::Newtype(newtype) => push_item_reference(newtype.item(), pending),
432 SchemaNode::Record(record) => {
433 for field in record.fields().fields() {
434 push_item_reference(field.value().item(), pending);
435 }
436 }
437 SchemaNode::Enum(r#enum) => {
438 for value in r#enum
439 .variants()
440 .iter()
441 .filter_map(crate::node::EnumVariant::value)
442 {
443 push_item_reference(value.item(), pending);
444 }
445 }
446 SchemaNode::List(list) => push_item_reference(list.item(), pending),
447 SchemaNode::Map(map) => {
448 push_item_reference(map.key(), pending);
449 push_item_reference(map.value().item(), pending);
450 }
451 SchemaNode::Set(set) => push_item_reference(set.item(), pending),
452 SchemaNode::Tuple(tuple) => {
453 for value in tuple.values() {
454 push_item_reference(value.item(), pending);
455 }
456 }
457 SchemaNode::Canister(_)
458 | SchemaNode::Entity(_)
459 | SchemaNode::Normalizer(_)
460 | SchemaNode::Store(_)
461 | SchemaNode::Validator(_) => {}
462 }
463}
464
465fn push_item_reference(item: &Item, pending: &mut Vec<String>) {
466 if let ItemTarget::Is(path) = item.target() {
467 pending.push((*path).to_string());
468 }
469}
470
471fn lower_source_rule_operation(
472 schema: &Schema,
473 target: &SchemaNode,
474 rule: &SourceRule,
475) -> Result<SourceRuleOperation, FragmentLoweringError> {
476 let args = rule.args().0;
477 let length_bound = |index: usize| {
478 args.get(index)
479 .and_then(|arg| match arg {
480 Arg::Number(value) => arg_u128(value),
481 _ => None,
482 })
483 .and_then(|value| u64::try_from(value).ok())
484 .ok_or_else(|| {
485 FragmentLoweringError::InvalidReference(format!(
486 "rule '{}' has an invalid length bound",
487 rule.name()
488 ))
489 })
490 };
491
492 let operation = match rule.kind() {
493 SourceRuleKind::NumericMinimum => {
494 let RuleValueShape::Scalar(primitive, item) = resolve_rule_value_shape(schema, target)?
495 else {
496 return Err(invalid_rule_target(rule));
497 };
498 let value = args
499 .first()
500 .and_then(|arg| lower_scalar_default(primitive, item, arg))
501 .ok_or_else(|| invalid_rule_target(rule))?;
502 SourceRuleOperation::NumericMinimumInclusive { value }
503 }
504 SourceRuleKind::NumericRange => {
505 let RuleValueShape::Scalar(primitive, item) = resolve_rule_value_shape(schema, target)?
506 else {
507 return Err(invalid_rule_target(rule));
508 };
509 let literal = |index: usize| {
510 args.get(index)
511 .and_then(|arg| lower_scalar_default(primitive, item, arg))
512 .ok_or_else(|| invalid_rule_target(rule))
513 };
514 SourceRuleOperation::NumericRangeInclusive {
515 min: literal(0)?,
516 max: literal(1)?,
517 }
518 }
519 SourceRuleKind::LengthRange => {
520 let shape = resolve_rule_value_shape(schema, target)?;
521 if !matches!(
522 shape,
523 RuleValueShape::Collection
524 | RuleValueShape::Scalar(Primitive::Blob | Primitive::Text, _)
525 ) {
526 return Err(invalid_rule_target(rule));
527 }
528 SourceRuleOperation::LengthRangeInclusive {
529 min: length_bound(0)?,
530 max: length_bound(1)?,
531 }
532 }
533 };
534 Ok(operation)
535}
536
537#[derive(Clone, Copy)]
538enum RuleValueShape<'schema> {
539 Collection,
540 Scalar(Primitive, &'schema Item),
541}
542
543fn resolve_rule_value_shape<'schema>(
544 schema: &'schema Schema,
545 mut target: &'schema SchemaNode,
546) -> Result<RuleValueShape<'schema>, FragmentLoweringError> {
547 let mut visited = BTreeSet::new();
548 loop {
549 let source = named_type_name(target)
550 .ok_or_else(|| FragmentLoweringError::InvalidReference("non-type rule".to_string()))?;
551 if !visited.insert(source) {
552 return Err(FragmentLoweringError::InvalidReference(format!(
553 "durable-rule target cycle at '{source}'"
554 )));
555 }
556 match target {
557 SchemaNode::List(_) | SchemaNode::Map(_) | SchemaNode::Set(_) => {
558 return Ok(RuleValueShape::Collection);
559 }
560 SchemaNode::Newtype(newtype) => match newtype.item().target() {
561 ItemTarget::Primitive(primitive) => {
562 return Ok(RuleValueShape::Scalar(*primitive, newtype.item()));
563 }
564 ItemTarget::Is(path) => {
565 target = schema
566 .get_node(path)
567 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.to_string()))?;
568 }
569 },
570 SchemaNode::Record(_) | SchemaNode::Enum(_) | SchemaNode::Tuple(_) => {
571 return Err(FragmentLoweringError::InvalidReference(format!(
572 "durable-rule target '{source}' has no supported scalar or collection value"
573 )));
574 }
575 SchemaNode::Canister(_)
576 | SchemaNode::Entity(_)
577 | SchemaNode::Normalizer(_)
578 | SchemaNode::Store(_)
579 | SchemaNode::Validator(_) => {
580 return Err(FragmentLoweringError::InvalidReference(
581 "non-type durable-rule target".to_string(),
582 ));
583 }
584 }
585 }
586}
587
588fn invalid_rule_target(rule: &SourceRule) -> FragmentLoweringError {
589 FragmentLoweringError::InvalidReference(format!(
590 "durable rule '{}' does not match its nominal target",
591 rule.name()
592 ))
593}
594
595fn entity_field_source_key(
596 entity: &Entity,
597 field_name: &str,
598) -> Result<FieldSourceKey, FragmentLoweringError> {
599 let field = entity
600 .fields()
601 .get(field_name)
602 .ok_or_else(|| FragmentLoweringError::InvalidReference(field_name.to_string()))?;
603 FieldSourceKey::try_new(field.name()).map_err(Into::into)
604}
605
606fn lower_reachable_types(
611 schema: &Schema,
612 mut pending: Vec<String>,
613) -> Result<Vec<NamedTypeFragment>, FragmentLoweringError> {
614 let mut lowered = BTreeMap::new();
615 while let Some(path) = pending.pop() {
616 let node = schema
617 .get_node(path.as_str())
618 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.clone()))?;
619 let source_key = named_type_name(node)
620 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.clone()))?;
621 if lowered.contains_key(source_key) {
622 continue;
623 }
624 let fragment = lower_named_type(schema, node, &mut pending)?;
625 lowered.insert(source_key.to_string(), fragment);
626 }
627 Ok(lowered.into_values().collect())
628}
629
630const fn named_type_name(node: &crate::node::SchemaNode) -> Option<&str> {
631 match node {
632 crate::node::SchemaNode::Enum(node) => Some(node.name()),
633 crate::node::SchemaNode::List(node) => Some(node.name()),
634 crate::node::SchemaNode::Map(node) => Some(node.name()),
635 crate::node::SchemaNode::Newtype(node) => Some(node.name()),
636 crate::node::SchemaNode::Record(node) => Some(node.name()),
637 crate::node::SchemaNode::Set(node) => Some(node.name()),
638 crate::node::SchemaNode::Tuple(node) => Some(node.name()),
639 crate::node::SchemaNode::Canister(_)
640 | crate::node::SchemaNode::Entity(_)
641 | crate::node::SchemaNode::Normalizer(_)
642 | crate::node::SchemaNode::Store(_)
643 | crate::node::SchemaNode::Validator(_) => None,
644 }
645}
646
647fn lower_named_type(
648 schema: &Schema,
649 node: &crate::node::SchemaNode,
650 pending: &mut Vec<String>,
651) -> Result<NamedTypeFragment, FragmentLoweringError> {
652 match node {
653 crate::node::SchemaNode::Record(record) => lower_record(schema, record, pending),
654 crate::node::SchemaNode::Enum(r#enum) => lower_enum(schema, r#enum, pending),
655 crate::node::SchemaNode::Newtype(newtype) => Ok(NamedTypeFragment::newtype(
656 SchemaName::try_new(newtype.name())?,
657 lower_item_type(schema, newtype.item(), pending)?,
658 )),
659 crate::node::SchemaNode::List(list) => lower_list(schema, list, pending),
660 crate::node::SchemaNode::Set(set) => lower_set(schema, set, pending),
661 crate::node::SchemaNode::Map(map) => lower_map(schema, map, pending),
662 crate::node::SchemaNode::Tuple(tuple) => lower_tuple(schema, tuple, pending),
663 crate::node::SchemaNode::Canister(_)
664 | crate::node::SchemaNode::Entity(_)
665 | crate::node::SchemaNode::Normalizer(_)
666 | crate::node::SchemaNode::Store(_)
667 | crate::node::SchemaNode::Validator(_) => Err(FragmentLoweringError::InvalidReference(
668 "non-type graph node".to_string(),
669 )),
670 }
671}
672
673fn lower_record(
674 schema: &Schema,
675 record: &Record,
676 pending: &mut Vec<String>,
677) -> Result<NamedTypeFragment, FragmentLoweringError> {
678 let fields = record
679 .fields()
680 .fields()
681 .iter()
682 .map(|field| {
683 Ok(RecordFieldFragment::new(
684 SchemaName::try_new(field.name())?,
685 lower_value_type(schema, field.value(), pending)?,
686 field.value().cardinality() == Cardinality::Opt,
687 ))
688 })
689 .collect::<Result<Vec<_>, FragmentLoweringError>>()?;
690 Ok(NamedTypeFragment::Record(RecordTypeFragment::try_new(
691 SchemaName::try_new(record.name())?,
692 fields,
693 )?))
694}
695
696fn lower_enum(
697 schema: &Schema,
698 r#enum: &Enum,
699 pending: &mut Vec<String>,
700) -> Result<NamedTypeFragment, FragmentLoweringError> {
701 let variants = r#enum
702 .variants()
703 .iter()
704 .map(|variant| {
705 let name = SchemaName::try_new(variant.name())?;
706 match variant.value() {
707 Some(value) if value.cardinality() == Cardinality::Opt => {
708 Err(FragmentLoweringError::UnsupportedCardinality(format!(
709 "{}::{}",
710 r#enum.def().path(),
711 variant.name()
712 )))
713 }
714 Some(value) => Ok(EnumVariantFragment::with_payload(
715 name,
716 lower_value_type(schema, value, pending)?,
717 )),
718 None => Ok(EnumVariantFragment::new(name)),
719 }
720 })
721 .collect::<Result<Vec<_>, _>>()?;
722 Ok(NamedTypeFragment::Enum(EnumTypeFragment::try_new(
723 SchemaName::try_new(r#enum.name())?,
724 variants,
725 )?))
726}
727
728fn lower_list(
729 schema: &Schema,
730 list: &List,
731 pending: &mut Vec<String>,
732) -> Result<NamedTypeFragment, FragmentLoweringError> {
733 Ok(NamedTypeFragment::list(
734 SchemaName::try_new(list.name())?,
735 lower_item_type(schema, list.item(), pending)?,
736 ))
737}
738
739fn lower_set(
740 schema: &Schema,
741 set: &Set,
742 pending: &mut Vec<String>,
743) -> Result<NamedTypeFragment, FragmentLoweringError> {
744 Ok(NamedTypeFragment::set(
745 SchemaName::try_new(set.name())?,
746 lower_item_type(schema, set.item(), pending)?,
747 ))
748}
749
750fn lower_map(
751 schema: &Schema,
752 map: &Map,
753 pending: &mut Vec<String>,
754) -> Result<NamedTypeFragment, FragmentLoweringError> {
755 if map.value().cardinality() == Cardinality::Opt {
756 return Err(FragmentLoweringError::UnsupportedCardinality(
757 map.def().path(),
758 ));
759 }
760 Ok(NamedTypeFragment::map(
761 SchemaName::try_new(map.name())?,
762 lower_item_type(schema, map.key(), pending)?,
763 lower_value_type(schema, map.value(), pending)?,
764 ))
765}
766
767fn lower_tuple(
768 schema: &Schema,
769 tuple: &Tuple,
770 pending: &mut Vec<String>,
771) -> Result<NamedTypeFragment, FragmentLoweringError> {
772 let members = tuple
773 .values()
774 .iter()
775 .map(|value| {
776 Ok::<_, FragmentLoweringError>(TupleElementFragment::new(
777 lower_value_type(schema, value, pending)?,
778 value.cardinality() == Cardinality::Opt,
779 ))
780 })
781 .collect::<Result<Vec<_>, _>>()?;
782 Ok(NamedTypeFragment::tuple(
783 SchemaName::try_new(tuple.name())?,
784 members,
785 ))
786}
787
788fn lower_value_type(
793 schema: &Schema,
794 value: &Value,
795 pending: &mut Vec<String>,
796) -> Result<FieldType, FragmentLoweringError> {
797 let item = lower_item_type(schema, value.item(), pending)?;
798 Ok(if value.cardinality() == Cardinality::Many {
799 FieldType::List(Box::new(item))
800 } else {
801 item
802 })
803}
804
805fn lower_item_type(
806 schema: &Schema,
807 item: &Item,
808 pending: &mut Vec<String>,
809) -> Result<FieldType, FragmentLoweringError> {
810 match item.target() {
811 ItemTarget::Is(path) => {
812 pending.push((*path).to_string());
813 Ok(FieldType::Named(TypeSourceKey::try_new(
814 type_source_key_for_path(schema, path)?,
815 )?))
816 }
817 ItemTarget::Primitive(primitive) => {
818 Ok(FieldType::Scalar(lower_scalar_type(*primitive, item)))
819 }
820 }
821}
822
823fn type_source_key_for_path<'schema>(
824 schema: &'schema Schema,
825 path: &str,
826) -> Result<&'schema str, FragmentLoweringError> {
827 let source = schema
828 .get_node(path)
829 .and_then(named_type_name)
830 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.to_string()))?;
831 Ok(source)
832}
833
834fn lower_scalar_type(primitive: Primitive, item: &Item) -> ScalarType {
835 match primitive {
836 Primitive::Account => ScalarType::Account,
837 Primitive::Blob => ScalarType::Blob {
838 max_len: item.max_len(),
839 },
840 Primitive::Bool => ScalarType::Bool,
841 Primitive::Date => ScalarType::Date,
842 Primitive::Decimal => ScalarType::Decimal {
843 scale: item.scale().unwrap_or(0),
844 },
845 Primitive::Duration => ScalarType::Duration,
846 Primitive::Float32 => ScalarType::Float32,
847 Primitive::Float64 => ScalarType::Float64,
848 Primitive::Int8 => ScalarType::Int8,
849 Primitive::Int16 => ScalarType::Int16,
850 Primitive::Int32 => ScalarType::Int32,
851 Primitive::Int64 => ScalarType::Int64,
852 Primitive::Int128 => ScalarType::Int128,
853 Primitive::IntBig => ScalarType::IntBig {
854 max_bytes: item.max_bytes().unwrap_or(DEFAULT_BIG_INT_MAX_BYTES),
855 },
856 Primitive::Nat8 => ScalarType::Nat8,
857 Primitive::Nat16 => ScalarType::Nat16,
858 Primitive::Nat32 => ScalarType::Nat32,
859 Primitive::Nat64 => ScalarType::Nat64,
860 Primitive::Nat128 => ScalarType::Nat128,
861 Primitive::NatBig => ScalarType::NatBig {
862 max_bytes: item.max_bytes().unwrap_or(DEFAULT_BIG_INT_MAX_BYTES),
863 },
864 Primitive::Principal => ScalarType::Principal,
865 Primitive::Subaccount => ScalarType::Subaccount,
866 Primitive::Text => ScalarType::Text {
867 max_len: item.max_len(),
868 },
869 Primitive::Timestamp => ScalarType::Timestamp,
870 Primitive::Ulid => ScalarType::Ulid,
871 Primitive::Unit => ScalarType::Unit,
872 }
873}
874
875fn lower_default(
880 schema: &Schema,
881 field: &Field,
882 default: &Arg,
883) -> Result<ScalarLiteral, FragmentLoweringError> {
884 if let ItemTarget::Is(path) = field.value().item().target() {
885 let Arg::ConstPath(default_path) = default else {
886 return Err(FragmentLoweringError::InvalidDefault(
887 field.name().to_string(),
888 ));
889 };
890 let variant = default_path.rsplit("::").next().unwrap_or(default_path);
891 return schema
892 .enum_unit_literal(path, variant)
893 .map_err(FragmentLoweringError::from);
894 }
895 let ItemTarget::Primitive(primitive) = field.value().item().target() else {
896 return Err(FragmentLoweringError::InvalidDefault(
897 field.name().to_string(),
898 ));
899 };
900 lower_scalar_default(*primitive, field.value().item(), default)
901 .ok_or_else(|| FragmentLoweringError::InvalidDefault(field.name().to_string()))
902}
903
904fn lower_scalar_default(primitive: Primitive, item: &Item, default: &Arg) -> Option<ScalarLiteral> {
905 if default_constructor_is_zero(default) {
906 return zero_scalar_literal(primitive, item);
907 }
908 match (primitive, default) {
909 (Primitive::Account, Arg::String(value)) => {
910 Account::from_str(value).ok().map(ScalarLiteral::Account)
911 }
912 (Primitive::Blob, Arg::String(value)) => Blob::try_new(value.as_bytes().to_vec())
913 .ok()
914 .map(ScalarLiteral::Blob),
915 (Primitive::Bool, Arg::Bool(value)) => Some(ScalarLiteral::Bool(*value)),
916 (Primitive::Date, Arg::String(value)) => Date::parse(value).map(ScalarLiteral::Date),
917 (Primitive::Date, Arg::Number(value)) => arg_i128(value)
918 .and_then(|value| i32::try_from(value).ok())
919 .map(Date::from_days_since_epoch)
920 .map(ScalarLiteral::Date),
921 (Primitive::Decimal, Arg::String(value)) => Decimal::from_str(value)
922 .ok()
923 .and_then(|value| decimal_at_scale(value, item.scale().unwrap_or(0)))
924 .map(ScalarLiteral::Decimal),
925 (Primitive::Decimal, Arg::Number(value)) => arg_decimal(value)
926 .and_then(|value| decimal_at_scale(value, item.scale().unwrap_or(0)))
927 .map(ScalarLiteral::Decimal),
928 (Primitive::Duration, Arg::String(value)) => Duration::parse_flexible(value)
929 .ok()
930 .map(ScalarLiteral::Duration),
931 (Primitive::Duration, Arg::Number(value)) => arg_u128(value)
932 .and_then(|value| u64::try_from(value).ok())
933 .map(Duration::from_millis)
934 .map(ScalarLiteral::Duration),
935 (Primitive::Float32, Arg::Number(ArgNumber::Float32(value))) => {
936 Float32::try_new(*value).map(ScalarLiteral::Float32)
937 }
938 (Primitive::Float64, Arg::Number(ArgNumber::Float64(value))) => {
939 Float64::try_new(*value).map(ScalarLiteral::Float64)
940 }
941 (
942 Primitive::Int8
943 | Primitive::Int16
944 | Primitive::Int32
945 | Primitive::Int64
946 | Primitive::Int128,
947 Arg::Number(value),
948 ) => arg_i128(value).map(ScalarLiteral::Int),
949 (Primitive::IntBig, Arg::Number(value)) => arg_i128(value)
950 .map(|value| value.to_string())
951 .and_then(|value| IntBig::from_str(value.as_str()).ok())
952 .map(ScalarLiteral::IntBig),
953 (Primitive::IntBig, Arg::String(value)) => {
954 IntBig::from_str(value).ok().map(ScalarLiteral::IntBig)
955 }
956 (
957 Primitive::Nat8
958 | Primitive::Nat16
959 | Primitive::Nat32
960 | Primitive::Nat64
961 | Primitive::Nat128,
962 Arg::Number(value),
963 ) => arg_u128(value).map(ScalarLiteral::Nat),
964 (Primitive::NatBig, Arg::Number(value)) => arg_u128(value)
965 .map(|value| value.to_string())
966 .and_then(|value| NatBig::from_str(value.as_str()).ok())
967 .map(ScalarLiteral::NatBig),
968 (Primitive::NatBig, Arg::String(value)) => {
969 NatBig::from_str(value).ok().map(ScalarLiteral::NatBig)
970 }
971 (Primitive::Principal, Arg::String(value)) => Principal::from_str(value)
972 .ok()
973 .map(ScalarLiteral::Principal),
974 (Primitive::Subaccount, Arg::String(value)) => parse_subaccount(value)
975 .map(Subaccount::from_array)
976 .map(ScalarLiteral::Subaccount),
977 (Primitive::Text, Arg::String(value)) => Some(ScalarLiteral::Text((*value).to_string())),
978 (Primitive::Timestamp, Arg::String(value)) => Timestamp::parse_flexible(value)
979 .ok()
980 .map(ScalarLiteral::Timestamp),
981 (Primitive::Timestamp, Arg::Number(value)) => arg_i128(value)
982 .and_then(|value| i64::try_from(value).ok())
983 .map(Timestamp::from_millis)
984 .map(ScalarLiteral::Timestamp),
985 (Primitive::Ulid, Arg::String(value)) => {
986 Ulid::from_str(value).ok().map(ScalarLiteral::Ulid)
987 }
988 (Primitive::Unit, Arg::ConstPath(path)) if path.ends_with("Unit") => {
989 Some(ScalarLiteral::Unit(Unit))
990 }
991 _ => None,
992 }
993}
994
995fn default_constructor_is_zero(default: &Arg) -> bool {
996 let Arg::FuncPath(path) = default else {
997 return false;
998 };
999 path.ends_with("::default")
1000 || path.ends_with("::new")
1001 || path.ends_with("::EPOCH")
1002 || path.ends_with("::nil")
1003}
1004
1005fn zero_scalar_literal(primitive: Primitive, item: &Item) -> Option<ScalarLiteral> {
1006 match primitive {
1007 Primitive::Blob => Blob::try_new(Vec::new()).ok().map(ScalarLiteral::Blob),
1008 Primitive::Bool => Some(ScalarLiteral::Bool(false)),
1009 Primitive::Date => Some(ScalarLiteral::Date(Date::EPOCH)),
1010 Primitive::Decimal => Decimal::try_from_i128_with_scale(0, item.scale().unwrap_or(0))
1011 .map(ScalarLiteral::Decimal),
1012 Primitive::Duration => Some(ScalarLiteral::Duration(Duration::ZERO)),
1013 Primitive::Float32 => Float32::try_new(0.0).map(ScalarLiteral::Float32),
1014 Primitive::Float64 => Float64::try_new(0.0).map(ScalarLiteral::Float64),
1015 Primitive::Int8
1016 | Primitive::Int16
1017 | Primitive::Int32
1018 | Primitive::Int64
1019 | Primitive::Int128 => Some(ScalarLiteral::Int(0)),
1020 Primitive::IntBig => IntBig::from_str("0").ok().map(ScalarLiteral::IntBig),
1021 Primitive::Nat8
1022 | Primitive::Nat16
1023 | Primitive::Nat32
1024 | Primitive::Nat64
1025 | Primitive::Nat128 => Some(ScalarLiteral::Nat(0)),
1026 Primitive::NatBig => NatBig::from_str("0").ok().map(ScalarLiteral::NatBig),
1027 Primitive::Text => Some(ScalarLiteral::Text(String::new())),
1028 Primitive::Timestamp => Some(ScalarLiteral::Timestamp(Timestamp::EPOCH)),
1029 Primitive::Ulid => Some(ScalarLiteral::Ulid(Ulid::nil())),
1030 Primitive::Unit => Some(ScalarLiteral::Unit(Unit)),
1031 Primitive::Account | Primitive::Principal | Primitive::Subaccount => None,
1032 }
1033}
1034
1035fn decimal_at_scale(value: Decimal, scale: u32) -> Option<Decimal> {
1040 match value.scale().cmp(&scale) {
1041 std::cmp::Ordering::Equal => Some(value),
1042 std::cmp::Ordering::Less => value
1043 .scale_to_integer(scale)
1044 .and_then(|mantissa| Decimal::try_from_i128_with_scale(mantissa, scale)),
1045 std::cmp::Ordering::Greater => Some(value.round_dp(scale)),
1046 }
1047}
1048
1049fn arg_i128(value: &ArgNumber) -> Option<i128> {
1050 match value {
1051 ArgNumber::Int8(value) => Some(i128::from(*value)),
1052 ArgNumber::Int16(value) => Some(i128::from(*value)),
1053 ArgNumber::Int32(value) => Some(i128::from(*value)),
1054 ArgNumber::Int64(value) => Some(i128::from(*value)),
1055 ArgNumber::Int128(value) => Some(*value),
1056 ArgNumber::Nat8(value) => Some(i128::from(*value)),
1057 ArgNumber::Nat16(value) => Some(i128::from(*value)),
1058 ArgNumber::Nat32(value) => Some(i128::from(*value)),
1059 ArgNumber::Nat64(value) => Some(i128::from(*value)),
1060 ArgNumber::Nat128(value) => i128::try_from(*value).ok(),
1061 ArgNumber::Float32(_) | ArgNumber::Float64(_) => None,
1062 }
1063}
1064
1065fn arg_u128(value: &ArgNumber) -> Option<u128> {
1066 match value {
1067 ArgNumber::Int8(value) => u128::try_from(*value).ok(),
1068 ArgNumber::Int16(value) => u128::try_from(*value).ok(),
1069 ArgNumber::Int32(value) => u128::try_from(*value).ok(),
1070 ArgNumber::Int64(value) => u128::try_from(*value).ok(),
1071 ArgNumber::Int128(value) => u128::try_from(*value).ok(),
1072 ArgNumber::Nat8(value) => Some(u128::from(*value)),
1073 ArgNumber::Nat16(value) => Some(u128::from(*value)),
1074 ArgNumber::Nat32(value) => Some(u128::from(*value)),
1075 ArgNumber::Nat64(value) => Some(u128::from(*value)),
1076 ArgNumber::Nat128(value) => Some(*value),
1077 ArgNumber::Float32(_) | ArgNumber::Float64(_) => None,
1078 }
1079}
1080
1081fn arg_decimal(value: &ArgNumber) -> Option<Decimal> {
1082 match value {
1083 ArgNumber::Float32(value) => Decimal::from_f32_lossy(*value),
1084 ArgNumber::Float64(value) => Decimal::from_f64_lossy(*value),
1085 _ => arg_i128(value).and_then(Decimal::from_i128),
1086 }
1087}
1088
1089fn parse_subaccount(value: &str) -> Option<[u8; 32]> {
1090 if value.len() != 64 {
1091 return None;
1092 }
1093 let mut bytes = [0; 32];
1094 for (index, chunk) in value.as_bytes().chunks_exact(2).enumerate() {
1095 let text = std::str::from_utf8(chunk).ok()?;
1096 bytes[index] = u8::from_str_radix(text, 16).ok()?;
1097 }
1098 Some(bytes)
1099}
1100
1101#[cfg(test)]
1102mod tests {
1103 use icydb_schema::{
1104 ConstraintFragmentKind, ConstraintSourceKey, FieldSourceKey, FieldType, NamedTypeFragment,
1105 RuleSourceKey, ScalarType, SourceRuleOperation, TypeSourceKey,
1106 };
1107
1108 use super::{Schema, lower_field_rules};
1109 use crate::{
1110 node::{
1111 Arg, ArgNumber, Args, Canister, Def, Entity, Enum, EnumVariant, Field, FieldList, Item,
1112 ItemTarget, Newtype, PrimaryKey, PrimaryKeySource, Record, SchemaNode, SourceRule,
1113 SourceRuleKind, Store, StoreHeapConfig, Type, Value,
1114 },
1115 types::{Cardinality, Primitive},
1116 };
1117
1118 static EMPTY_TYPE: Type = Type::new(&[], &[], &[]);
1119 static NUMERIC_RULE_ARGS: [Arg; 2] = [
1120 Arg::Number(ArgNumber::Int32(0)),
1121 Arg::Number(ArgNumber::Int32(360)),
1122 ];
1123 static NUMERIC_RULES: [SourceRule; 1] = [SourceRule::new(
1124 "range",
1125 SourceRuleKind::NumericRange,
1126 Args(&NUMERIC_RULE_ARGS),
1127 )];
1128 static NUMERIC_RULE_TYPE: Type = Type::new(&[], &[], &NUMERIC_RULES);
1129 static LENGTH_RULE_ARGS: [Arg; 2] = [
1130 Arg::Number(ArgNumber::Int32(2)),
1131 Arg::Number(ArgNumber::Int32(40)),
1132 ];
1133 static LENGTH_RULES: [SourceRule; 1] = [SourceRule::new(
1134 "length",
1135 SourceRuleKind::LengthRange,
1136 Args(&LENGTH_RULE_ARGS),
1137 )];
1138 static LENGTH_RULE_TYPE: Type = Type::new(&[], &[], &LENGTH_RULES);
1139 static NESTED_RULE_FIELDS: [Field; 1] = [Field::new(
1140 "degrees",
1141 Value::new(
1142 Cardinality::One,
1143 Item::new(
1144 ItemTarget::Is("test::Degrees"),
1145 None,
1146 None,
1147 None,
1148 None,
1149 &[],
1150 &[],
1151 false,
1152 ),
1153 ),
1154 None,
1155 None,
1156 None,
1157 )];
1158 static STATUS_VARIANTS: [EnumVariant; 2] = [
1159 EnumVariant::new("Active", None),
1160 EnumVariant::new(
1161 "Retries",
1162 Some(Value::new(
1163 Cardinality::Many,
1164 Item::new(
1165 ItemTarget::Primitive(Primitive::Nat16),
1166 None,
1167 None,
1168 None,
1169 None,
1170 &[],
1171 &[],
1172 false,
1173 ),
1174 )),
1175 ),
1176 ];
1177
1178 #[test]
1179 fn durable_rules_nested_below_structural_fields_lower_to_nominal_targets() {
1180 let mut schema = Schema::new();
1181 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1182 Def::new("test", "Degrees"),
1183 "Degrees",
1184 Item::new(
1185 ItemTarget::Primitive(Primitive::Nat16),
1186 None,
1187 None,
1188 None,
1189 None,
1190 &[],
1191 &[],
1192 false,
1193 ),
1194 None,
1195 NUMERIC_RULE_TYPE.clone(),
1196 )));
1197 schema.insert_node(SchemaNode::Record(Record::new(
1198 Def::new("test", "Nested"),
1199 "Nested",
1200 FieldList::new(&NESTED_RULE_FIELDS),
1201 EMPTY_TYPE.clone(),
1202 )));
1203
1204 let outer = Field::new(
1205 "nested",
1206 Value::new(
1207 Cardinality::One,
1208 Item::new(
1209 ItemTarget::Is("test::Nested"),
1210 None,
1211 None,
1212 None,
1213 None,
1214 &[],
1215 &[],
1216 false,
1217 ),
1218 ),
1219 None,
1220 None,
1221 None,
1222 );
1223 let constraints =
1224 lower_field_rules(&schema, &outer).expect("nested durable rule should lower");
1225 assert_eq!(constraints.len(), 1);
1226 let ConstraintFragmentKind::TargetedRule(rule) = constraints[0].kind() else {
1227 panic!("nested durable rule should use the targeted-rule contract")
1228 };
1229 assert_eq!(rule.root().as_str(), "nested");
1230 assert_eq!(rule.target_type().as_str(), "Degrees");
1231 assert!(matches!(
1232 rule.operation(),
1233 SourceRuleOperation::NumericRangeInclusive { .. }
1234 ));
1235 }
1236
1237 static ENTITY_FIELDS: [Field; 5] = [
1238 Field::new(
1239 "id",
1240 Value::new(
1241 Cardinality::One,
1242 Item::new(
1243 ItemTarget::Primitive(Primitive::Nat64),
1244 None,
1245 None,
1246 None,
1247 None,
1248 &[],
1249 &[],
1250 false,
1251 ),
1252 ),
1253 None,
1254 None,
1255 None,
1256 ),
1257 Field::new(
1258 "tags",
1259 Value::new(
1260 Cardinality::Many,
1261 Item::new(
1262 ItemTarget::Primitive(Primitive::Text),
1263 None,
1264 None,
1265 Some(32),
1266 None,
1267 &[],
1268 &[],
1269 false,
1270 ),
1271 ),
1272 None,
1273 None,
1274 None,
1275 ),
1276 Field::new(
1277 "status",
1278 Value::new(
1279 Cardinality::One,
1280 Item::new(
1281 ItemTarget::Is("test::Status"),
1282 None,
1283 None,
1284 None,
1285 None,
1286 &[],
1287 &[],
1288 false,
1289 ),
1290 ),
1291 Some(crate::node::Arg::ConstPath("test::Status::Active")),
1292 None,
1293 None,
1294 ),
1295 Field::new(
1296 "degrees",
1297 Value::new(
1298 Cardinality::One,
1299 Item::new(
1300 ItemTarget::Is("test::Degrees"),
1301 None,
1302 None,
1303 None,
1304 None,
1305 &[],
1306 &[],
1307 false,
1308 ),
1309 ),
1310 None,
1311 None,
1312 None,
1313 ),
1314 Field::new(
1315 "label",
1316 Value::new(
1317 Cardinality::One,
1318 Item::new(
1319 ItemTarget::Is("test::Label"),
1320 None,
1321 None,
1322 None,
1323 None,
1324 &[],
1325 &[],
1326 false,
1327 ),
1328 ),
1329 None,
1330 None,
1331 None,
1332 ),
1333 ];
1334
1335 #[test]
1336 #[expect(
1337 clippy::too_many_lines,
1338 reason = "one graph fixture proves the complete field, type, relation, and durable-rule closure"
1339 )]
1340 fn sealed_canister_graph_emits_store_free_database_closure() {
1341 let mut schema = Schema::new();
1342 schema.insert_node(SchemaNode::Canister(Canister::new(
1343 Def::new("test", "Canister"),
1344 "test",
1345 0,
1346 10,
1347 9,
1348 8,
1349 )));
1350 schema.insert_node(SchemaNode::Store(Store::new_heap(
1351 Def::new("test", "Store"),
1352 "test::Canister",
1353 StoreHeapConfig::new(),
1354 )));
1355 schema.insert_node(SchemaNode::Enum(Enum::new(
1356 Def::new("test", "Status"),
1357 "Status",
1358 &STATUS_VARIANTS,
1359 EMPTY_TYPE.clone(),
1360 )));
1361 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1362 Def::new("test", "Degrees"),
1363 "Degrees",
1364 Item::new(
1365 ItemTarget::Primitive(Primitive::Nat16),
1366 None,
1367 None,
1368 None,
1369 None,
1370 &[],
1371 &[],
1372 false,
1373 ),
1374 None,
1375 NUMERIC_RULE_TYPE.clone(),
1376 )));
1377 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1378 Def::new("test", "Label"),
1379 "Label",
1380 Item::new(
1381 ItemTarget::Primitive(Primitive::Text),
1382 None,
1383 None,
1384 None,
1385 None,
1386 &[],
1387 &[],
1388 false,
1389 ),
1390 None,
1391 LENGTH_RULE_TYPE.clone(),
1392 )));
1393 schema.insert_node(SchemaNode::Entity(Entity::new(
1394 Def::new("test", "Task"),
1395 "test::Store",
1396 1,
1397 PrimaryKey::new(&["id"], PrimaryKeySource::External),
1398 &[],
1399 &[],
1400 &[],
1401 FieldList::new(&ENTITY_FIELDS),
1402 EMPTY_TYPE.clone(),
1403 )));
1404 schema.seal().expect("fixture graph should seal");
1405
1406 let fragment = schema
1407 .schema_fragment_for_canister("test::Canister")
1408 .expect("sealed database closure should lower");
1409
1410 assert_eq!(fragment.entities().len(), 1);
1411 assert_eq!(fragment.types().len(), 3);
1412 let fields = fragment.entities()[0].fields();
1413 assert!(matches!(
1414 fields
1415 .iter()
1416 .find(|field| field.name().as_str() == "tags")
1417 .map(icydb_schema::FieldFragment::field_type),
1418 Some(FieldType::List(item))
1419 if matches!(item.as_ref(), FieldType::Scalar(ScalarType::Text { max_len: Some(32) }))
1420 ));
1421 assert!(matches!(
1422 fields
1423 .iter()
1424 .find(|field| field.name().as_str() == "status")
1425 .map(icydb_schema::FieldFragment::insert_policy),
1426 Some(icydb_schema::FieldInsertPolicy::Default(
1427 icydb_schema::ScalarLiteral::EnumUnit { .. }
1428 ))
1429 ));
1430 let NamedTypeFragment::Enum(status) = fragment
1431 .types()
1432 .iter()
1433 .find(|fragment| matches!(fragment, NamedTypeFragment::Enum(_)))
1434 .expect("reachable status type should remain an enum")
1435 else {
1436 panic!("reachable status type should remain an enum")
1437 };
1438 assert!(matches!(
1439 status
1440 .variants()
1441 .iter()
1442 .find(|variant| variant.name().as_str() == "Retries")
1443 .and_then(|variant| variant.payload()),
1444 Some(FieldType::List(item))
1445 if matches!(item.as_ref(), FieldType::Scalar(ScalarType::Nat16))
1446 ));
1447
1448 let constraints = fragment.entities()[0].constraints();
1449 assert_eq!(constraints.len(), 2);
1450 let degrees_source = ConstraintSourceKey::for_targeted_field_rule(
1451 &FieldSourceKey::try_new("degrees").expect("field name"),
1452 &TypeSourceKey::try_new("Degrees").expect("type name"),
1453 &RuleSourceKey::try_new("range").expect("rule name"),
1454 );
1455 let degrees = constraints
1456 .iter()
1457 .find(|constraint| constraint.source_key() == °rees_source)
1458 .expect("numeric rule should become one field-owned constraint");
1459 let ConstraintFragmentKind::TargetedRule(degrees) = degrees.kind() else {
1460 panic!("numeric rule should use the targeted-rule contract")
1461 };
1462 assert_eq!(degrees.root().as_str(), "degrees");
1463 assert_eq!(degrees.target_type().as_str(), "Degrees");
1464 assert!(matches!(
1465 degrees.operation(),
1466 SourceRuleOperation::NumericRangeInclusive { .. }
1467 ));
1468 let label = constraints
1469 .iter()
1470 .find(|constraint| constraint.source_key() != °rees_source)
1471 .expect("length rule should become one field-owned constraint");
1472 let ConstraintFragmentKind::TargetedRule(label) = label.kind() else {
1473 panic!("length rule should use the targeted-rule contract")
1474 };
1475 assert_eq!(label.target_type().as_str(), "Label");
1476 assert!(matches!(
1477 label.operation(),
1478 SourceRuleOperation::LengthRangeInclusive { min: 2, max: 40 }
1479 ));
1480 }
1481}