Skip to main content

corium_forms/
schemaform.rs

1//! Boundary conversion from Datomic-style EDN attribute maps to the engine
2//! schema model, used by `CreateDatabase`.
3
4use corium_core::{Attribute, Cardinality, EntityId, Partition, Schema, Unique, ValueType};
5use corium_db::{Idents, bootstrap};
6use corium_query::edn::Edn;
7use thiserror::Error;
8
9/// Sequence of the first installable attribute entity in the db partition.
10/// Lower sequences are reserved for the engine's own attributes
11/// (`corium_db::bootstrap`).
12pub const FIRST_ATTR_ID: u64 = 100;
13
14/// Schema form conversion failure.
15#[derive(Debug, Error, Eq, PartialEq)]
16pub enum SchemaFormError {
17    /// Attribute definition is not a map.
18    #[error("attribute definition must be a map: {0}")]
19    NotAMap(String),
20    /// `:db/ident` missing or not a keyword.
21    #[error("attribute requires a :db/ident keyword")]
22    MissingIdent,
23    /// `:db/valueType` missing or unknown.
24    #[error("unknown or missing :db/valueType {0}")]
25    BadValueType(String),
26    /// `:db/cardinality` unknown.
27    #[error("unknown :db/cardinality {0}")]
28    BadCardinality(String),
29    /// `:db/unique` unknown.
30    #[error("unknown :db/unique {0}")]
31    BadUnique(String),
32    /// Two attributes share one ident.
33    #[error("duplicate :db/ident {0}")]
34    DuplicateIdent(String),
35}
36
37fn kw(text: &str) -> Edn {
38    Edn::keyword(text)
39}
40
41/// Parses Datomic-style attribute maps into a schema and ident registry.
42///
43/// Attribute entity ids are assigned sequentially in the `Db` partition
44/// starting at [`FIRST_ATTR_ID`]. The engine's own attributes are installed
45/// first, so every database created this way — and every peer that receives
46/// the resulting handshake — knows `:db/txInstant`.
47///
48/// # Errors
49/// Returns [`SchemaFormError`] for malformed attribute definitions.
50pub fn schema_from_edn(forms: &[Edn]) -> Result<(Schema, Idents), SchemaFormError> {
51    let mut schema = Schema::default();
52    let mut idents = Idents::default();
53    bootstrap::install(&mut schema, &mut idents);
54    for (index, form) in forms.iter().enumerate() {
55        if !matches!(form, Edn::Map(_)) {
56            return Err(SchemaFormError::NotAMap(form.to_string()));
57        }
58        let id = EntityId::new(Partition::Db as u32, FIRST_ATTR_ID + index as u64);
59        let ident = form
60            .get(&kw("db/ident"))
61            .and_then(Edn::as_keyword)
62            .ok_or(SchemaFormError::MissingIdent)?
63            .clone();
64        if idents.entid(&ident).is_some() {
65            return Err(SchemaFormError::DuplicateIdent(ident.to_string()));
66        }
67        let value_type_name = form
68            .get(&kw("db/valueType"))
69            .and_then(Edn::as_keyword)
70            .map(|keyword| keyword.name.clone())
71            .ok_or_else(|| SchemaFormError::BadValueType("<missing>".into()))?;
72        let value_type = match value_type_name.as_str() {
73            "string" => ValueType::Str,
74            "long" => ValueType::Long,
75            "double" => ValueType::Double,
76            "boolean" => ValueType::Bool,
77            "instant" => ValueType::Instant,
78            "uuid" => ValueType::Uuid,
79            "keyword" => ValueType::Keyword,
80            "bytes" => ValueType::Bytes,
81            "ref" => ValueType::Ref,
82            other => return Err(SchemaFormError::BadValueType(other.to_owned())),
83        };
84        let cardinality = match form
85            .get(&kw("db/cardinality"))
86            .and_then(Edn::as_keyword)
87            .map(|keyword| keyword.name.clone())
88        {
89            None => Cardinality::One,
90            Some(name) => match name.as_str() {
91                "one" => Cardinality::One,
92                "many" => Cardinality::Many,
93                other => return Err(SchemaFormError::BadCardinality(other.to_owned())),
94            },
95        };
96        let unique = match form
97            .get(&kw("db/unique"))
98            .and_then(Edn::as_keyword)
99            .map(|keyword| keyword.name.clone())
100        {
101            None => None,
102            Some(name) => match name.as_str() {
103                "identity" => Some(Unique::Identity),
104                "value" => Some(Unique::Value),
105                other => return Err(SchemaFormError::BadUnique(other.to_owned())),
106            },
107        };
108        let flag = |name: &str| form.get(&kw(name)) == Some(&Edn::Bool(true));
109        schema.insert(Attribute {
110            id,
111            value_type,
112            cardinality,
113            unique,
114            is_component: flag("db/isComponent"),
115            indexed: flag("db/index") || unique.is_some(),
116            no_history: flag("db/noHistory"),
117        });
118        idents.insert(ident, id);
119    }
120    Ok((schema, idents))
121}