Skip to main content

corium_protocol/
txforms.rs

1//! Boundary conversion from wire EDN transaction forms to engine `TxItem`s.
2//!
3//! Accepts the Datomic-dialect forms used by the conformance corpus: map
4//! forms with `:db/id`, and list forms `[:db/add e a v]`, `[:db/retract e a
5//! v]`, `[:db/cas e a old new]`, `[:db/retractEntity e]`. Entity positions
6//! accept tempid strings, raw entity-id longs, `#eid` tags, idents, and
7//! lookup refs. Value positions for `ref` attributes accept the same except
8//! tempid strings (same-transaction value tempids are not supported by the
9//! transaction layer; clients resolve them against prior tempid maps).
10
11use corium_core::{EntityId, KeywordInterner, TotalF64, Value, ValueType};
12use corium_db::Db;
13use corium_query::edn::Edn;
14use corium_tx::{EntityMap, EntityRef, TxItem, TxOp};
15use thiserror::Error;
16
17/// Transaction form conversion failure.
18#[derive(Debug, Error, Eq, PartialEq)]
19pub enum TxFormError {
20    /// Form is not a map or list form.
21    #[error("bad transaction form: {0}")]
22    BadForm(String),
23    /// Unknown transaction operation keyword.
24    #[error("unknown transaction op {0}")]
25    UnknownOp(String),
26    /// Attribute keyword has no ident.
27    #[error("unknown attribute {0}")]
28    UnknownAttribute(String),
29    /// Entity position not understood.
30    #[error("bad entity position: {0}")]
31    BadEntity(String),
32    /// Value form not convertible for the attribute.
33    #[error("bad value {0}")]
34    BadValue(String),
35}
36
37fn kw(text: &str) -> Edn {
38    Edn::keyword(text)
39}
40
41fn attr_of(db: &Db, form: &Edn) -> Result<EntityId, TxFormError> {
42    let keyword = form
43        .as_keyword()
44        .ok_or_else(|| TxFormError::BadForm(format!("attribute position {form}")))?;
45    db.idents()
46        .entid(keyword)
47        .ok_or_else(|| TxFormError::UnknownAttribute(keyword.to_string()))
48}
49
50fn entity_ref(db: &Db, form: &Edn) -> Result<EntityRef, TxFormError> {
51    match form {
52        Edn::Str(name) => Ok(EntityRef::Temp(name.clone())),
53        Edn::Long(n) => u64::try_from(*n)
54            .map(|raw| EntityRef::Id(EntityId::from_raw(raw)))
55            .map_err(|_| TxFormError::BadEntity(form.to_string())),
56        Edn::Keyword(keyword) => db
57            .idents()
58            .entid(keyword)
59            .map(EntityRef::Id)
60            .ok_or_else(|| TxFormError::UnknownAttribute(keyword.to_string())),
61        Edn::Tagged(tag, value) if tag == "eid" => match value.as_ref() {
62            Edn::Long(n) => u64::try_from(*n)
63                .map(|raw| EntityRef::Id(EntityId::from_raw(raw)))
64                .map_err(|_| TxFormError::BadEntity(form.to_string())),
65            _ => Err(TxFormError::BadEntity(form.to_string())),
66        },
67        Edn::Vector(items) => {
68            let [attr_form, value_form] = items.as_slice() else {
69                return Err(TxFormError::BadEntity(form.to_string()));
70            };
71            let attr = attr_of(db, attr_form)?;
72            let value_type = db
73                .schema()
74                .get(attr)
75                .map(|meta| meta.value_type)
76                .ok_or_else(|| TxFormError::UnknownAttribute(format!("{attr:?}")))?;
77            // Lookup values never intern new keywords: an uninterned keyword
78            // cannot equal any stored value, so resolution would fail anyway.
79            let value = match value_form {
80                Edn::Keyword(keyword) => Value::Keyword(
81                    db.interner()
82                        .get(keyword)
83                        .unwrap_or(corium_query::exec::UNKNOWN_KEYWORD),
84                ),
85                other => {
86                    let mut scratch = KeywordInterner::default();
87                    scalar_value(other, &mut scratch)
88                        .ok_or_else(|| TxFormError::BadValue(form.to_string()))?
89                }
90            };
91            Ok(EntityRef::Lookup(attr, coerce(value, value_type)))
92        }
93        other => Err(TxFormError::BadEntity(other.to_string())),
94    }
95}
96
97/// Converts a scalar EDN form to a value, interning keywords into `interner`.
98fn scalar_value(form: &Edn, interner: &mut KeywordInterner) -> Option<Value> {
99    match form {
100        Edn::Bool(v) => Some(Value::Bool(*v)),
101        Edn::Long(v) => Some(Value::Long(*v)),
102        Edn::Double(v) => Some(Value::Double(*v)),
103        Edn::Str(v) => Some(Value::Str(v.as_str().into())),
104        Edn::Keyword(k) => Some(Value::Keyword(interner.intern(k.clone()))),
105        Edn::Tagged(tag, value) => match (tag.as_str(), value.as_ref()) {
106            ("eid", Edn::Long(n)) => u64::try_from(*n)
107                .ok()
108                .map(|raw| Value::Ref(EntityId::from_raw(raw))),
109            ("inst", Edn::Long(ms)) => Some(Value::Instant(*ms)),
110            ("uuid", Edn::Str(hex)) => u128::from_str_radix(hex, 16).ok().map(Value::Uuid),
111            ("bytes", Edn::Str(hex)) => decode_hex(hex).map(|b| Value::Bytes(b.into())),
112            _ => None,
113        },
114        _ => None,
115    }
116}
117
118fn decode_hex(hex: &str) -> Option<Vec<u8>> {
119    if !hex.len().is_multiple_of(2) {
120        return None;
121    }
122    (0..hex.len())
123        .step_by(2)
124        .map(|i| u8::from_str_radix(hex.get(i..i + 2)?, 16).ok())
125        .collect()
126}
127
128#[allow(clippy::cast_precision_loss)]
129fn coerce(value: Value, value_type: ValueType) -> Value {
130    match (&value, value_type) {
131        (Value::Long(n), ValueType::Ref) => u64::try_from(*n)
132            .map(|raw| Value::Ref(EntityId::from_raw(raw)))
133            .unwrap_or(value),
134        (Value::Long(n), ValueType::Instant) => Value::Instant(*n),
135        (Value::Long(n), ValueType::Double) => Value::Double(TotalF64(*n as f64)),
136        _ => value,
137    }
138}
139
140/// Converts a value-position form for `attr`, resolving reference values.
141fn tx_value(
142    db: &Db,
143    interner: &mut KeywordInterner,
144    attr: EntityId,
145    form: &Edn,
146) -> Result<Value, TxFormError> {
147    let value_type = db
148        .schema()
149        .get(attr)
150        .map(|meta| meta.value_type)
151        .ok_or_else(|| TxFormError::UnknownAttribute(format!("{attr:?}")))?;
152    if value_type == ValueType::Ref {
153        return match entity_ref(db, form)? {
154            EntityRef::Id(e) => Ok(Value::Ref(e)),
155            EntityRef::Temp(name) => Err(TxFormError::BadValue(format!(
156                "value-position tempid \"{name}\" must be resolved by the client"
157            ))),
158            EntityRef::Lookup(a, v) => db
159                .lookup(a, &v)
160                .map(Value::Ref)
161                .ok_or_else(|| TxFormError::BadValue(format!("lookup ref {form} did not resolve"))),
162        };
163    }
164    scalar_value(form, interner)
165        .map(|value| coerce(value, value_type))
166        .ok_or_else(|| TxFormError::BadValue(form.to_string()))
167}
168
169/// Converts wire EDN transaction forms into engine transaction items.
170///
171/// New keyword values are interned into `interner` (the caller persists the
172/// naming change before committing the transaction).
173///
174/// # Errors
175/// Returns [`TxFormError`] when a form is malformed or references unknown
176/// attributes/idents.
177pub fn tx_items_from_edn(
178    db: &Db,
179    interner: &mut KeywordInterner,
180    forms: &[Edn],
181) -> Result<Vec<TxItem>, TxFormError> {
182    forms
183        .iter()
184        .map(|form| match form {
185            Edn::Vector(items) => list_form(db, interner, items, form),
186            Edn::Map(pairs) => map_form(db, interner, pairs),
187            other => Err(TxFormError::BadForm(other.to_string())),
188        })
189        .collect()
190}
191
192fn list_form(
193    db: &Db,
194    interner: &mut KeywordInterner,
195    items: &[Edn],
196    form: &Edn,
197) -> Result<TxItem, TxFormError> {
198    let op = items
199        .first()
200        .and_then(Edn::as_keyword)
201        .ok_or_else(|| TxFormError::BadForm(form.to_string()))?;
202    let name = format!(
203        "{}/{}",
204        op.namespace.as_deref().unwrap_or_default(),
205        op.name
206    );
207    let arg = |index: usize| {
208        items
209            .get(index)
210            .ok_or_else(|| TxFormError::BadForm(form.to_string()))
211    };
212    Ok(TxItem::Op(match name.as_str() {
213        "db/add" => {
214            let attr = attr_of(db, arg(2)?)?;
215            TxOp::Add(
216                entity_ref(db, arg(1)?)?,
217                attr,
218                tx_value(db, interner, attr, arg(3)?)?,
219            )
220        }
221        "db/retract" => {
222            let attr = attr_of(db, arg(2)?)?;
223            TxOp::Retract(
224                entity_ref(db, arg(1)?)?,
225                attr,
226                tx_value(db, interner, attr, arg(3)?)?,
227            )
228        }
229        "db/cas" => {
230            let attr = attr_of(db, arg(2)?)?;
231            let old = match arg(3)? {
232                Edn::Nil => None,
233                other => Some(tx_value(db, interner, attr, other)?),
234            };
235            TxOp::Cas(
236                entity_ref(db, arg(1)?)?,
237                attr,
238                old,
239                tx_value(db, interner, attr, arg(4)?)?,
240            )
241        }
242        "db/retractEntity" => TxOp::RetractEntity(entity_ref(db, arg(1)?)?),
243        _ => return Err(TxFormError::UnknownOp(op.to_string())),
244    }))
245}
246
247fn map_form(
248    db: &Db,
249    interner: &mut KeywordInterner,
250    pairs: &[(Edn, Edn)],
251) -> Result<TxItem, TxFormError> {
252    let id_key = kw("db/id");
253    let entity = pairs
254        .iter()
255        .find(|(key, _)| *key == id_key)
256        .map(|(_, value)| entity_ref(db, value))
257        .ok_or_else(|| TxFormError::BadForm("map form requires :db/id".into()))??;
258    let mut attributes = Vec::new();
259    for (key, value) in pairs.iter().filter(|(key, _)| *key != id_key) {
260        let attr = attr_of(db, key)?;
261        // A vector value is a cardinality-many set of values unless it reads
262        // as a lookup ref (`[:attr value]`), matching the corpus convention.
263        let many = matches!(value, Edn::Vector(items)
264            if !(items.len() == 2 && items[0].as_keyword().is_some()));
265        let values = if many {
266            value
267                .as_seq()
268                .unwrap_or_default()
269                .iter()
270                .map(|item| tx_value(db, interner, attr, item))
271                .collect::<Result<Vec<_>, _>>()?
272        } else {
273            vec![tx_value(db, interner, attr, value)?]
274        };
275        attributes.push((attr, values));
276    }
277    Ok(TxItem::Map(EntityMap { entity, attributes }))
278}