1use corium_core::{EntityId, Keyword, KeywordInterner, TotalF64, Value, ValueType};
14use corium_db::Db;
15use corium_query::edn::Edn;
16use corium_tx::{EntityMap, EntityRef, TX_TEMPID, TxItem, TxOp};
17use thiserror::Error;
18
19fn is_current_tx(keyword: &Keyword) -> bool {
21 keyword.namespace.as_deref() == Some("db") && keyword.name == "current-tx"
22}
23
24#[derive(Debug, Error, Eq, PartialEq)]
26pub enum TxFormError {
27 #[error("bad transaction form: {0}")]
29 BadForm(String),
30 #[error("unknown transaction op {0}")]
32 UnknownOp(String),
33 #[error("unknown attribute {0}")]
35 UnknownAttribute(String),
36 #[error("bad entity position: {0}")]
38 BadEntity(String),
39 #[error("bad value {0}")]
41 BadValue(String),
42}
43
44fn kw(text: &str) -> Edn {
45 Edn::keyword(text)
46}
47
48fn attr_of(db: &Db, form: &Edn) -> Result<EntityId, TxFormError> {
49 let keyword = form
50 .as_keyword()
51 .ok_or_else(|| TxFormError::BadForm(format!("attribute position {form}")))?;
52 db.idents()
53 .entid(keyword)
54 .ok_or_else(|| TxFormError::UnknownAttribute(keyword.to_string()))
55}
56
57fn entity_ref(db: &Db, form: &Edn) -> Result<EntityRef, TxFormError> {
58 match form {
59 Edn::Str(name) => Ok(EntityRef::Temp(name.clone())),
60 Edn::Long(n) => u64::try_from(*n)
61 .map(|raw| EntityRef::Id(EntityId::from_raw(raw)))
62 .map_err(|_| TxFormError::BadEntity(form.to_string())),
63 Edn::Keyword(keyword) if is_current_tx(keyword) => {
67 Ok(EntityRef::Temp(TX_TEMPID.to_owned()))
68 }
69 Edn::Keyword(keyword) => db
70 .idents()
71 .entid(keyword)
72 .map(EntityRef::Id)
73 .ok_or_else(|| TxFormError::UnknownAttribute(keyword.to_string())),
74 Edn::Tagged(tag, value) if tag == "eid" => match value.as_ref() {
75 Edn::Long(n) => u64::try_from(*n)
76 .map(|raw| EntityRef::Id(EntityId::from_raw(raw)))
77 .map_err(|_| TxFormError::BadEntity(form.to_string())),
78 _ => Err(TxFormError::BadEntity(form.to_string())),
79 },
80 Edn::Vector(items) => {
81 let [attr_form, value_form] = items.as_slice() else {
82 return Err(TxFormError::BadEntity(form.to_string()));
83 };
84 let attr = attr_of(db, attr_form)?;
85 let value_type = db
86 .schema()
87 .get(attr)
88 .map(|meta| meta.value_type)
89 .ok_or_else(|| TxFormError::UnknownAttribute(format!("{attr:?}")))?;
90 let value = match value_form {
93 Edn::Keyword(keyword) => Value::Keyword(
94 db.interner()
95 .get(keyword)
96 .unwrap_or(corium_query::exec::UNKNOWN_KEYWORD),
97 ),
98 other => {
99 let mut scratch = KeywordInterner::default();
100 scalar_value(other, &mut scratch)
101 .ok_or_else(|| TxFormError::BadValue(form.to_string()))?
102 }
103 };
104 Ok(EntityRef::Lookup(attr, coerce(value, value_type)))
105 }
106 other => Err(TxFormError::BadEntity(other.to_string())),
107 }
108}
109
110fn scalar_value(form: &Edn, interner: &mut KeywordInterner) -> Option<Value> {
112 match form {
113 Edn::Bool(v) => Some(Value::Bool(*v)),
114 Edn::Long(v) => Some(Value::Long(*v)),
115 Edn::Double(v) => Some(Value::Double(*v)),
116 Edn::Str(v) => Some(Value::Str(v.as_str().into())),
117 Edn::Keyword(k) => Some(Value::Keyword(interner.intern(k.clone()))),
118 Edn::Tagged(tag, value) => match (tag.as_str(), value.as_ref()) {
119 ("eid", Edn::Long(n)) => u64::try_from(*n)
120 .ok()
121 .map(|raw| Value::Ref(EntityId::from_raw(raw))),
122 ("inst", Edn::Long(ms)) => Some(Value::Instant(*ms)),
123 ("uuid", Edn::Str(hex)) => u128::from_str_radix(hex, 16).ok().map(Value::Uuid),
124 ("bytes", Edn::Str(hex)) => decode_hex(hex).map(|b| Value::Bytes(b.into())),
125 _ => None,
126 },
127 _ => None,
128 }
129}
130
131fn decode_hex(hex: &str) -> Option<Vec<u8>> {
132 if !hex.len().is_multiple_of(2) {
133 return None;
134 }
135 (0..hex.len())
136 .step_by(2)
137 .map(|i| u8::from_str_radix(hex.get(i..i + 2)?, 16).ok())
138 .collect()
139}
140
141#[allow(clippy::cast_precision_loss)]
142fn coerce(value: Value, value_type: ValueType) -> Value {
143 match (&value, value_type) {
144 (Value::Long(n), ValueType::Ref) => u64::try_from(*n)
145 .map(|raw| Value::Ref(EntityId::from_raw(raw)))
146 .unwrap_or(value),
147 (Value::Long(n), ValueType::Instant) => Value::Instant(*n),
148 (Value::Long(n), ValueType::Double) => Value::Double(TotalF64(*n as f64)),
149 _ => value,
150 }
151}
152
153fn tx_value(
155 db: &Db,
156 interner: &mut KeywordInterner,
157 attr: EntityId,
158 form: &Edn,
159) -> Result<Value, TxFormError> {
160 let value_type = db
161 .schema()
162 .get(attr)
163 .map(|meta| meta.value_type)
164 .ok_or_else(|| TxFormError::UnknownAttribute(format!("{attr:?}")))?;
165 if value_type == ValueType::Ref {
166 return match entity_ref(db, form)? {
167 EntityRef::Id(e) => Ok(Value::Ref(e)),
168 EntityRef::Temp(name) => Err(TxFormError::BadValue(format!(
169 "value-position tempid \"{name}\" must be resolved by the client"
170 ))),
171 EntityRef::Lookup(a, v) => db
172 .lookup(a, &v)
173 .map(Value::Ref)
174 .ok_or_else(|| TxFormError::BadValue(format!("lookup ref {form} did not resolve"))),
175 };
176 }
177 scalar_value(form, interner)
178 .map(|value| coerce(value, value_type))
179 .ok_or_else(|| TxFormError::BadValue(form.to_string()))
180}
181
182pub fn tx_items_from_edn(
191 db: &Db,
192 interner: &mut KeywordInterner,
193 forms: &[Edn],
194) -> Result<Vec<TxItem>, TxFormError> {
195 forms
196 .iter()
197 .map(|form| match form {
198 Edn::Vector(items) => list_form(db, interner, items, form),
199 Edn::Map(pairs) => map_form(db, interner, pairs),
200 other => Err(TxFormError::BadForm(other.to_string())),
201 })
202 .collect()
203}
204
205fn list_form(
206 db: &Db,
207 interner: &mut KeywordInterner,
208 items: &[Edn],
209 form: &Edn,
210) -> Result<TxItem, TxFormError> {
211 let op = items
212 .first()
213 .and_then(Edn::as_keyword)
214 .ok_or_else(|| TxFormError::BadForm(form.to_string()))?;
215 let name = format!(
216 "{}/{}",
217 op.namespace.as_deref().unwrap_or_default(),
218 op.name
219 );
220 let arg = |index: usize| {
221 items
222 .get(index)
223 .ok_or_else(|| TxFormError::BadForm(form.to_string()))
224 };
225 Ok(TxItem::Op(match name.as_str() {
226 "db/add" => {
227 let attr = attr_of(db, arg(2)?)?;
228 TxOp::Add(
229 entity_ref(db, arg(1)?)?,
230 attr,
231 tx_value(db, interner, attr, arg(3)?)?,
232 )
233 }
234 "db/retract" => {
235 let attr = attr_of(db, arg(2)?)?;
236 TxOp::Retract(
237 entity_ref(db, arg(1)?)?,
238 attr,
239 tx_value(db, interner, attr, arg(3)?)?,
240 )
241 }
242 "db/cas" => {
243 let attr = attr_of(db, arg(2)?)?;
244 let old = match arg(3)? {
245 Edn::Nil => None,
246 other => Some(tx_value(db, interner, attr, other)?),
247 };
248 TxOp::Cas(
249 entity_ref(db, arg(1)?)?,
250 attr,
251 old,
252 tx_value(db, interner, attr, arg(4)?)?,
253 )
254 }
255 "db/retractEntity" => TxOp::RetractEntity(entity_ref(db, arg(1)?)?),
256 _ => return Err(TxFormError::UnknownOp(op.to_string())),
257 }))
258}
259
260fn map_form(
261 db: &Db,
262 interner: &mut KeywordInterner,
263 pairs: &[(Edn, Edn)],
264) -> Result<TxItem, TxFormError> {
265 let id_key = kw("db/id");
266 let entity = pairs
267 .iter()
268 .find(|(key, _)| *key == id_key)
269 .map(|(_, value)| entity_ref(db, value))
270 .ok_or_else(|| TxFormError::BadForm("map form requires :db/id".into()))??;
271 let mut attributes = Vec::new();
272 for (key, value) in pairs.iter().filter(|(key, _)| *key != id_key) {
273 let attr = attr_of(db, key)?;
274 let many = matches!(value, Edn::Vector(items)
277 if !(items.len() == 2 && items[0].as_keyword().is_some()));
278 let values = if many {
279 value
280 .as_seq()
281 .unwrap_or_default()
282 .iter()
283 .map(|item| tx_value(db, interner, attr, item))
284 .collect::<Result<Vec<_>, _>>()?
285 } else {
286 vec![tx_value(db, interner, attr, value)?]
287 };
288 attributes.push((attr, values));
289 }
290 Ok(TxItem::Map(EntityMap { entity, attributes }))
291}