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