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, ScalarLiteral, ScalarType, SchemaContractError, SchemaFragment, SchemaName,
21 Subaccount, Timestamp, TupleElementFragment, TypeSourceKey, Ulid, Unit,
22};
23use thiserror::Error;
24
25use crate::{
26 node::{
27 Arg, ArgNumber, Canister, CheckConstraint, Entity, Enum, Field, FieldWriteManagement,
28 Index, IndexExpression, IndexKeyItem, IndexKeyItemsRef, Item, ItemTarget, List, Map,
29 Record, RelationEdge, Schema, Set, Store, Tuple, Value,
30 },
31 types::{Cardinality, Primitive},
32};
33
34#[derive(Debug, Error)]
37pub enum FragmentLoweringError {
38 #[error("schema canister has no registered stores: {0}")]
40 CanisterHasNoStores(String),
41
42 #[error("schema canister path is not registered: {0}")]
44 CanisterNotFound(String),
45
46 #[error(transparent)]
48 Contract(#[from] SchemaContractError),
49
50 #[error("schema graph must be sealed before fragment lowering")]
52 GraphNotSealed,
53
54 #[error("schema field default cannot be lowered: {0}")]
56 InvalidDefault(String),
57
58 #[error("schema fragment reference is invalid: {0}")]
60 InvalidReference(String),
61
62 #[error("schema value cardinality is unsupported at {0}")]
64 UnsupportedCardinality(String),
65}
66
67impl Schema {
72 pub fn schema_fragment_for_canister(
83 &self,
84 canister_path: &str,
85 ) -> Result<SchemaFragment, FragmentLoweringError> {
86 if !self.is_sealed() {
87 return Err(FragmentLoweringError::GraphNotSealed);
88 }
89 self.cast_node::<Canister>(canister_path)
90 .map_err(|_| FragmentLoweringError::CanisterNotFound(canister_path.to_string()))?;
91 let stores = self
92 .filter_nodes::<Store>(|store| store.canister() == canister_path)
93 .map(|(path, _)| path.to_string())
94 .collect::<BTreeSet<_>>();
95 if stores.is_empty() {
96 return Err(FragmentLoweringError::CanisterHasNoStores(
97 canister_path.to_string(),
98 ));
99 }
100
101 let entities = self
102 .get_nodes::<Entity>()
103 .filter(|(_, entity)| stores.contains(entity.store()))
104 .map(|(_, entity)| entity)
105 .collect::<Vec<_>>();
106 let selected_entities = entities
107 .iter()
108 .map(|entity| entity.def().path())
109 .collect::<BTreeSet<_>>();
110 for entity in &entities {
111 ensure_relation_targets_in_database(self, entity, &selected_entities)?;
112 }
113
114 let mut pending_types = Vec::new();
115 let entity_fragments = entities
116 .iter()
117 .map(|entity| lower_entity(self, entity, &mut pending_types))
118 .collect::<Result<Vec<_>, _>>()?;
119 let types = lower_reachable_types(self, pending_types)?;
120
121 SchemaFragment::try_new(entity_fragments, types).map_err(Into::into)
122 }
123}
124
125fn ensure_relation_targets_in_database(
126 schema: &Schema,
127 entity: &Entity,
128 selected_entities: &BTreeSet<String>,
129) -> Result<(), FragmentLoweringError> {
130 for target in entity
131 .fields()
132 .fields()
133 .iter()
134 .filter_map(|field| field.value().item().relation())
135 .chain(entity.relations().iter().map(RelationEdge::target))
136 {
137 schema
138 .cast_node::<Entity>(target)
139 .map_err(|_| FragmentLoweringError::InvalidReference(target.to_string()))?;
140 if !selected_entities.contains(target) {
141 return Err(FragmentLoweringError::InvalidReference(format!(
142 "relation target '{target}' is outside the selected database"
143 )));
144 }
145 }
146 Ok(())
147}
148
149fn lower_entity(
150 schema: &Schema,
151 entity: &Entity,
152 pending_types: &mut Vec<String>,
153) -> Result<EntityFragment, FragmentLoweringError> {
154 let fields = entity
155 .fields()
156 .fields()
157 .iter()
158 .map(|field| lower_entity_field(schema, field, pending_types))
159 .collect::<Result<Vec<_>, _>>()?;
160 let primary_key = entity
161 .primary_key()
162 .fields()
163 .iter()
164 .map(|name| entity_field_source_key(entity, name))
165 .collect::<Result<Vec<_>, _>>()?;
166 let indexes = entity
167 .indexes()
168 .iter()
169 .map(|index| lower_index(schema, entity, index))
170 .collect::<Result<Vec<_>, _>>()?;
171 let mut relations = entity
172 .fields()
173 .fields()
174 .iter()
175 .filter(|field| field.value().item().relation().is_some())
176 .map(|field| lower_scalar_relation(schema, entity, field))
177 .collect::<Result<Vec<_>, _>>()?;
178 relations.extend(
179 entity
180 .relations()
181 .iter()
182 .map(|relation| lower_composite_relation(schema, entity, relation))
183 .collect::<Result<Vec<_>, _>>()?,
184 );
185 let constraints = entity
186 .constraints()
187 .iter()
188 .map(|constraint| lower_constraint(schema, constraint))
189 .collect::<Result<Vec<_>, _>>()?;
190
191 EntityFragment::try_new(
192 EntitySourceKey::try_new(entity.source_key())?,
193 SchemaName::try_new(entity.resolved_name())?,
194 fields,
195 primary_key,
196 indexes,
197 relations,
198 constraints,
199 )
200 .map_err(Into::into)
201}
202
203fn lower_entity_field(
204 schema: &Schema,
205 field: &Field,
206 pending_types: &mut Vec<String>,
207) -> Result<FieldFragment, FragmentLoweringError> {
208 let field_type = lower_value_type(schema, field.value(), pending_types)?;
209 let nullable = field.value().cardinality() == Cardinality::Opt;
210 let insert_policy = if field.generated().is_some() {
211 FieldInsertPolicy::Generated
212 } else if let Some(default) = field.default() {
213 FieldInsertPolicy::Default(lower_default(schema, field, default)?)
214 } else if nullable {
215 FieldInsertPolicy::Nullable
216 } else {
217 FieldInsertPolicy::Required
218 };
219 let management = match field.write_management() {
220 Some(FieldWriteManagement::CreatedAt) => Some(FieldManagementPolicy::CreatedAt),
221 Some(FieldWriteManagement::UpdatedAt) => Some(FieldManagementPolicy::UpdatedAt),
222 None => None,
223 };
224 Ok(FieldFragment::new(
225 FieldSourceKey::try_new(field.source_key())?,
226 SchemaName::try_new(field.ident())?,
227 field_type,
228 nullable,
229 insert_policy,
230 management,
231 ))
232}
233
234fn lower_index(
235 schema: &Schema,
236 entity: &Entity,
237 index: &Index,
238) -> Result<IndexFragment, FragmentLoweringError> {
239 let key = match index.key_items() {
240 IndexKeyItemsRef::Fields(fields) => fields
241 .iter()
242 .map(|field| entity_field_source_key(entity, field).map(IndexKeyFragment::Field))
243 .collect::<Result<Vec<_>, _>>()?,
244 IndexKeyItemsRef::Items(items) => items
245 .iter()
246 .map(|item| lower_index_key(entity, item))
247 .collect::<Result<Vec<_>, _>>()?,
248 };
249 IndexFragment::try_new(
250 IndexSourceKey::try_new(index.source_key())?,
251 SchemaName::try_new(index.name())?,
252 key,
253 index.is_unique(),
254 index.source_predicate(schema)?,
255 )
256 .map_err(Into::into)
257}
258
259fn lower_index_key(
260 entity: &Entity,
261 item: &IndexKeyItem,
262) -> Result<IndexKeyFragment, FragmentLoweringError> {
263 let field = entity_field_source_key(entity, item.field())?;
264 Ok(match item {
265 IndexKeyItem::Field(_) => IndexKeyFragment::Field(field),
266 IndexKeyItem::Expression(IndexExpression::Lower(_)) => IndexKeyFragment::Lower(field),
267 IndexKeyItem::Expression(IndexExpression::Upper(_)) => IndexKeyFragment::Upper(field),
268 IndexKeyItem::Expression(IndexExpression::Trim(_)) => IndexKeyFragment::Trim(field),
269 IndexKeyItem::Expression(IndexExpression::LowerTrim(_)) => {
270 IndexKeyFragment::LowerTrim(field)
271 }
272 IndexKeyItem::Expression(IndexExpression::Date(_)) => IndexKeyFragment::Date(field),
273 IndexKeyItem::Expression(IndexExpression::Year(_)) => IndexKeyFragment::Year(field),
274 IndexKeyItem::Expression(IndexExpression::Month(_)) => IndexKeyFragment::Month(field),
275 IndexKeyItem::Expression(IndexExpression::Day(_)) => IndexKeyFragment::Day(field),
276 })
277}
278
279fn lower_scalar_relation(
280 schema: &Schema,
281 entity: &Entity,
282 field: &Field,
283) -> Result<RelationFragment, FragmentLoweringError> {
284 let target_path = field
285 .value()
286 .item()
287 .relation()
288 .ok_or_else(|| FragmentLoweringError::InvalidReference(field.ident().to_string()))?;
289 let target = schema
290 .cast_node::<Entity>(target_path)
291 .map_err(|_| FragmentLoweringError::InvalidReference(target_path.to_string()))?;
292 RelationFragment::try_new(
293 RelationSourceKey::try_new(field.source_key())?,
294 SchemaName::try_new(field.ident())?,
295 vec![entity_field_source_key(entity, field.ident())?],
296 EntitySourceKey::try_new(target.source_key())?,
297 target
298 .primary_key()
299 .fields()
300 .iter()
301 .map(|field| entity_field_source_key(target, field))
302 .collect::<Result<Vec<_>, _>>()?,
303 RelationDeleteAction::Restrict,
304 )
305 .map_err(Into::into)
306}
307
308fn lower_composite_relation(
309 schema: &Schema,
310 entity: &Entity,
311 relation: &RelationEdge,
312) -> Result<RelationFragment, FragmentLoweringError> {
313 let target = schema
314 .cast_node::<Entity>(relation.target())
315 .map_err(|_| FragmentLoweringError::InvalidReference(relation.target().to_string()))?;
316 RelationFragment::try_new(
317 RelationSourceKey::try_new(relation.source_key())?,
318 SchemaName::try_new(relation.ident())?,
319 relation
320 .local_fields()
321 .iter()
322 .map(|field| entity_field_source_key(entity, field))
323 .collect::<Result<Vec<_>, _>>()?,
324 EntitySourceKey::try_new(target.source_key())?,
325 target
326 .primary_key()
327 .fields()
328 .iter()
329 .map(|field| entity_field_source_key(target, field))
330 .collect::<Result<Vec<_>, _>>()?,
331 RelationDeleteAction::Restrict,
332 )
333 .map_err(Into::into)
334}
335
336fn lower_constraint(
337 schema: &Schema,
338 constraint: &CheckConstraint,
339) -> Result<ConstraintFragment, FragmentLoweringError> {
340 Ok(ConstraintFragment::new(
341 ConstraintSourceKey::try_new(constraint.source_key())?,
342 SchemaName::try_new(constraint.name())?,
343 constraint.source_expression(schema)?,
344 ))
345}
346
347fn entity_field_source_key(
348 entity: &Entity,
349 field_name: &str,
350) -> Result<FieldSourceKey, FragmentLoweringError> {
351 let field = entity
352 .fields()
353 .get(field_name)
354 .ok_or_else(|| FragmentLoweringError::InvalidReference(field_name.to_string()))?;
355 FieldSourceKey::try_new(field.source_key()).map_err(Into::into)
356}
357
358fn lower_reachable_types(
363 schema: &Schema,
364 mut pending: Vec<String>,
365) -> Result<Vec<NamedTypeFragment>, FragmentLoweringError> {
366 let mut lowered = BTreeMap::new();
367 while let Some(path) = pending.pop() {
368 let node = schema
369 .get_node(path.as_str())
370 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.clone()))?;
371 let source_key = named_type_source_key(node)
372 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.clone()))?;
373 if lowered.contains_key(source_key) {
374 continue;
375 }
376 let fragment = lower_named_type(schema, node, &mut pending)?;
377 lowered.insert(source_key.to_string(), fragment);
378 }
379 Ok(lowered.into_values().collect())
380}
381
382const fn named_type_source_key(node: &crate::node::SchemaNode) -> Option<&str> {
383 match node {
384 crate::node::SchemaNode::Enum(node) => Some(node.source_key()),
385 crate::node::SchemaNode::List(node) => Some(node.source_key()),
386 crate::node::SchemaNode::Map(node) => Some(node.source_key()),
387 crate::node::SchemaNode::Newtype(node) => Some(node.source_key()),
388 crate::node::SchemaNode::Record(node) => Some(node.source_key()),
389 crate::node::SchemaNode::Set(node) => Some(node.source_key()),
390 crate::node::SchemaNode::Tuple(node) => Some(node.source_key()),
391 crate::node::SchemaNode::Canister(_)
392 | crate::node::SchemaNode::Entity(_)
393 | crate::node::SchemaNode::Normalizer(_)
394 | crate::node::SchemaNode::Store(_)
395 | crate::node::SchemaNode::Validator(_) => None,
396 }
397}
398
399fn lower_named_type(
400 schema: &Schema,
401 node: &crate::node::SchemaNode,
402 pending: &mut Vec<String>,
403) -> Result<NamedTypeFragment, FragmentLoweringError> {
404 match node {
405 crate::node::SchemaNode::Record(record) => lower_record(schema, record, pending),
406 crate::node::SchemaNode::Enum(r#enum) => lower_enum(schema, r#enum, pending),
407 crate::node::SchemaNode::Newtype(newtype) => Ok(NamedTypeFragment::Newtype {
408 source_key: TypeSourceKey::try_new(newtype.source_key())?,
409 name: SchemaName::try_new(newtype.def().ident())?,
410 inner: lower_item_type(schema, newtype.item(), pending)?,
411 }),
412 crate::node::SchemaNode::List(list) => lower_list(schema, list, pending),
413 crate::node::SchemaNode::Set(set) => lower_set(schema, set, pending),
414 crate::node::SchemaNode::Map(map) => lower_map(schema, map, pending),
415 crate::node::SchemaNode::Tuple(tuple) => lower_tuple(schema, tuple, pending),
416 crate::node::SchemaNode::Canister(_)
417 | crate::node::SchemaNode::Entity(_)
418 | crate::node::SchemaNode::Normalizer(_)
419 | crate::node::SchemaNode::Store(_)
420 | crate::node::SchemaNode::Validator(_) => Err(FragmentLoweringError::InvalidReference(
421 "non-type graph node".to_string(),
422 )),
423 }
424}
425
426fn lower_record(
427 schema: &Schema,
428 record: &Record,
429 pending: &mut Vec<String>,
430) -> Result<NamedTypeFragment, FragmentLoweringError> {
431 let fields = record
432 .fields()
433 .fields()
434 .iter()
435 .map(|field| {
436 Ok(RecordFieldFragment::new(
437 FieldSourceKey::try_new(field.source_key())?,
438 SchemaName::try_new(field.ident())?,
439 lower_value_type(schema, field.value(), pending)?,
440 field.value().cardinality() == Cardinality::Opt,
441 ))
442 })
443 .collect::<Result<Vec<_>, FragmentLoweringError>>()?;
444 Ok(NamedTypeFragment::Record(RecordTypeFragment::try_new(
445 TypeSourceKey::try_new(record.source_key())?,
446 SchemaName::try_new(record.def().ident())?,
447 fields,
448 )?))
449}
450
451fn lower_enum(
452 schema: &Schema,
453 r#enum: &Enum,
454 pending: &mut Vec<String>,
455) -> Result<NamedTypeFragment, FragmentLoweringError> {
456 let variants = r#enum
457 .variants()
458 .iter()
459 .map(|variant| {
460 let source = TypeSourceKey::try_new(variant.source_key())?;
461 let name = SchemaName::try_new(variant.ident())?;
462 match variant.value() {
463 Some(value) if value.cardinality() == Cardinality::Opt => {
464 Err(FragmentLoweringError::UnsupportedCardinality(format!(
465 "{}::{}",
466 r#enum.def().path(),
467 variant.ident()
468 )))
469 }
470 Some(value) => Ok(EnumVariantFragment::with_payload(
471 source,
472 name,
473 lower_value_type(schema, value, pending)?,
474 )),
475 None => Ok(EnumVariantFragment::new(source, name)),
476 }
477 })
478 .collect::<Result<Vec<_>, _>>()?;
479 Ok(NamedTypeFragment::Enum(EnumTypeFragment::try_new(
480 TypeSourceKey::try_new(r#enum.source_key())?,
481 SchemaName::try_new(r#enum.def().ident())?,
482 variants,
483 )?))
484}
485
486fn lower_list(
487 schema: &Schema,
488 list: &List,
489 pending: &mut Vec<String>,
490) -> Result<NamedTypeFragment, FragmentLoweringError> {
491 Ok(NamedTypeFragment::List {
492 source_key: TypeSourceKey::try_new(list.source_key())?,
493 name: SchemaName::try_new(list.def().ident())?,
494 item: lower_item_type(schema, list.item(), pending)?,
495 })
496}
497
498fn lower_set(
499 schema: &Schema,
500 set: &Set,
501 pending: &mut Vec<String>,
502) -> Result<NamedTypeFragment, FragmentLoweringError> {
503 Ok(NamedTypeFragment::Set {
504 source_key: TypeSourceKey::try_new(set.source_key())?,
505 name: SchemaName::try_new(set.def().ident())?,
506 item: lower_item_type(schema, set.item(), pending)?,
507 })
508}
509
510fn lower_map(
511 schema: &Schema,
512 map: &Map,
513 pending: &mut Vec<String>,
514) -> Result<NamedTypeFragment, FragmentLoweringError> {
515 if map.value().cardinality() == Cardinality::Opt {
516 return Err(FragmentLoweringError::UnsupportedCardinality(
517 map.def().path(),
518 ));
519 }
520 Ok(NamedTypeFragment::Map {
521 source_key: TypeSourceKey::try_new(map.source_key())?,
522 name: SchemaName::try_new(map.def().ident())?,
523 key: lower_item_type(schema, map.key(), pending)?,
524 value: lower_value_type(schema, map.value(), pending)?,
525 })
526}
527
528fn lower_tuple(
529 schema: &Schema,
530 tuple: &Tuple,
531 pending: &mut Vec<String>,
532) -> Result<NamedTypeFragment, FragmentLoweringError> {
533 let members = tuple
534 .values()
535 .iter()
536 .map(|value| {
537 Ok::<_, FragmentLoweringError>(TupleElementFragment::new(
538 lower_value_type(schema, value, pending)?,
539 value.cardinality() == Cardinality::Opt,
540 ))
541 })
542 .collect::<Result<Vec<_>, _>>()?;
543 Ok(NamedTypeFragment::Tuple {
544 source_key: TypeSourceKey::try_new(tuple.source_key())?,
545 name: SchemaName::try_new(tuple.def().ident())?,
546 members,
547 })
548}
549
550fn lower_value_type(
555 schema: &Schema,
556 value: &Value,
557 pending: &mut Vec<String>,
558) -> Result<FieldType, FragmentLoweringError> {
559 let item = lower_item_type(schema, value.item(), pending)?;
560 Ok(if value.cardinality() == Cardinality::Many {
561 FieldType::List(Box::new(item))
562 } else {
563 item
564 })
565}
566
567fn lower_item_type(
568 schema: &Schema,
569 item: &Item,
570 pending: &mut Vec<String>,
571) -> Result<FieldType, FragmentLoweringError> {
572 match item.target() {
573 ItemTarget::Is(path) => {
574 pending.push((*path).to_string());
575 Ok(FieldType::Named(TypeSourceKey::try_new(
576 type_source_key_for_path(schema, path)?,
577 )?))
578 }
579 ItemTarget::Primitive(primitive) => {
580 Ok(FieldType::Scalar(lower_scalar_type(*primitive, item)))
581 }
582 }
583}
584
585fn type_source_key_for_path<'schema>(
586 schema: &'schema Schema,
587 path: &str,
588) -> Result<&'schema str, FragmentLoweringError> {
589 let source = schema
590 .get_node(path)
591 .and_then(named_type_source_key)
592 .ok_or_else(|| FragmentLoweringError::InvalidReference(path.to_string()))?;
593 Ok(source)
594}
595
596fn lower_scalar_type(primitive: Primitive, item: &Item) -> ScalarType {
597 match primitive {
598 Primitive::Account => ScalarType::Account,
599 Primitive::Blob => ScalarType::Blob {
600 max_len: item.max_len(),
601 },
602 Primitive::Bool => ScalarType::Bool,
603 Primitive::Date => ScalarType::Date,
604 Primitive::Decimal => ScalarType::Decimal {
605 scale: item.scale().unwrap_or(0),
606 },
607 Primitive::Duration => ScalarType::Duration,
608 Primitive::Float32 => ScalarType::Float32,
609 Primitive::Float64 => ScalarType::Float64,
610 Primitive::Int8 => ScalarType::Int8,
611 Primitive::Int16 => ScalarType::Int16,
612 Primitive::Int32 => ScalarType::Int32,
613 Primitive::Int64 => ScalarType::Int64,
614 Primitive::Int128 => ScalarType::Int128,
615 Primitive::IntBig => ScalarType::IntBig {
616 max_bytes: item.max_bytes().unwrap_or(DEFAULT_BIG_INT_MAX_BYTES),
617 },
618 Primitive::Nat8 => ScalarType::Nat8,
619 Primitive::Nat16 => ScalarType::Nat16,
620 Primitive::Nat32 => ScalarType::Nat32,
621 Primitive::Nat64 => ScalarType::Nat64,
622 Primitive::Nat128 => ScalarType::Nat128,
623 Primitive::NatBig => ScalarType::NatBig {
624 max_bytes: item.max_bytes().unwrap_or(DEFAULT_BIG_INT_MAX_BYTES),
625 },
626 Primitive::Principal => ScalarType::Principal,
627 Primitive::Subaccount => ScalarType::Subaccount,
628 Primitive::Text => ScalarType::Text {
629 max_len: item.max_len(),
630 },
631 Primitive::Timestamp => ScalarType::Timestamp,
632 Primitive::Ulid => ScalarType::Ulid,
633 Primitive::Unit => ScalarType::Unit,
634 }
635}
636
637fn lower_default(
642 schema: &Schema,
643 field: &Field,
644 default: &Arg,
645) -> Result<ScalarLiteral, FragmentLoweringError> {
646 if let ItemTarget::Is(path) = field.value().item().target() {
647 let Arg::ConstPath(default_path) = default else {
648 return Err(FragmentLoweringError::InvalidDefault(
649 field.ident().to_string(),
650 ));
651 };
652 let variant = default_path.rsplit("::").next().unwrap_or(default_path);
653 return schema
654 .enum_unit_literal(path, variant)
655 .map_err(FragmentLoweringError::from);
656 }
657 let ItemTarget::Primitive(primitive) = field.value().item().target() else {
658 return Err(FragmentLoweringError::InvalidDefault(
659 field.ident().to_string(),
660 ));
661 };
662 lower_scalar_default(*primitive, field.value().item(), default)
663 .ok_or_else(|| FragmentLoweringError::InvalidDefault(field.ident().to_string()))
664}
665
666fn lower_scalar_default(primitive: Primitive, item: &Item, default: &Arg) -> Option<ScalarLiteral> {
667 if default_constructor_is_zero(default) {
668 return zero_scalar_literal(primitive, item);
669 }
670 match (primitive, default) {
671 (Primitive::Account, Arg::String(value)) => {
672 Account::from_str(value).ok().map(ScalarLiteral::Account)
673 }
674 (Primitive::Blob, Arg::String(value)) => Blob::try_new(value.as_bytes().to_vec())
675 .ok()
676 .map(ScalarLiteral::Blob),
677 (Primitive::Bool, Arg::Bool(value)) => Some(ScalarLiteral::Bool(*value)),
678 (Primitive::Date, Arg::String(value)) => Date::parse(value).map(ScalarLiteral::Date),
679 (Primitive::Date, Arg::Number(value)) => arg_i128(value)
680 .and_then(|value| i32::try_from(value).ok())
681 .map(Date::from_days_since_epoch)
682 .map(ScalarLiteral::Date),
683 (Primitive::Decimal, Arg::String(value)) => Decimal::from_str(value)
684 .ok()
685 .and_then(|value| decimal_at_scale(value, item.scale().unwrap_or(0)))
686 .map(ScalarLiteral::Decimal),
687 (Primitive::Decimal, Arg::Number(value)) => arg_decimal(value)
688 .and_then(|value| decimal_at_scale(value, item.scale().unwrap_or(0)))
689 .map(ScalarLiteral::Decimal),
690 (Primitive::Duration, Arg::String(value)) => Duration::parse_flexible(value)
691 .ok()
692 .map(ScalarLiteral::Duration),
693 (Primitive::Duration, Arg::Number(value)) => arg_u128(value)
694 .and_then(|value| u64::try_from(value).ok())
695 .map(Duration::from_millis)
696 .map(ScalarLiteral::Duration),
697 (Primitive::Float32, Arg::Number(ArgNumber::Float32(value))) => {
698 Float32::try_new(*value).map(ScalarLiteral::Float32)
699 }
700 (Primitive::Float64, Arg::Number(ArgNumber::Float64(value))) => {
701 Float64::try_new(*value).map(ScalarLiteral::Float64)
702 }
703 (
704 Primitive::Int8
705 | Primitive::Int16
706 | Primitive::Int32
707 | Primitive::Int64
708 | Primitive::Int128,
709 Arg::Number(value),
710 ) => arg_i128(value).map(ScalarLiteral::Int),
711 (Primitive::IntBig, Arg::Number(value)) => arg_i128(value)
712 .map(|value| value.to_string())
713 .and_then(|value| IntBig::from_str(value.as_str()).ok())
714 .map(ScalarLiteral::IntBig),
715 (Primitive::IntBig, Arg::String(value)) => {
716 IntBig::from_str(value).ok().map(ScalarLiteral::IntBig)
717 }
718 (
719 Primitive::Nat8
720 | Primitive::Nat16
721 | Primitive::Nat32
722 | Primitive::Nat64
723 | Primitive::Nat128,
724 Arg::Number(value),
725 ) => arg_u128(value).map(ScalarLiteral::Nat),
726 (Primitive::NatBig, Arg::Number(value)) => arg_u128(value)
727 .map(|value| value.to_string())
728 .and_then(|value| NatBig::from_str(value.as_str()).ok())
729 .map(ScalarLiteral::NatBig),
730 (Primitive::NatBig, Arg::String(value)) => {
731 NatBig::from_str(value).ok().map(ScalarLiteral::NatBig)
732 }
733 (Primitive::Principal, Arg::String(value)) => Principal::from_str(value)
734 .ok()
735 .map(ScalarLiteral::Principal),
736 (Primitive::Subaccount, Arg::String(value)) => parse_subaccount(value)
737 .map(Subaccount::from_array)
738 .map(ScalarLiteral::Subaccount),
739 (Primitive::Text, Arg::String(value)) => Some(ScalarLiteral::Text((*value).to_string())),
740 (Primitive::Timestamp, Arg::String(value)) => Timestamp::parse_flexible(value)
741 .ok()
742 .map(ScalarLiteral::Timestamp),
743 (Primitive::Timestamp, Arg::Number(value)) => arg_i128(value)
744 .and_then(|value| i64::try_from(value).ok())
745 .map(Timestamp::from_millis)
746 .map(ScalarLiteral::Timestamp),
747 (Primitive::Ulid, Arg::String(value)) => {
748 Ulid::from_str(value).ok().map(ScalarLiteral::Ulid)
749 }
750 (Primitive::Unit, Arg::ConstPath(path)) if path.ends_with("Unit") => {
751 Some(ScalarLiteral::Unit(Unit))
752 }
753 _ => None,
754 }
755}
756
757fn default_constructor_is_zero(default: &Arg) -> bool {
758 let Arg::FuncPath(path) = default else {
759 return false;
760 };
761 path.ends_with("::default")
762 || path.ends_with("::new")
763 || path.ends_with("::EPOCH")
764 || path.ends_with("::nil")
765}
766
767fn zero_scalar_literal(primitive: Primitive, item: &Item) -> Option<ScalarLiteral> {
768 match primitive {
769 Primitive::Blob => Blob::try_new(Vec::new()).ok().map(ScalarLiteral::Blob),
770 Primitive::Bool => Some(ScalarLiteral::Bool(false)),
771 Primitive::Date => Some(ScalarLiteral::Date(Date::EPOCH)),
772 Primitive::Decimal => Decimal::try_from_i128_with_scale(0, item.scale().unwrap_or(0))
773 .map(ScalarLiteral::Decimal),
774 Primitive::Duration => Some(ScalarLiteral::Duration(Duration::ZERO)),
775 Primitive::Float32 => Float32::try_new(0.0).map(ScalarLiteral::Float32),
776 Primitive::Float64 => Float64::try_new(0.0).map(ScalarLiteral::Float64),
777 Primitive::Int8
778 | Primitive::Int16
779 | Primitive::Int32
780 | Primitive::Int64
781 | Primitive::Int128 => Some(ScalarLiteral::Int(0)),
782 Primitive::IntBig => IntBig::from_str("0").ok().map(ScalarLiteral::IntBig),
783 Primitive::Nat8
784 | Primitive::Nat16
785 | Primitive::Nat32
786 | Primitive::Nat64
787 | Primitive::Nat128 => Some(ScalarLiteral::Nat(0)),
788 Primitive::NatBig => NatBig::from_str("0").ok().map(ScalarLiteral::NatBig),
789 Primitive::Text => Some(ScalarLiteral::Text(String::new())),
790 Primitive::Timestamp => Some(ScalarLiteral::Timestamp(Timestamp::EPOCH)),
791 Primitive::Ulid => Some(ScalarLiteral::Ulid(Ulid::nil())),
792 Primitive::Unit => Some(ScalarLiteral::Unit(Unit)),
793 Primitive::Account | Primitive::Principal | Primitive::Subaccount => None,
794 }
795}
796
797fn decimal_at_scale(value: Decimal, scale: u32) -> Option<Decimal> {
802 match value.scale().cmp(&scale) {
803 std::cmp::Ordering::Equal => Some(value),
804 std::cmp::Ordering::Less => value
805 .scale_to_integer(scale)
806 .and_then(|mantissa| Decimal::try_from_i128_with_scale(mantissa, scale)),
807 std::cmp::Ordering::Greater => Some(value.round_dp(scale)),
808 }
809}
810
811fn arg_i128(value: &ArgNumber) -> Option<i128> {
812 match value {
813 ArgNumber::Int8(value) => Some(i128::from(*value)),
814 ArgNumber::Int16(value) => Some(i128::from(*value)),
815 ArgNumber::Int32(value) => Some(i128::from(*value)),
816 ArgNumber::Int64(value) => Some(i128::from(*value)),
817 ArgNumber::Int128(value) => Some(*value),
818 ArgNumber::Nat8(value) => Some(i128::from(*value)),
819 ArgNumber::Nat16(value) => Some(i128::from(*value)),
820 ArgNumber::Nat32(value) => Some(i128::from(*value)),
821 ArgNumber::Nat64(value) => Some(i128::from(*value)),
822 ArgNumber::Nat128(value) => i128::try_from(*value).ok(),
823 ArgNumber::Float32(_) | ArgNumber::Float64(_) => None,
824 }
825}
826
827fn arg_u128(value: &ArgNumber) -> Option<u128> {
828 match value {
829 ArgNumber::Int8(value) => u128::try_from(*value).ok(),
830 ArgNumber::Int16(value) => u128::try_from(*value).ok(),
831 ArgNumber::Int32(value) => u128::try_from(*value).ok(),
832 ArgNumber::Int64(value) => u128::try_from(*value).ok(),
833 ArgNumber::Int128(value) => u128::try_from(*value).ok(),
834 ArgNumber::Nat8(value) => Some(u128::from(*value)),
835 ArgNumber::Nat16(value) => Some(u128::from(*value)),
836 ArgNumber::Nat32(value) => Some(u128::from(*value)),
837 ArgNumber::Nat64(value) => Some(u128::from(*value)),
838 ArgNumber::Nat128(value) => Some(*value),
839 ArgNumber::Float32(_) | ArgNumber::Float64(_) => None,
840 }
841}
842
843fn arg_decimal(value: &ArgNumber) -> Option<Decimal> {
844 match value {
845 ArgNumber::Float32(value) => Decimal::from_f32_lossy(*value),
846 ArgNumber::Float64(value) => Decimal::from_f64_lossy(*value),
847 _ => arg_i128(value).and_then(Decimal::from_i128),
848 }
849}
850
851fn parse_subaccount(value: &str) -> Option<[u8; 32]> {
852 if value.len() != 64 {
853 return None;
854 }
855 let mut bytes = [0; 32];
856 for (index, chunk) in value.as_bytes().chunks_exact(2).enumerate() {
857 let text = std::str::from_utf8(chunk).ok()?;
858 bytes[index] = u8::from_str_radix(text, 16).ok()?;
859 }
860 Some(bytes)
861}
862
863#[cfg(test)]
864mod tests {
865 use icydb_schema::{FieldType, NamedTypeFragment, ScalarType};
866
867 use super::Schema;
868 use crate::{
869 node::{
870 Canister, Def, Entity, Enum, EnumVariant, Field, FieldList, Item, ItemTarget,
871 PrimaryKey, PrimaryKeySource, SchemaNode, Store, StoreHeapConfig, Type, Value,
872 },
873 types::{Cardinality, Primitive},
874 };
875
876 static EMPTY_TYPE: Type = Type::new(&[], &[]);
877 static STATUS_VARIANTS: [EnumVariant; 2] = [
878 EnumVariant::new("variant/status/active", "Active", None),
879 EnumVariant::new(
880 "variant/status/retries",
881 "Retries",
882 Some(Value::new(
883 Cardinality::Many,
884 Item::new(
885 ItemTarget::Primitive(Primitive::Nat16),
886 None,
887 None,
888 None,
889 None,
890 &[],
891 &[],
892 false,
893 ),
894 )),
895 ),
896 ];
897 static ENTITY_FIELDS: [Field; 3] = [
898 Field::new(
899 "field/task/id",
900 "id",
901 Value::new(
902 Cardinality::One,
903 Item::new(
904 ItemTarget::Primitive(Primitive::Nat64),
905 None,
906 None,
907 None,
908 None,
909 &[],
910 &[],
911 false,
912 ),
913 ),
914 None,
915 None,
916 None,
917 ),
918 Field::new(
919 "field/task/tags",
920 "tags",
921 Value::new(
922 Cardinality::Many,
923 Item::new(
924 ItemTarget::Primitive(Primitive::Text),
925 None,
926 None,
927 Some(32),
928 None,
929 &[],
930 &[],
931 false,
932 ),
933 ),
934 None,
935 None,
936 None,
937 ),
938 Field::new(
939 "field/task/status",
940 "status",
941 Value::new(
942 Cardinality::One,
943 Item::new(
944 ItemTarget::Is("test::Status"),
945 None,
946 None,
947 None,
948 None,
949 &[],
950 &[],
951 false,
952 ),
953 ),
954 Some(crate::node::Arg::ConstPath("test::Status::Active")),
955 None,
956 None,
957 ),
958 ];
959
960 #[test]
961 fn sealed_canister_graph_emits_store_free_database_closure() {
962 let mut schema = Schema::new();
963 schema.insert_node(SchemaNode::Canister(Canister::new(
964 Def::new("test", "Canister"),
965 "test",
966 0,
967 10,
968 9,
969 8,
970 )));
971 schema.insert_node(SchemaNode::Store(Store::new_heap(
972 Def::new("test", "Store"),
973 "Store",
974 "store",
975 "test::Canister",
976 StoreHeapConfig::new(),
977 )));
978 schema.insert_node(SchemaNode::Enum(Enum::new(
979 Def::new("test", "Status"),
980 "type/status",
981 &STATUS_VARIANTS,
982 EMPTY_TYPE.clone(),
983 )));
984 schema.insert_node(SchemaNode::Entity(Entity::new(
985 Def::new("test", "Task"),
986 "entity/task",
987 "test::Store",
988 1,
989 PrimaryKey::new(&["id"], PrimaryKeySource::External),
990 None,
991 &[],
992 &[],
993 &[],
994 FieldList::new(&ENTITY_FIELDS),
995 EMPTY_TYPE.clone(),
996 )));
997 schema.seal().expect("fixture graph should seal");
998
999 let fragment = schema
1000 .schema_fragment_for_canister("test::Canister")
1001 .expect("sealed database closure should lower");
1002
1003 assert_eq!(fragment.entities().len(), 1);
1004 assert_eq!(fragment.types().len(), 1);
1005 let fields = fragment.entities()[0].fields();
1006 assert!(matches!(
1007 fields
1008 .iter()
1009 .find(|field| field.name().as_str() == "tags")
1010 .map(icydb_schema::FieldFragment::field_type),
1011 Some(FieldType::List(item))
1012 if matches!(item.as_ref(), FieldType::Scalar(ScalarType::Text { max_len: Some(32) }))
1013 ));
1014 assert!(matches!(
1015 fields
1016 .iter()
1017 .find(|field| field.name().as_str() == "status")
1018 .map(icydb_schema::FieldFragment::insert_policy),
1019 Some(icydb_schema::FieldInsertPolicy::Default(
1020 icydb_schema::ScalarLiteral::EnumUnit { .. }
1021 ))
1022 ));
1023 let NamedTypeFragment::Enum(status) = &fragment.types()[0] else {
1024 panic!("reachable status type should remain an enum")
1025 };
1026 assert!(matches!(
1027 status
1028 .variants()
1029 .iter()
1030 .find(|variant| variant.name().as_str() == "Retries")
1031 .and_then(|variant| variant.payload()),
1032 Some(FieldType::List(item))
1033 if matches!(item.as_ref(), FieldType::Scalar(ScalarType::Nat16))
1034 ));
1035 }
1036}