Skip to main content

triplox_client/
ops.rs

1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use edn::symbols::Keyword;
4use edn::types::Value;
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7use uuid::Uuid;
8
9pub type Entid = i64;
10
11/// How to identify an entity in a transaction.
12#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Eq, Hash)]
13pub enum EntityRef {
14    Id(i64),
15    TempId(String),
16    Ident(Keyword),
17    /// Accepted in the type but errors at expansion (not yet supported).
18    LookupRef(Keyword, DataType),
19}
20
21impl From<i64> for EntityRef {
22    fn from(v: i64) -> Self {
23        EntityRef::Id(v)
24    }
25}
26
27impl From<Keyword> for EntityRef {
28    fn from(v: Keyword) -> Self {
29        EntityRef::Ident(v)
30    }
31}
32
33impl From<&str> for EntityRef {
34    fn from(v: &str) -> Self {
35        EntityRef::TempId(v.to_string())
36    }
37}
38
39impl From<String> for EntityRef {
40    fn from(v: String) -> Self {
41        EntityRef::TempId(v)
42    }
43}
44
45#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
46pub enum DataType {
47    BigInt(i128),                    // Arbitrary large integers
48    Boolean(bool),                   // Booleans (true or false)
49    Bytes(Vec<u8>),                  // Binary data (as bytes)
50    Double(f64),                     // Double precision floating point
51    Float(f32),                      // Single precision floating point
52    Instant(DateTime<Utc>),          // Timestamps or instants
53    Keyword(Keyword),                // Keywords
54    Long(i64),                       // Long integers (also used for Ref values; see ValueType::Ref)
55    String(String),                  // Strings
56    Uuid(Uuid),                      // Universally unique identifier
57    Vector(Vec<DataType>),           // List (vector of DataTypes)
58    Map(BTreeMap<String, DataType>), // Map (BTreeMap of string keys and DataType values)
59}
60
61impl Eq for DataType {}
62
63impl std::hash::Hash for DataType {
64    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
65        std::mem::discriminant(self).hash(state);
66        match self {
67            DataType::BigInt(v) => v.hash(state),
68            DataType::Boolean(v) => v.hash(state),
69            DataType::Bytes(v) => v.hash(state),
70            DataType::Double(v) => v.to_bits().hash(state),
71            DataType::Float(v) => v.to_bits().hash(state),
72            DataType::Instant(v) => v.hash(state),
73            DataType::Keyword(v) => v.hash(state),
74            DataType::Long(v) => v.hash(state),
75            DataType::String(v) => v.hash(state),
76            DataType::Uuid(v) => v.hash(state),
77            DataType::Vector(v) => v.hash(state),
78            DataType::Map(v) => v.hash(state),
79        }
80    }
81}
82
83impl DataType {
84    /// Variant name, for diagnostics. Stays inside this crate so the type
85    /// stays decoupled from the server-side `ValueType`.
86    fn variant_name(&self) -> &'static str {
87        match self {
88            DataType::BigInt(_) => "BigInt",
89            DataType::Boolean(_) => "Boolean",
90            DataType::Bytes(_) => "Bytes",
91            DataType::Double(_) => "Double",
92            DataType::Float(_) => "Float",
93            DataType::Instant(_) => "Instant",
94            DataType::Keyword(_) => "Keyword",
95            DataType::Long(_) => "Long",
96            DataType::String(_) => "String",
97            DataType::Uuid(_) => "Uuid",
98            DataType::Vector(_) => "Vector",
99            DataType::Map(_) => "Map",
100        }
101    }
102
103    /// Compare two DataType values. Returns an error if the types are incompatible
104    /// or if floats are NaN.
105    pub fn partial_compare(&self, other: &DataType) -> Result<std::cmp::Ordering> {
106        use DataType::*;
107        let nan_err = || anyhow::anyhow!("cannot compare NaN values");
108        match (self, other) {
109            (Long(a), Long(b)) => Ok(a.cmp(b)),
110            (BigInt(a), BigInt(b)) => Ok(a.cmp(b)),
111            (Double(a), Double(b)) => a.partial_cmp(b).ok_or_else(nan_err),
112            (Float(a), Float(b)) => a.partial_cmp(b).ok_or_else(nan_err),
113            (String(a), String(b)) => Ok(a.cmp(b)),
114            (Boolean(a), Boolean(b)) => Ok(a.cmp(b)),
115            (Instant(a), Instant(b)) => Ok(a.cmp(b)),
116            // Cross-numeric promotion
117            // NOTE: casting BigInt(i128) to f32/f64 may lose precision for large values.
118            (Long(a), BigInt(b)) => Ok((*a as i128).cmp(b)),
119            (BigInt(a), Long(b)) => Ok(a.cmp(&(*b as i128))),
120            (Long(a), Double(b)) => (*a as f64).partial_cmp(b).ok_or_else(nan_err),
121            (Double(a), Long(b)) => a.partial_cmp(&(*b as f64)).ok_or_else(nan_err),
122            (Long(a), Float(b)) => (*a as f32).partial_cmp(b).ok_or_else(nan_err),
123            (Float(a), Long(b)) => a.partial_cmp(&(*b as f32)).ok_or_else(nan_err),
124            (BigInt(a), Float(b)) => (*a as f32).partial_cmp(b).ok_or_else(nan_err),
125            (Float(a), BigInt(b)) => a.partial_cmp(&(*b as f32)).ok_or_else(nan_err),
126            (BigInt(a), Double(b)) => (*a as f64).partial_cmp(b).ok_or_else(nan_err),
127            (Double(a), BigInt(b)) => a.partial_cmp(&(*b as f64)).ok_or_else(nan_err),
128            (Float(a), Double(b)) => (*a as f64).partial_cmp(b).ok_or_else(nan_err),
129            (Double(a), Float(b)) => a.partial_cmp(&(*b as f64)).ok_or_else(nan_err),
130            _ => Err(anyhow::anyhow!(
131                "cannot compare {} with {}",
132                self.variant_name(),
133                other.variant_name()
134            )),
135        }
136    }
137}
138
139macro_rules! impl_from_for_enum {
140    ($enum_name:ident, $(($variant:ident, $type:ty)),*) => {
141        $(
142            impl From<$type> for $enum_name {
143                fn from(value: $type) -> Self {
144                    $enum_name::$variant(value)
145                }
146            }
147        )*
148    };
149}
150
151impl_from_for_enum!(
152    DataType,
153    (BigInt, i128),
154    (Boolean, bool),
155    (Bytes, Vec<u8>),
156    (Double, f64),
157    (Float, f32),
158    (Instant, DateTime<Utc>),
159    (Keyword, Keyword),
160    (Long, i64),
161    (String, String),
162    (Uuid, Uuid),
163    (Vector, Vec<DataType>),
164    (Map, BTreeMap<String, DataType>)
165);
166
167impl From<&str> for DataType {
168    fn from(v: &str) -> Self {
169        DataType::String(v.to_string())
170    }
171}
172
173#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
174pub enum TxOp {
175    Put(BTreeMap<Keyword, DataType>),
176    Add {
177        entity: EntityRef,
178        attribute: Keyword,
179        value: DataType,
180    },
181    Retract {
182        entity: EntityRef,
183        attribute: Keyword,
184        value: DataType,
185    },
186    Delete(EntityRef),
187    Erase(EntityRef),
188}
189
190impl TxOp {
191    /// Build a Put without explicit entity ID (auto-allocated).
192    pub fn put(attrs: Vec<(Keyword, DataType)>) -> Self {
193        TxOp::Put(attrs.into_iter().collect())
194    }
195}
196
197/// Convert an EDN `Value` to a `DataType` for the value position of a TxOp.
198///
199/// Map keys here must be `Value::Text` because `DataType::Map` uses `String` keys.
200/// (Top-level Put maps use `Value::Keyword` keys — that's handled in `tx_op_from_value`.)
201pub fn value_to_data_type(value: Value) -> Result<DataType> {
202    match value {
203        Value::Boolean(b) => Ok(DataType::Boolean(b)),
204        Value::Integer(i) => Ok(DataType::Long(i)),
205        Value::BigInteger(bi) => bi
206            .to_string()
207            .parse::<i128>()
208            .map(DataType::BigInt)
209            .map_err(|_| anyhow::anyhow!("BigInt out of i128 range: {}", bi)),
210        Value::Float(f) => Ok(DataType::Double(f.into_inner())),
211        Value::Text(s) => Ok(DataType::String(s)),
212        Value::Uuid(u) => Ok(DataType::Uuid(u)),
213        Value::Instant(d) => Ok(DataType::Instant(d)),
214        Value::Keyword(k) => Ok(DataType::Keyword(k)),
215        Value::Vector(items) => items
216            .into_iter()
217            .map(value_to_data_type)
218            .collect::<Result<Vec<_>>>()
219            .map(DataType::Vector),
220        Value::Map(m) => {
221            let mut out = BTreeMap::new();
222            for (k, v) in m {
223                let key = match k {
224                    Value::Text(s) => s,
225                    other => anyhow::bail!("nested map keys must be strings, got {:?}", other),
226                };
227                out.insert(key, value_to_data_type(v)?);
228            }
229            Ok(DataType::Map(out))
230        }
231        Value::Nil => anyhow::bail!("nil is not a valid TxOp value"),
232        Value::Set(_) => anyhow::bail!("set is not a valid TxOp value"),
233        v @ Value::List(_) => anyhow::bail!("invalid TxOp value: {:?}", v),
234        Value::PlainSymbol(s) => anyhow::bail!("symbol {} is not a valid TxOp value", s),
235        Value::NamespacedSymbol(s) => anyhow::bail!("symbol {} is not a valid TxOp value", s),
236    }
237}
238
239/// Convert an EDN `Value` to an `EntityRef`.
240/// Accepts: integer (`Id`), text (`TempId`), namespaced keyword (`Ident`),
241/// or `[:attr value]` 2-vector (`LookupRef`, Datomic-style).
242pub fn value_to_entity_ref(value: Value) -> Result<EntityRef> {
243    match value {
244        Value::Integer(i) => Ok(EntityRef::Id(i)),
245        Value::Text(s) => Ok(EntityRef::TempId(s)),
246        Value::Keyword(k) => {
247            if k.is_backward() {
248                anyhow::bail!("reverse keyword {} not supported in entity position", k);
249            }
250            Ok(EntityRef::Ident(k))
251        }
252        Value::Vector(items) => {
253            if items.len() != 2 {
254                anyhow::bail!(
255                    "lookup ref must be [:attr value] 2-vector, got {} elements",
256                    items.len()
257                );
258            }
259            let mut iter = items.into_iter();
260            let attr = match iter.next().unwrap() {
261                Value::Keyword(k) => k,
262                other => anyhow::bail!("lookup ref attribute must be a keyword, got {:?}", other),
263            };
264            let v = value_to_data_type(iter.next().unwrap())?;
265            Ok(EntityRef::LookupRef(attr, v))
266        }
267        other => anyhow::bail!(
268            "expected entity ref (integer, string, keyword, or [:attr value] lookup ref), got {:?}",
269            other
270        ),
271    }
272}
273
274/// Convert an EDN `Value` to a single `TxOp`.
275///
276/// Accepted forms:
277/// - `[:db/add e a v]`     → `TxOp::Add`
278/// - `[:db/retract e a v]` → `TxOp::Retract`
279/// - `[:db/delete e]`      → `TxOp::Delete`
280/// - `[:db/erase e]`       → `TxOp::Erase`
281/// - `{:attr v ...}`       → `TxOp::Put` (`:db/id` is passed through as a normal entry)
282pub fn tx_op_from_value(value: Value) -> Result<TxOp> {
283    match value {
284        Value::Vector(items) => {
285            let mut iter = items.into_iter();
286            let head = iter
287                .next()
288                .ok_or_else(|| anyhow::anyhow!("empty vector is not a valid TxOp"))?;
289            let op_kw = match head {
290                Value::Keyword(k) => k,
291                other => {
292                    anyhow::bail!("expected operation keyword (e.g. :db/add), got {:?}", other)
293                }
294            };
295            match (op_kw.namespace(), op_kw.name()) {
296                (Some("db"), name @ ("add" | "retract")) => {
297                    let entity = match iter.next() {
298                        Some(v) => value_to_entity_ref(v)?,
299                        None => anyhow::bail!("{} missing entity", op_kw),
300                    };
301                    let attribute = match iter.next() {
302                        Some(Value::Keyword(k)) => k,
303                        Some(other) => {
304                            anyhow::bail!("{} attribute must be a keyword, got {:?}", op_kw, other)
305                        }
306                        None => anyhow::bail!("{} missing attribute", op_kw),
307                    };
308                    let value = match iter.next() {
309                        Some(v) => value_to_data_type(v)?,
310                        None => anyhow::bail!("{} missing value", op_kw),
311                    };
312                    if iter.next().is_some() {
313                        anyhow::bail!("{} takes exactly 3 arguments", op_kw);
314                    }
315                    Ok(if name == "add" {
316                        TxOp::Add {
317                            entity,
318                            attribute,
319                            value,
320                        }
321                    } else {
322                        TxOp::Retract {
323                            entity,
324                            attribute,
325                            value,
326                        }
327                    })
328                }
329                (Some("db"), name @ ("delete" | "erase")) => {
330                    let entity = match iter.next() {
331                        Some(v) => value_to_entity_ref(v)?,
332                        None => anyhow::bail!("{} missing entity", op_kw),
333                    };
334                    if iter.next().is_some() {
335                        anyhow::bail!("{} takes exactly 1 argument", op_kw);
336                    }
337                    Ok(if name == "delete" {
338                        TxOp::Delete(entity)
339                    } else {
340                        TxOp::Erase(entity)
341                    })
342                }
343                _ => anyhow::bail!("unknown TxOp operation: {}", op_kw),
344            }
345        }
346        Value::Map(m) => {
347            let mut out = BTreeMap::new();
348            for (k, v) in m {
349                let key = match k {
350                    Value::Keyword(kw) => kw,
351                    other => {
352                        anyhow::bail!("Put map keys must be keywords, got {:?}", other)
353                    }
354                };
355                out.insert(key, value_to_data_type(v)?);
356            }
357            Ok(TxOp::Put(out))
358        }
359        other => anyhow::bail!("expected TxOp (vector or map form), got {:?}", other),
360    }
361}
362
363impl std::str::FromStr for TxOp {
364    type Err = anyhow::Error;
365
366    fn from_str(s: &str) -> Result<Self> {
367        let value = edn::parse::value(s)
368            .map_err(|e| anyhow::anyhow!("EDN parse error: {}", e))?
369            .without_spans();
370        tx_op_from_value(value)
371    }
372}
373
374/// A query input argument corresponding to an `:in` binding form.
375#[derive(Debug, Clone, PartialEq)]
376pub enum QueryArg {
377    Scalar(DataType),
378    Collection(Vec<DataType>),
379    // TODO: Tuple and Relation are not yet supported in the query engine
380    Tuple(Vec<DataType>),
381    Relation(Vec<Vec<DataType>>),
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387    use bincode;
388    use edn::kw;
389
390    #[test]
391    fn test_partial_compare_same_type() {
392        use std::cmp::Ordering;
393        assert_eq!(
394            DataType::Long(1)
395                .partial_compare(&DataType::Long(2))
396                .unwrap(),
397            Ordering::Less
398        );
399        assert_eq!(
400            DataType::Long(2)
401                .partial_compare(&DataType::Long(2))
402                .unwrap(),
403            Ordering::Equal
404        );
405        assert_eq!(
406            DataType::Long(3)
407                .partial_compare(&DataType::Long(2))
408                .unwrap(),
409            Ordering::Greater
410        );
411
412        assert_eq!(
413            DataType::String("a".into())
414                .partial_compare(&DataType::String("b".into()))
415                .unwrap(),
416            Ordering::Less,
417        );
418        assert_eq!(
419            DataType::Boolean(false)
420                .partial_compare(&DataType::Boolean(true))
421                .unwrap(),
422            Ordering::Less,
423        );
424        assert_eq!(
425            DataType::Double(1.5)
426                .partial_compare(&DataType::Double(2.5))
427                .unwrap(),
428            Ordering::Less,
429        );
430    }
431
432    #[test]
433    fn test_partial_compare_cross_numeric() {
434        use std::cmp::Ordering;
435        assert_eq!(
436            DataType::Long(10)
437                .partial_compare(&DataType::BigInt(20))
438                .unwrap(),
439            Ordering::Less
440        );
441        assert_eq!(
442            DataType::BigInt(20)
443                .partial_compare(&DataType::Long(10))
444                .unwrap(),
445            Ordering::Greater
446        );
447        assert_eq!(
448            DataType::Long(5)
449                .partial_compare(&DataType::Double(5.0))
450                .unwrap(),
451            Ordering::Equal
452        );
453        assert_eq!(
454            DataType::Double(3.0)
455                .partial_compare(&DataType::Long(4))
456                .unwrap(),
457            Ordering::Less
458        );
459
460        // Float cross-numeric
461        assert_eq!(
462            DataType::Float(1.0)
463                .partial_compare(&DataType::Long(2))
464                .unwrap(),
465            Ordering::Less
466        );
467        assert_eq!(
468            DataType::Long(2)
469                .partial_compare(&DataType::Float(1.0))
470                .unwrap(),
471            Ordering::Greater
472        );
473        assert_eq!(
474            DataType::Float(1.0)
475                .partial_compare(&DataType::Double(1.0))
476                .unwrap(),
477            Ordering::Equal
478        );
479        assert_eq!(
480            DataType::Double(2.0)
481                .partial_compare(&DataType::Float(1.0))
482                .unwrap(),
483            Ordering::Greater
484        );
485        assert_eq!(
486            DataType::Float(1.0)
487                .partial_compare(&DataType::BigInt(2))
488                .unwrap(),
489            Ordering::Less
490        );
491        assert_eq!(
492            DataType::BigInt(2)
493                .partial_compare(&DataType::Float(1.0))
494                .unwrap(),
495            Ordering::Greater
496        );
497        assert_eq!(
498            DataType::BigInt(1)
499                .partial_compare(&DataType::Double(2.0))
500                .unwrap(),
501            Ordering::Less
502        );
503        assert_eq!(
504            DataType::Double(2.0)
505                .partial_compare(&DataType::BigInt(1))
506                .unwrap(),
507            Ordering::Greater
508        );
509    }
510
511    #[test]
512    fn test_partial_compare_incompatible() {
513        assert!(DataType::Long(1)
514            .partial_compare(&DataType::String("a".into()))
515            .is_err());
516        assert!(DataType::Boolean(true)
517            .partial_compare(&DataType::Long(1))
518            .is_err());
519    }
520
521    #[test]
522    fn test_partial_compare_nan() {
523        // Same-type NaN
524        assert!(DataType::Double(f64::NAN)
525            .partial_compare(&DataType::Double(1.0))
526            .is_err());
527        assert!(DataType::Float(f32::NAN)
528            .partial_compare(&DataType::Float(1.0))
529            .is_err());
530        // Cross-type NaN
531        assert!(DataType::Float(f32::NAN)
532            .partial_compare(&DataType::Long(1))
533            .is_err());
534        assert!(DataType::Float(f32::NAN)
535            .partial_compare(&DataType::Double(1.0))
536            .is_err());
537        assert!(DataType::Float(f32::NAN)
538            .partial_compare(&DataType::BigInt(1))
539            .is_err());
540        assert!(DataType::Double(f64::NAN)
541            .partial_compare(&DataType::Long(1))
542            .is_err());
543        assert!(DataType::Double(f64::NAN)
544            .partial_compare(&DataType::BigInt(1))
545            .is_err());
546    }
547
548    #[test]
549    fn test_op_put_bincode() {
550        let op = TxOp::put(vec![
551            (kw!(:string), "string_value".into()),
552            (kw!(:int), 1i64.into()),
553        ]);
554        let serialized = bincode::serialize(&op).unwrap();
555        let deserialized: TxOp = bincode::deserialize(&serialized).unwrap();
556        assert_eq!(op, deserialized);
557    }
558
559    #[test]
560    fn test_op_add_bincode() {
561        let op = TxOp::Add {
562            entity: EntityRef::Id(1),
563            attribute: kw!(:string),
564            value: DataType::String("string_value".to_string()),
565        };
566        let serialized = bincode::serialize(&op).unwrap();
567        let deserialized: TxOp = bincode::deserialize(&serialized).unwrap();
568        assert_eq!(op, deserialized);
569    }
570
571    #[test]
572    fn test_op_retract_bincode() {
573        let op = TxOp::Retract {
574            entity: EntityRef::Id(1),
575            attribute: kw!(:string),
576            value: DataType::String("string_value".to_string()),
577        };
578        let serialized = bincode::serialize(&op).unwrap();
579        let deserialized: TxOp = bincode::deserialize(&serialized).unwrap();
580        assert_eq!(op, deserialized);
581    }
582
583    #[test]
584    fn test_op_delete_bincode() {
585        let op = TxOp::Delete(EntityRef::Id(1));
586        let serialized = bincode::serialize(&op).unwrap();
587        let deserialized: TxOp = bincode::deserialize(&serialized).unwrap();
588        assert_eq!(op, deserialized);
589    }
590
591    #[test]
592    fn test_op_erase_bincode() {
593        let op = TxOp::Erase(EntityRef::Id(1));
594        let serialized = bincode::serialize(&op).unwrap();
595        let deserialized: TxOp = bincode::deserialize(&serialized).unwrap();
596        assert_eq!(op, deserialized);
597    }
598
599    #[test]
600    fn test_entity_ref_from_impls() {
601        assert_eq!(EntityRef::from(42_i64), EntityRef::Id(42));
602        assert_eq!(
603            EntityRef::from(kw!(:person/name)),
604            EntityRef::Ident(kw!(:person/name))
605        );
606        assert_eq!(
607            EntityRef::from("temp-1"),
608            EntityRef::TempId("temp-1".to_string())
609        );
610    }
611
612    #[test]
613    fn test_parse_add_with_id() {
614        let op: TxOp = "[:db/add 1 :user/name \"Alice\"]".parse().unwrap();
615        assert_eq!(
616            op,
617            TxOp::Add {
618                entity: EntityRef::Id(1),
619                attribute: kw!(:user/name),
620                value: DataType::String("Alice".to_string()),
621            }
622        );
623    }
624
625    #[test]
626    fn test_parse_add_with_tempid() {
627        let op: TxOp = "[:db/add \"alice\" :user/age 30]".parse().unwrap();
628        assert_eq!(
629            op,
630            TxOp::Add {
631                entity: EntityRef::TempId("alice".to_string()),
632                attribute: kw!(:user/age),
633                value: DataType::Long(30),
634            }
635        );
636    }
637
638    #[test]
639    fn test_parse_add_with_ident() {
640        let op: TxOp = "[:db/add :user/me :user/name \"Me\"]".parse().unwrap();
641        assert_eq!(
642            op,
643            TxOp::Add {
644                entity: EntityRef::Ident(kw!(:user/me)),
645                attribute: kw!(:user/name),
646                value: DataType::String("Me".to_string()),
647            }
648        );
649    }
650
651    #[test]
652    fn test_parse_add_with_lookup_ref() {
653        let op: TxOp = "[:db/add [:user/email \"a@b.c\"] :user/name \"A\"]"
654            .parse()
655            .unwrap();
656        assert_eq!(
657            op,
658            TxOp::Add {
659                entity: EntityRef::LookupRef(
660                    kw!(:user/email),
661                    DataType::String("a@b.c".to_string())
662                ),
663                attribute: kw!(:user/name),
664                value: DataType::String("A".to_string()),
665            }
666        );
667    }
668
669    #[test]
670    fn test_parse_retract() {
671        let op: TxOp = "[:db/retract 7 :user/age 30]".parse().unwrap();
672        assert_eq!(
673            op,
674            TxOp::Retract {
675                entity: EntityRef::Id(7),
676                attribute: kw!(:user/age),
677                value: DataType::Long(30),
678            }
679        );
680    }
681
682    #[test]
683    fn test_parse_delete() {
684        let op: TxOp = "[:db/delete 42]".parse().unwrap();
685        assert_eq!(op, TxOp::Delete(EntityRef::Id(42)));
686    }
687
688    #[test]
689    fn test_parse_erase() {
690        let op: TxOp = "[:db/erase \"tempid-1\"]".parse().unwrap();
691        assert_eq!(op, TxOp::Erase(EntityRef::TempId("tempid-1".to_string())));
692    }
693
694    #[test]
695    fn test_parse_put_no_id() {
696        let op: TxOp = "{:user/name \"Alice\" :user/age 30}".parse().unwrap();
697        let mut expected = BTreeMap::new();
698        expected.insert(kw!(:user/name), DataType::String("Alice".to_string()));
699        expected.insert(kw!(:user/age), DataType::Long(30));
700        assert_eq!(op, TxOp::Put(expected));
701    }
702
703    #[test]
704    fn test_parse_put_with_db_id_long() {
705        let op: TxOp = "{:db/id 100 :user/name \"X\"}".parse().unwrap();
706        let mut expected = BTreeMap::new();
707        expected.insert(kw!(:db/id), DataType::Long(100));
708        expected.insert(kw!(:user/name), DataType::String("X".to_string()));
709        assert_eq!(op, TxOp::Put(expected));
710    }
711
712    #[test]
713    fn test_parse_put_with_db_id_tempid() {
714        let op: TxOp = "{:db/id \"alice\" :user/name \"Alice\"}".parse().unwrap();
715        let mut expected = BTreeMap::new();
716        expected.insert(kw!(:db/id), DataType::String("alice".to_string()));
717        expected.insert(kw!(:user/name), DataType::String("Alice".to_string()));
718        assert_eq!(op, TxOp::Put(expected));
719    }
720
721    #[test]
722    fn test_parse_put_with_db_id_ident() {
723        let op: TxOp = "{:db/id :user/me :user/name \"Me\"}".parse().unwrap();
724        let mut expected = BTreeMap::new();
725        expected.insert(kw!(:db/id), DataType::Keyword(kw!(:user/me)));
726        expected.insert(kw!(:user/name), DataType::String("Me".to_string()));
727        assert_eq!(op, TxOp::Put(expected));
728    }
729
730    #[test]
731    fn test_parse_value_types() {
732        let op: TxOp = "[:db/add 1 :a/b true]".parse().unwrap();
733        assert!(matches!(
734            op,
735            TxOp::Add {
736                value: DataType::Boolean(true),
737                ..
738            }
739        ));
740        let op: TxOp = "[:db/add 1 :a/b 3.14]".parse().unwrap();
741        assert!(matches!(
742            op,
743            TxOp::Add {
744                value: DataType::Double(_),
745                ..
746            }
747        ));
748        let op: TxOp = "[:db/add 1 :a/b :some/kw]".parse().unwrap();
749        assert!(matches!(
750            op,
751            TxOp::Add {
752                value: DataType::Keyword(_),
753                ..
754            }
755        ));
756        let op: TxOp = "[:db/add 1 :a/b [1 2 3]]".parse().unwrap();
757        if let TxOp::Add {
758            value: DataType::Vector(v),
759            ..
760        } = op
761        {
762            assert_eq!(v.len(), 3);
763        } else {
764            panic!("expected vector value");
765        }
766    }
767
768    #[test]
769    fn test_parse_errors() {
770        // Wrong arity
771        assert!("[:db/add 1 :a]".parse::<TxOp>().is_err());
772        assert!("[:db/add 1 :a 2 3]".parse::<TxOp>().is_err());
773        assert!("[:db/delete 1 2]".parse::<TxOp>().is_err());
774        // Unknown op
775        assert!("[:db/frobnicate 1]".parse::<TxOp>().is_err());
776        // Bad shape
777        assert!("42".parse::<TxOp>().is_err());
778        assert!("[]".parse::<TxOp>().is_err());
779        // Map key not a keyword
780        assert!("{\"name\" \"Alice\"}".parse::<TxOp>().is_err());
781        // Bad EDN
782        assert!("[:db/add 1 :a".parse::<TxOp>().is_err());
783        // Reverse keyword in entity position
784        assert!("[:db/add :user/_friend :a 1]".parse::<TxOp>().is_err());
785        // Set in value position
786        assert!("[:db/add 1 :a #{1 2}]".parse::<TxOp>().is_err());
787    }
788}