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#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Eq, Hash)]
13pub enum EntityRef {
14 Id(i64),
15 TempId(String),
16 Ident(Keyword),
17 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), Boolean(bool), Bytes(Vec<u8>), Double(f64), Float(f32), Instant(DateTime<Utc>), Keyword(Keyword), Long(i64), String(String), Uuid(Uuid), Vector(Vec<DataType>), Map(BTreeMap<String, DataType>), }
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 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 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 (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 pub fn put(attrs: Vec<(Keyword, DataType)>) -> Self {
193 TxOp::Put(attrs.into_iter().collect())
194 }
195}
196
197pub 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
239pub 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
274pub 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#[derive(Debug, Clone, PartialEq)]
376pub enum QueryArg {
377 Scalar(DataType),
378 Collection(Vec<DataType>),
379 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 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 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 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 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 assert!("[:db/frobnicate 1]".parse::<TxOp>().is_err());
776 assert!("42".parse::<TxOp>().is_err());
778 assert!("[]".parse::<TxOp>().is_err());
779 assert!("{\"name\" \"Alice\"}".parse::<TxOp>().is_err());
781 assert!("[:db/add 1 :a".parse::<TxOp>().is_err());
783 assert!("[:db/add :user/_friend :a 1]".parse::<TxOp>().is_err());
785 assert!("[:db/add 1 :a #{1 2}]".parse::<TxOp>().is_err());
787 }
788}