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