1use 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#[derive(Debug, Error, Eq, PartialEq)]
19pub enum TxFormError {
20 #[error("bad transaction form: {0}")]
22 BadForm(String),
23 #[error("unknown transaction op {0}")]
25 UnknownOp(String),
26 #[error("unknown attribute {0}")]
28 UnknownAttribute(String),
29 #[error("bad entity position: {0}")]
31 BadEntity(String),
32 #[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 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
97fn 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() % 2 != 0 {
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
140fn 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
169pub 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 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}