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 as ProposalSourceRuleOperation, Subaccount, TargetedRuleFragment,
22 Timestamp, TupleElementFragment, 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, RuleNumber, Schema, SchemaNode, Set, SourceRule,
31 SourceRuleAuthoringOperation, Store, Tuple, Value,
32 },
33 types::{Cardinality, Primitive},
34};
35
36#[derive(Debug, Error)]
39pub enum FragmentLoweringError {
40 #[error("schema canister has no registered stores: {0}")]
42 CanisterHasNoStores(String),
43
44 #[error("schema canister path is not registered: {0}")]
46 CanisterNotFound(String),
47
48 #[error(transparent)]
50 Contract(#[from] SchemaContractError),
51
52 #[error("schema graph must be sealed before fragment lowering")]
54 GraphNotSealed,
55
56 #[error("schema field default cannot be lowered: {0}")]
58 InvalidDefault(String),
59
60 #[error("schema fragment reference is invalid: {0}")]
62 InvalidReference(String),
63
64 #[error("schema value cardinality is unsupported at {0}")]
66 UnsupportedCardinality(String),
67}
68
69impl Schema {
74 pub fn schema_fragment_for_canister(
85 &self,
86 canister_path: &str,
87 ) -> Result<SchemaFragment, FragmentLoweringError> {
88 if !self.is_sealed() {
89 return Err(FragmentLoweringError::GraphNotSealed);
90 }
91 self.cast_node::<Canister>(canister_path)
92 .map_err(|_| FragmentLoweringError::CanisterNotFound(canister_path.to_string()))?;
93 let stores = self
94 .filter_nodes::<Store>(|store| store.canister() == canister_path)
95 .map(|(path, _)| path.to_string())
96 .collect::<BTreeSet<_>>();
97 if stores.is_empty() {
98 return Err(FragmentLoweringError::CanisterHasNoStores(
99 canister_path.to_string(),
100 ));
101 }
102
103 let entities = self
104 .get_nodes::<Entity>()
105 .filter(|(_, entity)| stores.contains(entity.store()))
106 .map(|(_, entity)| entity)
107 .collect::<Vec<_>>();
108 let selected_entities = entities
109 .iter()
110 .map(|entity| entity.def().path())
111 .collect::<BTreeSet<_>>();
112 for entity in &entities {
113 ensure_relation_targets_in_database(self, entity, &selected_entities)?;
114 }
115
116 let mut pending_types = Vec::new();
117 let entity_fragments = entities
118 .iter()
119 .map(|entity| lower_entity(self, entity, &mut pending_types))
120 .collect::<Result<Vec<_>, _>>()?;
121 let types = lower_reachable_types(self, pending_types)?;
122
123 SchemaFragment::try_new(entity_fragments, types).map_err(Into::into)
124 }
125}
126
127fn ensure_relation_targets_in_database(
128 schema: &Schema,
129 entity: &Entity,
130 selected_entities: &BTreeSet<String>,
131) -> Result<(), FragmentLoweringError> {
132 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<ProposalSourceRuleOperation, FragmentLoweringError> {
476 let length_bound = |value: &RuleNumber| {
477 rule_integer_u128(value)
478 .and_then(|value| u64::try_from(value).ok())
479 .ok_or_else(|| {
480 FragmentLoweringError::InvalidReference(format!(
481 "rule '{}' has an invalid length bound",
482 rule.name()
483 ))
484 })
485 };
486
487 let operation = match rule.operation() {
488 SourceRuleAuthoringOperation::NumericMinimumInclusive { value } => {
489 let RuleValueShape::Scalar(primitive, item) = resolve_rule_value_shape(schema, target)?
490 else {
491 return Err(invalid_rule_target(rule));
492 };
493 let value = lower_rule_numeric_literal(primitive, item, value)
494 .ok_or_else(|| invalid_rule_target(rule))?;
495 ProposalSourceRuleOperation::NumericMinimumInclusive { value }
496 }
497 SourceRuleAuthoringOperation::NumericMaximumInclusive { value } => {
498 let RuleValueShape::Scalar(primitive, item) = resolve_rule_value_shape(schema, target)?
499 else {
500 return Err(invalid_rule_target(rule));
501 };
502 let value = lower_rule_numeric_literal(primitive, item, value)
503 .ok_or_else(|| invalid_rule_target(rule))?;
504 ProposalSourceRuleOperation::NumericMaximumInclusive { value }
505 }
506 SourceRuleAuthoringOperation::NumericRangeInclusive { min, max } => {
507 let RuleValueShape::Scalar(primitive, item) = resolve_rule_value_shape(schema, target)?
508 else {
509 return Err(invalid_rule_target(rule));
510 };
511 let literal = |value: &RuleNumber| {
512 lower_rule_numeric_literal(primitive, item, value)
513 .ok_or_else(|| invalid_rule_target(rule))
514 };
515 ProposalSourceRuleOperation::NumericRangeInclusive {
516 min: literal(min)?,
517 max: literal(max)?,
518 }
519 }
520 SourceRuleAuthoringOperation::LengthRangeInclusive { min, max } => {
521 let shape = resolve_rule_value_shape(schema, target)?;
522 if !matches!(
523 shape,
524 RuleValueShape::Collection
525 | RuleValueShape::Scalar(Primitive::Blob | Primitive::Text, _)
526 ) {
527 return Err(invalid_rule_target(rule));
528 }
529 ProposalSourceRuleOperation::LengthRangeInclusive {
530 min: length_bound(min)?,
531 max: length_bound(max)?,
532 }
533 }
534 SourceRuleAuthoringOperation::MultipleOf { divisor } => {
535 let RuleValueShape::Scalar(primitive, item) = resolve_rule_value_shape(schema, target)?
536 else {
537 return Err(invalid_rule_target(rule));
538 };
539 let divisor = lower_rule_numeric_literal(primitive, item, divisor)
540 .ok_or_else(|| invalid_rule_target(rule))?;
541 ProposalSourceRuleOperation::MultipleOf { divisor }
542 }
543 };
544 Ok(operation)
545}
546
547fn lower_rule_numeric_literal(
548 primitive: Primitive,
549 item: &Item,
550 value: &RuleNumber,
551) -> Option<ScalarLiteral> {
552 match primitive {
553 Primitive::Decimal => rule_decimal(value)
554 .and_then(|value| exact_decimal_at_scale(value, item.scale().unwrap_or(0)))
555 .map(ScalarLiteral::Decimal),
556 Primitive::Float32 => match value {
557 RuleNumber::Float32(value) => Float32::try_new(*value).map(ScalarLiteral::Float32),
558 RuleNumber::Integer(_) | RuleNumber::Decimal(_) | RuleNumber::Float64(_) => None,
559 },
560 Primitive::Float64 => match value {
561 RuleNumber::Decimal(value) => value
562 .parse::<f64>()
563 .ok()
564 .and_then(Float64::try_new)
565 .map(ScalarLiteral::Float64),
566 RuleNumber::Float64(value) => Float64::try_new(*value).map(ScalarLiteral::Float64),
567 RuleNumber::Integer(_) | RuleNumber::Float32(_) => None,
568 },
569 Primitive::Int8
570 | Primitive::Int16
571 | Primitive::Int32
572 | Primitive::Int64
573 | Primitive::Int128 => rule_integer_i128(value).map(ScalarLiteral::Int),
574 Primitive::IntBig => rule_integer_text(value)
575 .and_then(|value| IntBig::from_str(value).ok())
576 .map(ScalarLiteral::IntBig),
577 Primitive::Nat8
578 | Primitive::Nat16
579 | Primitive::Nat32
580 | Primitive::Nat64
581 | Primitive::Nat128 => rule_integer_u128(value).map(ScalarLiteral::Nat),
582 Primitive::NatBig => rule_integer_text(value)
583 .and_then(|value| NatBig::from_str(value).ok())
584 .map(ScalarLiteral::NatBig),
585 Primitive::Account
586 | Primitive::Blob
587 | Primitive::Bool
588 | Primitive::Date
589 | Primitive::Duration
590 | Primitive::Principal
591 | Primitive::Subaccount
592 | Primitive::Text
593 | Primitive::Timestamp
594 | Primitive::Ulid
595 | Primitive::Unit => None,
596 }
597}
598
599const fn rule_integer_text(value: &RuleNumber) -> Option<&str> {
600 let RuleNumber::Integer(value) = value else {
601 return None;
602 };
603 Some(value)
604}
605
606fn rule_integer_i128(value: &RuleNumber) -> Option<i128> {
607 rule_integer_text(value)?.parse().ok()
608}
609
610fn rule_integer_u128(value: &RuleNumber) -> Option<u128> {
611 rule_integer_text(value)?.parse().ok()
612}
613
614fn rule_decimal(value: &RuleNumber) -> Option<Decimal> {
615 match value {
616 RuleNumber::Integer(value) | RuleNumber::Decimal(value) => Decimal::from_str(value).ok(),
617 RuleNumber::Float32(_) | RuleNumber::Float64(_) => None,
618 }
619}
620
621fn exact_decimal_at_scale(value: Decimal, scale: u32) -> Option<Decimal> {
622 let value = value.normalize();
623 value
624 .scale_to_integer(scale)
625 .and_then(|mantissa| Decimal::try_from_i128_with_scale(mantissa, scale))
626}
627
628#[derive(Clone, Copy)]
629enum RuleValueShape<'schema> {
630 Collection,
631 Scalar(Primitive, &'schema Item),
632}
633
634fn resolve_rule_value_shape<'schema>(
635 schema: &'schema Schema,
636 mut target: &'schema SchemaNode,
637) -> Result<RuleValueShape<'schema>, FragmentLoweringError> {
638 let mut visited = BTreeSet::new();
639 loop {
640 let source = named_type_name(target)
641 .ok_or_else(|| FragmentLoweringError::InvalidReference("non-type rule".to_string()))?;
642 if !visited.insert(source) {
643 return Err(FragmentLoweringError::InvalidReference(format!(
644 "durable-rule target cycle at '{source}'"
645 )));
646 }
647 match target {
648 SchemaNode::List(_) | SchemaNode::Map(_) | SchemaNode::Set(_) => {
649 return Ok(RuleValueShape::Collection);
650 }
651 SchemaNode::Newtype(newtype) => match newtype.item().target() {
652 ItemTarget::Primitive(primitive) => {
653 return Ok(RuleValueShape::Scalar(*primitive, newtype.item()));
654 }
655 ItemTarget::Is(path) => {
656 target = schema
657 .get_node(path)
658 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.to_string()))?;
659 }
660 },
661 SchemaNode::Record(_) | SchemaNode::Enum(_) | SchemaNode::Tuple(_) => {
662 return Err(FragmentLoweringError::InvalidReference(format!(
663 "durable-rule target '{source}' has no supported scalar or collection value"
664 )));
665 }
666 SchemaNode::Canister(_)
667 | SchemaNode::Entity(_)
668 | SchemaNode::Normalizer(_)
669 | SchemaNode::Store(_)
670 | SchemaNode::Validator(_) => {
671 return Err(FragmentLoweringError::InvalidReference(
672 "non-type durable-rule target".to_string(),
673 ));
674 }
675 }
676 }
677}
678
679fn invalid_rule_target(rule: &SourceRule) -> FragmentLoweringError {
680 FragmentLoweringError::InvalidReference(format!(
681 "durable rule '{}' does not match its nominal target",
682 rule.name()
683 ))
684}
685
686fn entity_field_source_key(
687 entity: &Entity,
688 field_name: &str,
689) -> Result<FieldSourceKey, FragmentLoweringError> {
690 let field = entity
691 .fields()
692 .get(field_name)
693 .ok_or_else(|| FragmentLoweringError::InvalidReference(field_name.to_string()))?;
694 FieldSourceKey::try_new(field.name()).map_err(Into::into)
695}
696
697fn lower_reachable_types(
702 schema: &Schema,
703 mut pending: Vec<String>,
704) -> Result<Vec<NamedTypeFragment>, FragmentLoweringError> {
705 let mut lowered = BTreeMap::new();
706 while let Some(path) = pending.pop() {
707 let node = schema
708 .get_node(path.as_str())
709 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.clone()))?;
710 let source_key = named_type_name(node)
711 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.clone()))?;
712 if lowered.contains_key(source_key) {
713 continue;
714 }
715 let fragment = lower_named_type(schema, node, &mut pending)?;
716 lowered.insert(source_key.to_string(), fragment);
717 }
718 Ok(lowered.into_values().collect())
719}
720
721const fn named_type_name(node: &crate::node::SchemaNode) -> Option<&str> {
722 match node {
723 crate::node::SchemaNode::Enum(node) => Some(node.name()),
724 crate::node::SchemaNode::List(node) => Some(node.name()),
725 crate::node::SchemaNode::Map(node) => Some(node.name()),
726 crate::node::SchemaNode::Newtype(node) => Some(node.name()),
727 crate::node::SchemaNode::Record(node) => Some(node.name()),
728 crate::node::SchemaNode::Set(node) => Some(node.name()),
729 crate::node::SchemaNode::Tuple(node) => Some(node.name()),
730 crate::node::SchemaNode::Canister(_)
731 | crate::node::SchemaNode::Entity(_)
732 | crate::node::SchemaNode::Normalizer(_)
733 | crate::node::SchemaNode::Store(_)
734 | crate::node::SchemaNode::Validator(_) => None,
735 }
736}
737
738fn lower_named_type(
739 schema: &Schema,
740 node: &crate::node::SchemaNode,
741 pending: &mut Vec<String>,
742) -> Result<NamedTypeFragment, FragmentLoweringError> {
743 match node {
744 crate::node::SchemaNode::Record(record) => lower_record(schema, record, pending),
745 crate::node::SchemaNode::Enum(r#enum) => lower_enum(schema, r#enum, pending),
746 crate::node::SchemaNode::Newtype(newtype) => Ok(NamedTypeFragment::newtype(
747 SchemaName::try_new(newtype.name())?,
748 lower_item_type(schema, newtype.item(), pending)?,
749 )),
750 crate::node::SchemaNode::List(list) => lower_list(schema, list, pending),
751 crate::node::SchemaNode::Set(set) => lower_set(schema, set, pending),
752 crate::node::SchemaNode::Map(map) => lower_map(schema, map, pending),
753 crate::node::SchemaNode::Tuple(tuple) => lower_tuple(schema, tuple, pending),
754 crate::node::SchemaNode::Canister(_)
755 | crate::node::SchemaNode::Entity(_)
756 | crate::node::SchemaNode::Normalizer(_)
757 | crate::node::SchemaNode::Store(_)
758 | crate::node::SchemaNode::Validator(_) => Err(FragmentLoweringError::InvalidReference(
759 "non-type graph node".to_string(),
760 )),
761 }
762}
763
764fn lower_record(
765 schema: &Schema,
766 record: &Record,
767 pending: &mut Vec<String>,
768) -> Result<NamedTypeFragment, FragmentLoweringError> {
769 let fields = record
770 .fields()
771 .fields()
772 .iter()
773 .map(|field| {
774 Ok(RecordFieldFragment::new(
775 SchemaName::try_new(field.name())?,
776 lower_value_type(schema, field.value(), pending)?,
777 field.value().cardinality() == Cardinality::Opt,
778 ))
779 })
780 .collect::<Result<Vec<_>, FragmentLoweringError>>()?;
781 Ok(NamedTypeFragment::Record(RecordTypeFragment::try_new(
782 SchemaName::try_new(record.name())?,
783 fields,
784 )?))
785}
786
787fn lower_enum(
788 schema: &Schema,
789 r#enum: &Enum,
790 pending: &mut Vec<String>,
791) -> Result<NamedTypeFragment, FragmentLoweringError> {
792 let variants = r#enum
793 .variants()
794 .iter()
795 .map(|variant| {
796 let name = SchemaName::try_new(variant.name())?;
797 match variant.value() {
798 Some(value) if value.cardinality() == Cardinality::Opt => {
799 Err(FragmentLoweringError::UnsupportedCardinality(format!(
800 "{}::{}",
801 r#enum.def().path(),
802 variant.name()
803 )))
804 }
805 Some(value) => Ok(EnumVariantFragment::with_payload(
806 name,
807 lower_value_type(schema, value, pending)?,
808 )),
809 None => Ok(EnumVariantFragment::new(name)),
810 }
811 })
812 .collect::<Result<Vec<_>, _>>()?;
813 Ok(NamedTypeFragment::Enum(EnumTypeFragment::try_new(
814 SchemaName::try_new(r#enum.name())?,
815 variants,
816 )?))
817}
818
819fn lower_list(
820 schema: &Schema,
821 list: &List,
822 pending: &mut Vec<String>,
823) -> Result<NamedTypeFragment, FragmentLoweringError> {
824 Ok(NamedTypeFragment::list(
825 SchemaName::try_new(list.name())?,
826 lower_item_type(schema, list.item(), pending)?,
827 ))
828}
829
830fn lower_set(
831 schema: &Schema,
832 set: &Set,
833 pending: &mut Vec<String>,
834) -> Result<NamedTypeFragment, FragmentLoweringError> {
835 Ok(NamedTypeFragment::set(
836 SchemaName::try_new(set.name())?,
837 lower_item_type(schema, set.item(), pending)?,
838 ))
839}
840
841fn lower_map(
842 schema: &Schema,
843 map: &Map,
844 pending: &mut Vec<String>,
845) -> Result<NamedTypeFragment, FragmentLoweringError> {
846 if map.value().cardinality() == Cardinality::Opt {
847 return Err(FragmentLoweringError::UnsupportedCardinality(
848 map.def().path(),
849 ));
850 }
851 Ok(NamedTypeFragment::map(
852 SchemaName::try_new(map.name())?,
853 lower_item_type(schema, map.key(), pending)?,
854 lower_value_type(schema, map.value(), pending)?,
855 ))
856}
857
858fn lower_tuple(
859 schema: &Schema,
860 tuple: &Tuple,
861 pending: &mut Vec<String>,
862) -> Result<NamedTypeFragment, FragmentLoweringError> {
863 let members = tuple
864 .values()
865 .iter()
866 .map(|value| {
867 Ok::<_, FragmentLoweringError>(TupleElementFragment::new(
868 lower_value_type(schema, value, pending)?,
869 value.cardinality() == Cardinality::Opt,
870 ))
871 })
872 .collect::<Result<Vec<_>, _>>()?;
873 Ok(NamedTypeFragment::tuple(
874 SchemaName::try_new(tuple.name())?,
875 members,
876 ))
877}
878
879fn lower_value_type(
884 schema: &Schema,
885 value: &Value,
886 pending: &mut Vec<String>,
887) -> Result<FieldType, FragmentLoweringError> {
888 let item = lower_item_type(schema, value.item(), pending)?;
889 Ok(if value.cardinality() == Cardinality::Many {
890 FieldType::List(Box::new(item))
891 } else {
892 item
893 })
894}
895
896fn lower_item_type(
897 schema: &Schema,
898 item: &Item,
899 pending: &mut Vec<String>,
900) -> Result<FieldType, FragmentLoweringError> {
901 match item.target() {
902 ItemTarget::Is(path) => {
903 pending.push((*path).to_string());
904 Ok(FieldType::Named(TypeSourceKey::try_new(
905 type_source_key_for_path(schema, path)?,
906 )?))
907 }
908 ItemTarget::Primitive(primitive) => {
909 Ok(FieldType::Scalar(lower_scalar_type(*primitive, item)))
910 }
911 }
912}
913
914fn type_source_key_for_path<'schema>(
915 schema: &'schema Schema,
916 path: &str,
917) -> Result<&'schema str, FragmentLoweringError> {
918 let source = schema
919 .get_node(path)
920 .and_then(named_type_name)
921 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.to_string()))?;
922 Ok(source)
923}
924
925fn lower_scalar_type(primitive: Primitive, item: &Item) -> ScalarType {
926 match primitive {
927 Primitive::Account => ScalarType::Account,
928 Primitive::Blob => ScalarType::Blob {
929 max_len: item.max_len(),
930 },
931 Primitive::Bool => ScalarType::Bool,
932 Primitive::Date => ScalarType::Date,
933 Primitive::Decimal => ScalarType::Decimal {
934 scale: item.scale().unwrap_or(0),
935 },
936 Primitive::Duration => ScalarType::Duration,
937 Primitive::Float32 => ScalarType::Float32,
938 Primitive::Float64 => ScalarType::Float64,
939 Primitive::Int8 => ScalarType::Int8,
940 Primitive::Int16 => ScalarType::Int16,
941 Primitive::Int32 => ScalarType::Int32,
942 Primitive::Int64 => ScalarType::Int64,
943 Primitive::Int128 => ScalarType::Int128,
944 Primitive::IntBig => ScalarType::IntBig {
945 max_bytes: item.max_bytes().unwrap_or(DEFAULT_BIG_INT_MAX_BYTES),
946 },
947 Primitive::Nat8 => ScalarType::Nat8,
948 Primitive::Nat16 => ScalarType::Nat16,
949 Primitive::Nat32 => ScalarType::Nat32,
950 Primitive::Nat64 => ScalarType::Nat64,
951 Primitive::Nat128 => ScalarType::Nat128,
952 Primitive::NatBig => ScalarType::NatBig {
953 max_bytes: item.max_bytes().unwrap_or(DEFAULT_BIG_INT_MAX_BYTES),
954 },
955 Primitive::Principal => ScalarType::Principal,
956 Primitive::Subaccount => ScalarType::Subaccount,
957 Primitive::Text => ScalarType::Text {
958 max_len: item.max_len(),
959 },
960 Primitive::Timestamp => ScalarType::Timestamp,
961 Primitive::Ulid => ScalarType::Ulid,
962 Primitive::Unit => ScalarType::Unit,
963 }
964}
965
966fn lower_default(
971 schema: &Schema,
972 field: &Field,
973 default: &Arg,
974) -> Result<ScalarLiteral, FragmentLoweringError> {
975 if let ItemTarget::Is(path) = field.value().item().target() {
976 let Arg::ConstPath(default_path) = default else {
977 return Err(FragmentLoweringError::InvalidDefault(
978 field.name().to_string(),
979 ));
980 };
981 let variant = default_path.rsplit("::").next().unwrap_or(default_path);
982 return schema
983 .enum_unit_literal(path, variant)
984 .map_err(FragmentLoweringError::from);
985 }
986 let ItemTarget::Primitive(primitive) = field.value().item().target() else {
987 return Err(FragmentLoweringError::InvalidDefault(
988 field.name().to_string(),
989 ));
990 };
991 lower_scalar_default(*primitive, field.value().item(), default)
992 .ok_or_else(|| FragmentLoweringError::InvalidDefault(field.name().to_string()))
993}
994
995fn lower_scalar_default(primitive: Primitive, item: &Item, default: &Arg) -> Option<ScalarLiteral> {
996 if default_constructor_is_zero(default) {
997 return zero_scalar_literal(primitive, item);
998 }
999 match (primitive, default) {
1000 (Primitive::Account, Arg::String(value)) => {
1001 Account::from_str(value).ok().map(ScalarLiteral::Account)
1002 }
1003 (Primitive::Blob, Arg::String(value)) => Blob::try_new(value.as_bytes().to_vec())
1004 .ok()
1005 .map(ScalarLiteral::Blob),
1006 (Primitive::Bool, Arg::Bool(value)) => Some(ScalarLiteral::Bool(*value)),
1007 (Primitive::Date, Arg::String(value)) => Date::parse(value).map(ScalarLiteral::Date),
1008 (Primitive::Date, Arg::Number(value)) => arg_i128(value)
1009 .and_then(|value| i32::try_from(value).ok())
1010 .map(Date::from_days_since_epoch)
1011 .map(ScalarLiteral::Date),
1012 (Primitive::Decimal, Arg::String(value)) => Decimal::from_str(value)
1013 .ok()
1014 .and_then(|value| decimal_at_scale(value, item.scale().unwrap_or(0)))
1015 .map(ScalarLiteral::Decimal),
1016 (Primitive::Decimal, Arg::Number(value)) => arg_decimal(value)
1017 .and_then(|value| decimal_at_scale(value, item.scale().unwrap_or(0)))
1018 .map(ScalarLiteral::Decimal),
1019 (Primitive::Duration, Arg::String(value)) => Duration::parse_flexible(value)
1020 .ok()
1021 .map(ScalarLiteral::Duration),
1022 (Primitive::Duration, Arg::Number(value)) => arg_u128(value)
1023 .and_then(|value| u64::try_from(value).ok())
1024 .map(Duration::from_millis)
1025 .map(ScalarLiteral::Duration),
1026 (Primitive::Float32, Arg::Number(ArgNumber::Float32(value))) => {
1027 Float32::try_new(*value).map(ScalarLiteral::Float32)
1028 }
1029 (Primitive::Float64, Arg::Number(ArgNumber::Float64(value))) => {
1030 Float64::try_new(*value).map(ScalarLiteral::Float64)
1031 }
1032 (
1033 Primitive::Int8
1034 | Primitive::Int16
1035 | Primitive::Int32
1036 | Primitive::Int64
1037 | Primitive::Int128,
1038 Arg::Number(value),
1039 ) => arg_i128(value).map(ScalarLiteral::Int),
1040 (Primitive::IntBig, Arg::Number(value)) => arg_i128(value)
1041 .map(|value| value.to_string())
1042 .and_then(|value| IntBig::from_str(value.as_str()).ok())
1043 .map(ScalarLiteral::IntBig),
1044 (Primitive::IntBig, Arg::String(value)) => {
1045 IntBig::from_str(value).ok().map(ScalarLiteral::IntBig)
1046 }
1047 (
1048 Primitive::Nat8
1049 | Primitive::Nat16
1050 | Primitive::Nat32
1051 | Primitive::Nat64
1052 | Primitive::Nat128,
1053 Arg::Number(value),
1054 ) => arg_u128(value).map(ScalarLiteral::Nat),
1055 (Primitive::NatBig, Arg::Number(value)) => arg_u128(value)
1056 .map(|value| value.to_string())
1057 .and_then(|value| NatBig::from_str(value.as_str()).ok())
1058 .map(ScalarLiteral::NatBig),
1059 (Primitive::NatBig, Arg::String(value)) => {
1060 NatBig::from_str(value).ok().map(ScalarLiteral::NatBig)
1061 }
1062 (Primitive::Principal, Arg::String(value)) => Principal::from_str(value)
1063 .ok()
1064 .map(ScalarLiteral::Principal),
1065 (Primitive::Subaccount, Arg::String(value)) => parse_subaccount(value)
1066 .map(Subaccount::from_array)
1067 .map(ScalarLiteral::Subaccount),
1068 (Primitive::Text, Arg::String(value)) => Some(ScalarLiteral::Text((*value).to_string())),
1069 (Primitive::Timestamp, Arg::String(value)) => Timestamp::parse_flexible(value)
1070 .ok()
1071 .map(ScalarLiteral::Timestamp),
1072 (Primitive::Timestamp, Arg::Number(value)) => arg_i128(value)
1073 .and_then(|value| i64::try_from(value).ok())
1074 .map(Timestamp::from_millis)
1075 .map(ScalarLiteral::Timestamp),
1076 (Primitive::Ulid, Arg::String(value)) => {
1077 Ulid::from_str(value).ok().map(ScalarLiteral::Ulid)
1078 }
1079 (Primitive::Unit, Arg::ConstPath(path)) if path.ends_with("Unit") => {
1080 Some(ScalarLiteral::Unit(Unit))
1081 }
1082 _ => None,
1083 }
1084}
1085
1086fn default_constructor_is_zero(default: &Arg) -> bool {
1087 let Arg::FuncPath(path) = default else {
1088 return false;
1089 };
1090 path.ends_with("::default")
1091 || path.ends_with("::new")
1092 || path.ends_with("::EPOCH")
1093 || path.ends_with("::nil")
1094}
1095
1096fn zero_scalar_literal(primitive: Primitive, item: &Item) -> Option<ScalarLiteral> {
1097 match primitive {
1098 Primitive::Blob => Blob::try_new(Vec::new()).ok().map(ScalarLiteral::Blob),
1099 Primitive::Bool => Some(ScalarLiteral::Bool(false)),
1100 Primitive::Date => Some(ScalarLiteral::Date(Date::EPOCH)),
1101 Primitive::Decimal => Decimal::try_from_i128_with_scale(0, item.scale().unwrap_or(0))
1102 .map(ScalarLiteral::Decimal),
1103 Primitive::Duration => Some(ScalarLiteral::Duration(Duration::ZERO)),
1104 Primitive::Float32 => Float32::try_new(0.0).map(ScalarLiteral::Float32),
1105 Primitive::Float64 => Float64::try_new(0.0).map(ScalarLiteral::Float64),
1106 Primitive::Int8
1107 | Primitive::Int16
1108 | Primitive::Int32
1109 | Primitive::Int64
1110 | Primitive::Int128 => Some(ScalarLiteral::Int(0)),
1111 Primitive::IntBig => IntBig::from_str("0").ok().map(ScalarLiteral::IntBig),
1112 Primitive::Nat8
1113 | Primitive::Nat16
1114 | Primitive::Nat32
1115 | Primitive::Nat64
1116 | Primitive::Nat128 => Some(ScalarLiteral::Nat(0)),
1117 Primitive::NatBig => NatBig::from_str("0").ok().map(ScalarLiteral::NatBig),
1118 Primitive::Text => Some(ScalarLiteral::Text(String::new())),
1119 Primitive::Timestamp => Some(ScalarLiteral::Timestamp(Timestamp::EPOCH)),
1120 Primitive::Ulid => Some(ScalarLiteral::Ulid(Ulid::nil())),
1121 Primitive::Unit => Some(ScalarLiteral::Unit(Unit)),
1122 Primitive::Account | Primitive::Principal | Primitive::Subaccount => None,
1123 }
1124}
1125
1126fn decimal_at_scale(value: Decimal, scale: u32) -> Option<Decimal> {
1131 match value.scale().cmp(&scale) {
1132 std::cmp::Ordering::Equal => Some(value),
1133 std::cmp::Ordering::Less => value
1134 .scale_to_integer(scale)
1135 .and_then(|mantissa| Decimal::try_from_i128_with_scale(mantissa, scale)),
1136 std::cmp::Ordering::Greater => Some(value.round_dp(scale)),
1137 }
1138}
1139
1140fn arg_i128(value: &ArgNumber) -> Option<i128> {
1141 match value {
1142 ArgNumber::Int8(value) => Some(i128::from(*value)),
1143 ArgNumber::Int16(value) => Some(i128::from(*value)),
1144 ArgNumber::Int32(value) => Some(i128::from(*value)),
1145 ArgNumber::Int64(value) => Some(i128::from(*value)),
1146 ArgNumber::Int128(value) => Some(*value),
1147 ArgNumber::Nat8(value) => Some(i128::from(*value)),
1148 ArgNumber::Nat16(value) => Some(i128::from(*value)),
1149 ArgNumber::Nat32(value) => Some(i128::from(*value)),
1150 ArgNumber::Nat64(value) => Some(i128::from(*value)),
1151 ArgNumber::Nat128(value) => i128::try_from(*value).ok(),
1152 ArgNumber::Float32(_) | ArgNumber::Float64(_) => None,
1153 }
1154}
1155
1156fn arg_u128(value: &ArgNumber) -> Option<u128> {
1157 match value {
1158 ArgNumber::Int8(value) => u128::try_from(*value).ok(),
1159 ArgNumber::Int16(value) => u128::try_from(*value).ok(),
1160 ArgNumber::Int32(value) => u128::try_from(*value).ok(),
1161 ArgNumber::Int64(value) => u128::try_from(*value).ok(),
1162 ArgNumber::Int128(value) => u128::try_from(*value).ok(),
1163 ArgNumber::Nat8(value) => Some(u128::from(*value)),
1164 ArgNumber::Nat16(value) => Some(u128::from(*value)),
1165 ArgNumber::Nat32(value) => Some(u128::from(*value)),
1166 ArgNumber::Nat64(value) => Some(u128::from(*value)),
1167 ArgNumber::Nat128(value) => Some(*value),
1168 ArgNumber::Float32(_) | ArgNumber::Float64(_) => None,
1169 }
1170}
1171
1172fn arg_decimal(value: &ArgNumber) -> Option<Decimal> {
1173 match value {
1174 ArgNumber::Float32(value) => Decimal::from_f32_lossy(*value),
1175 ArgNumber::Float64(value) => Decimal::from_f64_lossy(*value),
1176 _ => arg_i128(value).and_then(Decimal::from_i128),
1177 }
1178}
1179
1180fn parse_subaccount(value: &str) -> Option<[u8; 32]> {
1181 if value.len() != 64 {
1182 return None;
1183 }
1184 let mut bytes = [0; 32];
1185 for (index, chunk) in value.as_bytes().chunks_exact(2).enumerate() {
1186 let text = std::str::from_utf8(chunk).ok()?;
1187 bytes[index] = u8::from_str_radix(text, 16).ok()?;
1188 }
1189 Some(bytes)
1190}
1191
1192#[cfg(test)]
1193mod tests {
1194 use icydb_schema::{
1195 ConstraintFragmentKind, ConstraintSourceKey, Decimal, FieldSourceKey, FieldType,
1196 NamedTypeFragment, RuleSourceKey, ScalarLiteral, ScalarType, SourceRuleOperation,
1197 TypeSourceKey,
1198 };
1199
1200 use super::{Schema, lower_field_rules};
1201 use crate::{
1202 node::{
1203 Args, Canister, Def, Entity, Enum, EnumVariant, Field, FieldList, Item, ItemTarget,
1204 Newtype, Normalizer, PrimaryKey, PrimaryKeySource, Record, RuleNumber, SchemaNode,
1205 SourceRule, SourceRuleAuthoringOperation, Store, StoreHeapConfig, Type, TypeNormalizer,
1206 TypeValidator, Validator, Value,
1207 },
1208 types::{Cardinality, Primitive},
1209 };
1210
1211 static EMPTY_TYPE: Type = Type::new(&[], &[], &[]);
1212 static APPLICATION_FIELDS: [Field; 1] = [Field::new(
1213 "id",
1214 Value::new(
1215 Cardinality::One,
1216 Item::new(
1217 ItemTarget::Primitive(Primitive::Nat64),
1218 None,
1219 None,
1220 None,
1221 None,
1222 &[],
1223 &[],
1224 false,
1225 ),
1226 ),
1227 None,
1228 None,
1229 None,
1230 )];
1231 static APPLICATION_NORMALIZERS_A: [TypeNormalizer; 1] =
1232 [TypeNormalizer::new("test::NormalizeA", Args(&[]))];
1233 static APPLICATION_NORMALIZERS_B: [TypeNormalizer; 1] =
1234 [TypeNormalizer::new("test::NormalizeB", Args(&[]))];
1235 static APPLICATION_VALIDATORS_A: [TypeValidator; 1] =
1236 [TypeValidator::new("test::ValidateA", Args(&[]))];
1237 static APPLICATION_VALIDATORS_B: [TypeValidator; 1] =
1238 [TypeValidator::new("test::ValidateB", Args(&[]))];
1239 static NUMERIC_RULES: [SourceRule; 1] = [SourceRule::new(
1240 "range",
1241 SourceRuleAuthoringOperation::NumericRangeInclusive {
1242 min: RuleNumber::Integer("0"),
1243 max: RuleNumber::Integer("360"),
1244 },
1245 )];
1246 static NUMERIC_RULE_TYPE: Type = Type::new(&[], &[], &NUMERIC_RULES);
1247 static LENGTH_RULES: [SourceRule; 1] = [SourceRule::new(
1248 "length",
1249 SourceRuleAuthoringOperation::LengthRangeInclusive {
1250 min: RuleNumber::Integer("2"),
1251 max: RuleNumber::Integer("40"),
1252 },
1253 )];
1254 static LENGTH_RULE_TYPE: Type = Type::new(&[], &[], &LENGTH_RULES);
1255 static NAT_EXACT_RULES: [SourceRule; 2] = [
1256 SourceRule::new(
1257 "maximum",
1258 SourceRuleAuthoringOperation::NumericMaximumInclusive {
1259 value: RuleNumber::Integer("100"),
1260 },
1261 ),
1262 SourceRule::new(
1263 "step",
1264 SourceRuleAuthoringOperation::MultipleOf {
1265 divisor: RuleNumber::Integer("5"),
1266 },
1267 ),
1268 ];
1269 static NAT_EXACT_RULE_TYPE: Type = Type::new(&[], &[], &NAT_EXACT_RULES);
1270 static DECIMAL_EXACT_RULES: [SourceRule; 1] = [SourceRule::new(
1271 "step",
1272 SourceRuleAuthoringOperation::MultipleOf {
1273 divisor: RuleNumber::Decimal("0.25"),
1274 },
1275 )];
1276 static DECIMAL_EXACT_RULE_TYPE: Type = Type::new(&[], &[], &DECIMAL_EXACT_RULES);
1277 static INEXACT_DECIMAL_RULES: [SourceRule; 1] = [SourceRule::new(
1278 "step",
1279 SourceRuleAuthoringOperation::MultipleOf {
1280 divisor: RuleNumber::Decimal("0.251"),
1281 },
1282 )];
1283 static INEXACT_DECIMAL_RULE_TYPE: Type = Type::new(&[], &[], &INEXACT_DECIMAL_RULES);
1284 static NESTED_RULE_FIELDS: [Field; 1] = [Field::new(
1285 "degrees",
1286 Value::new(
1287 Cardinality::One,
1288 Item::new(
1289 ItemTarget::Is("test::Degrees"),
1290 None,
1291 None,
1292 None,
1293 None,
1294 &[],
1295 &[],
1296 false,
1297 ),
1298 ),
1299 None,
1300 None,
1301 None,
1302 )];
1303 static STATUS_VARIANTS: [EnumVariant; 2] = [
1304 EnumVariant::new("Active", None),
1305 EnumVariant::new(
1306 "Retries",
1307 Some(Value::new(
1308 Cardinality::Many,
1309 Item::new(
1310 ItemTarget::Primitive(Primitive::Nat16),
1311 None,
1312 None,
1313 None,
1314 None,
1315 &[],
1316 &[],
1317 false,
1318 ),
1319 )),
1320 ),
1321 ];
1322
1323 fn application_behavior_fragment(
1324 normalizers: &'static [TypeNormalizer],
1325 validators: &'static [TypeValidator],
1326 normalizer_name: &'static str,
1327 validator_name: &'static str,
1328 ) -> icydb_schema::SchemaFragment {
1329 let mut schema = Schema::new();
1330 schema.insert_node(SchemaNode::Canister(Canister::new(
1331 Def::new("test", "Canister"),
1332 "test",
1333 0,
1334 10,
1335 9,
1336 8,
1337 )));
1338 schema.insert_node(SchemaNode::Store(Store::new_heap(
1339 Def::new("test", "Store"),
1340 "test::Canister",
1341 StoreHeapConfig::new(),
1342 )));
1343 schema.insert_node(SchemaNode::Normalizer(Normalizer::new(Def::new(
1344 "test",
1345 normalizer_name,
1346 ))));
1347 schema.insert_node(SchemaNode::Validator(Validator::new(Def::new(
1348 "test",
1349 validator_name,
1350 ))));
1351 schema.insert_node(SchemaNode::Entity(Entity::new(
1352 Def::new("test", "ApplicationOnly"),
1353 "test::Store",
1354 1,
1355 PrimaryKey::new(&["id"], PrimaryKeySource::External),
1356 &[],
1357 &[],
1358 &[],
1359 FieldList::new(&APPLICATION_FIELDS),
1360 Type::new(normalizers, validators, &[]),
1361 )));
1362 schema.seal().expect("application-only fixture should seal");
1363 schema
1364 .schema_fragment_for_canister("test::Canister")
1365 .expect("application-only fixture should lower")
1366 }
1367
1368 #[test]
1369 fn validator_and_normalizer_edits_do_not_change_database_fragment() {
1370 let before = application_behavior_fragment(
1371 &APPLICATION_NORMALIZERS_A,
1372 &APPLICATION_VALIDATORS_A,
1373 "NormalizeA",
1374 "ValidateA",
1375 );
1376 let after = application_behavior_fragment(
1377 &APPLICATION_NORMALIZERS_B,
1378 &APPLICATION_VALIDATORS_B,
1379 "NormalizeB",
1380 "ValidateB",
1381 );
1382
1383 assert_eq!(before, after);
1384 }
1385
1386 #[test]
1387 fn durable_rules_nested_below_structural_fields_lower_to_nominal_targets() {
1388 let mut schema = Schema::new();
1389 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1390 Def::new("test", "Degrees"),
1391 "Degrees",
1392 Item::new(
1393 ItemTarget::Primitive(Primitive::Nat16),
1394 None,
1395 None,
1396 None,
1397 None,
1398 &[],
1399 &[],
1400 false,
1401 ),
1402 None,
1403 NUMERIC_RULE_TYPE.clone(),
1404 )));
1405 schema.insert_node(SchemaNode::Record(Record::new(
1406 Def::new("test", "Nested"),
1407 "Nested",
1408 FieldList::new(&NESTED_RULE_FIELDS),
1409 EMPTY_TYPE.clone(),
1410 )));
1411
1412 let outer = Field::new(
1413 "nested",
1414 Value::new(
1415 Cardinality::One,
1416 Item::new(
1417 ItemTarget::Is("test::Nested"),
1418 None,
1419 None,
1420 None,
1421 None,
1422 &[],
1423 &[],
1424 false,
1425 ),
1426 ),
1427 None,
1428 None,
1429 None,
1430 );
1431 let constraints =
1432 lower_field_rules(&schema, &outer).expect("nested durable rule should lower");
1433 assert_eq!(constraints.len(), 1);
1434 let ConstraintFragmentKind::TargetedRule(rule) = constraints[0].kind() else {
1435 panic!("nested durable rule should use the targeted-rule contract")
1436 };
1437 assert_eq!(rule.root().as_str(), "nested");
1438 assert_eq!(rule.target_type().as_str(), "Degrees");
1439 assert!(matches!(
1440 rule.operation(),
1441 SourceRuleOperation::NumericRangeInclusive { .. }
1442 ));
1443 }
1444
1445 #[test]
1446 fn exact_maximum_and_multiple_of_lower_without_float_reconstruction() {
1447 let mut schema = Schema::new();
1448 for (name, primitive, scale, rules) in [
1449 (
1450 "Counter",
1451 Primitive::Nat64,
1452 None,
1453 NAT_EXACT_RULE_TYPE.clone(),
1454 ),
1455 (
1456 "PriceStep",
1457 Primitive::Decimal,
1458 Some(2),
1459 DECIMAL_EXACT_RULE_TYPE.clone(),
1460 ),
1461 ] {
1462 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1463 Def::new("test", name),
1464 name,
1465 Item::new(
1466 ItemTarget::Primitive(primitive),
1467 None,
1468 scale,
1469 None,
1470 None,
1471 &[],
1472 &[],
1473 false,
1474 ),
1475 None,
1476 rules,
1477 )));
1478 }
1479
1480 let field = |name| {
1481 Field::new(
1482 name,
1483 Value::new(
1484 Cardinality::One,
1485 Item::new(
1486 ItemTarget::Is(if name == "counter" {
1487 "test::Counter"
1488 } else {
1489 "test::PriceStep"
1490 }),
1491 None,
1492 None,
1493 None,
1494 None,
1495 &[],
1496 &[],
1497 false,
1498 ),
1499 ),
1500 None,
1501 None,
1502 None,
1503 )
1504 };
1505 let counter = lower_field_rules(&schema, &field("counter"))
1506 .expect("exact integer rules should lower");
1507 assert!(matches!(
1508 counter[0].kind(),
1509 ConstraintFragmentKind::TargetedRule(rule)
1510 if matches!(
1511 rule.operation(),
1512 SourceRuleOperation::NumericMaximumInclusive {
1513 value: ScalarLiteral::Nat(100)
1514 }
1515 )
1516 ));
1517 assert!(matches!(
1518 counter[1].kind(),
1519 ConstraintFragmentKind::TargetedRule(rule)
1520 if matches!(
1521 rule.operation(),
1522 SourceRuleOperation::MultipleOf {
1523 divisor: ScalarLiteral::Nat(5)
1524 }
1525 )
1526 ));
1527
1528 let decimal = lower_field_rules(&schema, &field("price"))
1529 .expect("exact decimal multiple should lower");
1530 assert!(matches!(
1531 decimal[0].kind(),
1532 ConstraintFragmentKind::TargetedRule(rule)
1533 if matches!(
1534 rule.operation(),
1535 SourceRuleOperation::MultipleOf {
1536 divisor: ScalarLiteral::Decimal(value)
1537 } if *value == Decimal::new(25, 2)
1538 )
1539 ));
1540 }
1541
1542 #[test]
1543 fn inexact_decimal_rule_operand_rejects_before_proposal_composition() {
1544 let mut schema = Schema::new();
1545 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1546 Def::new("test", "InexactStep"),
1547 "InexactStep",
1548 Item::new(
1549 ItemTarget::Primitive(Primitive::Decimal),
1550 None,
1551 Some(2),
1552 None,
1553 None,
1554 &[],
1555 &[],
1556 false,
1557 ),
1558 None,
1559 INEXACT_DECIMAL_RULE_TYPE.clone(),
1560 )));
1561 let field = Field::new(
1562 "price",
1563 Value::new(
1564 Cardinality::One,
1565 Item::new(
1566 ItemTarget::Is("test::InexactStep"),
1567 None,
1568 None,
1569 None,
1570 None,
1571 &[],
1572 &[],
1573 false,
1574 ),
1575 ),
1576 None,
1577 None,
1578 None,
1579 );
1580 assert!(lower_field_rules(&schema, &field).is_err());
1581 }
1582
1583 static ENTITY_FIELDS: [Field; 5] = [
1584 Field::new(
1585 "id",
1586 Value::new(
1587 Cardinality::One,
1588 Item::new(
1589 ItemTarget::Primitive(Primitive::Nat64),
1590 None,
1591 None,
1592 None,
1593 None,
1594 &[],
1595 &[],
1596 false,
1597 ),
1598 ),
1599 None,
1600 None,
1601 None,
1602 ),
1603 Field::new(
1604 "tags",
1605 Value::new(
1606 Cardinality::Many,
1607 Item::new(
1608 ItemTarget::Primitive(Primitive::Text),
1609 None,
1610 None,
1611 Some(32),
1612 None,
1613 &[],
1614 &[],
1615 false,
1616 ),
1617 ),
1618 None,
1619 None,
1620 None,
1621 ),
1622 Field::new(
1623 "status",
1624 Value::new(
1625 Cardinality::One,
1626 Item::new(
1627 ItemTarget::Is("test::Status"),
1628 None,
1629 None,
1630 None,
1631 None,
1632 &[],
1633 &[],
1634 false,
1635 ),
1636 ),
1637 Some(crate::node::Arg::ConstPath("test::Status::Active")),
1638 None,
1639 None,
1640 ),
1641 Field::new(
1642 "degrees",
1643 Value::new(
1644 Cardinality::One,
1645 Item::new(
1646 ItemTarget::Is("test::Degrees"),
1647 None,
1648 None,
1649 None,
1650 None,
1651 &[],
1652 &[],
1653 false,
1654 ),
1655 ),
1656 None,
1657 None,
1658 None,
1659 ),
1660 Field::new(
1661 "label",
1662 Value::new(
1663 Cardinality::One,
1664 Item::new(
1665 ItemTarget::Is("test::Label"),
1666 None,
1667 None,
1668 None,
1669 None,
1670 &[],
1671 &[],
1672 false,
1673 ),
1674 ),
1675 None,
1676 None,
1677 None,
1678 ),
1679 ];
1680
1681 #[test]
1682 #[expect(
1683 clippy::too_many_lines,
1684 reason = "one graph fixture proves the complete field, type, relation, and durable-rule closure"
1685 )]
1686 fn sealed_canister_graph_emits_store_free_database_closure() {
1687 let mut schema = Schema::new();
1688 schema.insert_node(SchemaNode::Canister(Canister::new(
1689 Def::new("test", "Canister"),
1690 "test",
1691 0,
1692 10,
1693 9,
1694 8,
1695 )));
1696 schema.insert_node(SchemaNode::Store(Store::new_heap(
1697 Def::new("test", "Store"),
1698 "test::Canister",
1699 StoreHeapConfig::new(),
1700 )));
1701 schema.insert_node(SchemaNode::Enum(Enum::new(
1702 Def::new("test", "Status"),
1703 "Status",
1704 &STATUS_VARIANTS,
1705 EMPTY_TYPE.clone(),
1706 )));
1707 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1708 Def::new("test", "Degrees"),
1709 "Degrees",
1710 Item::new(
1711 ItemTarget::Primitive(Primitive::Nat16),
1712 None,
1713 None,
1714 None,
1715 None,
1716 &[],
1717 &[],
1718 false,
1719 ),
1720 None,
1721 NUMERIC_RULE_TYPE.clone(),
1722 )));
1723 schema.insert_node(SchemaNode::Newtype(Newtype::new(
1724 Def::new("test", "Label"),
1725 "Label",
1726 Item::new(
1727 ItemTarget::Primitive(Primitive::Text),
1728 None,
1729 None,
1730 None,
1731 None,
1732 &[],
1733 &[],
1734 false,
1735 ),
1736 None,
1737 LENGTH_RULE_TYPE.clone(),
1738 )));
1739 schema.insert_node(SchemaNode::Entity(Entity::new(
1740 Def::new("test", "Task"),
1741 "test::Store",
1742 1,
1743 PrimaryKey::new(&["id"], PrimaryKeySource::External),
1744 &[],
1745 &[],
1746 &[],
1747 FieldList::new(&ENTITY_FIELDS),
1748 EMPTY_TYPE.clone(),
1749 )));
1750 schema.seal().expect("fixture graph should seal");
1751
1752 let fragment = schema
1753 .schema_fragment_for_canister("test::Canister")
1754 .expect("sealed database closure should lower");
1755
1756 assert_eq!(fragment.entities().len(), 1);
1757 assert_eq!(fragment.types().len(), 3);
1758 let fields = fragment.entities()[0].fields();
1759 assert!(matches!(
1760 fields
1761 .iter()
1762 .find(|field| field.name().as_str() == "tags")
1763 .map(icydb_schema::FieldFragment::field_type),
1764 Some(FieldType::List(item))
1765 if matches!(item.as_ref(), FieldType::Scalar(ScalarType::Text { max_len: Some(32) }))
1766 ));
1767 assert!(matches!(
1768 fields
1769 .iter()
1770 .find(|field| field.name().as_str() == "status")
1771 .map(icydb_schema::FieldFragment::insert_policy),
1772 Some(icydb_schema::FieldInsertPolicy::Default(
1773 icydb_schema::ScalarLiteral::EnumUnit { .. }
1774 ))
1775 ));
1776 let NamedTypeFragment::Enum(status) = fragment
1777 .types()
1778 .iter()
1779 .find(|fragment| matches!(fragment, NamedTypeFragment::Enum(_)))
1780 .expect("reachable status type should remain an enum")
1781 else {
1782 panic!("reachable status type should remain an enum")
1783 };
1784 assert!(matches!(
1785 status
1786 .variants()
1787 .iter()
1788 .find(|variant| variant.name().as_str() == "Retries")
1789 .and_then(|variant| variant.payload()),
1790 Some(FieldType::List(item))
1791 if matches!(item.as_ref(), FieldType::Scalar(ScalarType::Nat16))
1792 ));
1793
1794 let constraints = fragment.entities()[0].constraints();
1795 assert_eq!(constraints.len(), 2);
1796 let degrees_source = ConstraintSourceKey::for_targeted_field_rule(
1797 &FieldSourceKey::try_new("degrees").expect("field name"),
1798 &TypeSourceKey::try_new("Degrees").expect("type name"),
1799 &RuleSourceKey::try_new("range").expect("rule name"),
1800 );
1801 let degrees = constraints
1802 .iter()
1803 .find(|constraint| constraint.source_key() == °rees_source)
1804 .expect("numeric rule should become one field-owned constraint");
1805 let ConstraintFragmentKind::TargetedRule(degrees) = degrees.kind() else {
1806 panic!("numeric rule should use the targeted-rule contract")
1807 };
1808 assert_eq!(degrees.root().as_str(), "degrees");
1809 assert_eq!(degrees.target_type().as_str(), "Degrees");
1810 assert!(matches!(
1811 degrees.operation(),
1812 SourceRuleOperation::NumericRangeInclusive { .. }
1813 ));
1814 let label = constraints
1815 .iter()
1816 .find(|constraint| constraint.source_key() != °rees_source)
1817 .expect("length rule should become one field-owned constraint");
1818 let ConstraintFragmentKind::TargetedRule(label) = label.kind() else {
1819 panic!("length rule should use the targeted-rule contract")
1820 };
1821 assert_eq!(label.target_type().as_str(), "Label");
1822 assert!(matches!(
1823 label.operation(),
1824 SourceRuleOperation::LengthRangeInclusive { min: 2, max: 40 }
1825 ));
1826 }
1827}