Skip to main content

icydb_model/
fragment.rs

1//! Module: fragment
2//!
3//! Responsibility: lower one sealed host graph into a store-free database closure.
4//!
5//! Does not own: proposal routing, accepted identity, deployment configuration, or persistence.
6//!
7//! Boundary: converts compiler-authored logical facts into bounded public schema fragments.
8
9use 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/// Failure while projecting one validated host graph into public fragments.
35
36#[derive(Debug, Error)]
37pub enum FragmentLoweringError {
38    /// A selected canister has no registered stores.
39    #[error("schema canister has no registered stores: {0}")]
40    CanisterHasNoStores(String),
41
42    /// The selected canister path is not registered.
43    #[error("schema canister path is not registered: {0}")]
44    CanisterNotFound(String),
45
46    /// The public bounded proposal contract rejected the projection.
47    #[error(transparent)]
48    Contract(#[from] SchemaContractError),
49
50    /// Fragment projection requires the immutable post-validation graph.
51    #[error("schema graph must be sealed before fragment lowering")]
52    GraphNotSealed,
53
54    /// A declared default cannot be represented by the public proposal atom.
55    #[error("schema field default cannot be lowered: {0}")]
56    InvalidDefault(String),
57
58    /// One graph reference no longer resolves to the expected node kind.
59    #[error("schema fragment reference is invalid: {0}")]
60    InvalidReference(String),
61
62    /// One authored value cardinality has no accepted proposal representation.
63    #[error("schema value cardinality is unsupported at {0}")]
64    UnsupportedCardinality(String),
65}
66
67// -----------------------------------------------------------------------------
68// Database closure
69// -----------------------------------------------------------------------------
70
71impl Schema {
72    /// Lower every persisted entity belonging to one canister, plus its exact
73    /// reachable named-type and relation closure, into one store-free fragment.
74    ///
75    /// Store assignment remains a later proposal-composition concern.
76    ///
77    /// # Errors
78    ///
79    /// Returns a typed error when the graph is not sealed, the selected
80    /// canister/store closure is incomplete, or one authored fact cannot be
81    /// represented by the bounded public proposal contract.
82    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
358// -----------------------------------------------------------------------------
359// Reachable named-type closure
360// -----------------------------------------------------------------------------
361
362fn 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
550// -----------------------------------------------------------------------------
551// Exact field contracts
552// -----------------------------------------------------------------------------
553
554fn 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
637// -----------------------------------------------------------------------------
638// Authored database defaults
639// -----------------------------------------------------------------------------
640
641fn 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
797// -----------------------------------------------------------------------------
798// Literal conversion helpers
799// -----------------------------------------------------------------------------
800
801fn 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}