1use std::{
2 collections::{BTreeMap, BTreeSet, HashMap},
3 fmt,
4};
5
6use async_trait::async_trait;
7use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
8
9pub type Result<T> = std::result::Result<T, GrustError>;
10pub type Props = BTreeMap<String, Value>;
11
12#[derive(Debug, thiserror::Error)]
13pub enum GrustError {
14 #[error("backend error: {0}")]
15 Backend(String),
16 #[error("schema error: {0}")]
17 Schema(String),
18 #[error("unsupported graph feature: {0}")]
19 Unsupported(String),
20 #[error("Cypher syntax error: {0}")]
21 CypherSyntax(String),
22 #[error("Cypher unresolved identity: {0}")]
23 CypherUnresolvedIdentity(String),
24 #[error("Cypher unsupported cardinality: {0}")]
25 CypherUnsupportedCardinality(String),
26 #[error("Cypher execution error: {0}")]
27 CypherExecution(String),
28 #[error("serialization error: {0}")]
29 Serialization(String),
30}
31
32macro_rules! string_newtype {
33 ($(#[$meta:meta])* $name:ident) => {
34 $(#[$meta])*
35 #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
36 pub struct $name(String);
37
38 impl $name {
39 pub fn new(value: impl Into<String>) -> Self {
40 Self(value.into())
41 }
42
43 pub fn as_str(&self) -> &str {
44 &self.0
45 }
46
47 pub fn into_string(self) -> String {
48 self.0
49 }
50 }
51
52 impl fmt::Display for $name {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 f.write_str(&self.0)
55 }
56 }
57
58 impl From<String> for $name {
59 fn from(value: String) -> Self {
60 Self::new(value)
61 }
62 }
63
64 impl From<&str> for $name {
65 fn from(value: &str) -> Self {
66 Self::new(value)
67 }
68 }
69
70 impl From<&String> for $name {
71 fn from(value: &String) -> Self {
72 Self::new(value.clone())
73 }
74 }
75
76 impl From<&$name> for $name {
77 fn from(value: &$name) -> Self {
78 value.clone()
79 }
80 }
81 };
82}
83
84string_newtype!(
85 NodeId
87);
88string_newtype!(
89 EdgeId
91);
92string_newtype!(
93 Label
95);
96
97#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
99pub struct RfcDate(String);
100
101impl RfcDate {
102 pub fn parse(value: impl Into<String>) -> Result<Self> {
105 let value = value.into();
106 if is_rfc3339_datetime(&value) {
107 Ok(Self(value))
108 } else {
109 Err(GrustError::Schema(format!(
110 "'{value}' is not an RFC 3339 date-time"
111 )))
112 }
113 }
114
115 pub fn as_str(&self) -> &str {
116 &self.0
117 }
118
119 pub fn into_string(self) -> String {
120 self.0
121 }
122}
123
124impl fmt::Display for RfcDate {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 f.write_str(&self.0)
127 }
128}
129
130impl Serialize for RfcDate {
131 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
132 where
133 S: Serializer,
134 {
135 serializer.serialize_str(self.as_str())
136 }
137}
138
139impl<'de> Deserialize<'de> for RfcDate {
140 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
141 where
142 D: Deserializer<'de>,
143 {
144 let value = String::deserialize(deserializer)?;
145 Self::parse(value).map_err(de::Error::custom)
146 }
147}
148
149#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
157pub struct Decimal {
158 mantissa: i128,
159 scale: u32,
160}
161
162impl Decimal {
163 pub fn parse(value: impl AsRef<str>) -> Result<Self> {
166 let raw = value.as_ref().trim();
167 let bad = || GrustError::Schema(format!("'{raw}' is not a decimal numeral"));
168 if raw.is_empty() {
169 return Err(bad());
170 }
171 let (neg, body) = match raw.strip_prefix('-') {
172 Some(rest) => (true, rest),
173 None => (false, raw.strip_prefix('+').unwrap_or(raw)),
174 };
175 let (int_part, frac_part) = match body.split_once('.') {
176 Some((i, f)) => (i, f),
177 None => (body, ""),
178 };
179 if int_part.is_empty() && frac_part.is_empty() {
180 return Err(bad());
181 }
182 if !int_part.bytes().all(|b| b.is_ascii_digit())
183 || !frac_part.bytes().all(|b| b.is_ascii_digit())
184 {
185 return Err(bad());
186 }
187 let digits: String = int_part.chars().chain(frac_part.chars()).collect();
188 let mantissa: i128 = if digits.is_empty() {
189 0
190 } else {
191 digits.parse().map_err(|_| {
192 GrustError::Schema(format!("decimal '{raw}' exceeds 38 significant digits"))
193 })?
194 };
195 let mantissa = if neg { -mantissa } else { mantissa };
196 Ok(Self::normalize(mantissa, frac_part.len() as u32))
197 }
198
199 pub fn from_parts(mantissa: i128, scale: u32) -> Self {
201 Self::normalize(mantissa, scale)
202 }
203
204 fn normalize(mut mantissa: i128, mut scale: u32) -> Self {
205 while scale > 0 && mantissa % 10 == 0 {
206 mantissa /= 10;
207 scale -= 1;
208 }
209 if mantissa == 0 {
210 scale = 0;
211 }
212 Self { mantissa, scale }
213 }
214
215 pub fn mantissa(&self) -> i128 {
216 self.mantissa
217 }
218
219 pub fn scale(&self) -> u32 {
220 self.scale
221 }
222
223 pub fn to_canonical_string(&self) -> String {
225 if self.scale == 0 {
226 return self.mantissa.to_string();
227 }
228 let neg = self.mantissa < 0;
229 let digits = self.mantissa.unsigned_abs().to_string();
230 let scale = self.scale as usize;
231 let s = if digits.len() <= scale {
232 format!("0.{:0>width$}", digits, width = scale)
233 } else {
234 let point = digits.len() - scale;
235 format!("{}.{}", &digits[..point], &digits[point..])
236 };
237 if neg {
238 format!("-{s}")
239 } else {
240 s
241 }
242 }
243
244 fn aligned(&self, other: &Self) -> Option<(i128, i128, u32)> {
247 let scale = self.scale.max(other.scale);
248 let lift = |m: i128, s: u32| 10i128.checked_pow(scale - s).and_then(|f| m.checked_mul(f));
249 Some((lift(self.mantissa, self.scale)?, lift(other.mantissa, other.scale)?, scale))
250 }
251
252 pub fn checked_add(&self, other: &Self) -> Option<Self> {
253 let (a, b, scale) = self.aligned(other)?;
254 Some(Self::normalize(a.checked_add(b)?, scale))
255 }
256
257 pub fn checked_sub(&self, other: &Self) -> Option<Self> {
258 let (a, b, scale) = self.aligned(other)?;
259 Some(Self::normalize(a.checked_sub(b)?, scale))
260 }
261
262 pub fn checked_mul(&self, other: &Self) -> Option<Self> {
263 let mantissa = self.mantissa.checked_mul(other.mantissa)?;
264 Some(Self::normalize(mantissa, self.scale + other.scale))
265 }
266
267 pub fn to_f64(&self) -> f64 {
269 self.mantissa as f64 / 10f64.powi(self.scale as i32)
270 }
271}
272
273impl Ord for Decimal {
274 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
275 match self.aligned(other) {
276 Some((a, b, _)) => a.cmp(&b),
277 None => self
280 .to_f64()
281 .partial_cmp(&other.to_f64())
282 .unwrap_or(std::cmp::Ordering::Equal),
283 }
284 }
285}
286
287impl PartialOrd for Decimal {
288 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
289 Some(self.cmp(other))
290 }
291}
292
293impl fmt::Display for Decimal {
294 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295 f.write_str(&self.to_canonical_string())
296 }
297}
298
299impl Serialize for Decimal {
300 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
301 where
302 S: Serializer,
303 {
304 serializer.serialize_str(&self.to_canonical_string())
305 }
306}
307
308impl<'de> Deserialize<'de> for Decimal {
309 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
310 where
311 D: Deserializer<'de>,
312 {
313 let value = String::deserialize(deserializer)?;
314 Self::parse(value).map_err(de::Error::custom)
315 }
316}
317
318#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
326pub struct Duration {
327 pub months: i64,
328 pub days: i64,
329 pub seconds: i64,
330 pub nanos: i32,
331}
332
333impl Duration {
334 pub fn parse(value: impl AsRef<str>) -> Result<Self> {
338 let raw = value.as_ref().trim();
339 let bad = || GrustError::Schema(format!("'{raw}' is not an ISO 8601 duration"));
340 let body = raw.strip_prefix('P').ok_or_else(bad)?;
341 let (date_part, time_part) = match body.split_once('T') {
342 Some((d, t)) => (d, Some(t)),
343 None => (body, None),
344 };
345 let mut dur = Duration::default();
346 let mut any = false;
347 let mut num = String::new();
349 for ch in date_part.chars() {
350 if ch.is_ascii_digit() || ch == '-' {
351 num.push(ch);
352 } else {
353 let n: i64 = num.parse().map_err(|_| bad())?;
354 num.clear();
355 any = true;
356 match ch {
357 'Y' => dur.months += n * 12,
358 'M' => dur.months += n,
359 'W' => dur.days += n * 7,
360 'D' => dur.days += n,
361 _ => return Err(bad()),
362 }
363 }
364 }
365 if !num.is_empty() {
366 return Err(bad());
367 }
368 if let Some(time_part) = time_part {
370 let mut tok = String::new();
371 for ch in time_part.chars() {
372 if ch.is_ascii_digit() || ch == '-' || ch == '.' {
373 tok.push(ch);
374 } else {
375 any = true;
376 match ch {
377 'H' => dur.seconds += tok.parse::<i64>().map_err(|_| bad())? * 3600,
378 'M' => dur.seconds += tok.parse::<i64>().map_err(|_| bad())? * 60,
379 'S' => {
380 let (secs, nanos) = parse_fractional_seconds(&tok).ok_or_else(bad)?;
381 dur.seconds += secs;
382 dur.nanos += nanos;
383 }
384 _ => return Err(bad()),
385 }
386 tok.clear();
387 }
388 }
389 if !tok.is_empty() {
390 return Err(bad());
391 }
392 }
393 if !any {
394 return Err(bad());
395 }
396 Ok(dur.carry_nanos())
397 }
398
399 fn carry_nanos(mut self) -> Self {
400 if self.nanos.abs() >= 1_000_000_000 {
401 self.seconds += (self.nanos / 1_000_000_000) as i64;
402 self.nanos %= 1_000_000_000;
403 }
404 self
405 }
406
407 pub fn to_iso_string(&self) -> String {
409 let mut out = String::from("P");
410 if self.months != 0 {
411 out.push_str(&format!("{}M", self.months));
412 }
413 if self.days != 0 {
414 out.push_str(&format!("{}D", self.days));
415 }
416 if self.seconds != 0 || self.nanos != 0 {
417 out.push('T');
418 if self.nanos != 0 {
419 let frac = format!("{:09}", self.nanos.unsigned_abs());
420 let frac = frac.trim_end_matches('0');
421 out.push_str(&format!("{}.{}S", self.seconds, frac));
422 } else {
423 out.push_str(&format!("{}S", self.seconds));
424 }
425 }
426 if out == "P" {
427 out.push_str("T0S");
428 }
429 out
430 }
431
432 pub fn checked_add(&self, other: &Self) -> Option<Self> {
433 Some(
434 Self {
435 months: self.months.checked_add(other.months)?,
436 days: self.days.checked_add(other.days)?,
437 seconds: self.seconds.checked_add(other.seconds)?,
438 nanos: self.nanos.checked_add(other.nanos)?,
439 }
440 .carry_nanos(),
441 )
442 }
443
444 pub fn negated(&self) -> Self {
445 Self {
446 months: -self.months,
447 days: -self.days,
448 seconds: -self.seconds,
449 nanos: -self.nanos,
450 }
451 }
452}
453
454fn parse_fractional_seconds(tok: &str) -> Option<(i64, i32)> {
456 match tok.split_once('.') {
457 None => Some((tok.parse().ok()?, 0)),
458 Some((whole, frac)) => {
459 if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) {
460 return None;
461 }
462 let secs: i64 = if whole.is_empty() || whole == "-" {
463 0
464 } else {
465 whole.parse().ok()?
466 };
467 let frac9: String = frac.chars().chain(std::iter::repeat('0')).take(9).collect();
468 let mut nanos: i32 = frac9.parse().ok()?;
469 if whole.starts_with('-') {
470 nanos = -nanos;
471 }
472 Some((secs, nanos))
473 }
474 }
475}
476
477impl fmt::Display for Duration {
478 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
479 f.write_str(&self.to_iso_string())
480 }
481}
482
483impl Serialize for Duration {
484 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
485 where
486 S: Serializer,
487 {
488 serializer.serialize_str(&self.to_iso_string())
489 }
490}
491
492impl<'de> Deserialize<'de> for Duration {
493 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
494 where
495 D: Deserializer<'de>,
496 {
497 let value = String::deserialize(deserializer)?;
498 Self::parse(value).map_err(de::Error::custom)
499 }
500}
501
502#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
503#[serde(tag = "type", content = "value", rename_all = "snake_case")]
504pub enum Value {
505 Null,
506 Bool(bool),
507 Int(i64),
508 Float(f64),
509 String(String),
510 DateTime(RfcDate),
513 Decimal(Decimal),
516 Duration(Duration),
518 StringArray(Vec<String>),
519 IntArray(Vec<i64>),
520 FloatArray(Vec<f64>),
521 Json(serde_json::Value),
522}
523
524impl Value {
525 pub fn datetime(value: impl Into<String>) -> Result<Self> {
529 RfcDate::parse(value).map(Self::DateTime)
530 }
531
532 pub fn decimal(value: impl AsRef<str>) -> Result<Self> {
534 Decimal::parse(value).map(Self::Decimal)
535 }
536
537 pub fn duration(value: impl AsRef<str>) -> Result<Self> {
539 Duration::parse(value).map(Self::Duration)
540 }
541
542 pub fn as_decimal(&self) -> Option<&Decimal> {
543 match self {
544 Self::Decimal(value) => Some(value),
545 _ => None,
546 }
547 }
548
549 pub fn as_duration(&self) -> Option<&Duration> {
550 match self {
551 Self::Duration(value) => Some(value),
552 _ => None,
553 }
554 }
555
556 pub fn as_str(&self) -> Option<&str> {
557 match self {
558 Self::String(value) => Some(value),
559 _ => None,
560 }
561 }
562
563 pub fn as_datetime(&self) -> Option<&str> {
564 match self {
565 Self::DateTime(value) => Some(value.as_str()),
566 _ => None,
567 }
568 }
569
570 pub fn as_string_array(&self) -> Option<&[String]> {
571 match self {
572 Self::StringArray(values) => Some(values),
573 _ => None,
574 }
575 }
576
577 pub fn as_int_array(&self) -> Option<&[i64]> {
578 match self {
579 Self::IntArray(values) => Some(values),
580 _ => None,
581 }
582 }
583
584 pub fn as_float_array(&self) -> Option<&[f64]> {
585 match self {
586 Self::FloatArray(values) => Some(values),
587 _ => None,
588 }
589 }
590
591 pub fn to_json(&self) -> serde_json::Value {
594 match self {
595 Self::Null => serde_json::Value::Null,
596 Self::Bool(value) => serde_json::Value::Bool(*value),
597 Self::Int(value) => serde_json::Value::from(*value),
598 Self::Float(value) => serde_json::Value::from(*value),
599 Self::String(value) => serde_json::Value::String(value.clone()),
600 Self::DateTime(value) => serde_json::Value::String(value.as_str().to_string()),
601 Self::Decimal(value) => serde_json::Value::String(value.to_canonical_string()),
602 Self::Duration(value) => serde_json::Value::String(value.to_iso_string()),
603 Self::StringArray(values) => serde_json::Value::from(values.clone()),
604 Self::IntArray(values) => serde_json::Value::from(values.clone()),
605 Self::FloatArray(values) => serde_json::Value::from(values.clone()),
606 Self::Json(value) => value.clone(),
607 }
608 }
609
610 pub fn from_json(value: serde_json::Value) -> Self {
614 if let serde_json::Value::Object(mapping) = &value
615 && mapping.contains_key("type")
616 && mapping.contains_key("value")
617 && let Ok(tagged) = serde_json::from_value(value.clone())
618 {
619 return tagged;
620 }
621 Self::from(value)
622 }
623}
624
625fn is_rfc3339_datetime(value: &str) -> bool {
627 let bytes = value.as_bytes();
628 if bytes.len() < 20 {
629 return false;
630 }
631 let digit = |i: usize| bytes[i].is_ascii_digit();
632 let all_digits = |range: std::ops::Range<usize>| range.clone().all(digit);
633 let pair = |i: usize| (bytes[i] - b'0') * 10 + (bytes[i + 1] - b'0');
634
635 if !(all_digits(0..4)
636 && bytes[4] == b'-'
637 && all_digits(5..7)
638 && bytes[7] == b'-'
639 && all_digits(8..10)
640 && bytes[10] == b'T'
641 && all_digits(11..13)
642 && bytes[13] == b':'
643 && all_digits(14..16)
644 && bytes[16] == b':'
645 && all_digits(17..19))
646 {
647 return false;
648 }
649 let year = bytes[0..4]
650 .iter()
651 .fold(0u16, |acc, digit| acc * 10 + u16::from(digit - b'0'));
652 let month = pair(5);
653 let day = pair(8);
654 let leap_year = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
655 let max_day = match month {
656 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
657 4 | 6 | 9 | 11 => 30,
658 2 if leap_year => 29,
659 2 => 28,
660 _ => return false,
661 };
662 if !(day >= 1 && day <= max_day && pair(11) < 24 && pair(14) < 60 && pair(17) <= 60) {
663 return false;
664 }
665
666 let mut i = 19;
667 if bytes[i] == b'.' {
668 let start = i + 1;
669 i = start;
670 while i < bytes.len() && bytes[i].is_ascii_digit() {
671 i += 1;
672 }
673 if i == start {
674 return false;
675 }
676 }
677 match bytes.get(i) {
678 Some(b'Z') => i + 1 == bytes.len(),
679 Some(b'+' | b'-') => {
680 i + 6 == bytes.len()
681 && all_digits(i + 1..i + 3)
682 && bytes[i + 3] == b':'
683 && all_digits(i + 4..i + 6)
684 && pair(i + 1) < 24
685 && pair(i + 4) < 60
686 }
687 _ => false,
688 }
689}
690
691impl From<String> for Value {
692 fn from(value: String) -> Self {
693 Self::String(value)
694 }
695}
696
697impl From<&str> for Value {
698 fn from(value: &str) -> Self {
699 Self::String(value.to_string())
700 }
701}
702
703impl From<&String> for Value {
704 fn from(value: &String) -> Self {
705 Self::String(value.clone())
706 }
707}
708
709impl From<Vec<String>> for Value {
710 fn from(value: Vec<String>) -> Self {
711 Self::StringArray(value)
712 }
713}
714
715impl From<Vec<i64>> for Value {
716 fn from(value: Vec<i64>) -> Self {
717 Self::IntArray(value)
718 }
719}
720
721impl From<Vec<f64>> for Value {
722 fn from(value: Vec<f64>) -> Self {
723 Self::FloatArray(value)
724 }
725}
726
727impl From<bool> for Value {
728 fn from(value: bool) -> Self {
729 Self::Bool(value)
730 }
731}
732
733impl From<i64> for Value {
734 fn from(value: i64) -> Self {
735 Self::Int(value)
736 }
737}
738
739impl From<i32> for Value {
740 fn from(value: i32) -> Self {
741 Self::Int(i64::from(value))
742 }
743}
744
745impl From<usize> for Value {
746 fn from(value: usize) -> Self {
747 Self::Int(value as i64)
748 }
749}
750
751impl From<f64> for Value {
752 fn from(value: f64) -> Self {
753 Self::Float(value)
754 }
755}
756
757impl From<serde_json::Value> for Value {
758 fn from(value: serde_json::Value) -> Self {
759 match value {
760 serde_json::Value::Null => Self::Null,
761 serde_json::Value::Bool(value) => Self::Bool(value),
762 serde_json::Value::Number(value) => {
763 if let Some(value) = value.as_i64() {
764 Self::Int(value)
765 } else if let Some(value) = value.as_f64() {
766 Self::Float(value)
767 } else {
768 Self::Json(serde_json::Value::Number(value))
769 }
770 }
771 serde_json::Value::String(value) => Self::String(value),
772 serde_json::Value::Array(values) => {
773 let ints = values
774 .iter()
775 .filter_map(serde_json::Value::as_i64)
776 .collect::<Vec<_>>();
777 if !values.is_empty() && ints.len() == values.len() {
778 return Self::IntArray(ints);
779 }
780 let floats = values
781 .iter()
782 .filter_map(serde_json::Value::as_f64)
783 .collect::<Vec<_>>();
784 if !values.is_empty() && floats.len() == values.len() {
785 return Self::FloatArray(floats);
786 }
787 let strings = values
788 .iter()
789 .filter_map(|value| value.as_str().map(ToString::to_string))
790 .collect::<Vec<_>>();
791 if strings.len() == values.len() {
792 Self::StringArray(strings)
793 } else {
794 Self::Json(serde_json::Value::Array(values))
795 }
796 }
797 serde_json::Value::Object(value) => Self::Json(serde_json::Value::Object(value)),
798 }
799 }
800}
801
802#[cfg(feature = "typed-garde")]
803pub mod typed {
804 use serde::{Serialize, de::DeserializeOwned};
805
806 use crate::{
807 Edge, EdgeId, Graph, GraphBuilder, GrustError, Node, NodeId, Props, PutOutcome, Result,
808 Value,
809 };
810
811 pub use garde;
812 #[cfg(feature = "typed-zod-rs")]
813 pub use zod_rs;
814
815 pub trait TypedNode: garde::Validate + Serialize {
816 const LABEL: &'static str;
817
818 fn node_id(&self) -> NodeId;
819
820 fn node_props(&self) -> Result<Props> {
821 props_from_serialize(self)
822 }
823
824 fn from_node(node: &Node) -> Result<Self>
825 where
826 Self: Sized + DeserializeOwned,
827 Self::Context: Default,
828 {
829 let ctx = Self::Context::default();
830 Self::from_node_with(node, &ctx)
831 }
832
833 fn from_node_with(node: &Node, ctx: &Self::Context) -> Result<Self>
834 where
835 Self: Sized + DeserializeOwned,
836 {
837 if node.label.as_str() != Self::LABEL {
838 return Err(GrustError::Schema(format!(
839 "node '{}' has label '{}', expected '{}'",
840 node.id.as_str(),
841 node.label.as_str(),
842 Self::LABEL
843 )));
844 }
845 let typed: Self =
846 serde_json::from_value(props_to_plain_json(&node.props)).map_err(|err| {
847 GrustError::Serialization(format!("typed node decode error: {err}"))
848 })?;
849 typed
850 .validate_with(ctx)
851 .map_err(|err| validation_error(Self::LABEL, err))?;
852 let typed_id = typed.node_id();
853 if typed_id != node.id {
854 return Err(GrustError::Schema(format!(
855 "typed node '{}' decoded id '{}', expected '{}'",
856 Self::LABEL,
857 typed_id.as_str(),
858 node.id.as_str()
859 )));
860 }
861 Ok(typed)
862 }
863 }
864
865 pub trait TypedEdge: garde::Validate + Serialize {
866 const LABEL: &'static str;
867
868 fn source_node_id(&self) -> NodeId;
869
870 fn target_node_id(&self) -> NodeId;
871
872 fn edge_id(&self) -> Option<EdgeId> {
873 None
874 }
875
876 fn edge_props(&self) -> Result<Props> {
877 props_from_serialize(self)
878 }
879
880 fn from_edge(edge: &Edge) -> Result<Self>
881 where
882 Self: Sized + DeserializeOwned,
883 Self::Context: Default,
884 {
885 let ctx = Self::Context::default();
886 Self::from_edge_with(edge, &ctx)
887 }
888
889 fn from_edge_with(edge: &Edge, ctx: &Self::Context) -> Result<Self>
890 where
891 Self: Sized + DeserializeOwned,
892 {
893 if edge.label.as_str() != Self::LABEL {
894 return Err(GrustError::Schema(format!(
895 "edge from '{}' to '{}' has label '{}', expected '{}'",
896 edge.from.as_str(),
897 edge.to.as_str(),
898 edge.label.as_str(),
899 Self::LABEL
900 )));
901 }
902 let typed: Self =
903 serde_json::from_value(props_to_plain_json(&edge.props)).map_err(|err| {
904 GrustError::Serialization(format!("typed edge decode error: {err}"))
905 })?;
906 typed
907 .validate_with(ctx)
908 .map_err(|err| validation_error(Self::LABEL, err))?;
909 if typed.source_node_id() != edge.from || typed.target_node_id() != edge.to {
910 return Err(GrustError::Schema(format!(
911 "typed edge '{}' decoded endpoints '{}' -> '{}', expected '{}' -> '{}'",
912 Self::LABEL,
913 typed.source_node_id().as_str(),
914 typed.target_node_id().as_str(),
915 edge.from.as_str(),
916 edge.to.as_str()
917 )));
918 }
919 if let Some(decoded_id) = typed.edge_id()
920 && edge
921 .id
922 .as_ref()
923 .is_some_and(|edge_id| edge_id != &decoded_id)
924 {
925 return Err(GrustError::Schema(format!(
926 "typed edge '{}' decoded id '{}', expected '{}'",
927 Self::LABEL,
928 decoded_id.as_str(),
929 edge.id.as_ref().expect("edge id checked").as_str()
930 )));
931 }
932 Ok(typed)
933 }
934 }
935
936 #[derive(Clone, Debug, Default)]
937 pub struct TypedGraphBuilder {
938 builder: GraphBuilder,
939 }
940
941 impl TypedGraphBuilder {
942 pub fn new() -> Self {
943 Self::default()
944 }
945
946 pub fn from_builder(builder: GraphBuilder) -> Self {
947 Self { builder }
948 }
949
950 pub fn from_graph(graph: Graph) -> Self {
951 let mut builder = GraphBuilder::new();
952 for node in graph.nodes {
953 builder.add_node(node);
954 }
955 for edge in graph.edges {
956 builder.add_edge(edge);
957 }
958 Self { builder }
959 }
960
961 pub fn add_raw_node(&mut self, node: Node) -> NodeId {
962 self.builder.add_node(node)
963 }
964
965 pub fn add_raw_edge(&mut self, edge: Edge) -> PutOutcome {
966 self.builder.add_edge(edge)
967 }
968
969 pub fn add_node<T>(&mut self, node: &T) -> Result<NodeId>
970 where
971 T: TypedNode,
972 T::Context: Default,
973 {
974 node.validate()
975 .map_err(|err| validation_error(T::LABEL, err))?;
976 self.add_validated_node(node)
977 }
978
979 pub fn add_node_with<T>(&mut self, node: &T, ctx: &T::Context) -> Result<NodeId>
980 where
981 T: TypedNode,
982 {
983 node.validate_with(ctx)
984 .map_err(|err| validation_error(T::LABEL, err))?;
985 self.add_validated_node(node)
986 }
987
988 pub fn add_edge<T>(&mut self, edge: &T) -> Result<PutOutcome>
989 where
990 T: TypedEdge,
991 T::Context: Default,
992 {
993 edge.validate()
994 .map_err(|err| validation_error(T::LABEL, err))?;
995 self.add_validated_edge(edge)
996 }
997
998 pub fn add_edge_with<T>(&mut self, edge: &T, ctx: &T::Context) -> Result<PutOutcome>
999 where
1000 T: TypedEdge,
1001 {
1002 edge.validate_with(ctx)
1003 .map_err(|err| validation_error(T::LABEL, err))?;
1004 self.add_validated_edge(edge)
1005 }
1006
1007 #[cfg(feature = "typed-zod-rs")]
1008 pub fn add_node_from_json<T, S>(
1009 &mut self,
1010 schema: &S,
1011 value: &serde_json::Value,
1012 ) -> Result<NodeId>
1013 where
1014 T: TypedNode + DeserializeOwned,
1015 T::Context: Default,
1016 S: zod_rs::Schema<serde_json::Value>,
1017 {
1018 let node = parse_typed_json::<T, S>(schema, value)?;
1019 self.add_validated_node(&node)
1020 }
1021
1022 #[cfg(feature = "typed-zod-rs")]
1023 pub fn add_node_from_json_with<T, S>(
1024 &mut self,
1025 schema: &S,
1026 value: &serde_json::Value,
1027 ctx: &T::Context,
1028 ) -> Result<NodeId>
1029 where
1030 T: TypedNode + DeserializeOwned,
1031 S: zod_rs::Schema<serde_json::Value>,
1032 {
1033 let node = parse_typed_json_with::<T, S>(schema, value, ctx)?;
1034 self.add_validated_node(&node)
1035 }
1036
1037 #[cfg(feature = "typed-zod-rs")]
1038 pub fn add_edge_from_json<T, S>(
1039 &mut self,
1040 schema: &S,
1041 value: &serde_json::Value,
1042 ) -> Result<PutOutcome>
1043 where
1044 T: TypedEdge + DeserializeOwned,
1045 T::Context: Default,
1046 S: zod_rs::Schema<serde_json::Value>,
1047 {
1048 let edge = parse_typed_json::<T, S>(schema, value)?;
1049 self.add_validated_edge(&edge)
1050 }
1051
1052 #[cfg(feature = "typed-zod-rs")]
1053 pub fn add_edge_from_json_with<T, S>(
1054 &mut self,
1055 schema: &S,
1056 value: &serde_json::Value,
1057 ctx: &T::Context,
1058 ) -> Result<PutOutcome>
1059 where
1060 T: TypedEdge + DeserializeOwned,
1061 S: zod_rs::Schema<serde_json::Value>,
1062 {
1063 let edge = parse_typed_json_with::<T, S>(schema, value, ctx)?;
1064 self.add_validated_edge(&edge)
1065 }
1066
1067 #[must_use = "discarding this means the typed graph was not built"]
1068 pub fn build(self) -> Graph {
1069 self.builder.build()
1070 }
1071
1072 pub fn into_builder(self) -> GraphBuilder {
1073 self.builder
1074 }
1075
1076 fn add_validated_node<T>(&mut self, node: &T) -> Result<NodeId>
1077 where
1078 T: TypedNode,
1079 {
1080 let node_id = node.node_id();
1081 let mut props = node.node_props()?;
1082 props
1083 .entry("id".to_string())
1084 .or_insert_with(|| Value::from(node_id.as_str()));
1085 let graph_node = Node::new(T::LABEL, node_id, props);
1086 Ok(self.builder.add_node(graph_node))
1087 }
1088
1089 fn add_validated_edge<T>(&mut self, edge: &T) -> Result<PutOutcome>
1090 where
1091 T: TypedEdge,
1092 {
1093 let mut graph_edge = Edge::new(
1094 T::LABEL,
1095 edge.source_node_id(),
1096 edge.target_node_id(),
1097 edge.edge_props()?,
1098 );
1099 graph_edge.id = edge.edge_id();
1100 Ok(self.builder.add_edge(graph_edge))
1101 }
1102 }
1103
1104 pub fn props_from_serialize<T>(value: &T) -> Result<Props>
1105 where
1106 T: Serialize + ?Sized,
1107 {
1108 let serialized = serde_json::to_value(value)
1109 .map_err(|err| GrustError::Serialization(format!("typed props error: {err}")))?;
1110 let serde_json::Value::Object(fields) = serialized else {
1111 return Err(GrustError::Schema(
1112 "typed graph values must serialize as JSON objects".to_string(),
1113 ));
1114 };
1115
1116 Ok(fields
1117 .into_iter()
1118 .map(|(key, value)| (key, Value::from(value)))
1119 .collect())
1120 }
1121
1122 fn props_to_plain_json(props: &Props) -> serde_json::Value {
1123 serde_json::Value::Object(
1124 props
1125 .iter()
1126 .map(|(key, value)| (key.clone(), value.to_json()))
1127 .collect(),
1128 )
1129 }
1130
1131 #[cfg(feature = "typed-zod-rs")]
1132 pub fn parse_typed_json<T, S>(schema: &S, value: &serde_json::Value) -> Result<T>
1133 where
1134 T: DeserializeOwned + garde::Validate,
1135 T::Context: Default,
1136 S: zod_rs::Schema<serde_json::Value>,
1137 {
1138 let ctx = T::Context::default();
1139 parse_typed_json_with(schema, value, &ctx)
1140 }
1141
1142 #[cfg(feature = "typed-zod-rs")]
1143 pub fn parse_typed_json_with<T, S>(
1144 schema: &S,
1145 value: &serde_json::Value,
1146 ctx: &T::Context,
1147 ) -> Result<T>
1148 where
1149 T: DeserializeOwned + garde::Validate,
1150 S: zod_rs::Schema<serde_json::Value>,
1151 {
1152 schema
1153 .safe_parse(value)
1154 .map_err(|err| GrustError::Schema(format!("zod-rs validation failed: {err}")))?;
1155 let typed: T = serde_json::from_value(value.clone())
1156 .map_err(|err| GrustError::Serialization(format!("typed JSON decode error: {err}")))?;
1157 typed
1158 .validate_with(ctx)
1159 .map_err(|err| GrustError::Schema(format!("typed validation failed: {err}")))?;
1160 Ok(typed)
1161 }
1162
1163 fn validation_error(label: &str, err: garde::Report) -> GrustError {
1164 GrustError::Schema(format!("{label} validation failed: {err}"))
1165 }
1166}
1167
1168#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1169pub struct Node {
1170 pub id: NodeId,
1171 pub label: Label,
1172 pub props: Props,
1173}
1174
1175impl Node {
1176 pub fn new(label: impl Into<Label>, id: impl Into<NodeId>, props: impl Into<Props>) -> Self {
1177 let id = id.into();
1178 let mut props = props.into();
1179 props
1180 .entry("id".to_string())
1181 .or_insert_with(|| Value::from(id.as_str()));
1182 Self {
1183 id,
1184 label: label.into(),
1185 props,
1186 }
1187 }
1188}
1189
1190#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1191pub struct Edge {
1192 pub id: Option<EdgeId>,
1193 pub from: NodeId,
1194 pub to: NodeId,
1195 pub label: Label,
1196 pub props: Props,
1197}
1198
1199impl Edge {
1200 pub fn new(
1201 label: impl Into<Label>,
1202 from: impl Into<NodeId>,
1203 to: impl Into<NodeId>,
1204 props: impl Into<Props>,
1205 ) -> Self {
1206 Self {
1207 id: None,
1208 from: from.into(),
1209 to: to.into(),
1210 label: label.into(),
1211 props: props.into(),
1212 }
1213 }
1214
1215 pub fn with_id(mut self, id: impl Into<EdgeId>) -> Self {
1216 self.id = Some(id.into());
1217 self
1218 }
1219}
1220
1221pub fn relationship_type(value: &str) -> String {
1226 let relationship = value
1227 .chars()
1228 .map(|ch| {
1229 if ch.is_ascii_alphanumeric() {
1230 ch.to_ascii_uppercase()
1231 } else {
1232 '_'
1233 }
1234 })
1235 .collect::<String>();
1236 if relationship.is_empty() {
1237 "RELATED_TO".to_string()
1238 } else {
1239 relationship
1240 }
1241}
1242
1243pub fn schema_identifier(value: &str) -> Result<String> {
1248 let identifier = value
1249 .chars()
1250 .map(|ch| {
1251 if ch.is_ascii_alphanumeric() {
1252 ch.to_ascii_lowercase()
1253 } else {
1254 '_'
1255 }
1256 })
1257 .collect::<String>();
1258 if identifier.is_empty()
1259 || identifier
1260 .chars()
1261 .next()
1262 .is_some_and(|ch| ch.is_ascii_digit())
1263 {
1264 return Err(GrustError::Schema(format!(
1265 "invalid schema identifier '{value}'"
1266 )));
1267 }
1268 Ok(identifier)
1269}
1270
1271pub fn edge_key(edge: &Edge) -> String {
1277 edge.id
1278 .as_ref()
1279 .map(EdgeId::as_str)
1280 .map(ToString::to_string)
1281 .unwrap_or_else(|| {
1282 let from = edge.from.as_str();
1283 let label = edge.label.as_str();
1284 let to = edge.to.as_str();
1285 let mut key = String::with_capacity(from.len() + label.len() + to.len() + 2);
1286 key.push_str(from);
1287 key.push('\u{1f}');
1288 key.push_str(label);
1289 key.push('\u{1f}');
1290 key.push_str(to);
1291 key
1292 })
1293}
1294
1295#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1296pub struct Graph {
1297 pub nodes: Vec<Node>,
1298 pub edges: Vec<Edge>,
1299}
1300
1301impl Graph {
1302 pub fn new(nodes: Vec<Node>, edges: Vec<Edge>) -> Self {
1303 Self { nodes, edges }
1304 }
1305
1306 pub fn from_yaml(yaml: &str) -> Result<Self> {
1307 yaml::graph_from_yaml(yaml)
1308 }
1309
1310 pub fn to_yaml(&self) -> Result<String> {
1311 yaml::graph_to_yaml(self)
1312 }
1313
1314 pub fn from_json(json: &str) -> Result<Self> {
1315 json::graph_from_json(json)
1316 }
1317
1318 pub fn to_json(&self) -> Result<String> {
1319 json::graph_to_json(self)
1320 }
1321
1322 pub fn from_xml(xml: &str) -> Result<Self> {
1323 xml::graph_from_xml(xml)
1324 }
1325
1326 pub fn to_xml(&self) -> Result<String> {
1327 xml::graph_to_xml(self)
1328 }
1329
1330 pub fn builder() -> GraphBuilder {
1331 GraphBuilder::new()
1332 }
1333}
1334
1335#[derive(Clone, Debug, PartialEq)]
1342pub struct GraphIndex {
1343 vertex_by_id: HashMap<NodeId, usize>,
1344 outgoing_by_vertex: Vec<Vec<usize>>,
1345 incoming_by_vertex: Vec<Vec<usize>>,
1346 edge_endpoints: Vec<(usize, usize)>,
1347}
1348
1349impl GraphIndex {
1350 pub fn new(graph: &Graph) -> Result<Self> {
1351 let mut vertex_by_id = HashMap::with_capacity(graph.nodes.len());
1352 for (index, vertex) in graph.nodes.iter().enumerate() {
1353 if vertex_by_id.insert(vertex.id.clone(), index).is_some() {
1354 return Err(GrustError::Schema(format!(
1355 "duplicate vertex id '{}'",
1356 vertex.id.as_str()
1357 )));
1358 }
1359 }
1360
1361 let mut outgoing_by_vertex = vec![Vec::<usize>::new(); graph.nodes.len()];
1362 let mut incoming_by_vertex = vec![Vec::<usize>::new(); graph.nodes.len()];
1363 let mut edge_endpoints = Vec::<(usize, usize)>::with_capacity(graph.edges.len());
1364
1365 for (edge_index, edge) in graph.edges.iter().enumerate() {
1366 let Some(&from_index) = vertex_by_id.get(&edge.from) else {
1367 return Err(GrustError::Schema(format!(
1368 "edge source '{}' is not present in vertices",
1369 edge.from.as_str()
1370 )));
1371 };
1372 let Some(&to_index) = vertex_by_id.get(&edge.to) else {
1373 return Err(GrustError::Schema(format!(
1374 "edge destination '{}' is not present in vertices",
1375 edge.to.as_str()
1376 )));
1377 };
1378
1379 outgoing_by_vertex[from_index].push(edge_index);
1380 incoming_by_vertex[to_index].push(edge_index);
1381 edge_endpoints.push((from_index, to_index));
1382 }
1383
1384 Ok(Self {
1385 vertex_by_id,
1386 outgoing_by_vertex,
1387 incoming_by_vertex,
1388 edge_endpoints,
1389 })
1390 }
1391
1392 pub fn vertex_index(&self, id: &NodeId) -> Option<usize> {
1393 self.vertex_by_id.get(id).copied()
1394 }
1395
1396 pub fn require_vertex_index(&self, id: &NodeId) -> Result<usize> {
1397 self.vertex_index(id)
1398 .ok_or_else(|| GrustError::Schema(format!("vertex '{}' is not present", id.as_str())))
1399 }
1400
1401 pub fn outgoing_edges(&self, id: &NodeId) -> &[usize] {
1402 self.vertex_index(id)
1403 .map(|index| self.outgoing_by_vertex(index))
1404 .unwrap_or(&[])
1405 }
1406
1407 pub fn incoming_edges(&self, id: &NodeId) -> &[usize] {
1408 self.vertex_index(id)
1409 .map(|index| self.incoming_by_vertex(index))
1410 .unwrap_or(&[])
1411 }
1412
1413 pub fn outgoing_by_vertex(&self, index: usize) -> &[usize] {
1414 self.outgoing_by_vertex
1415 .get(index)
1416 .map(Vec::as_slice)
1417 .unwrap_or(&[])
1418 }
1419
1420 pub fn incoming_by_vertex(&self, index: usize) -> &[usize] {
1421 self.incoming_by_vertex
1422 .get(index)
1423 .map(Vec::as_slice)
1424 .unwrap_or(&[])
1425 }
1426
1427 pub fn edge_endpoints(&self, edge_index: usize) -> (usize, usize) {
1428 self.edge_endpoints[edge_index]
1429 }
1430
1431 pub fn edge_endpoints_slice(&self) -> &[(usize, usize)] {
1432 &self.edge_endpoints
1433 }
1434
1435 pub fn out_degree(&self, index: usize) -> usize {
1436 self.outgoing_by_vertex[index].len()
1437 }
1438
1439 pub fn in_degree(&self, index: usize) -> usize {
1440 self.incoming_by_vertex[index].len()
1441 }
1442
1443 pub fn degree(&self, index: usize) -> usize {
1444 self.in_degree(index) + self.out_degree(index)
1445 }
1446}
1447
1448mod graph_doc {
1449 use std::collections::{BTreeMap, BTreeSet};
1450
1451 use serde::{Deserialize, Serialize};
1452
1453 use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
1454
1455 #[derive(Debug, Serialize, Deserialize)]
1456 pub(super) struct GraphDoc {
1457 #[serde(default)]
1458 pub(super) nodes: Vec<NodeDoc>,
1459 #[serde(default)]
1460 pub(super) edges: Vec<EdgeDoc>,
1461 }
1462
1463 #[derive(Debug, Serialize, Deserialize)]
1464 pub(super) struct NodeDoc {
1465 pub(super) id: NodeId,
1466 pub(super) label: Label,
1467 #[serde(default, deserialize_with = "deserialize_props")]
1468 pub(super) props: Props,
1469 }
1470
1471 #[derive(Debug, Serialize, Deserialize)]
1472 pub(super) struct NodeDocOut {
1473 pub(super) id: NodeId,
1474 pub(super) label: Label,
1475 #[serde(default)]
1476 pub(super) props: Props,
1477 }
1478
1479 #[derive(Debug, Serialize, Deserialize)]
1480 pub(super) struct EdgeDoc {
1481 #[serde(default)]
1482 pub(super) id: Option<EdgeId>,
1483 pub(super) label: Label,
1484 pub(super) from: NodeId,
1485 pub(super) to: NodeId,
1486 #[serde(default, deserialize_with = "deserialize_props")]
1487 pub(super) props: Props,
1488 }
1489
1490 #[derive(Debug, Serialize, Deserialize)]
1491 pub(super) struct EdgeDocOut {
1492 #[serde(default)]
1493 pub(super) id: Option<EdgeId>,
1494 pub(super) label: Label,
1495 pub(super) from: NodeId,
1496 pub(super) to: NodeId,
1497 #[serde(default)]
1498 pub(super) props: Props,
1499 }
1500
1501 pub(super) fn graph_from_doc(doc: GraphDoc) -> super::Result<Graph> {
1502 let mut ids = BTreeSet::new();
1503 for node in &doc.nodes {
1504 if !ids.insert(node.id.clone()) {
1505 return Err(GrustError::Schema(format!(
1506 "duplicate node id '{}'",
1507 node.id
1508 )));
1509 }
1510 }
1511
1512 let mut edges = Vec::with_capacity(doc.edges.len());
1513 for edge in doc.edges {
1514 if !ids.contains(&edge.from) {
1515 return Err(GrustError::Schema(format!(
1516 "edge '{}' references unknown from node '{}'",
1517 edge.label, edge.from
1518 )));
1519 }
1520 if !ids.contains(&edge.to) {
1521 return Err(GrustError::Schema(format!(
1522 "edge '{}' references unknown to node '{}'",
1523 edge.label, edge.to
1524 )));
1525 }
1526
1527 let mut graph_edge = Edge::new(edge.label, edge.from, edge.to, edge.props);
1528 graph_edge.id = edge.id;
1529 edges.push(graph_edge);
1530 }
1531
1532 let nodes = doc
1533 .nodes
1534 .into_iter()
1535 .map(|node| Node::new(node.label, node.id, node.props))
1536 .collect();
1537
1538 Ok(Graph::new(nodes, edges))
1539 }
1540
1541 pub(super) fn graph_to_doc(graph: &Graph) -> GraphDocOut {
1542 GraphDocOut {
1543 nodes: graph
1544 .nodes
1545 .iter()
1546 .map(|node| NodeDocOut {
1547 id: node.id.clone(),
1548 label: node.label.clone(),
1549 props: without_generated_id(&node.props, &node.id),
1550 })
1551 .collect(),
1552 edges: graph
1553 .edges
1554 .iter()
1555 .map(|edge| EdgeDocOut {
1556 id: edge.id.clone(),
1557 label: edge.label.clone(),
1558 from: edge.from.clone(),
1559 to: edge.to.clone(),
1560 props: edge.props.clone(),
1561 })
1562 .collect(),
1563 }
1564 }
1565
1566 fn without_generated_id(props: &Props, id: &NodeId) -> Props {
1567 let mut props = props.clone();
1568 if props.get("id") == Some(&Value::from(id.as_str())) {
1569 props.remove("id");
1570 }
1571 props
1572 }
1573
1574 fn deserialize_props<'de, D>(deserializer: D) -> std::result::Result<Props, D::Error>
1575 where
1576 D: serde::Deserializer<'de>,
1577 {
1578 let raw = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1579 raw.into_iter()
1580 .map(|(key, value)| {
1581 value_from_json(value)
1582 .map(|value| (key, value))
1583 .map_err(serde::de::Error::custom)
1584 })
1585 .collect()
1586 }
1587
1588 fn value_from_json(value: serde_json::Value) -> std::result::Result<Value, String> {
1589 if let serde_json::Value::Object(mapping) = &value
1590 && mapping.contains_key("type")
1591 && mapping.contains_key("value")
1592 {
1593 return serde_json::from_value(value)
1594 .map_err(|err| format!("invalid tagged Grust value: {err}"));
1595 }
1596
1597 Ok(Value::from_json(value))
1598 }
1599
1600 #[derive(Debug, Serialize, Deserialize)]
1601 pub(super) struct GraphDocOut {
1602 pub(super) nodes: Vec<NodeDocOut>,
1603 pub(super) edges: Vec<EdgeDocOut>,
1604 }
1605}
1606
1607mod yaml {
1608 use crate::{Graph, GrustError};
1609
1610 pub(super) fn graph_from_yaml(yaml: &str) -> super::Result<Graph> {
1611 let doc: super::graph_doc::GraphDoc = serde_yaml::from_str(yaml)
1612 .map_err(|err| GrustError::Serialization(format!("YAML parse error: {err}")))?;
1613 super::graph_doc::graph_from_doc(doc)
1614 }
1615
1616 pub(super) fn graph_to_yaml(graph: &Graph) -> super::Result<String> {
1617 serde_yaml::to_string(&super::graph_doc::graph_to_doc(graph))
1618 .map_err(|err| GrustError::Serialization(format!("YAML serialization error: {err}")))
1619 }
1620}
1621
1622mod json {
1623 use crate::{Graph, GrustError};
1624
1625 pub(super) fn graph_from_json(json: &str) -> super::Result<Graph> {
1626 let doc: super::graph_doc::GraphDoc = serde_json::from_str(json)
1627 .map_err(|err| GrustError::Serialization(format!("JSON parse error: {err}")))?;
1628 super::graph_doc::graph_from_doc(doc)
1629 }
1630
1631 pub(super) fn graph_to_json(graph: &Graph) -> super::Result<String> {
1632 serde_json::to_string_pretty(&super::graph_doc::graph_to_doc(graph))
1633 .map_err(|err| GrustError::Serialization(format!("JSON serialization error: {err}")))
1634 }
1635}
1636
1637mod xml {
1638 use serde::{Deserialize, Serialize};
1639
1640 use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
1641
1642 #[derive(Debug, Serialize, Deserialize)]
1643 #[serde(rename = "graph")]
1644 struct GraphXml {
1645 #[serde(default)]
1646 nodes: NodesXml,
1647 #[serde(default)]
1648 edges: EdgesXml,
1649 }
1650
1651 #[derive(Debug, Default, Serialize, Deserialize)]
1652 struct NodesXml {
1653 #[serde(rename = "node", default)]
1654 items: Vec<NodeXml>,
1655 }
1656
1657 #[derive(Debug, Default, Serialize, Deserialize)]
1658 struct EdgesXml {
1659 #[serde(rename = "edge", default)]
1660 items: Vec<EdgeXml>,
1661 }
1662
1663 #[derive(Debug, Serialize, Deserialize)]
1664 struct NodeXml {
1665 id: NodeId,
1666 label: Label,
1667 #[serde(default)]
1668 props: PropsXml,
1669 }
1670
1671 #[derive(Debug, Serialize, Deserialize)]
1672 struct EdgeXml {
1673 #[serde(default, skip_serializing_if = "Option::is_none")]
1674 id: Option<EdgeId>,
1675 label: Label,
1676 from: NodeId,
1677 to: NodeId,
1678 #[serde(default)]
1679 props: PropsXml,
1680 }
1681
1682 #[derive(Debug, Default, Serialize, Deserialize)]
1683 struct PropsXml {
1684 #[serde(rename = "prop", default)]
1685 items: Vec<PropXml>,
1686 }
1687
1688 #[derive(Debug, Serialize, Deserialize)]
1689 struct PropXml {
1690 key: String,
1691 value: Value,
1692 }
1693
1694 pub(super) fn graph_from_xml(xml: &str) -> super::Result<Graph> {
1695 let doc: GraphXml = quick_xml::de::from_str(xml)
1696 .map_err(|err| GrustError::Serialization(format!("XML parse error: {err}")))?;
1697 super::graph_doc::graph_from_doc(doc.into())
1698 }
1699
1700 pub(super) fn graph_to_xml(graph: &Graph) -> super::Result<String> {
1701 quick_xml::se::to_string(&GraphXml::from(graph))
1702 .map_err(|err| GrustError::Serialization(format!("XML serialization error: {err}")))
1703 }
1704
1705 impl From<GraphXml> for super::graph_doc::GraphDoc {
1706 fn from(value: GraphXml) -> Self {
1707 Self {
1708 nodes: value.nodes.items.into_iter().map(Into::into).collect(),
1709 edges: value.edges.items.into_iter().map(Into::into).collect(),
1710 }
1711 }
1712 }
1713
1714 impl From<NodeXml> for super::graph_doc::NodeDoc {
1715 fn from(value: NodeXml) -> Self {
1716 Self {
1717 id: value.id,
1718 label: value.label,
1719 props: value.props.into(),
1720 }
1721 }
1722 }
1723
1724 impl From<EdgeXml> for super::graph_doc::EdgeDoc {
1725 fn from(value: EdgeXml) -> Self {
1726 Self {
1727 id: value.id,
1728 label: value.label,
1729 from: value.from,
1730 to: value.to,
1731 props: value.props.into(),
1732 }
1733 }
1734 }
1735
1736 impl From<PropsXml> for Props {
1737 fn from(value: PropsXml) -> Self {
1738 value
1739 .items
1740 .into_iter()
1741 .map(|prop| (prop.key, prop.value))
1742 .collect()
1743 }
1744 }
1745
1746 impl From<&Graph> for GraphXml {
1747 fn from(graph: &Graph) -> Self {
1748 Self {
1749 nodes: NodesXml {
1750 items: graph.nodes.iter().map(NodeXml::from).collect(),
1751 },
1752 edges: EdgesXml {
1753 items: graph.edges.iter().map(EdgeXml::from).collect(),
1754 },
1755 }
1756 }
1757 }
1758
1759 impl From<&Node> for NodeXml {
1760 fn from(node: &Node) -> Self {
1761 let props = super::graph_doc::graph_to_doc(&Graph::new(vec![node.clone()], Vec::new()))
1762 .nodes
1763 .into_iter()
1764 .next()
1765 .expect("node exists")
1766 .props;
1767 Self {
1768 id: node.id.clone(),
1769 label: node.label.clone(),
1770 props: props.into(),
1771 }
1772 }
1773 }
1774
1775 impl From<&Edge> for EdgeXml {
1776 fn from(edge: &Edge) -> Self {
1777 Self {
1778 id: edge.id.clone(),
1779 label: edge.label.clone(),
1780 from: edge.from.clone(),
1781 to: edge.to.clone(),
1782 props: edge.props.clone().into(),
1783 }
1784 }
1785 }
1786
1787 impl From<Props> for PropsXml {
1788 fn from(value: Props) -> Self {
1789 Self {
1790 items: value
1791 .into_iter()
1792 .map(|(key, value)| PropXml { key, value })
1793 .collect(),
1794 }
1795 }
1796 }
1797}
1798
1799#[derive(Clone, Debug, Default, Eq, PartialEq)]
1800pub enum EdgePolicy {
1801 AllowDuplicates,
1802 #[default]
1803 DedupeByFromLabelTo,
1804}
1805
1806#[derive(Clone, Debug, Default)]
1807pub struct GraphBuilder {
1808 nodes: BTreeMap<NodeId, Node>,
1809 edges: Vec<Edge>,
1810 edge_keys: BTreeSet<(NodeId, Label, NodeId)>,
1811 edge_policy: EdgePolicy,
1812}
1813
1814impl GraphBuilder {
1815 pub fn new() -> Self {
1816 Self::default()
1817 }
1818
1819 pub fn edge_policy(mut self, edge_policy: EdgePolicy) -> Self {
1820 self.edge_policy = edge_policy;
1821 self
1822 }
1823
1824 pub fn node<'a>(
1825 &'a mut self,
1826 label: impl Into<Label>,
1827 id: impl Into<NodeId>,
1828 ) -> NodeBuilder<'a> {
1829 NodeBuilder {
1830 builder: self,
1831 label: label.into(),
1832 id: id.into(),
1833 props: Props::new(),
1834 }
1835 }
1836
1837 pub fn edge<'a>(
1838 &'a mut self,
1839 label: impl Into<Label>,
1840 from: impl Into<NodeId>,
1841 to: impl Into<NodeId>,
1842 ) -> EdgeBuilder<'a> {
1843 EdgeBuilder {
1844 builder: self,
1845 id: None,
1846 label: label.into(),
1847 from: from.into(),
1848 to: to.into(),
1849 props: Props::new(),
1850 }
1851 }
1852
1853 pub fn add_node(&mut self, node: Node) -> NodeId {
1860 let id = node.id.clone();
1861 self.nodes
1862 .entry(id.clone())
1863 .and_modify(|existing| {
1864 if existing.label == node.label {
1865 existing.props.extend(node.props.clone());
1866 } else {
1867 *existing = node.clone();
1868 }
1869 })
1870 .or_insert(node);
1871 id
1872 }
1873
1874 pub fn add_edge(&mut self, edge: Edge) -> PutOutcome {
1877 match self.edge_policy {
1878 EdgePolicy::AllowDuplicates => {
1879 self.edges.push(edge);
1880 PutOutcome::Inserted
1881 }
1882 EdgePolicy::DedupeByFromLabelTo => {
1883 let key = (edge.from.clone(), edge.label.clone(), edge.to.clone());
1884 if self.edge_keys.insert(key) {
1885 self.edges.push(edge);
1886 PutOutcome::Inserted
1887 } else {
1888 PutOutcome::Deduped
1889 }
1890 }
1891 }
1892 }
1893
1894 #[must_use = "discarding this means the graph was not built"]
1895 pub fn build(self) -> Graph {
1896 Graph {
1897 nodes: self.nodes.into_values().collect(),
1898 edges: self.edges,
1899 }
1900 }
1901}
1902
1903pub struct NodeBuilder<'a> {
1904 builder: &'a mut GraphBuilder,
1905 label: Label,
1906 id: NodeId,
1907 props: Props,
1908}
1909
1910impl<'a> NodeBuilder<'a> {
1911 pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1912 self.props.insert(key.into(), value.into());
1913 self
1914 }
1915
1916 pub fn props(mut self, props: Props) -> Self {
1917 self.props.extend(props);
1918 self
1919 }
1920
1921 #[must_use = "discarding this means the node was not added to the builder"]
1922 pub fn finish(self) -> NodeId {
1923 let node = Node::new(self.label, self.id, self.props);
1924 self.builder.add_node(node)
1925 }
1926}
1927
1928pub struct EdgeBuilder<'a> {
1929 builder: &'a mut GraphBuilder,
1930 id: Option<EdgeId>,
1931 label: Label,
1932 from: NodeId,
1933 to: NodeId,
1934 props: Props,
1935}
1936
1937impl<'a> EdgeBuilder<'a> {
1938 pub fn id(mut self, id: impl Into<EdgeId>) -> Self {
1939 self.id = Some(id.into());
1940 self
1941 }
1942
1943 pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1944 self.props.insert(key.into(), value.into());
1945 self
1946 }
1947
1948 pub fn props(mut self, props: Props) -> Self {
1949 self.props.extend(props);
1950 self
1951 }
1952
1953 #[must_use = "discarding this means the edge was not added to the builder"]
1954 pub fn finish(self) -> PutOutcome {
1955 let mut edge = Edge::new(self.label, self.from, self.to, self.props);
1956 edge.id = self.id;
1957 self.builder.add_edge(edge)
1958 }
1959}
1960
1961#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1962pub struct GraphSchema {
1963 pub nodes: Vec<NodeType>,
1964 pub edges: Vec<EdgeType>,
1965 pub constraints: Vec<GraphConstraint>,
1966}
1967
1968impl GraphSchema {
1969 pub fn builder() -> GraphSchemaBuilder {
1970 GraphSchemaBuilder::default()
1971 }
1972
1973 pub fn node_type(&self, label: &Label) -> Option<&NodeType> {
1974 self.nodes
1975 .iter()
1976 .find(|node_type| &node_type.label == label)
1977 }
1978
1979 pub fn edge_type(&self, label: &Label) -> Option<&EdgeType> {
1980 self.edges
1981 .iter()
1982 .find(|edge_type| &edge_type.label == label)
1983 }
1984
1985 pub fn constraints_for_label(&self, label: &Label) -> Vec<&GraphConstraint> {
1986 self.constraints
1987 .iter()
1988 .filter(|constraint| constraint.label() == label)
1989 .collect()
1990 }
1991
1992 pub fn validate_graph(&self, graph: &Graph) -> Result<()> {
1993 for node in &graph.nodes {
1994 self.validate_node(node)?;
1995 }
1996 let labels: BTreeMap<&NodeId, &Label> = graph
1997 .nodes
1998 .iter()
1999 .map(|node| (&node.id, &node.label))
2000 .collect();
2001 for edge in &graph.edges {
2002 self.validate_edge_with(edge, |id| labels.get(id).copied())?;
2003 }
2004 self.validate_edge_uniqueness(graph)?;
2005 self.validate_unique_property_constraints(graph)
2006 }
2007
2008 fn validate_edge_uniqueness(&self, graph: &Graph) -> Result<()> {
2011 let mut seen = BTreeSet::new();
2012 for edge in &graph.edges {
2013 let Some(edge_type) = self.edge_type(&edge.label) else {
2014 continue;
2015 };
2016 if edge_type.uniqueness == EdgeUniqueness::None {
2017 continue;
2018 }
2019 let (a, b) = if edge_type.directed || edge.from <= edge.to {
2020 (&edge.from, &edge.to)
2021 } else {
2022 (&edge.to, &edge.from)
2023 };
2024 if !seen.insert((edge.label.clone(), a.clone(), b.clone())) {
2025 return Err(GrustError::Schema(format!(
2026 "duplicate edge '{}' between '{}' and '{}' violates {:?} uniqueness",
2027 edge.label.as_str(),
2028 a.as_str(),
2029 b.as_str(),
2030 edge_type.uniqueness
2031 )));
2032 }
2033 }
2034 Ok(())
2035 }
2036
2037 fn validate_unique_property_constraints(&self, graph: &Graph) -> Result<()> {
2038 for constraint in &self.constraints {
2039 match constraint {
2040 GraphConstraint::NodePropertyUnique { label, key } => {
2041 let mut seen: Vec<(&NodeId, &Value)> = Vec::new();
2042 for node in graph.nodes.iter().filter(|node| &node.label == label) {
2043 let Some(value) = node.props.get(key) else {
2044 continue;
2045 };
2046 if let Some((existing_id, _)) =
2047 seen.iter().find(|(_, existing)| *existing == value)
2048 {
2049 return Err(GrustError::Schema(format!(
2050 "node '{}' with label '{}' duplicates unique constrained property '{}' from node '{}'",
2051 node.id.as_str(),
2052 label.as_str(),
2053 key,
2054 existing_id.as_str()
2055 )));
2056 }
2057 seen.push((&node.id, value));
2058 }
2059 }
2060 GraphConstraint::EdgePropertyUnique { label, key } => {
2061 let mut seen: Vec<(String, &Value)> = Vec::new();
2062 for edge in graph.edges.iter().filter(|edge| &edge.label == label) {
2063 let Some(value) = edge.props.get(key) else {
2064 continue;
2065 };
2066 if let Some((existing_key, _)) =
2067 seen.iter().find(|(_, existing)| *existing == value)
2068 {
2069 return Err(GrustError::Schema(format!(
2070 "edge '{}' duplicates unique constrained property '{}' from edge '{}'",
2071 edge_key(edge),
2072 key,
2073 existing_key
2074 )));
2075 }
2076 seen.push((edge_key(edge), value));
2077 }
2078 }
2079 GraphConstraint::NodePropertyRequired { .. }
2080 | GraphConstraint::EdgePropertyRequired { .. } => {}
2081 }
2082 }
2083 Ok(())
2084 }
2085
2086 pub fn validate_node(&self, node: &Node) -> Result<()> {
2087 let node_type = self.node_type(&node.label).ok_or_else(|| {
2088 GrustError::Schema(format!("schema has no node type '{}'", node.label.as_str()))
2089 })?;
2090 validate_props(
2091 &node.props,
2092 &node_type.fields,
2093 &format!("node '{}'", node.id.as_str()),
2094 )?;
2095 for constraint in &self.constraints {
2096 if let GraphConstraint::NodePropertyRequired { label, key } = constraint
2097 && label == &node.label
2098 && !node.props.contains_key(key)
2099 {
2100 return Err(GrustError::Schema(format!(
2101 "node '{}' with label '{}' is missing required constrained property '{}'",
2102 node.id.as_str(),
2103 node.label.as_str(),
2104 key
2105 )));
2106 }
2107 }
2108 Ok(())
2109 }
2110
2111 pub fn validate_edge(&self, edge: &Edge, graph: &Graph) -> Result<()> {
2112 self.validate_edge_with(edge, |id| {
2113 graph
2114 .nodes
2115 .iter()
2116 .find(|node| &node.id == id)
2117 .map(|node| &node.label)
2118 })
2119 }
2120
2121 pub fn validate_edge_with<'a>(
2124 &self,
2125 edge: &Edge,
2126 lookup: impl Fn(&NodeId) -> Option<&'a Label>,
2127 ) -> Result<()> {
2128 let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
2129 GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
2130 })?;
2131
2132 let from_label = lookup(&edge.from).ok_or_else(|| {
2133 GrustError::Schema(format!(
2134 "edge '{}' references unknown from node '{}'",
2135 edge.label.as_str(),
2136 edge.from.as_str()
2137 ))
2138 })?;
2139 let to_label = lookup(&edge.to).ok_or_else(|| {
2140 GrustError::Schema(format!(
2141 "edge '{}' references unknown to node '{}'",
2142 edge.label.as_str(),
2143 edge.to.as_str()
2144 ))
2145 })?;
2146
2147 let from_matches =
2148 |label: &Label| edge_type.from.is_empty() || edge_type.from.contains(label);
2149 let to_matches = |label: &Label| edge_type.to.is_empty() || edge_type.to.contains(label);
2150 let endpoints_ok = (from_matches(from_label) && to_matches(to_label))
2153 || (!edge_type.directed && from_matches(to_label) && to_matches(from_label));
2154 if !endpoints_ok {
2155 if !from_matches(from_label) {
2156 return Err(GrustError::Schema(format!(
2157 "edge '{}' cannot start from node label '{}'",
2158 edge.label.as_str(),
2159 from_label.as_str()
2160 )));
2161 }
2162 return Err(GrustError::Schema(format!(
2163 "edge '{}' cannot end at node label '{}'",
2164 edge.label.as_str(),
2165 to_label.as_str()
2166 )));
2167 }
2168
2169 validate_props(
2170 &edge.props,
2171 &edge_type.fields,
2172 &format!("edge '{}'", edge.label.as_str()),
2173 )?;
2174 self.validate_edge_required_constraints(edge)
2175 }
2176
2177 pub fn validate_edge_props(&self, edge: &Edge) -> Result<()> {
2181 let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
2182 GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
2183 })?;
2184 validate_props(
2185 &edge.props,
2186 &edge_type.fields,
2187 &format!("edge '{}'", edge.label.as_str()),
2188 )?;
2189 self.validate_edge_required_constraints(edge)
2190 }
2191
2192 fn validate_edge_required_constraints(&self, edge: &Edge) -> Result<()> {
2193 for constraint in &self.constraints {
2194 if let GraphConstraint::EdgePropertyRequired { label, key } = constraint
2195 && label == &edge.label
2196 && !edge.props.contains_key(key)
2197 {
2198 return Err(GrustError::Schema(format!(
2199 "edge '{}' from '{}' to '{}' is missing required constrained property '{}'",
2200 edge.label.as_str(),
2201 edge.from.as_str(),
2202 edge.to.as_str(),
2203 key
2204 )));
2205 }
2206 }
2207 Ok(())
2208 }
2209}
2210
2211#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2212pub struct NodeType {
2213 pub label: Label,
2214 pub fields: Vec<Field>,
2215}
2216
2217#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2218pub struct EdgeType {
2219 pub label: Label,
2220 pub from: Vec<Label>,
2221 pub to: Vec<Label>,
2222 pub fields: Vec<Field>,
2223 pub directed: bool,
2224 pub uniqueness: EdgeUniqueness,
2225}
2226
2227#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2228pub enum GraphConstraint {
2229 NodePropertyUnique { label: Label, key: String },
2230 NodePropertyRequired { label: Label, key: String },
2231 EdgePropertyUnique { label: Label, key: String },
2232 EdgePropertyRequired { label: Label, key: String },
2233}
2234
2235impl GraphConstraint {
2236 pub fn label(&self) -> &Label {
2237 match self {
2238 Self::NodePropertyUnique { label, .. }
2239 | Self::NodePropertyRequired { label, .. }
2240 | Self::EdgePropertyUnique { label, .. }
2241 | Self::EdgePropertyRequired { label, .. } => label,
2242 }
2243 }
2244
2245 pub fn key(&self) -> &str {
2246 match self {
2247 Self::NodePropertyUnique { key, .. }
2248 | Self::NodePropertyRequired { key, .. }
2249 | Self::EdgePropertyUnique { key, .. }
2250 | Self::EdgePropertyRequired { key, .. } => key,
2251 }
2252 }
2253}
2254
2255#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
2256pub enum GraphConstraintCapability {
2257 #[default]
2258 MetadataOnly,
2259 ValidateBeforeWrite,
2260 EnforcedByBackend,
2261}
2262
2263#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
2272pub enum GraphNativeConstraintCapability {
2273 #[default]
2275 Unsupported,
2276 NativeIndex,
2279 NativeConstraint,
2282}
2283
2284#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2285pub struct GraphNativeConstraintRequest {
2286 pub constraint: GraphConstraint,
2287 pub if_not_exists: bool,
2288}
2289
2290#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
2291pub struct GraphNativeConstraintReport {
2292 pub applied: usize,
2293 pub skipped: usize,
2294}
2295
2296#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2297pub struct Field {
2298 pub name: String,
2299 pub ty: FieldType,
2300 pub required: bool,
2301}
2302
2303#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2304pub enum FieldType {
2305 String,
2306 Int,
2307 Float,
2308 Bool,
2309 DateTime,
2310 StringArray,
2311 IntArray,
2312 FloatArray,
2313 Json,
2314}
2315
2316#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2324pub enum EdgeUniqueness {
2325 None,
2326 FromTo,
2327 FromLabelTo,
2328}
2329
2330#[derive(Clone, Debug, Default)]
2331pub struct GraphSchemaBuilder {
2332 nodes: Vec<NodeType>,
2333 edges: Vec<EdgeType>,
2334 constraints: Vec<GraphConstraint>,
2335}
2336
2337impl GraphSchemaBuilder {
2338 pub fn node(mut self, label: impl Into<Label>, fields: impl Into<Vec<Field>>) -> Self {
2339 self.nodes.push(NodeType {
2340 label: label.into(),
2341 fields: fields.into(),
2342 });
2343 self
2344 }
2345
2346 pub fn edge(
2347 mut self,
2348 label: impl Into<Label>,
2349 from: impl Into<Vec<Label>>,
2350 to: impl Into<Vec<Label>>,
2351 fields: impl Into<Vec<Field>>,
2352 ) -> Self {
2353 self.edges.push(EdgeType {
2354 label: label.into(),
2355 from: from.into(),
2356 to: to.into(),
2357 fields: fields.into(),
2358 directed: true,
2359 uniqueness: EdgeUniqueness::FromLabelTo,
2360 });
2361 self
2362 }
2363
2364 pub fn edge_type(mut self, edge_type: EdgeType) -> Self {
2365 self.edges.push(edge_type);
2366 self
2367 }
2368
2369 pub fn constraint(mut self, constraint: GraphConstraint) -> Self {
2370 self.constraints.push(constraint);
2371 self
2372 }
2373
2374 pub fn unique_node_property(self, label: impl Into<Label>, key: impl Into<String>) -> Self {
2375 self.constraint(GraphConstraint::NodePropertyUnique {
2376 label: label.into(),
2377 key: key.into(),
2378 })
2379 }
2380
2381 pub fn required_node_property(self, label: impl Into<Label>, key: impl Into<String>) -> Self {
2382 self.constraint(GraphConstraint::NodePropertyRequired {
2383 label: label.into(),
2384 key: key.into(),
2385 })
2386 }
2387
2388 pub fn unique_edge_property(self, label: impl Into<Label>, key: impl Into<String>) -> Self {
2389 self.constraint(GraphConstraint::EdgePropertyUnique {
2390 label: label.into(),
2391 key: key.into(),
2392 })
2393 }
2394
2395 pub fn required_edge_property(self, label: impl Into<Label>, key: impl Into<String>) -> Self {
2396 self.constraint(GraphConstraint::EdgePropertyRequired {
2397 label: label.into(),
2398 key: key.into(),
2399 })
2400 }
2401
2402 pub fn build(self) -> GraphSchema {
2403 GraphSchema {
2404 nodes: self.nodes,
2405 edges: self.edges,
2406 constraints: self.constraints,
2407 }
2408 }
2409}
2410
2411impl Field {
2412 pub fn required(name: impl Into<String>, ty: FieldType) -> Self {
2413 Self {
2414 name: name.into(),
2415 ty,
2416 required: true,
2417 }
2418 }
2419
2420 pub fn optional(name: impl Into<String>, ty: FieldType) -> Self {
2421 Self {
2422 name: name.into(),
2423 ty,
2424 required: false,
2425 }
2426 }
2427}
2428
2429fn validate_props(props: &Props, fields: &[Field], context: &str) -> Result<()> {
2430 for field in fields {
2431 match props.get(&field.name) {
2432 Some(value) => validate_field_value(value, &field.ty, context, &field.name)?,
2433 None if field.required => {
2434 return Err(GrustError::Schema(format!(
2435 "{context} missing required field '{}'",
2436 field.name
2437 )));
2438 }
2439 None => {}
2440 }
2441 }
2442 Ok(())
2443}
2444
2445fn validate_field_value(
2446 value: &Value,
2447 ty: &FieldType,
2448 context: &str,
2449 field_name: &str,
2450) -> Result<()> {
2451 let matches = match (value, ty) {
2452 (Value::String(_), FieldType::String)
2453 | (Value::Int(_), FieldType::Int)
2454 | (Value::Float(_), FieldType::Float)
2455 | (Value::Bool(_), FieldType::Bool)
2456 | (Value::DateTime(_), FieldType::DateTime)
2457 | (Value::StringArray(_), FieldType::StringArray)
2458 | (Value::IntArray(_), FieldType::IntArray)
2459 | (Value::FloatArray(_), FieldType::FloatArray)
2460 | (_, FieldType::Json) => true,
2461 (Value::String(value), FieldType::DateTime) => is_rfc3339_datetime(value),
2464 _ => false,
2465 };
2466 if matches {
2467 Ok(())
2468 } else {
2469 Err(GrustError::Schema(format!(
2470 "{context} field '{field_name}' expected {ty:?}, got {value:?}"
2471 )))
2472 }
2473}
2474
2475#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2476pub struct Traversal {
2477 pub start: Start,
2478 pub steps: Vec<Step>,
2479 pub limit: Option<u32>,
2480}
2481
2482impl Traversal {
2483 pub fn from_node(id: impl Into<NodeId>) -> Self {
2484 Self {
2485 start: Start::Node(id.into()),
2486 steps: Vec::new(),
2487 limit: None,
2488 }
2489 }
2490
2491 pub fn out(mut self, edge: impl Into<Label>) -> Self {
2492 self.steps.push(Step {
2493 direction: Direction::Out,
2494 edge: Some(edge.into()),
2495 node: None,
2496 });
2497 self
2498 }
2499
2500 pub fn in_(mut self, edge: impl Into<Label>) -> Self {
2501 self.steps.push(Step {
2502 direction: Direction::In,
2503 edge: Some(edge.into()),
2504 node: None,
2505 });
2506 self
2507 }
2508
2509 pub fn both(mut self, edge: impl Into<Label>) -> Self {
2510 self.steps.push(Step {
2511 direction: Direction::Both,
2512 edge: Some(edge.into()),
2513 node: None,
2514 });
2515 self
2516 }
2517
2518 pub fn to(mut self, node: impl Into<Label>) -> Self {
2525 let step = self
2526 .steps
2527 .last_mut()
2528 .expect("Traversal::to() must follow out(), in_(), or both()");
2529 step.node = Some(node.into());
2530 self
2531 }
2532
2533 pub fn limit(mut self, limit: u32) -> Self {
2534 self.limit = Some(limit);
2535 self
2536 }
2537}
2538
2539#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2540pub enum Start {
2541 Node(NodeId),
2542 NodesByLabel(Label),
2543 NodesByProperty {
2544 label: Label,
2545 key: String,
2546 value: Value,
2547 },
2548}
2549
2550#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2551pub struct Step {
2552 pub direction: Direction,
2553 pub edge: Option<Label>,
2554 pub node: Option<Label>,
2555}
2556
2557#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2558pub enum Direction {
2559 Out,
2560 In,
2561 Both,
2562}
2563
2564#[derive(Clone, Debug, Default, PartialEq)]
2565pub struct EdgeQuery {
2566 pub from: Option<NodeId>,
2567 pub to: Option<NodeId>,
2568 pub label: Option<Label>,
2569}
2570
2571#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2581pub enum PutOutcome {
2582 Inserted,
2584 Updated,
2587 Upserted,
2590 Deduped,
2592}
2593
2594impl PutOutcome {
2595 pub fn written(self) -> bool {
2597 !matches!(self, Self::Deduped)
2598 }
2599}
2600
2601#[derive(Clone, Debug, Default, Eq, PartialEq)]
2604pub struct LoadReport {
2605 pub nodes: usize,
2606 pub edges: usize,
2607}
2608
2609#[async_trait]
2610pub trait GraphStore: Send + Sync {
2611 async fn apply_schema(&self, _schema: &GraphSchema) -> Result<()> {
2626 Ok(())
2627 }
2628
2629 fn constraint_capability(&self, _constraint: &GraphConstraint) -> GraphConstraintCapability {
2638 GraphConstraintCapability::MetadataOnly
2639 }
2640
2641 fn native_constraint_capability(
2649 &self,
2650 _constraint: &GraphConstraint,
2651 ) -> GraphNativeConstraintCapability {
2652 GraphNativeConstraintCapability::Unsupported
2653 }
2654
2655 async fn apply_native_constraint(
2663 &self,
2664 request: GraphNativeConstraintRequest,
2665 ) -> Result<GraphNativeConstraintReport> {
2666 match self.native_constraint_capability(&request.constraint) {
2667 GraphNativeConstraintCapability::Unsupported => Err(GrustError::Unsupported(format!(
2668 "backend-native DDL is not supported for graph constraint {:?}",
2669 request.constraint
2670 ))),
2671 GraphNativeConstraintCapability::NativeIndex
2672 | GraphNativeConstraintCapability::NativeConstraint => Err(GrustError::Unsupported(
2673 "backend advertises native graph constraint support but does not implement apply_native_constraint"
2674 .to_string(),
2675 )),
2676 }
2677 }
2678
2679 async fn put_node(&self, node: &Node) -> Result<PutOutcome>;
2685
2686 async fn put_edge(&self, edge: &Edge) -> Result<PutOutcome>;
2692
2693 async fn put_graph(&self, graph: &Graph) -> Result<LoadReport> {
2694 let mut report = LoadReport::default();
2695 for node in &graph.nodes {
2696 if self.put_node(node).await?.written() {
2697 report.nodes += 1;
2698 }
2699 }
2700 for edge in &graph.edges {
2701 if self.put_edge(edge).await?.written() {
2702 report.edges += 1;
2703 }
2704 }
2705 Ok(report)
2706 }
2707
2708 async fn put_typed_graph(&self, schema: &GraphSchema, graph: &Graph) -> Result<LoadReport> {
2709 schema.validate_graph(graph)?;
2710 self.apply_schema(schema).await?;
2711 self.put_graph(graph).await
2712 }
2713
2714 async fn get_node(&self, id: &NodeId) -> Result<Option<Node>>;
2715
2716 async fn get_nodes(&self, ids: &[NodeId]) -> Result<Vec<Node>> {
2723 let mut nodes = Vec::new();
2724 for id in ids {
2725 if let Some(node) = self.get_node(id).await? {
2726 nodes.push(node);
2727 }
2728 }
2729 Ok(nodes)
2730 }
2731
2732 async fn get_edges(&self, query: EdgeQuery) -> Result<Vec<Edge>>;
2733 async fn traverse(&self, traversal: Traversal) -> Result<Vec<Node>>;
2734}
2735
2736#[async_trait]
2737pub trait GraphAdminStore: GraphStore {
2738 async fn bootstrap(&self) -> Result<()> {
2739 Ok(())
2740 }
2741
2742 async fn clear(&self) -> Result<()>;
2743}
2744
2745#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2747pub enum GraphMutation {
2748 UpsertNode(Node),
2749 PatchNode {
2750 id: NodeId,
2751 props: Props,
2752 },
2753 PatchMatchingNodes {
2754 label: Option<Label>,
2755 props: Props,
2756 predicates: Vec<GraphPropertyPredicate>,
2757 patch: Props,
2758 },
2759 UpdateMatchingNodeProperty {
2760 label: Option<Label>,
2761 props: Props,
2762 predicates: Vec<GraphPropertyPredicate>,
2763 target_key: String,
2764 source_key: String,
2765 op: GraphNumericOp,
2766 operand: Value,
2767 },
2768 PatchEdge {
2769 from: NodeId,
2770 label: Label,
2771 to: NodeId,
2772 id: Option<EdgeId>,
2773 props: Props,
2774 },
2775 PatchMatchingEdges {
2776 relationship: GraphRelationshipMatch,
2777 patch: Props,
2778 },
2779 UpdateMatchingEdgeProperty {
2780 relationship: GraphRelationshipMatch,
2781 target_key: String,
2782 source_key: String,
2783 op: GraphNumericOp,
2784 operand: Value,
2785 },
2786 RemoveNodeProps {
2787 id: NodeId,
2788 keys: Vec<String>,
2789 },
2790 RemoveMatchingNodeProps {
2791 label: Option<Label>,
2792 props: Props,
2793 predicates: Vec<GraphPropertyPredicate>,
2794 keys: Vec<String>,
2795 },
2796 RemoveEdgeProps {
2797 from: NodeId,
2798 label: Label,
2799 to: NodeId,
2800 id: Option<EdgeId>,
2801 keys: Vec<String>,
2802 },
2803 RemoveMatchingEdgeProps {
2804 relationship: GraphRelationshipMatch,
2805 keys: Vec<String>,
2806 },
2807 DeleteMatchingNodes {
2808 label: Option<Label>,
2809 props: Props,
2810 predicates: Vec<GraphPropertyPredicate>,
2811 },
2812 DeleteNode(NodeId),
2813 UpsertEdge(Edge),
2814 UpsertEdgesFromNodeMatches {
2815 kind: GraphMutationPlanKind,
2816 from: GraphNodeMatch,
2817 to: GraphNodeMatch,
2818 label: Label,
2819 props: Props,
2820 edge_id_policy: GraphRowEdgeIdPolicy,
2821 },
2822 DeleteEdge {
2823 from: NodeId,
2824 label: Label,
2825 to: NodeId,
2826 },
2827 DeleteMatchingEdges {
2828 relationship: GraphRelationshipMatch,
2829 },
2830 DeleteRelationshipRows {
2831 relationship: GraphRelationshipMatch,
2832 delete_edges: bool,
2833 endpoint_nodes: Vec<GraphRelationshipEndpoint>,
2834 },
2835 SetMatchingNodeFromNode {
2837 target_label: Option<Label>,
2838 target_props: Props,
2839 target_predicates: Vec<GraphPropertyPredicate>,
2840 target_key: String,
2841 source_label: Option<Label>,
2842 source_props: Props,
2843 source_predicates: Vec<GraphPropertyPredicate>,
2844 source_key: String,
2845 op: Option<GraphNumericOp>,
2846 operand: Value,
2847 correlation: GraphWriteCorrelation,
2848 cardinality: GraphMutationCardinality,
2849 },
2850}
2851
2852#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2853pub enum GraphMutationAtomicity {
2854 OrderedNonAtomic,
2855 Transactional,
2856}
2857
2858#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2859pub enum GraphMutationPlanKind {
2860 Create,
2861 Merge,
2862}
2863
2864#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2865pub enum GraphRowEdgeIdPolicy {
2866 ExplicitOnly,
2867 GenerateForCreate,
2868 GenerateForCreateAndMerge,
2869}
2870
2871#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2872pub enum GraphRelationshipEndpoint {
2873 From,
2874 To,
2875}
2876
2877impl Default for GraphRowEdgeIdPolicy {
2878 fn default() -> Self {
2879 Self::ExplicitOnly
2880 }
2881}
2882
2883#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2884pub enum GraphMutationCardinality {
2885 SingleIdentity,
2886 BoundedMany,
2887 UnboundedMany,
2888}
2889
2890pub fn generated_row_edge_id(from: &NodeId, label: &Label, to: &NodeId, props: &Props) -> EdgeId {
2891 const FNV_OFFSET: u64 = 0xcbf29ce484222325;
2892 const FNV_PRIME: u64 = 0x100000001b3;
2893
2894 fn write_part(hash: &mut u64, value: &str) {
2895 for byte in value.as_bytes() {
2896 *hash ^= u64::from(*byte);
2897 *hash = hash.wrapping_mul(FNV_PRIME);
2898 }
2899 *hash ^= 0xff;
2900 *hash = hash.wrapping_mul(FNV_PRIME);
2901 }
2902
2903 let mut hash = FNV_OFFSET;
2904 write_part(&mut hash, from.as_str());
2905 write_part(&mut hash, label.as_str());
2906 write_part(&mut hash, to.as_str());
2907 for (key, value) in props {
2908 write_part(&mut hash, key);
2909 write_part(&mut hash, &value.to_json().to_string());
2910 }
2911 EdgeId::new(format!("edge-{hash:016x}"))
2912}
2913
2914#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2915pub enum GraphNumericOp {
2916 Add,
2917 Subtract,
2918 Multiply,
2919 Divide,
2920}
2921
2922pub fn evaluate_numeric_update(
2923 current: &Value,
2924 op: GraphNumericOp,
2925 operand: &Value,
2926) -> Result<Value> {
2927 match (current, operand) {
2928 (Value::Int(lhs), Value::Int(rhs)) if op != GraphNumericOp::Divide => {
2929 let value = match op {
2930 GraphNumericOp::Add => lhs.checked_add(*rhs),
2931 GraphNumericOp::Subtract => lhs.checked_sub(*rhs),
2932 GraphNumericOp::Multiply => lhs.checked_mul(*rhs),
2933 GraphNumericOp::Divide => unreachable!("division handled as floating point"),
2934 }
2935 .ok_or_else(|| GrustError::CypherExecution("numeric expression overflow".into()))?;
2936 Ok(Value::Int(value))
2937 }
2938 (Value::Int(lhs), Value::Int(rhs)) => numeric_float_result(*lhs as f64, op, *rhs as f64),
2939 (Value::Int(lhs), Value::Float(rhs)) => numeric_float_result(*lhs as f64, op, *rhs),
2940 (Value::Float(lhs), Value::Int(rhs)) => numeric_float_result(*lhs, op, *rhs as f64),
2941 (Value::Float(lhs), Value::Float(rhs)) => numeric_float_result(*lhs, op, *rhs),
2942 (Value::Null, _) | (_, Value::Null) => Err(GrustError::CypherExecution(
2943 "numeric expression cannot read null values".into(),
2944 )),
2945 _ => Err(GrustError::CypherExecution(
2946 "numeric expression requires integer or float values".into(),
2947 )),
2948 }
2949}
2950
2951fn numeric_float_result(lhs: f64, op: GraphNumericOp, rhs: f64) -> Result<Value> {
2952 let value = match op {
2953 GraphNumericOp::Add => lhs + rhs,
2954 GraphNumericOp::Subtract => lhs - rhs,
2955 GraphNumericOp::Multiply => lhs * rhs,
2956 GraphNumericOp::Divide => {
2957 if rhs == 0.0 {
2958 return Err(GrustError::CypherExecution(
2959 "numeric expression division by zero".into(),
2960 ));
2961 }
2962 lhs / rhs
2963 }
2964 };
2965 if value.is_finite() {
2966 Ok(Value::Float(value))
2967 } else {
2968 Err(GrustError::CypherExecution(
2969 "numeric expression produced a non-finite float".into(),
2970 ))
2971 }
2972}
2973
2974#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2975pub enum GraphPredicateOp {
2976 Equal,
2977 NotEqual,
2978 IsNull,
2979 IsNotNull,
2980 StartsWith,
2981 NotStartsWith,
2982 StartsWithAny,
2983 NotStartsWithAny,
2984 EndsWith,
2985 NotEndsWith,
2986 EndsWithAny,
2987 NotEndsWithAny,
2988 Contains,
2989 NotContains,
2990 ContainsAny,
2991 NotContainsAny,
2992 In,
2993 NotIn,
2994 GreaterThan,
2995 GreaterThanOrEqual,
2996 LessThan,
2997 LessThanOrEqual,
2998}
2999
3000#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3001pub struct GraphPropertyPredicate {
3002 pub key: String,
3003 pub op: GraphPredicateOp,
3004 pub value: Value,
3005}
3006
3007impl GraphPropertyPredicate {
3008 pub fn matches(&self, actual: Option<&Value>) -> bool {
3009 if matches!(self.op, GraphPredicateOp::IsNull) {
3010 return actual.is_none_or(|value| matches!(value, Value::Null));
3011 }
3012 let Some(actual) = actual else {
3013 return false;
3014 };
3015 match self.op {
3016 GraphPredicateOp::Equal => actual == &self.value,
3017 GraphPredicateOp::NotEqual => actual != &self.value,
3018 GraphPredicateOp::IsNull => matches!(actual, Value::Null),
3019 GraphPredicateOp::IsNotNull => !matches!(actual, Value::Null),
3020 GraphPredicateOp::StartsWith => string_predicate_values(actual, &self.value)
3021 .is_some_and(|(actual, needle)| actual.starts_with(needle)),
3022 GraphPredicateOp::NotStartsWith => string_predicate_values(actual, &self.value)
3023 .is_some_and(|(actual, needle)| !actual.starts_with(needle)),
3024 GraphPredicateOp::StartsWithAny => string_list_predicate_values(actual, &self.value)
3025 .is_some_and(|(actual, needles)| {
3026 needles.iter().any(|needle| actual.starts_with(needle))
3027 }),
3028 GraphPredicateOp::NotStartsWithAny => string_list_predicate_values(actual, &self.value)
3029 .is_some_and(|(actual, needles)| {
3030 needles.iter().all(|needle| !actual.starts_with(needle))
3031 }),
3032 GraphPredicateOp::EndsWith => string_predicate_values(actual, &self.value)
3033 .is_some_and(|(actual, needle)| actual.ends_with(needle)),
3034 GraphPredicateOp::NotEndsWith => string_predicate_values(actual, &self.value)
3035 .is_some_and(|(actual, needle)| !actual.ends_with(needle)),
3036 GraphPredicateOp::EndsWithAny => string_list_predicate_values(actual, &self.value)
3037 .is_some_and(|(actual, needles)| {
3038 needles.iter().any(|needle| actual.ends_with(needle))
3039 }),
3040 GraphPredicateOp::NotEndsWithAny => string_list_predicate_values(actual, &self.value)
3041 .is_some_and(|(actual, needles)| {
3042 needles.iter().all(|needle| !actual.ends_with(needle))
3043 }),
3044 GraphPredicateOp::Contains => string_predicate_values(actual, &self.value)
3045 .is_some_and(|(actual, needle)| actual.contains(needle)),
3046 GraphPredicateOp::NotContains => string_predicate_values(actual, &self.value)
3047 .is_some_and(|(actual, needle)| !actual.contains(needle)),
3048 GraphPredicateOp::ContainsAny => string_list_predicate_values(actual, &self.value)
3049 .is_some_and(|(actual, needles)| {
3050 needles.iter().any(|needle| actual.contains(needle))
3051 }),
3052 GraphPredicateOp::NotContainsAny => string_list_predicate_values(actual, &self.value)
3053 .is_some_and(|(actual, needles)| {
3054 needles.iter().all(|needle| !actual.contains(needle))
3055 }),
3056 GraphPredicateOp::In => list_predicate_values(&self.value)
3057 .is_some_and(|values| values.iter().any(|value| actual == value)),
3058 GraphPredicateOp::NotIn => list_predicate_values(&self.value)
3059 .is_some_and(|values| values.iter().all(|value| actual != value)),
3060 GraphPredicateOp::GreaterThan
3061 | GraphPredicateOp::GreaterThanOrEqual
3062 | GraphPredicateOp::LessThan
3063 | GraphPredicateOp::LessThanOrEqual => compare_ordered_values(actual, &self.value)
3064 .is_some_and(|ordering| match self.op {
3065 GraphPredicateOp::GreaterThan => ordering.is_gt(),
3066 GraphPredicateOp::GreaterThanOrEqual => ordering.is_gt() || ordering.is_eq(),
3067 GraphPredicateOp::LessThan => ordering.is_lt(),
3068 GraphPredicateOp::LessThanOrEqual => ordering.is_lt() || ordering.is_eq(),
3069 GraphPredicateOp::Equal
3070 | GraphPredicateOp::NotEqual
3071 | GraphPredicateOp::IsNull
3072 | GraphPredicateOp::IsNotNull
3073 | GraphPredicateOp::StartsWith
3074 | GraphPredicateOp::NotStartsWith
3075 | GraphPredicateOp::StartsWithAny
3076 | GraphPredicateOp::NotStartsWithAny
3077 | GraphPredicateOp::EndsWith
3078 | GraphPredicateOp::NotEndsWith
3079 | GraphPredicateOp::EndsWithAny
3080 | GraphPredicateOp::NotEndsWithAny
3081 | GraphPredicateOp::Contains
3082 | GraphPredicateOp::NotContains
3083 | GraphPredicateOp::ContainsAny
3084 | GraphPredicateOp::NotContainsAny
3085 | GraphPredicateOp::In
3086 | GraphPredicateOp::NotIn => unreachable!(),
3087 }),
3088 }
3089 }
3090}
3091
3092fn string_predicate_values<'a>(actual: &'a Value, value: &'a Value) -> Option<(&'a str, &'a str)> {
3093 match (actual, value) {
3094 (Value::String(actual), Value::String(needle)) => Some((actual.as_str(), needle.as_str())),
3095 _ => None,
3096 }
3097}
3098
3099fn string_list_predicate_values<'a>(
3100 actual: &'a Value,
3101 value: &'a Value,
3102) -> Option<(&'a str, Vec<&'a str>)> {
3103 match (actual, value) {
3104 (Value::String(actual), Value::StringArray(needles)) => Some((
3105 actual.as_str(),
3106 needles.iter().map(String::as_str).collect(),
3107 )),
3108 _ => None,
3109 }
3110}
3111
3112fn list_predicate_values(value: &Value) -> Option<Vec<Value>> {
3113 match value {
3114 Value::StringArray(values) => Some(values.iter().map(Value::from).collect()),
3115 Value::IntArray(values) => Some(values.iter().copied().map(Value::Int).collect()),
3116 Value::FloatArray(values) => Some(values.iter().copied().map(Value::Float).collect()),
3117 Value::Json(serde_json::Value::Array(values)) => values
3118 .iter()
3119 .map(|value| match value {
3120 serde_json::Value::Bool(value) => Some(Value::Bool(*value)),
3121 serde_json::Value::Number(value) => value
3122 .as_i64()
3123 .map(Value::Int)
3124 .or_else(|| value.as_f64().map(Value::Float)),
3125 serde_json::Value::String(value) => Some(Value::from(value)),
3126 serde_json::Value::Null
3127 | serde_json::Value::Array(_)
3128 | serde_json::Value::Object(_) => None,
3129 })
3130 .collect(),
3131 _ => None,
3132 }
3133}
3134
3135fn compare_ordered_values(lhs: &Value, rhs: &Value) -> Option<std::cmp::Ordering> {
3136 match (lhs, rhs) {
3137 (Value::Int(lhs), Value::Int(rhs)) => Some(lhs.cmp(rhs)),
3138 (Value::Int(lhs), Value::Float(rhs)) => (*lhs as f64).partial_cmp(rhs),
3139 (Value::Float(lhs), Value::Int(rhs)) => lhs.partial_cmp(&(*rhs as f64)),
3140 (Value::Float(lhs), Value::Float(rhs)) => lhs.partial_cmp(rhs),
3141 (Value::String(lhs), Value::String(rhs)) => Some(lhs.cmp(rhs)),
3142 _ => None,
3143 }
3144}
3145
3146#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
3147pub struct GraphNodeMatch {
3148 pub label: Option<Label>,
3149 pub props: Props,
3150 pub predicates: Vec<GraphPropertyPredicate>,
3151}
3152
3153#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3154pub struct GraphRelationshipMatch {
3155 pub from: GraphNodeMatch,
3156 pub label: Label,
3157 pub to: GraphNodeMatch,
3158 pub id: Option<EdgeId>,
3159 pub props: Props,
3160 pub predicates: Vec<GraphPropertyPredicate>,
3161}
3162
3163#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3164pub enum GraphMutationPlanOp {
3165 UpsertNode {
3166 kind: GraphMutationPlanKind,
3167 node: Node,
3168 },
3169 PatchNode {
3170 id: NodeId,
3171 props: Props,
3172 },
3173 PatchMatchingNodes {
3174 label: Option<Label>,
3175 props: Props,
3176 predicates: Vec<GraphPropertyPredicate>,
3177 patch: Props,
3178 cardinality: GraphMutationCardinality,
3179 },
3180 UpdateMatchingNodeProperty {
3181 label: Option<Label>,
3182 props: Props,
3183 predicates: Vec<GraphPropertyPredicate>,
3184 target_key: String,
3185 source_key: String,
3186 op: GraphNumericOp,
3187 operand: Value,
3188 cardinality: GraphMutationCardinality,
3189 },
3190 PatchEdge {
3191 from: NodeId,
3192 label: Label,
3193 to: NodeId,
3194 id: Option<EdgeId>,
3195 props: Props,
3196 },
3197 PatchMatchingEdges {
3198 relationship: GraphRelationshipMatch,
3199 patch: Props,
3200 cardinality: GraphMutationCardinality,
3201 },
3202 UpdateMatchingEdgeProperty {
3203 relationship: GraphRelationshipMatch,
3204 target_key: String,
3205 source_key: String,
3206 op: GraphNumericOp,
3207 operand: Value,
3208 cardinality: GraphMutationCardinality,
3209 },
3210 RemoveNodeProps {
3211 id: NodeId,
3212 keys: Vec<String>,
3213 },
3214 RemoveMatchingNodeProps {
3215 label: Option<Label>,
3216 props: Props,
3217 predicates: Vec<GraphPropertyPredicate>,
3218 keys: Vec<String>,
3219 cardinality: GraphMutationCardinality,
3220 },
3221 RemoveEdgeProps {
3222 from: NodeId,
3223 label: Label,
3224 to: NodeId,
3225 id: Option<EdgeId>,
3226 keys: Vec<String>,
3227 },
3228 RemoveMatchingEdgeProps {
3229 relationship: GraphRelationshipMatch,
3230 keys: Vec<String>,
3231 cardinality: GraphMutationCardinality,
3232 },
3233 DeleteMatchingNodes {
3234 label: Option<Label>,
3235 props: Props,
3236 predicates: Vec<GraphPropertyPredicate>,
3237 cardinality: GraphMutationCardinality,
3238 },
3239 UpsertEdge {
3240 kind: GraphMutationPlanKind,
3241 edge: Edge,
3242 },
3243 UpsertEdgesFromNodeMatches {
3244 kind: GraphMutationPlanKind,
3245 from: GraphNodeMatch,
3246 to: GraphNodeMatch,
3247 label: Label,
3248 props: Props,
3249 edge_id_policy: GraphRowEdgeIdPolicy,
3250 cardinality: GraphMutationCardinality,
3251 },
3252 DeleteNode(NodeId),
3253 DeleteEdge {
3254 from: NodeId,
3255 label: Label,
3256 to: NodeId,
3257 },
3258 DeleteMatchingEdges {
3259 relationship: GraphRelationshipMatch,
3260 cardinality: GraphMutationCardinality,
3261 },
3262 DeleteRelationshipRows {
3263 relationship: GraphRelationshipMatch,
3264 delete_edges: bool,
3265 endpoint_nodes: Vec<GraphRelationshipEndpoint>,
3266 target_count: usize,
3267 cardinality: GraphMutationCardinality,
3268 },
3269 SetMatchingNodeFromNode {
3274 target_label: Option<Label>,
3275 target_props: Props,
3276 target_predicates: Vec<GraphPropertyPredicate>,
3277 target_key: String,
3278 source_label: Option<Label>,
3279 source_props: Props,
3280 source_predicates: Vec<GraphPropertyPredicate>,
3281 source_key: String,
3282 op: Option<GraphNumericOp>,
3283 operand: Value,
3284 correlation: GraphWriteCorrelation,
3285 cardinality: GraphMutationCardinality,
3286 },
3287}
3288
3289#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
3292pub enum GraphWriteCorrelation {
3293 Cartesian,
3295 OutgoingRelationship { label: Label },
3297 IncomingRelationship { label: Label },
3299}
3300
3301#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
3302pub struct GraphMutationPlan {
3303 pub operations: Vec<GraphMutationPlanOp>,
3304}
3305
3306impl GraphMutationPlan {
3307 pub fn new(operations: Vec<GraphMutationPlanOp>) -> Self {
3308 Self { operations }
3309 }
3310
3311 pub fn push(&mut self, operation: GraphMutationPlanOp) {
3312 self.operations.push(operation);
3313 }
3314
3315 pub fn report(&self) -> GraphMutationReport {
3316 let mut report = GraphMutationReport::default();
3317 for operation in &self.operations {
3318 report.record(operation);
3319 }
3320 report
3321 }
3322
3323 pub fn into_mutations(self) -> Vec<GraphMutation> {
3324 self.operations
3325 .into_iter()
3326 .map(GraphMutation::from)
3327 .collect()
3328 }
3329}
3330
3331#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
3340pub struct GraphMutationReport {
3341 pub creates: usize,
3342 pub merges: usize,
3343 pub deletes: usize,
3344 pub patches: usize,
3345 pub property_removes: usize,
3346 pub matched_rows: usize,
3347 pub changed_nodes: usize,
3348 pub changed_edges: usize,
3349 pub node_upserts: usize,
3350 pub edge_upserts: usize,
3351 pub node_deletes: usize,
3352 pub edge_deletes: usize,
3353 pub node_patches: usize,
3354 pub edge_patches: usize,
3355 pub node_property_removes: usize,
3356 pub edge_property_removes: usize,
3357 pub node_inserts: usize,
3366 pub node_updates: usize,
3368 pub edge_inserts: usize,
3370 pub edge_updates: usize,
3372}
3373
3374impl GraphMutationReport {
3375 pub fn record(&mut self, operation: &GraphMutationPlanOp) {
3376 match operation {
3377 GraphMutationPlanOp::UpsertNode { kind, .. }
3378 | GraphMutationPlanOp::UpsertEdge { kind, .. } => {
3379 match kind {
3380 GraphMutationPlanKind::Create => self.creates += 1,
3381 GraphMutationPlanKind::Merge => self.merges += 1,
3382 }
3383 match operation {
3384 GraphMutationPlanOp::UpsertNode { .. } => {
3385 self.node_upserts += 1;
3386 self.changed_nodes += 1;
3387 }
3388 GraphMutationPlanOp::UpsertEdge { .. } => {
3389 self.edge_upserts += 1;
3390 self.changed_edges += 1;
3391 }
3392 _ => {}
3393 }
3394 }
3395 GraphMutationPlanOp::UpsertEdgesFromNodeMatches { kind, .. } => match kind {
3396 GraphMutationPlanKind::Create => self.creates += 1,
3397 GraphMutationPlanKind::Merge => self.merges += 1,
3398 },
3399 GraphMutationPlanOp::PatchNode { .. } => {
3400 self.patches += 1;
3401 self.node_patches += 1;
3402 self.changed_nodes += 1;
3403 }
3404 GraphMutationPlanOp::PatchMatchingNodes { .. } => {
3405 self.patches += 1;
3406 }
3407 GraphMutationPlanOp::UpdateMatchingNodeProperty { .. } => {
3408 self.patches += 1;
3409 }
3410 GraphMutationPlanOp::SetMatchingNodeFromNode { .. } => {
3411 self.patches += 1;
3412 }
3413 GraphMutationPlanOp::PatchEdge { .. } => {
3414 self.patches += 1;
3415 self.edge_patches += 1;
3416 self.changed_edges += 1;
3417 }
3418 GraphMutationPlanOp::PatchMatchingEdges { .. } => {
3419 self.patches += 1;
3420 }
3421 GraphMutationPlanOp::UpdateMatchingEdgeProperty { .. } => {
3422 self.patches += 1;
3423 }
3424 GraphMutationPlanOp::RemoveNodeProps { .. } => {
3425 self.property_removes += 1;
3426 self.node_property_removes += 1;
3427 self.changed_nodes += 1;
3428 }
3429 GraphMutationPlanOp::RemoveMatchingNodeProps { .. } => {
3430 self.property_removes += 1;
3431 }
3432 GraphMutationPlanOp::RemoveEdgeProps { .. } => {
3433 self.property_removes += 1;
3434 self.edge_property_removes += 1;
3435 self.changed_edges += 1;
3436 }
3437 GraphMutationPlanOp::RemoveMatchingEdgeProps { .. } => {
3438 self.property_removes += 1;
3439 }
3440 GraphMutationPlanOp::DeleteMatchingNodes { .. } => {
3441 self.deletes += 1;
3442 }
3443 GraphMutationPlanOp::DeleteNode(_) => {
3444 self.deletes += 1;
3445 self.node_deletes += 1;
3446 self.changed_nodes += 1;
3447 }
3448 GraphMutationPlanOp::DeleteEdge { .. } => {
3449 self.deletes += 1;
3450 self.edge_deletes += 1;
3451 self.changed_edges += 1;
3452 }
3453 GraphMutationPlanOp::DeleteMatchingEdges { .. } => {
3454 self.deletes += 1;
3455 }
3456 GraphMutationPlanOp::DeleteRelationshipRows { target_count, .. } => {
3457 self.deletes += *target_count;
3458 }
3459 }
3460 }
3461}
3462
3463impl From<GraphMutationPlanOp> for GraphMutation {
3464 fn from(operation: GraphMutationPlanOp) -> Self {
3465 match operation {
3466 GraphMutationPlanOp::UpsertNode { node, .. } => Self::UpsertNode(node),
3467 GraphMutationPlanOp::PatchNode { id, props } => Self::PatchNode { id, props },
3468 GraphMutationPlanOp::PatchMatchingNodes {
3469 label,
3470 props,
3471 predicates,
3472 patch,
3473 ..
3474 } => Self::PatchMatchingNodes {
3475 label,
3476 props,
3477 predicates,
3478 patch,
3479 },
3480 GraphMutationPlanOp::UpdateMatchingNodeProperty {
3481 label,
3482 props,
3483 predicates,
3484 target_key,
3485 source_key,
3486 op,
3487 operand,
3488 ..
3489 } => Self::UpdateMatchingNodeProperty {
3490 label,
3491 props,
3492 predicates,
3493 target_key,
3494 source_key,
3495 op,
3496 operand,
3497 },
3498 GraphMutationPlanOp::PatchEdge {
3499 from,
3500 label,
3501 to,
3502 id,
3503 props,
3504 } => Self::PatchEdge {
3505 from,
3506 label,
3507 to,
3508 id,
3509 props,
3510 },
3511 GraphMutationPlanOp::PatchMatchingEdges {
3512 relationship,
3513 patch,
3514 ..
3515 } => Self::PatchMatchingEdges {
3516 relationship,
3517 patch,
3518 },
3519 GraphMutationPlanOp::UpdateMatchingEdgeProperty {
3520 relationship,
3521 target_key,
3522 source_key,
3523 op,
3524 operand,
3525 ..
3526 } => Self::UpdateMatchingEdgeProperty {
3527 relationship,
3528 target_key,
3529 source_key,
3530 op,
3531 operand,
3532 },
3533 GraphMutationPlanOp::RemoveNodeProps { id, keys } => Self::RemoveNodeProps { id, keys },
3534 GraphMutationPlanOp::RemoveEdgeProps {
3535 from,
3536 label,
3537 to,
3538 id,
3539 keys,
3540 } => Self::RemoveEdgeProps {
3541 from,
3542 label,
3543 to,
3544 id,
3545 keys,
3546 },
3547 GraphMutationPlanOp::RemoveMatchingEdgeProps {
3548 relationship, keys, ..
3549 } => Self::RemoveMatchingEdgeProps { relationship, keys },
3550 GraphMutationPlanOp::RemoveMatchingNodeProps {
3551 label,
3552 props,
3553 predicates,
3554 keys,
3555 ..
3556 } => Self::RemoveMatchingNodeProps {
3557 label,
3558 props,
3559 predicates,
3560 keys,
3561 },
3562 GraphMutationPlanOp::DeleteMatchingNodes {
3563 label,
3564 props,
3565 predicates,
3566 ..
3567 } => Self::DeleteMatchingNodes {
3568 label,
3569 props,
3570 predicates,
3571 },
3572 GraphMutationPlanOp::UpsertEdge { edge, .. } => Self::UpsertEdge(edge),
3573 GraphMutationPlanOp::UpsertEdgesFromNodeMatches {
3574 kind,
3575 from,
3576 to,
3577 label,
3578 props,
3579 edge_id_policy,
3580 ..
3581 } => Self::UpsertEdgesFromNodeMatches {
3582 kind,
3583 from,
3584 to,
3585 label,
3586 props,
3587 edge_id_policy,
3588 },
3589 GraphMutationPlanOp::DeleteNode(id) => Self::DeleteNode(id),
3590 GraphMutationPlanOp::DeleteEdge { from, label, to } => {
3591 Self::DeleteEdge { from, label, to }
3592 }
3593 GraphMutationPlanOp::DeleteMatchingEdges { relationship, .. } => {
3594 Self::DeleteMatchingEdges { relationship }
3595 }
3596 GraphMutationPlanOp::DeleteRelationshipRows {
3597 relationship,
3598 delete_edges,
3599 endpoint_nodes,
3600 ..
3601 } => Self::DeleteRelationshipRows {
3602 relationship,
3603 delete_edges,
3604 endpoint_nodes,
3605 },
3606 GraphMutationPlanOp::SetMatchingNodeFromNode {
3607 target_label,
3608 target_props,
3609 target_predicates,
3610 target_key,
3611 source_label,
3612 source_props,
3613 source_predicates,
3614 source_key,
3615 op,
3616 operand,
3617 correlation,
3618 cardinality,
3619 } => Self::SetMatchingNodeFromNode {
3620 target_label,
3621 target_props,
3622 target_predicates,
3623 target_key,
3624 source_label,
3625 source_props,
3626 source_predicates,
3627 source_key,
3628 op,
3629 operand,
3630 correlation,
3631 cardinality,
3632 },
3633 }
3634 }
3635}
3636
3637#[async_trait]
3644pub trait CypherMutationExecutor: GraphMutationStore {
3645 async fn execute_cypher_mutation_plan(
3646 &self,
3647 plan: &GraphMutationPlan,
3648 ) -> Result<GraphMutationReport> {
3649 let mut report = plan.report();
3650 for operation in &plan.operations {
3651 match operation {
3652 GraphMutationPlanOp::DeleteMatchingNodes { .. } => {
3653 return Err(GrustError::CypherExecution(
3654 "matched node deletes require backend-specific query support".to_string(),
3655 ));
3656 }
3657 GraphMutationPlanOp::PatchMatchingNodes { .. } => {
3658 return Err(GrustError::CypherExecution(
3659 "matched node patches require backend-specific query support".to_string(),
3660 ));
3661 }
3662 GraphMutationPlanOp::UpdateMatchingNodeProperty { .. } => {
3663 return Err(GrustError::CypherExecution(
3664 "matched node expression updates require backend-specific query support"
3665 .to_string(),
3666 ));
3667 }
3668 GraphMutationPlanOp::RemoveMatchingNodeProps { .. } => {
3669 return Err(GrustError::CypherExecution(
3670 "matched node property removals require backend-specific query support"
3671 .to_string(),
3672 ));
3673 }
3674 GraphMutationPlanOp::PatchMatchingEdges { .. } => {
3675 return Err(GrustError::CypherExecution(
3676 "matched edge patches require backend-specific query support".to_string(),
3677 ));
3678 }
3679 GraphMutationPlanOp::UpdateMatchingEdgeProperty { .. } => {
3680 return Err(GrustError::CypherExecution(
3681 "matched edge expression updates require backend-specific query support"
3682 .to_string(),
3683 ));
3684 }
3685 GraphMutationPlanOp::RemoveMatchingEdgeProps { .. } => {
3686 return Err(GrustError::CypherExecution(
3687 "matched edge property removals require backend-specific query support"
3688 .to_string(),
3689 ));
3690 }
3691 GraphMutationPlanOp::DeleteMatchingEdges { .. } => {
3692 return Err(GrustError::CypherExecution(
3693 "matched edge deletes require backend-specific query support".to_string(),
3694 ));
3695 }
3696 GraphMutationPlanOp::UpsertEdgesFromNodeMatches { .. } => {
3697 return Err(GrustError::CypherExecution(
3698 "row-producing edge upserts require backend-specific query support"
3699 .to_string(),
3700 ));
3701 }
3702 GraphMutationPlanOp::UpsertNode { node, .. } => {
3703 classify_node_upsert(self.put_node(node).await?, &mut report);
3704 }
3705 GraphMutationPlanOp::UpsertEdge { edge, .. } => {
3706 classify_edge_upsert(self.put_edge(edge).await?, &mut report);
3707 }
3708 _ => {
3709 let mutation = GraphMutation::from(operation.clone());
3710 self.apply_mutations(std::slice::from_ref(&mutation))
3711 .await?;
3712 }
3713 }
3714 }
3715 Ok(report)
3716 }
3717}
3718
3719pub fn classify_node_upsert(outcome: PutOutcome, report: &mut GraphMutationReport) {
3723 match outcome {
3724 PutOutcome::Inserted => report.node_inserts += 1,
3725 PutOutcome::Updated => report.node_updates += 1,
3726 PutOutcome::Upserted | PutOutcome::Deduped => {}
3727 }
3728}
3729
3730pub fn classify_edge_upsert(outcome: PutOutcome, report: &mut GraphMutationReport) {
3733 match outcome {
3734 PutOutcome::Inserted => report.edge_inserts += 1,
3735 PutOutcome::Updated => report.edge_updates += 1,
3736 PutOutcome::Upserted | PutOutcome::Deduped => {}
3737 }
3738}
3739
3740#[async_trait]
3745pub trait GraphMutationStore: GraphStore {
3746 fn mutation_atomicity(&self) -> GraphMutationAtomicity {
3747 GraphMutationAtomicity::OrderedNonAtomic
3748 }
3749
3750 async fn delete_node(&self, id: &NodeId) -> Result<()>;
3752
3753 async fn delete_edge(&self, from: &NodeId, label: &Label, to: &NodeId) -> Result<()>;
3755
3756 async fn apply_mutations(&self, mutations: &[GraphMutation]) -> Result<()> {
3763 for mutation in mutations {
3764 match mutation {
3765 GraphMutation::UpsertNode(node) => {
3766 self.put_node(node).await?;
3767 }
3768 GraphMutation::PatchNode { id, props } => {
3769 if let Some(mut node) = self.get_node(id).await? {
3770 for (key, value) in props {
3771 node.props.insert(key.clone(), value.clone());
3772 }
3773 self.put_node(&node).await?;
3774 }
3775 }
3776 GraphMutation::PatchMatchingNodes { .. } => {
3777 return Err(GrustError::Unsupported(
3778 "matched node patches require backend-specific query support".to_string(),
3779 ));
3780 }
3781 GraphMutation::UpdateMatchingNodeProperty { .. } => {
3782 return Err(GrustError::Unsupported(
3783 "matched node expression updates require backend-specific query support"
3784 .to_string(),
3785 ));
3786 }
3787 GraphMutation::SetMatchingNodeFromNode { .. } => {
3788 return Err(GrustError::Unsupported(
3789 "cross-variable correlated updates require backend-specific query support"
3790 .to_string(),
3791 ));
3792 }
3793 GraphMutation::PatchEdge {
3794 from,
3795 label,
3796 to,
3797 id,
3798 props,
3799 } => {
3800 let mut edges = self
3801 .get_edges(EdgeQuery {
3802 from: Some(from.clone()),
3803 to: Some(to.clone()),
3804 label: Some(label.clone()),
3805 })
3806 .await?;
3807 if let Some(id) = id {
3808 edges.retain(|edge| edge.id.as_ref() == Some(id));
3809 }
3810 match edges.len() {
3811 0 => {}
3812 1 => {
3813 let mut edge = edges.remove(0);
3814 for (key, value) in props {
3815 edge.props.insert(key.clone(), value.clone());
3816 }
3817 self.put_edge(&edge).await?;
3818 }
3819 count => {
3820 return Err(GrustError::CypherUnsupportedCardinality(format!(
3821 "edge patch matched {count} edges; add an explicit edge id"
3822 )));
3823 }
3824 }
3825 }
3826 GraphMutation::PatchMatchingEdges { .. } => {
3827 return Err(GrustError::Unsupported(
3828 "matched edge patches require backend-specific query support".to_string(),
3829 ));
3830 }
3831 GraphMutation::UpdateMatchingEdgeProperty { .. } => {
3832 return Err(GrustError::Unsupported(
3833 "matched edge expression updates require backend-specific query support"
3834 .to_string(),
3835 ));
3836 }
3837 GraphMutation::RemoveNodeProps { id, keys } => {
3838 if let Some(mut node) = self.get_node(id).await? {
3839 for key in keys {
3840 node.props.remove(key);
3841 }
3842 self.put_node(&node).await?;
3843 }
3844 }
3845 GraphMutation::RemoveMatchingNodeProps { .. } => {
3846 return Err(GrustError::Unsupported(
3847 "matched node property removal requires backend-specific query support"
3848 .to_string(),
3849 ));
3850 }
3851 GraphMutation::RemoveEdgeProps {
3852 from,
3853 label,
3854 to,
3855 id,
3856 keys,
3857 } => {
3858 let mut edges = self
3859 .get_edges(EdgeQuery {
3860 from: Some(from.clone()),
3861 to: Some(to.clone()),
3862 label: Some(label.clone()),
3863 })
3864 .await?;
3865 if let Some(id) = id {
3866 edges.retain(|edge| edge.id.as_ref() == Some(id));
3867 }
3868 match edges.len() {
3869 0 => {}
3870 1 => {
3871 let mut edge = edges.remove(0);
3872 for key in keys {
3873 edge.props.remove(key);
3874 }
3875 self.put_edge(&edge).await?;
3876 }
3877 count => {
3878 return Err(GrustError::CypherUnsupportedCardinality(format!(
3879 "edge property removal matched {count} edges; add an explicit edge id"
3880 )));
3881 }
3882 }
3883 }
3884 GraphMutation::RemoveMatchingEdgeProps { .. } => {
3885 return Err(GrustError::Unsupported(
3886 "matched edge property removal requires backend-specific query support"
3887 .to_string(),
3888 ));
3889 }
3890 GraphMutation::DeleteMatchingNodes { .. } => {
3891 return Err(GrustError::Unsupported(
3892 "matched node deletes require backend-specific query support".to_string(),
3893 ));
3894 }
3895 GraphMutation::DeleteNode(id) => self.delete_node(id).await?,
3896 GraphMutation::UpsertEdge(edge) => {
3897 self.put_edge(edge).await?;
3898 }
3899 GraphMutation::UpsertEdgesFromNodeMatches { .. } => {
3900 return Err(GrustError::Unsupported(
3901 "row-producing edge upserts require backend-specific query support"
3902 .to_string(),
3903 ));
3904 }
3905 GraphMutation::DeleteEdge { from, label, to } => {
3906 self.delete_edge(from, label, to).await?
3907 }
3908 GraphMutation::DeleteMatchingEdges { .. } => {
3909 return Err(GrustError::Unsupported(
3910 "matched edge deletes require backend-specific query support".to_string(),
3911 ));
3912 }
3913 GraphMutation::DeleteRelationshipRows { .. } => {
3914 return Err(GrustError::Unsupported(
3915 "relationship-row deletes require backend-specific query support"
3916 .to_string(),
3917 ));
3918 }
3919 }
3920 }
3921 Ok(())
3922 }
3923}
3924
3925pub mod prelude {
3926 pub use crate::{
3927 CypherMutationExecutor, Decimal, Direction, Duration, Edge, EdgeId, EdgePolicy, EdgeQuery, EdgeType,
3928 EdgeUniqueness, Field, FieldType, Graph, GraphAdminStore, GraphBuilder, GraphConstraint,
3929 GraphConstraintCapability, GraphIndex, GraphMutation, GraphMutationAtomicity,
3930 GraphMutationCardinality, GraphMutationPlan, GraphMutationPlanKind, GraphMutationPlanOp,
3931 GraphMutationReport, GraphMutationStore, GraphNativeConstraintCapability,
3932 GraphNativeConstraintReport, GraphNativeConstraintRequest, GraphNodeMatch, GraphNumericOp,
3933 GraphPredicateOp, GraphPropertyPredicate, GraphRelationshipEndpoint,
3934 GraphRelationshipMatch, GraphRowEdgeIdPolicy, GraphSchema, GraphSchemaBuilder, GraphStore,
3935 GraphWriteCorrelation,
3936 GrustError, Label, LoadReport, Node, NodeId, NodeType, Props, PutOutcome, Result, RfcDate,
3937 Start, Step, Traversal, Value, classify_edge_upsert, classify_node_upsert, edge_key,
3938 evaluate_numeric_update, generated_row_edge_id, relationship_type, schema_identifier,
3939 };
3940
3941 #[cfg(feature = "typed-garde")]
3942 pub use crate::typed::{TypedEdge, TypedGraphBuilder, TypedNode, garde, props_from_serialize};
3943
3944 #[cfg(feature = "typed-zod-rs")]
3945 pub use crate::typed::{parse_typed_json, parse_typed_json_with, zod_rs};
3946}
3947
3948#[cfg(test)]
3949mod tests;