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, Debug, PartialEq, Serialize, Deserialize)]
150#[serde(tag = "type", content = "value", rename_all = "snake_case")]
151pub enum Value {
152 Null,
153 Bool(bool),
154 Int(i64),
155 Float(f64),
156 String(String),
157 DateTime(RfcDate),
160 StringArray(Vec<String>),
161 IntArray(Vec<i64>),
162 FloatArray(Vec<f64>),
163 Json(serde_json::Value),
164}
165
166impl Value {
167 pub fn datetime(value: impl Into<String>) -> Result<Self> {
171 RfcDate::parse(value).map(Self::DateTime)
172 }
173
174 pub fn as_str(&self) -> Option<&str> {
175 match self {
176 Self::String(value) => Some(value),
177 _ => None,
178 }
179 }
180
181 pub fn as_datetime(&self) -> Option<&str> {
182 match self {
183 Self::DateTime(value) => Some(value.as_str()),
184 _ => None,
185 }
186 }
187
188 pub fn as_string_array(&self) -> Option<&[String]> {
189 match self {
190 Self::StringArray(values) => Some(values),
191 _ => None,
192 }
193 }
194
195 pub fn as_int_array(&self) -> Option<&[i64]> {
196 match self {
197 Self::IntArray(values) => Some(values),
198 _ => None,
199 }
200 }
201
202 pub fn as_float_array(&self) -> Option<&[f64]> {
203 match self {
204 Self::FloatArray(values) => Some(values),
205 _ => None,
206 }
207 }
208
209 pub fn to_json(&self) -> serde_json::Value {
212 match self {
213 Self::Null => serde_json::Value::Null,
214 Self::Bool(value) => serde_json::Value::Bool(*value),
215 Self::Int(value) => serde_json::Value::from(*value),
216 Self::Float(value) => serde_json::Value::from(*value),
217 Self::String(value) => serde_json::Value::String(value.clone()),
218 Self::DateTime(value) => serde_json::Value::String(value.as_str().to_string()),
219 Self::StringArray(values) => serde_json::Value::from(values.clone()),
220 Self::IntArray(values) => serde_json::Value::from(values.clone()),
221 Self::FloatArray(values) => serde_json::Value::from(values.clone()),
222 Self::Json(value) => value.clone(),
223 }
224 }
225
226 pub fn from_json(value: serde_json::Value) -> Self {
230 if let serde_json::Value::Object(mapping) = &value
231 && mapping.contains_key("type")
232 && mapping.contains_key("value")
233 && let Ok(tagged) = serde_json::from_value(value.clone())
234 {
235 return tagged;
236 }
237 Self::from(value)
238 }
239}
240
241fn is_rfc3339_datetime(value: &str) -> bool {
243 let bytes = value.as_bytes();
244 if bytes.len() < 20 {
245 return false;
246 }
247 let digit = |i: usize| bytes[i].is_ascii_digit();
248 let all_digits = |range: std::ops::Range<usize>| range.clone().all(digit);
249 let pair = |i: usize| (bytes[i] - b'0') * 10 + (bytes[i + 1] - b'0');
250
251 if !(all_digits(0..4)
252 && bytes[4] == b'-'
253 && all_digits(5..7)
254 && bytes[7] == b'-'
255 && all_digits(8..10)
256 && bytes[10] == b'T'
257 && all_digits(11..13)
258 && bytes[13] == b':'
259 && all_digits(14..16)
260 && bytes[16] == b':'
261 && all_digits(17..19))
262 {
263 return false;
264 }
265 let year = bytes[0..4]
266 .iter()
267 .fold(0u16, |acc, digit| acc * 10 + u16::from(digit - b'0'));
268 let month = pair(5);
269 let day = pair(8);
270 let leap_year = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
271 let max_day = match month {
272 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
273 4 | 6 | 9 | 11 => 30,
274 2 if leap_year => 29,
275 2 => 28,
276 _ => return false,
277 };
278 if !(day >= 1 && day <= max_day && pair(11) < 24 && pair(14) < 60 && pair(17) <= 60) {
279 return false;
280 }
281
282 let mut i = 19;
283 if bytes[i] == b'.' {
284 let start = i + 1;
285 i = start;
286 while i < bytes.len() && bytes[i].is_ascii_digit() {
287 i += 1;
288 }
289 if i == start {
290 return false;
291 }
292 }
293 match bytes.get(i) {
294 Some(b'Z') => i + 1 == bytes.len(),
295 Some(b'+' | b'-') => {
296 i + 6 == bytes.len()
297 && all_digits(i + 1..i + 3)
298 && bytes[i + 3] == b':'
299 && all_digits(i + 4..i + 6)
300 && pair(i + 1) < 24
301 && pair(i + 4) < 60
302 }
303 _ => false,
304 }
305}
306
307impl From<String> for Value {
308 fn from(value: String) -> Self {
309 Self::String(value)
310 }
311}
312
313impl From<&str> for Value {
314 fn from(value: &str) -> Self {
315 Self::String(value.to_string())
316 }
317}
318
319impl From<&String> for Value {
320 fn from(value: &String) -> Self {
321 Self::String(value.clone())
322 }
323}
324
325impl From<Vec<String>> for Value {
326 fn from(value: Vec<String>) -> Self {
327 Self::StringArray(value)
328 }
329}
330
331impl From<Vec<i64>> for Value {
332 fn from(value: Vec<i64>) -> Self {
333 Self::IntArray(value)
334 }
335}
336
337impl From<Vec<f64>> for Value {
338 fn from(value: Vec<f64>) -> Self {
339 Self::FloatArray(value)
340 }
341}
342
343impl From<bool> for Value {
344 fn from(value: bool) -> Self {
345 Self::Bool(value)
346 }
347}
348
349impl From<i64> for Value {
350 fn from(value: i64) -> Self {
351 Self::Int(value)
352 }
353}
354
355impl From<i32> for Value {
356 fn from(value: i32) -> Self {
357 Self::Int(i64::from(value))
358 }
359}
360
361impl From<usize> for Value {
362 fn from(value: usize) -> Self {
363 Self::Int(value as i64)
364 }
365}
366
367impl From<f64> for Value {
368 fn from(value: f64) -> Self {
369 Self::Float(value)
370 }
371}
372
373impl From<serde_json::Value> for Value {
374 fn from(value: serde_json::Value) -> Self {
375 match value {
376 serde_json::Value::Null => Self::Null,
377 serde_json::Value::Bool(value) => Self::Bool(value),
378 serde_json::Value::Number(value) => {
379 if let Some(value) = value.as_i64() {
380 Self::Int(value)
381 } else if let Some(value) = value.as_f64() {
382 Self::Float(value)
383 } else {
384 Self::Json(serde_json::Value::Number(value))
385 }
386 }
387 serde_json::Value::String(value) => Self::String(value),
388 serde_json::Value::Array(values) => {
389 let ints = values
390 .iter()
391 .filter_map(serde_json::Value::as_i64)
392 .collect::<Vec<_>>();
393 if !values.is_empty() && ints.len() == values.len() {
394 return Self::IntArray(ints);
395 }
396 let floats = values
397 .iter()
398 .filter_map(serde_json::Value::as_f64)
399 .collect::<Vec<_>>();
400 if !values.is_empty() && floats.len() == values.len() {
401 return Self::FloatArray(floats);
402 }
403 let strings = values
404 .iter()
405 .filter_map(|value| value.as_str().map(ToString::to_string))
406 .collect::<Vec<_>>();
407 if strings.len() == values.len() {
408 Self::StringArray(strings)
409 } else {
410 Self::Json(serde_json::Value::Array(values))
411 }
412 }
413 serde_json::Value::Object(value) => Self::Json(serde_json::Value::Object(value)),
414 }
415 }
416}
417
418#[cfg(feature = "typed-garde")]
419pub mod typed {
420 use serde::{Serialize, de::DeserializeOwned};
421
422 use crate::{
423 Edge, EdgeId, Graph, GraphBuilder, GrustError, Node, NodeId, Props, PutOutcome, Result,
424 Value,
425 };
426
427 pub use garde;
428 #[cfg(feature = "typed-zod-rs")]
429 pub use zod_rs;
430
431 pub trait TypedNode: garde::Validate + Serialize {
432 const LABEL: &'static str;
433
434 fn node_id(&self) -> NodeId;
435
436 fn node_props(&self) -> Result<Props> {
437 props_from_serialize(self)
438 }
439
440 fn from_node(node: &Node) -> Result<Self>
441 where
442 Self: Sized + DeserializeOwned,
443 Self::Context: Default,
444 {
445 let ctx = Self::Context::default();
446 Self::from_node_with(node, &ctx)
447 }
448
449 fn from_node_with(node: &Node, ctx: &Self::Context) -> Result<Self>
450 where
451 Self: Sized + DeserializeOwned,
452 {
453 if node.label.as_str() != Self::LABEL {
454 return Err(GrustError::Schema(format!(
455 "node '{}' has label '{}', expected '{}'",
456 node.id.as_str(),
457 node.label.as_str(),
458 Self::LABEL
459 )));
460 }
461 let typed: Self =
462 serde_json::from_value(props_to_plain_json(&node.props)).map_err(|err| {
463 GrustError::Serialization(format!("typed node decode error: {err}"))
464 })?;
465 typed
466 .validate_with(ctx)
467 .map_err(|err| validation_error(Self::LABEL, err))?;
468 let typed_id = typed.node_id();
469 if typed_id != node.id {
470 return Err(GrustError::Schema(format!(
471 "typed node '{}' decoded id '{}', expected '{}'",
472 Self::LABEL,
473 typed_id.as_str(),
474 node.id.as_str()
475 )));
476 }
477 Ok(typed)
478 }
479 }
480
481 pub trait TypedEdge: garde::Validate + Serialize {
482 const LABEL: &'static str;
483
484 fn source_node_id(&self) -> NodeId;
485
486 fn target_node_id(&self) -> NodeId;
487
488 fn edge_id(&self) -> Option<EdgeId> {
489 None
490 }
491
492 fn edge_props(&self) -> Result<Props> {
493 props_from_serialize(self)
494 }
495
496 fn from_edge(edge: &Edge) -> Result<Self>
497 where
498 Self: Sized + DeserializeOwned,
499 Self::Context: Default,
500 {
501 let ctx = Self::Context::default();
502 Self::from_edge_with(edge, &ctx)
503 }
504
505 fn from_edge_with(edge: &Edge, ctx: &Self::Context) -> Result<Self>
506 where
507 Self: Sized + DeserializeOwned,
508 {
509 if edge.label.as_str() != Self::LABEL {
510 return Err(GrustError::Schema(format!(
511 "edge from '{}' to '{}' has label '{}', expected '{}'",
512 edge.from.as_str(),
513 edge.to.as_str(),
514 edge.label.as_str(),
515 Self::LABEL
516 )));
517 }
518 let typed: Self =
519 serde_json::from_value(props_to_plain_json(&edge.props)).map_err(|err| {
520 GrustError::Serialization(format!("typed edge decode error: {err}"))
521 })?;
522 typed
523 .validate_with(ctx)
524 .map_err(|err| validation_error(Self::LABEL, err))?;
525 if typed.source_node_id() != edge.from || typed.target_node_id() != edge.to {
526 return Err(GrustError::Schema(format!(
527 "typed edge '{}' decoded endpoints '{}' -> '{}', expected '{}' -> '{}'",
528 Self::LABEL,
529 typed.source_node_id().as_str(),
530 typed.target_node_id().as_str(),
531 edge.from.as_str(),
532 edge.to.as_str()
533 )));
534 }
535 if let Some(decoded_id) = typed.edge_id()
536 && edge
537 .id
538 .as_ref()
539 .is_some_and(|edge_id| edge_id != &decoded_id)
540 {
541 return Err(GrustError::Schema(format!(
542 "typed edge '{}' decoded id '{}', expected '{}'",
543 Self::LABEL,
544 decoded_id.as_str(),
545 edge.id.as_ref().expect("edge id checked").as_str()
546 )));
547 }
548 Ok(typed)
549 }
550 }
551
552 #[derive(Clone, Debug, Default)]
553 pub struct TypedGraphBuilder {
554 builder: GraphBuilder,
555 }
556
557 impl TypedGraphBuilder {
558 pub fn new() -> Self {
559 Self::default()
560 }
561
562 pub fn from_builder(builder: GraphBuilder) -> Self {
563 Self { builder }
564 }
565
566 pub fn from_graph(graph: Graph) -> Self {
567 let mut builder = GraphBuilder::new();
568 for node in graph.nodes {
569 builder.add_node(node);
570 }
571 for edge in graph.edges {
572 builder.add_edge(edge);
573 }
574 Self { builder }
575 }
576
577 pub fn add_raw_node(&mut self, node: Node) -> NodeId {
578 self.builder.add_node(node)
579 }
580
581 pub fn add_raw_edge(&mut self, edge: Edge) -> PutOutcome {
582 self.builder.add_edge(edge)
583 }
584
585 pub fn add_node<T>(&mut self, node: &T) -> Result<NodeId>
586 where
587 T: TypedNode,
588 T::Context: Default,
589 {
590 node.validate()
591 .map_err(|err| validation_error(T::LABEL, err))?;
592 self.add_validated_node(node)
593 }
594
595 pub fn add_node_with<T>(&mut self, node: &T, ctx: &T::Context) -> Result<NodeId>
596 where
597 T: TypedNode,
598 {
599 node.validate_with(ctx)
600 .map_err(|err| validation_error(T::LABEL, err))?;
601 self.add_validated_node(node)
602 }
603
604 pub fn add_edge<T>(&mut self, edge: &T) -> Result<PutOutcome>
605 where
606 T: TypedEdge,
607 T::Context: Default,
608 {
609 edge.validate()
610 .map_err(|err| validation_error(T::LABEL, err))?;
611 self.add_validated_edge(edge)
612 }
613
614 pub fn add_edge_with<T>(&mut self, edge: &T, ctx: &T::Context) -> Result<PutOutcome>
615 where
616 T: TypedEdge,
617 {
618 edge.validate_with(ctx)
619 .map_err(|err| validation_error(T::LABEL, err))?;
620 self.add_validated_edge(edge)
621 }
622
623 #[cfg(feature = "typed-zod-rs")]
624 pub fn add_node_from_json<T, S>(
625 &mut self,
626 schema: &S,
627 value: &serde_json::Value,
628 ) -> Result<NodeId>
629 where
630 T: TypedNode + DeserializeOwned,
631 T::Context: Default,
632 S: zod_rs::Schema<serde_json::Value>,
633 {
634 let node = parse_typed_json::<T, S>(schema, value)?;
635 self.add_validated_node(&node)
636 }
637
638 #[cfg(feature = "typed-zod-rs")]
639 pub fn add_node_from_json_with<T, S>(
640 &mut self,
641 schema: &S,
642 value: &serde_json::Value,
643 ctx: &T::Context,
644 ) -> Result<NodeId>
645 where
646 T: TypedNode + DeserializeOwned,
647 S: zod_rs::Schema<serde_json::Value>,
648 {
649 let node = parse_typed_json_with::<T, S>(schema, value, ctx)?;
650 self.add_validated_node(&node)
651 }
652
653 #[cfg(feature = "typed-zod-rs")]
654 pub fn add_edge_from_json<T, S>(
655 &mut self,
656 schema: &S,
657 value: &serde_json::Value,
658 ) -> Result<PutOutcome>
659 where
660 T: TypedEdge + DeserializeOwned,
661 T::Context: Default,
662 S: zod_rs::Schema<serde_json::Value>,
663 {
664 let edge = parse_typed_json::<T, S>(schema, value)?;
665 self.add_validated_edge(&edge)
666 }
667
668 #[cfg(feature = "typed-zod-rs")]
669 pub fn add_edge_from_json_with<T, S>(
670 &mut self,
671 schema: &S,
672 value: &serde_json::Value,
673 ctx: &T::Context,
674 ) -> Result<PutOutcome>
675 where
676 T: TypedEdge + DeserializeOwned,
677 S: zod_rs::Schema<serde_json::Value>,
678 {
679 let edge = parse_typed_json_with::<T, S>(schema, value, ctx)?;
680 self.add_validated_edge(&edge)
681 }
682
683 #[must_use = "discarding this means the typed graph was not built"]
684 pub fn build(self) -> Graph {
685 self.builder.build()
686 }
687
688 pub fn into_builder(self) -> GraphBuilder {
689 self.builder
690 }
691
692 fn add_validated_node<T>(&mut self, node: &T) -> Result<NodeId>
693 where
694 T: TypedNode,
695 {
696 let node_id = node.node_id();
697 let mut props = node.node_props()?;
698 props
699 .entry("id".to_string())
700 .or_insert_with(|| Value::from(node_id.as_str()));
701 let graph_node = Node::new(T::LABEL, node_id, props);
702 Ok(self.builder.add_node(graph_node))
703 }
704
705 fn add_validated_edge<T>(&mut self, edge: &T) -> Result<PutOutcome>
706 where
707 T: TypedEdge,
708 {
709 let mut graph_edge = Edge::new(
710 T::LABEL,
711 edge.source_node_id(),
712 edge.target_node_id(),
713 edge.edge_props()?,
714 );
715 graph_edge.id = edge.edge_id();
716 Ok(self.builder.add_edge(graph_edge))
717 }
718 }
719
720 pub fn props_from_serialize<T>(value: &T) -> Result<Props>
721 where
722 T: Serialize + ?Sized,
723 {
724 let serialized = serde_json::to_value(value)
725 .map_err(|err| GrustError::Serialization(format!("typed props error: {err}")))?;
726 let serde_json::Value::Object(fields) = serialized else {
727 return Err(GrustError::Schema(
728 "typed graph values must serialize as JSON objects".to_string(),
729 ));
730 };
731
732 Ok(fields
733 .into_iter()
734 .map(|(key, value)| (key, Value::from(value)))
735 .collect())
736 }
737
738 fn props_to_plain_json(props: &Props) -> serde_json::Value {
739 serde_json::Value::Object(
740 props
741 .iter()
742 .map(|(key, value)| (key.clone(), value.to_json()))
743 .collect(),
744 )
745 }
746
747 #[cfg(feature = "typed-zod-rs")]
748 pub fn parse_typed_json<T, S>(schema: &S, value: &serde_json::Value) -> Result<T>
749 where
750 T: DeserializeOwned + garde::Validate,
751 T::Context: Default,
752 S: zod_rs::Schema<serde_json::Value>,
753 {
754 let ctx = T::Context::default();
755 parse_typed_json_with(schema, value, &ctx)
756 }
757
758 #[cfg(feature = "typed-zod-rs")]
759 pub fn parse_typed_json_with<T, S>(
760 schema: &S,
761 value: &serde_json::Value,
762 ctx: &T::Context,
763 ) -> Result<T>
764 where
765 T: DeserializeOwned + garde::Validate,
766 S: zod_rs::Schema<serde_json::Value>,
767 {
768 schema
769 .safe_parse(value)
770 .map_err(|err| GrustError::Schema(format!("zod-rs validation failed: {err}")))?;
771 let typed: T = serde_json::from_value(value.clone())
772 .map_err(|err| GrustError::Serialization(format!("typed JSON decode error: {err}")))?;
773 typed
774 .validate_with(ctx)
775 .map_err(|err| GrustError::Schema(format!("typed validation failed: {err}")))?;
776 Ok(typed)
777 }
778
779 fn validation_error(label: &str, err: garde::Report) -> GrustError {
780 GrustError::Schema(format!("{label} validation failed: {err}"))
781 }
782}
783
784#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
785pub struct Node {
786 pub id: NodeId,
787 pub label: Label,
788 pub props: Props,
789}
790
791impl Node {
792 pub fn new(label: impl Into<Label>, id: impl Into<NodeId>, props: impl Into<Props>) -> Self {
793 let id = id.into();
794 let mut props = props.into();
795 props
796 .entry("id".to_string())
797 .or_insert_with(|| Value::from(id.as_str()));
798 Self {
799 id,
800 label: label.into(),
801 props,
802 }
803 }
804}
805
806#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
807pub struct Edge {
808 pub id: Option<EdgeId>,
809 pub from: NodeId,
810 pub to: NodeId,
811 pub label: Label,
812 pub props: Props,
813}
814
815impl Edge {
816 pub fn new(
817 label: impl Into<Label>,
818 from: impl Into<NodeId>,
819 to: impl Into<NodeId>,
820 props: impl Into<Props>,
821 ) -> Self {
822 Self {
823 id: None,
824 from: from.into(),
825 to: to.into(),
826 label: label.into(),
827 props: props.into(),
828 }
829 }
830
831 pub fn with_id(mut self, id: impl Into<EdgeId>) -> Self {
832 self.id = Some(id.into());
833 self
834 }
835}
836
837pub fn relationship_type(value: &str) -> String {
842 let relationship = value
843 .chars()
844 .map(|ch| {
845 if ch.is_ascii_alphanumeric() {
846 ch.to_ascii_uppercase()
847 } else {
848 '_'
849 }
850 })
851 .collect::<String>();
852 if relationship.is_empty() {
853 "RELATED_TO".to_string()
854 } else {
855 relationship
856 }
857}
858
859pub fn schema_identifier(value: &str) -> Result<String> {
864 let identifier = value
865 .chars()
866 .map(|ch| {
867 if ch.is_ascii_alphanumeric() {
868 ch.to_ascii_lowercase()
869 } else {
870 '_'
871 }
872 })
873 .collect::<String>();
874 if identifier.is_empty()
875 || identifier
876 .chars()
877 .next()
878 .is_some_and(|ch| ch.is_ascii_digit())
879 {
880 return Err(GrustError::Schema(format!(
881 "invalid schema identifier '{value}'"
882 )));
883 }
884 Ok(identifier)
885}
886
887pub fn edge_key(edge: &Edge) -> String {
893 edge.id
894 .as_ref()
895 .map(EdgeId::as_str)
896 .map(ToString::to_string)
897 .unwrap_or_else(|| {
898 let from = edge.from.as_str();
899 let label = edge.label.as_str();
900 let to = edge.to.as_str();
901 let mut key = String::with_capacity(from.len() + label.len() + to.len() + 2);
902 key.push_str(from);
903 key.push('\u{1f}');
904 key.push_str(label);
905 key.push('\u{1f}');
906 key.push_str(to);
907 key
908 })
909}
910
911#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
912pub struct Graph {
913 pub nodes: Vec<Node>,
914 pub edges: Vec<Edge>,
915}
916
917impl Graph {
918 pub fn new(nodes: Vec<Node>, edges: Vec<Edge>) -> Self {
919 Self { nodes, edges }
920 }
921
922 pub fn from_yaml(yaml: &str) -> Result<Self> {
923 yaml::graph_from_yaml(yaml)
924 }
925
926 pub fn to_yaml(&self) -> Result<String> {
927 yaml::graph_to_yaml(self)
928 }
929
930 pub fn from_json(json: &str) -> Result<Self> {
931 json::graph_from_json(json)
932 }
933
934 pub fn to_json(&self) -> Result<String> {
935 json::graph_to_json(self)
936 }
937
938 pub fn from_xml(xml: &str) -> Result<Self> {
939 xml::graph_from_xml(xml)
940 }
941
942 pub fn to_xml(&self) -> Result<String> {
943 xml::graph_to_xml(self)
944 }
945
946 pub fn builder() -> GraphBuilder {
947 GraphBuilder::new()
948 }
949}
950
951#[derive(Clone, Debug, PartialEq)]
958pub struct GraphIndex {
959 vertex_by_id: HashMap<NodeId, usize>,
960 outgoing_by_vertex: Vec<Vec<usize>>,
961 incoming_by_vertex: Vec<Vec<usize>>,
962 edge_endpoints: Vec<(usize, usize)>,
963}
964
965impl GraphIndex {
966 pub fn new(graph: &Graph) -> Result<Self> {
967 let mut vertex_by_id = HashMap::with_capacity(graph.nodes.len());
968 for (index, vertex) in graph.nodes.iter().enumerate() {
969 if vertex_by_id.insert(vertex.id.clone(), index).is_some() {
970 return Err(GrustError::Schema(format!(
971 "duplicate vertex id '{}'",
972 vertex.id.as_str()
973 )));
974 }
975 }
976
977 let mut outgoing_by_vertex = vec![Vec::<usize>::new(); graph.nodes.len()];
978 let mut incoming_by_vertex = vec![Vec::<usize>::new(); graph.nodes.len()];
979 let mut edge_endpoints = Vec::<(usize, usize)>::with_capacity(graph.edges.len());
980
981 for (edge_index, edge) in graph.edges.iter().enumerate() {
982 let Some(&from_index) = vertex_by_id.get(&edge.from) else {
983 return Err(GrustError::Schema(format!(
984 "edge source '{}' is not present in vertices",
985 edge.from.as_str()
986 )));
987 };
988 let Some(&to_index) = vertex_by_id.get(&edge.to) else {
989 return Err(GrustError::Schema(format!(
990 "edge destination '{}' is not present in vertices",
991 edge.to.as_str()
992 )));
993 };
994
995 outgoing_by_vertex[from_index].push(edge_index);
996 incoming_by_vertex[to_index].push(edge_index);
997 edge_endpoints.push((from_index, to_index));
998 }
999
1000 Ok(Self {
1001 vertex_by_id,
1002 outgoing_by_vertex,
1003 incoming_by_vertex,
1004 edge_endpoints,
1005 })
1006 }
1007
1008 pub fn vertex_index(&self, id: &NodeId) -> Option<usize> {
1009 self.vertex_by_id.get(id).copied()
1010 }
1011
1012 pub fn require_vertex_index(&self, id: &NodeId) -> Result<usize> {
1013 self.vertex_index(id)
1014 .ok_or_else(|| GrustError::Schema(format!("vertex '{}' is not present", id.as_str())))
1015 }
1016
1017 pub fn outgoing_edges(&self, id: &NodeId) -> &[usize] {
1018 self.vertex_index(id)
1019 .map(|index| self.outgoing_by_vertex(index))
1020 .unwrap_or(&[])
1021 }
1022
1023 pub fn incoming_edges(&self, id: &NodeId) -> &[usize] {
1024 self.vertex_index(id)
1025 .map(|index| self.incoming_by_vertex(index))
1026 .unwrap_or(&[])
1027 }
1028
1029 pub fn outgoing_by_vertex(&self, index: usize) -> &[usize] {
1030 self.outgoing_by_vertex
1031 .get(index)
1032 .map(Vec::as_slice)
1033 .unwrap_or(&[])
1034 }
1035
1036 pub fn incoming_by_vertex(&self, index: usize) -> &[usize] {
1037 self.incoming_by_vertex
1038 .get(index)
1039 .map(Vec::as_slice)
1040 .unwrap_or(&[])
1041 }
1042
1043 pub fn edge_endpoints(&self, edge_index: usize) -> (usize, usize) {
1044 self.edge_endpoints[edge_index]
1045 }
1046
1047 pub fn edge_endpoints_slice(&self) -> &[(usize, usize)] {
1048 &self.edge_endpoints
1049 }
1050
1051 pub fn out_degree(&self, index: usize) -> usize {
1052 self.outgoing_by_vertex[index].len()
1053 }
1054
1055 pub fn in_degree(&self, index: usize) -> usize {
1056 self.incoming_by_vertex[index].len()
1057 }
1058
1059 pub fn degree(&self, index: usize) -> usize {
1060 self.in_degree(index) + self.out_degree(index)
1061 }
1062}
1063
1064mod graph_doc {
1065 use std::collections::{BTreeMap, BTreeSet};
1066
1067 use serde::{Deserialize, Serialize};
1068
1069 use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
1070
1071 #[derive(Debug, Serialize, Deserialize)]
1072 pub(super) struct GraphDoc {
1073 #[serde(default)]
1074 pub(super) nodes: Vec<NodeDoc>,
1075 #[serde(default)]
1076 pub(super) edges: Vec<EdgeDoc>,
1077 }
1078
1079 #[derive(Debug, Serialize, Deserialize)]
1080 pub(super) struct NodeDoc {
1081 pub(super) id: NodeId,
1082 pub(super) label: Label,
1083 #[serde(default, deserialize_with = "deserialize_props")]
1084 pub(super) props: Props,
1085 }
1086
1087 #[derive(Debug, Serialize, Deserialize)]
1088 pub(super) struct NodeDocOut {
1089 pub(super) id: NodeId,
1090 pub(super) label: Label,
1091 #[serde(default)]
1092 pub(super) props: Props,
1093 }
1094
1095 #[derive(Debug, Serialize, Deserialize)]
1096 pub(super) struct EdgeDoc {
1097 #[serde(default)]
1098 pub(super) id: Option<EdgeId>,
1099 pub(super) label: Label,
1100 pub(super) from: NodeId,
1101 pub(super) to: NodeId,
1102 #[serde(default, deserialize_with = "deserialize_props")]
1103 pub(super) props: Props,
1104 }
1105
1106 #[derive(Debug, Serialize, Deserialize)]
1107 pub(super) struct EdgeDocOut {
1108 #[serde(default)]
1109 pub(super) id: Option<EdgeId>,
1110 pub(super) label: Label,
1111 pub(super) from: NodeId,
1112 pub(super) to: NodeId,
1113 #[serde(default)]
1114 pub(super) props: Props,
1115 }
1116
1117 pub(super) fn graph_from_doc(doc: GraphDoc) -> super::Result<Graph> {
1118 let mut ids = BTreeSet::new();
1119 for node in &doc.nodes {
1120 if !ids.insert(node.id.clone()) {
1121 return Err(GrustError::Schema(format!(
1122 "duplicate node id '{}'",
1123 node.id
1124 )));
1125 }
1126 }
1127
1128 let mut edges = Vec::with_capacity(doc.edges.len());
1129 for edge in doc.edges {
1130 if !ids.contains(&edge.from) {
1131 return Err(GrustError::Schema(format!(
1132 "edge '{}' references unknown from node '{}'",
1133 edge.label, edge.from
1134 )));
1135 }
1136 if !ids.contains(&edge.to) {
1137 return Err(GrustError::Schema(format!(
1138 "edge '{}' references unknown to node '{}'",
1139 edge.label, edge.to
1140 )));
1141 }
1142
1143 let mut graph_edge = Edge::new(edge.label, edge.from, edge.to, edge.props);
1144 graph_edge.id = edge.id;
1145 edges.push(graph_edge);
1146 }
1147
1148 let nodes = doc
1149 .nodes
1150 .into_iter()
1151 .map(|node| Node::new(node.label, node.id, node.props))
1152 .collect();
1153
1154 Ok(Graph::new(nodes, edges))
1155 }
1156
1157 pub(super) fn graph_to_doc(graph: &Graph) -> GraphDocOut {
1158 GraphDocOut {
1159 nodes: graph
1160 .nodes
1161 .iter()
1162 .map(|node| NodeDocOut {
1163 id: node.id.clone(),
1164 label: node.label.clone(),
1165 props: without_generated_id(&node.props, &node.id),
1166 })
1167 .collect(),
1168 edges: graph
1169 .edges
1170 .iter()
1171 .map(|edge| EdgeDocOut {
1172 id: edge.id.clone(),
1173 label: edge.label.clone(),
1174 from: edge.from.clone(),
1175 to: edge.to.clone(),
1176 props: edge.props.clone(),
1177 })
1178 .collect(),
1179 }
1180 }
1181
1182 fn without_generated_id(props: &Props, id: &NodeId) -> Props {
1183 let mut props = props.clone();
1184 if props.get("id") == Some(&Value::from(id.as_str())) {
1185 props.remove("id");
1186 }
1187 props
1188 }
1189
1190 fn deserialize_props<'de, D>(deserializer: D) -> std::result::Result<Props, D::Error>
1191 where
1192 D: serde::Deserializer<'de>,
1193 {
1194 let raw = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
1195 raw.into_iter()
1196 .map(|(key, value)| {
1197 value_from_json(value)
1198 .map(|value| (key, value))
1199 .map_err(serde::de::Error::custom)
1200 })
1201 .collect()
1202 }
1203
1204 fn value_from_json(value: serde_json::Value) -> std::result::Result<Value, String> {
1205 if let serde_json::Value::Object(mapping) = &value
1206 && mapping.contains_key("type")
1207 && mapping.contains_key("value")
1208 {
1209 return serde_json::from_value(value)
1210 .map_err(|err| format!("invalid tagged Grust value: {err}"));
1211 }
1212
1213 Ok(Value::from_json(value))
1214 }
1215
1216 #[derive(Debug, Serialize, Deserialize)]
1217 pub(super) struct GraphDocOut {
1218 pub(super) nodes: Vec<NodeDocOut>,
1219 pub(super) edges: Vec<EdgeDocOut>,
1220 }
1221}
1222
1223mod yaml {
1224 use crate::{Graph, GrustError};
1225
1226 pub(super) fn graph_from_yaml(yaml: &str) -> super::Result<Graph> {
1227 let doc: super::graph_doc::GraphDoc = serde_yaml::from_str(yaml)
1228 .map_err(|err| GrustError::Serialization(format!("YAML parse error: {err}")))?;
1229 super::graph_doc::graph_from_doc(doc)
1230 }
1231
1232 pub(super) fn graph_to_yaml(graph: &Graph) -> super::Result<String> {
1233 serde_yaml::to_string(&super::graph_doc::graph_to_doc(graph))
1234 .map_err(|err| GrustError::Serialization(format!("YAML serialization error: {err}")))
1235 }
1236}
1237
1238mod json {
1239 use crate::{Graph, GrustError};
1240
1241 pub(super) fn graph_from_json(json: &str) -> super::Result<Graph> {
1242 let doc: super::graph_doc::GraphDoc = serde_json::from_str(json)
1243 .map_err(|err| GrustError::Serialization(format!("JSON parse error: {err}")))?;
1244 super::graph_doc::graph_from_doc(doc)
1245 }
1246
1247 pub(super) fn graph_to_json(graph: &Graph) -> super::Result<String> {
1248 serde_json::to_string_pretty(&super::graph_doc::graph_to_doc(graph))
1249 .map_err(|err| GrustError::Serialization(format!("JSON serialization error: {err}")))
1250 }
1251}
1252
1253mod xml {
1254 use serde::{Deserialize, Serialize};
1255
1256 use crate::{Edge, EdgeId, Graph, GrustError, Label, Node, NodeId, Props, Value};
1257
1258 #[derive(Debug, Serialize, Deserialize)]
1259 #[serde(rename = "graph")]
1260 struct GraphXml {
1261 #[serde(default)]
1262 nodes: NodesXml,
1263 #[serde(default)]
1264 edges: EdgesXml,
1265 }
1266
1267 #[derive(Debug, Default, Serialize, Deserialize)]
1268 struct NodesXml {
1269 #[serde(rename = "node", default)]
1270 items: Vec<NodeXml>,
1271 }
1272
1273 #[derive(Debug, Default, Serialize, Deserialize)]
1274 struct EdgesXml {
1275 #[serde(rename = "edge", default)]
1276 items: Vec<EdgeXml>,
1277 }
1278
1279 #[derive(Debug, Serialize, Deserialize)]
1280 struct NodeXml {
1281 id: NodeId,
1282 label: Label,
1283 #[serde(default)]
1284 props: PropsXml,
1285 }
1286
1287 #[derive(Debug, Serialize, Deserialize)]
1288 struct EdgeXml {
1289 #[serde(default, skip_serializing_if = "Option::is_none")]
1290 id: Option<EdgeId>,
1291 label: Label,
1292 from: NodeId,
1293 to: NodeId,
1294 #[serde(default)]
1295 props: PropsXml,
1296 }
1297
1298 #[derive(Debug, Default, Serialize, Deserialize)]
1299 struct PropsXml {
1300 #[serde(rename = "prop", default)]
1301 items: Vec<PropXml>,
1302 }
1303
1304 #[derive(Debug, Serialize, Deserialize)]
1305 struct PropXml {
1306 key: String,
1307 value: Value,
1308 }
1309
1310 pub(super) fn graph_from_xml(xml: &str) -> super::Result<Graph> {
1311 let doc: GraphXml = quick_xml::de::from_str(xml)
1312 .map_err(|err| GrustError::Serialization(format!("XML parse error: {err}")))?;
1313 super::graph_doc::graph_from_doc(doc.into())
1314 }
1315
1316 pub(super) fn graph_to_xml(graph: &Graph) -> super::Result<String> {
1317 quick_xml::se::to_string(&GraphXml::from(graph))
1318 .map_err(|err| GrustError::Serialization(format!("XML serialization error: {err}")))
1319 }
1320
1321 impl From<GraphXml> for super::graph_doc::GraphDoc {
1322 fn from(value: GraphXml) -> Self {
1323 Self {
1324 nodes: value.nodes.items.into_iter().map(Into::into).collect(),
1325 edges: value.edges.items.into_iter().map(Into::into).collect(),
1326 }
1327 }
1328 }
1329
1330 impl From<NodeXml> for super::graph_doc::NodeDoc {
1331 fn from(value: NodeXml) -> Self {
1332 Self {
1333 id: value.id,
1334 label: value.label,
1335 props: value.props.into(),
1336 }
1337 }
1338 }
1339
1340 impl From<EdgeXml> for super::graph_doc::EdgeDoc {
1341 fn from(value: EdgeXml) -> Self {
1342 Self {
1343 id: value.id,
1344 label: value.label,
1345 from: value.from,
1346 to: value.to,
1347 props: value.props.into(),
1348 }
1349 }
1350 }
1351
1352 impl From<PropsXml> for Props {
1353 fn from(value: PropsXml) -> Self {
1354 value
1355 .items
1356 .into_iter()
1357 .map(|prop| (prop.key, prop.value))
1358 .collect()
1359 }
1360 }
1361
1362 impl From<&Graph> for GraphXml {
1363 fn from(graph: &Graph) -> Self {
1364 Self {
1365 nodes: NodesXml {
1366 items: graph.nodes.iter().map(NodeXml::from).collect(),
1367 },
1368 edges: EdgesXml {
1369 items: graph.edges.iter().map(EdgeXml::from).collect(),
1370 },
1371 }
1372 }
1373 }
1374
1375 impl From<&Node> for NodeXml {
1376 fn from(node: &Node) -> Self {
1377 let props = super::graph_doc::graph_to_doc(&Graph::new(vec![node.clone()], Vec::new()))
1378 .nodes
1379 .into_iter()
1380 .next()
1381 .expect("node exists")
1382 .props;
1383 Self {
1384 id: node.id.clone(),
1385 label: node.label.clone(),
1386 props: props.into(),
1387 }
1388 }
1389 }
1390
1391 impl From<&Edge> for EdgeXml {
1392 fn from(edge: &Edge) -> Self {
1393 Self {
1394 id: edge.id.clone(),
1395 label: edge.label.clone(),
1396 from: edge.from.clone(),
1397 to: edge.to.clone(),
1398 props: edge.props.clone().into(),
1399 }
1400 }
1401 }
1402
1403 impl From<Props> for PropsXml {
1404 fn from(value: Props) -> Self {
1405 Self {
1406 items: value
1407 .into_iter()
1408 .map(|(key, value)| PropXml { key, value })
1409 .collect(),
1410 }
1411 }
1412 }
1413}
1414
1415#[derive(Clone, Debug, Default, Eq, PartialEq)]
1416pub enum EdgePolicy {
1417 AllowDuplicates,
1418 #[default]
1419 DedupeByFromLabelTo,
1420}
1421
1422#[derive(Clone, Debug, Default)]
1423pub struct GraphBuilder {
1424 nodes: BTreeMap<NodeId, Node>,
1425 edges: Vec<Edge>,
1426 edge_keys: BTreeSet<(NodeId, Label, NodeId)>,
1427 edge_policy: EdgePolicy,
1428}
1429
1430impl GraphBuilder {
1431 pub fn new() -> Self {
1432 Self::default()
1433 }
1434
1435 pub fn edge_policy(mut self, edge_policy: EdgePolicy) -> Self {
1436 self.edge_policy = edge_policy;
1437 self
1438 }
1439
1440 pub fn node<'a>(
1441 &'a mut self,
1442 label: impl Into<Label>,
1443 id: impl Into<NodeId>,
1444 ) -> NodeBuilder<'a> {
1445 NodeBuilder {
1446 builder: self,
1447 label: label.into(),
1448 id: id.into(),
1449 props: Props::new(),
1450 }
1451 }
1452
1453 pub fn edge<'a>(
1454 &'a mut self,
1455 label: impl Into<Label>,
1456 from: impl Into<NodeId>,
1457 to: impl Into<NodeId>,
1458 ) -> EdgeBuilder<'a> {
1459 EdgeBuilder {
1460 builder: self,
1461 id: None,
1462 label: label.into(),
1463 from: from.into(),
1464 to: to.into(),
1465 props: Props::new(),
1466 }
1467 }
1468
1469 pub fn add_node(&mut self, node: Node) -> NodeId {
1476 let id = node.id.clone();
1477 self.nodes
1478 .entry(id.clone())
1479 .and_modify(|existing| {
1480 if existing.label == node.label {
1481 existing.props.extend(node.props.clone());
1482 } else {
1483 *existing = node.clone();
1484 }
1485 })
1486 .or_insert(node);
1487 id
1488 }
1489
1490 pub fn add_edge(&mut self, edge: Edge) -> PutOutcome {
1493 match self.edge_policy {
1494 EdgePolicy::AllowDuplicates => {
1495 self.edges.push(edge);
1496 PutOutcome::Inserted
1497 }
1498 EdgePolicy::DedupeByFromLabelTo => {
1499 let key = (edge.from.clone(), edge.label.clone(), edge.to.clone());
1500 if self.edge_keys.insert(key) {
1501 self.edges.push(edge);
1502 PutOutcome::Inserted
1503 } else {
1504 PutOutcome::Deduped
1505 }
1506 }
1507 }
1508 }
1509
1510 #[must_use = "discarding this means the graph was not built"]
1511 pub fn build(self) -> Graph {
1512 Graph {
1513 nodes: self.nodes.into_values().collect(),
1514 edges: self.edges,
1515 }
1516 }
1517}
1518
1519pub struct NodeBuilder<'a> {
1520 builder: &'a mut GraphBuilder,
1521 label: Label,
1522 id: NodeId,
1523 props: Props,
1524}
1525
1526impl<'a> NodeBuilder<'a> {
1527 pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1528 self.props.insert(key.into(), value.into());
1529 self
1530 }
1531
1532 pub fn props(mut self, props: Props) -> Self {
1533 self.props.extend(props);
1534 self
1535 }
1536
1537 #[must_use = "discarding this means the node was not added to the builder"]
1538 pub fn finish(self) -> NodeId {
1539 let node = Node::new(self.label, self.id, self.props);
1540 self.builder.add_node(node)
1541 }
1542}
1543
1544pub struct EdgeBuilder<'a> {
1545 builder: &'a mut GraphBuilder,
1546 id: Option<EdgeId>,
1547 label: Label,
1548 from: NodeId,
1549 to: NodeId,
1550 props: Props,
1551}
1552
1553impl<'a> EdgeBuilder<'a> {
1554 pub fn id(mut self, id: impl Into<EdgeId>) -> Self {
1555 self.id = Some(id.into());
1556 self
1557 }
1558
1559 pub fn prop(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1560 self.props.insert(key.into(), value.into());
1561 self
1562 }
1563
1564 pub fn props(mut self, props: Props) -> Self {
1565 self.props.extend(props);
1566 self
1567 }
1568
1569 #[must_use = "discarding this means the edge was not added to the builder"]
1570 pub fn finish(self) -> PutOutcome {
1571 let mut edge = Edge::new(self.label, self.from, self.to, self.props);
1572 edge.id = self.id;
1573 self.builder.add_edge(edge)
1574 }
1575}
1576
1577#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1578pub struct GraphSchema {
1579 pub nodes: Vec<NodeType>,
1580 pub edges: Vec<EdgeType>,
1581 pub constraints: Vec<GraphConstraint>,
1582}
1583
1584impl GraphSchema {
1585 pub fn builder() -> GraphSchemaBuilder {
1586 GraphSchemaBuilder::default()
1587 }
1588
1589 pub fn node_type(&self, label: &Label) -> Option<&NodeType> {
1590 self.nodes
1591 .iter()
1592 .find(|node_type| &node_type.label == label)
1593 }
1594
1595 pub fn edge_type(&self, label: &Label) -> Option<&EdgeType> {
1596 self.edges
1597 .iter()
1598 .find(|edge_type| &edge_type.label == label)
1599 }
1600
1601 pub fn constraints_for_label(&self, label: &Label) -> Vec<&GraphConstraint> {
1602 self.constraints
1603 .iter()
1604 .filter(|constraint| constraint.label() == label)
1605 .collect()
1606 }
1607
1608 pub fn validate_graph(&self, graph: &Graph) -> Result<()> {
1609 for node in &graph.nodes {
1610 self.validate_node(node)?;
1611 }
1612 let labels: BTreeMap<&NodeId, &Label> = graph
1613 .nodes
1614 .iter()
1615 .map(|node| (&node.id, &node.label))
1616 .collect();
1617 for edge in &graph.edges {
1618 self.validate_edge_with(edge, |id| labels.get(id).copied())?;
1619 }
1620 self.validate_edge_uniqueness(graph)?;
1621 self.validate_unique_property_constraints(graph)
1622 }
1623
1624 fn validate_edge_uniqueness(&self, graph: &Graph) -> Result<()> {
1627 let mut seen = BTreeSet::new();
1628 for edge in &graph.edges {
1629 let Some(edge_type) = self.edge_type(&edge.label) else {
1630 continue;
1631 };
1632 if edge_type.uniqueness == EdgeUniqueness::None {
1633 continue;
1634 }
1635 let (a, b) = if edge_type.directed || edge.from <= edge.to {
1636 (&edge.from, &edge.to)
1637 } else {
1638 (&edge.to, &edge.from)
1639 };
1640 if !seen.insert((edge.label.clone(), a.clone(), b.clone())) {
1641 return Err(GrustError::Schema(format!(
1642 "duplicate edge '{}' between '{}' and '{}' violates {:?} uniqueness",
1643 edge.label.as_str(),
1644 a.as_str(),
1645 b.as_str(),
1646 edge_type.uniqueness
1647 )));
1648 }
1649 }
1650 Ok(())
1651 }
1652
1653 fn validate_unique_property_constraints(&self, graph: &Graph) -> Result<()> {
1654 for constraint in &self.constraints {
1655 match constraint {
1656 GraphConstraint::NodePropertyUnique { label, key } => {
1657 let mut seen: Vec<(&NodeId, &Value)> = Vec::new();
1658 for node in graph.nodes.iter().filter(|node| &node.label == label) {
1659 let Some(value) = node.props.get(key) else {
1660 continue;
1661 };
1662 if let Some((existing_id, _)) =
1663 seen.iter().find(|(_, existing)| *existing == value)
1664 {
1665 return Err(GrustError::Schema(format!(
1666 "node '{}' with label '{}' duplicates unique constrained property '{}' from node '{}'",
1667 node.id.as_str(),
1668 label.as_str(),
1669 key,
1670 existing_id.as_str()
1671 )));
1672 }
1673 seen.push((&node.id, value));
1674 }
1675 }
1676 GraphConstraint::EdgePropertyUnique { label, key } => {
1677 let mut seen: Vec<(String, &Value)> = Vec::new();
1678 for edge in graph.edges.iter().filter(|edge| &edge.label == label) {
1679 let Some(value) = edge.props.get(key) else {
1680 continue;
1681 };
1682 if let Some((existing_key, _)) =
1683 seen.iter().find(|(_, existing)| *existing == value)
1684 {
1685 return Err(GrustError::Schema(format!(
1686 "edge '{}' duplicates unique constrained property '{}' from edge '{}'",
1687 edge_key(edge),
1688 key,
1689 existing_key
1690 )));
1691 }
1692 seen.push((edge_key(edge), value));
1693 }
1694 }
1695 GraphConstraint::NodePropertyRequired { .. }
1696 | GraphConstraint::EdgePropertyRequired { .. } => {}
1697 }
1698 }
1699 Ok(())
1700 }
1701
1702 pub fn validate_node(&self, node: &Node) -> Result<()> {
1703 let node_type = self.node_type(&node.label).ok_or_else(|| {
1704 GrustError::Schema(format!("schema has no node type '{}'", node.label.as_str()))
1705 })?;
1706 validate_props(
1707 &node.props,
1708 &node_type.fields,
1709 &format!("node '{}'", node.id.as_str()),
1710 )?;
1711 for constraint in &self.constraints {
1712 if let GraphConstraint::NodePropertyRequired { label, key } = constraint
1713 && label == &node.label
1714 && !node.props.contains_key(key)
1715 {
1716 return Err(GrustError::Schema(format!(
1717 "node '{}' with label '{}' is missing required constrained property '{}'",
1718 node.id.as_str(),
1719 node.label.as_str(),
1720 key
1721 )));
1722 }
1723 }
1724 Ok(())
1725 }
1726
1727 pub fn validate_edge(&self, edge: &Edge, graph: &Graph) -> Result<()> {
1728 self.validate_edge_with(edge, |id| {
1729 graph
1730 .nodes
1731 .iter()
1732 .find(|node| &node.id == id)
1733 .map(|node| &node.label)
1734 })
1735 }
1736
1737 pub fn validate_edge_with<'a>(
1740 &self,
1741 edge: &Edge,
1742 lookup: impl Fn(&NodeId) -> Option<&'a Label>,
1743 ) -> Result<()> {
1744 let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
1745 GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
1746 })?;
1747
1748 let from_label = lookup(&edge.from).ok_or_else(|| {
1749 GrustError::Schema(format!(
1750 "edge '{}' references unknown from node '{}'",
1751 edge.label.as_str(),
1752 edge.from.as_str()
1753 ))
1754 })?;
1755 let to_label = lookup(&edge.to).ok_or_else(|| {
1756 GrustError::Schema(format!(
1757 "edge '{}' references unknown to node '{}'",
1758 edge.label.as_str(),
1759 edge.to.as_str()
1760 ))
1761 })?;
1762
1763 let from_matches =
1764 |label: &Label| edge_type.from.is_empty() || edge_type.from.contains(label);
1765 let to_matches = |label: &Label| edge_type.to.is_empty() || edge_type.to.contains(label);
1766 let endpoints_ok = (from_matches(from_label) && to_matches(to_label))
1769 || (!edge_type.directed && from_matches(to_label) && to_matches(from_label));
1770 if !endpoints_ok {
1771 if !from_matches(from_label) {
1772 return Err(GrustError::Schema(format!(
1773 "edge '{}' cannot start from node label '{}'",
1774 edge.label.as_str(),
1775 from_label.as_str()
1776 )));
1777 }
1778 return Err(GrustError::Schema(format!(
1779 "edge '{}' cannot end at node label '{}'",
1780 edge.label.as_str(),
1781 to_label.as_str()
1782 )));
1783 }
1784
1785 validate_props(
1786 &edge.props,
1787 &edge_type.fields,
1788 &format!("edge '{}'", edge.label.as_str()),
1789 )?;
1790 self.validate_edge_required_constraints(edge)
1791 }
1792
1793 pub fn validate_edge_props(&self, edge: &Edge) -> Result<()> {
1797 let edge_type = self.edge_type(&edge.label).ok_or_else(|| {
1798 GrustError::Schema(format!("schema has no edge type '{}'", edge.label.as_str()))
1799 })?;
1800 validate_props(
1801 &edge.props,
1802 &edge_type.fields,
1803 &format!("edge '{}'", edge.label.as_str()),
1804 )?;
1805 self.validate_edge_required_constraints(edge)
1806 }
1807
1808 fn validate_edge_required_constraints(&self, edge: &Edge) -> Result<()> {
1809 for constraint in &self.constraints {
1810 if let GraphConstraint::EdgePropertyRequired { label, key } = constraint
1811 && label == &edge.label
1812 && !edge.props.contains_key(key)
1813 {
1814 return Err(GrustError::Schema(format!(
1815 "edge '{}' from '{}' to '{}' is missing required constrained property '{}'",
1816 edge.label.as_str(),
1817 edge.from.as_str(),
1818 edge.to.as_str(),
1819 key
1820 )));
1821 }
1822 }
1823 Ok(())
1824 }
1825}
1826
1827#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1828pub struct NodeType {
1829 pub label: Label,
1830 pub fields: Vec<Field>,
1831}
1832
1833#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1834pub struct EdgeType {
1835 pub label: Label,
1836 pub from: Vec<Label>,
1837 pub to: Vec<Label>,
1838 pub fields: Vec<Field>,
1839 pub directed: bool,
1840 pub uniqueness: EdgeUniqueness,
1841}
1842
1843#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1844pub enum GraphConstraint {
1845 NodePropertyUnique { label: Label, key: String },
1846 NodePropertyRequired { label: Label, key: String },
1847 EdgePropertyUnique { label: Label, key: String },
1848 EdgePropertyRequired { label: Label, key: String },
1849}
1850
1851impl GraphConstraint {
1852 pub fn label(&self) -> &Label {
1853 match self {
1854 Self::NodePropertyUnique { label, .. }
1855 | Self::NodePropertyRequired { label, .. }
1856 | Self::EdgePropertyUnique { label, .. }
1857 | Self::EdgePropertyRequired { label, .. } => label,
1858 }
1859 }
1860
1861 pub fn key(&self) -> &str {
1862 match self {
1863 Self::NodePropertyUnique { key, .. }
1864 | Self::NodePropertyRequired { key, .. }
1865 | Self::EdgePropertyUnique { key, .. }
1866 | Self::EdgePropertyRequired { key, .. } => key,
1867 }
1868 }
1869}
1870
1871#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1872pub enum GraphConstraintCapability {
1873 #[default]
1874 MetadataOnly,
1875 ValidateBeforeWrite,
1876 EnforcedByBackend,
1877}
1878
1879#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1888pub enum GraphNativeConstraintCapability {
1889 #[default]
1891 Unsupported,
1892 NativeIndex,
1895 NativeConstraint,
1898}
1899
1900#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1901pub struct GraphNativeConstraintRequest {
1902 pub constraint: GraphConstraint,
1903 pub if_not_exists: bool,
1904}
1905
1906#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1907pub struct GraphNativeConstraintReport {
1908 pub applied: usize,
1909 pub skipped: usize,
1910}
1911
1912#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1913pub struct Field {
1914 pub name: String,
1915 pub ty: FieldType,
1916 pub required: bool,
1917}
1918
1919#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1920pub enum FieldType {
1921 String,
1922 Int,
1923 Float,
1924 Bool,
1925 DateTime,
1926 StringArray,
1927 IntArray,
1928 FloatArray,
1929 Json,
1930}
1931
1932#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1940pub enum EdgeUniqueness {
1941 None,
1942 FromTo,
1943 FromLabelTo,
1944}
1945
1946#[derive(Clone, Debug, Default)]
1947pub struct GraphSchemaBuilder {
1948 nodes: Vec<NodeType>,
1949 edges: Vec<EdgeType>,
1950 constraints: Vec<GraphConstraint>,
1951}
1952
1953impl GraphSchemaBuilder {
1954 pub fn node(mut self, label: impl Into<Label>, fields: impl Into<Vec<Field>>) -> Self {
1955 self.nodes.push(NodeType {
1956 label: label.into(),
1957 fields: fields.into(),
1958 });
1959 self
1960 }
1961
1962 pub fn edge(
1963 mut self,
1964 label: impl Into<Label>,
1965 from: impl Into<Vec<Label>>,
1966 to: impl Into<Vec<Label>>,
1967 fields: impl Into<Vec<Field>>,
1968 ) -> Self {
1969 self.edges.push(EdgeType {
1970 label: label.into(),
1971 from: from.into(),
1972 to: to.into(),
1973 fields: fields.into(),
1974 directed: true,
1975 uniqueness: EdgeUniqueness::FromLabelTo,
1976 });
1977 self
1978 }
1979
1980 pub fn edge_type(mut self, edge_type: EdgeType) -> Self {
1981 self.edges.push(edge_type);
1982 self
1983 }
1984
1985 pub fn constraint(mut self, constraint: GraphConstraint) -> Self {
1986 self.constraints.push(constraint);
1987 self
1988 }
1989
1990 pub fn unique_node_property(self, label: impl Into<Label>, key: impl Into<String>) -> Self {
1991 self.constraint(GraphConstraint::NodePropertyUnique {
1992 label: label.into(),
1993 key: key.into(),
1994 })
1995 }
1996
1997 pub fn required_node_property(self, label: impl Into<Label>, key: impl Into<String>) -> Self {
1998 self.constraint(GraphConstraint::NodePropertyRequired {
1999 label: label.into(),
2000 key: key.into(),
2001 })
2002 }
2003
2004 pub fn unique_edge_property(self, label: impl Into<Label>, key: impl Into<String>) -> Self {
2005 self.constraint(GraphConstraint::EdgePropertyUnique {
2006 label: label.into(),
2007 key: key.into(),
2008 })
2009 }
2010
2011 pub fn required_edge_property(self, label: impl Into<Label>, key: impl Into<String>) -> Self {
2012 self.constraint(GraphConstraint::EdgePropertyRequired {
2013 label: label.into(),
2014 key: key.into(),
2015 })
2016 }
2017
2018 pub fn build(self) -> GraphSchema {
2019 GraphSchema {
2020 nodes: self.nodes,
2021 edges: self.edges,
2022 constraints: self.constraints,
2023 }
2024 }
2025}
2026
2027impl Field {
2028 pub fn required(name: impl Into<String>, ty: FieldType) -> Self {
2029 Self {
2030 name: name.into(),
2031 ty,
2032 required: true,
2033 }
2034 }
2035
2036 pub fn optional(name: impl Into<String>, ty: FieldType) -> Self {
2037 Self {
2038 name: name.into(),
2039 ty,
2040 required: false,
2041 }
2042 }
2043}
2044
2045fn validate_props(props: &Props, fields: &[Field], context: &str) -> Result<()> {
2046 for field in fields {
2047 match props.get(&field.name) {
2048 Some(value) => validate_field_value(value, &field.ty, context, &field.name)?,
2049 None if field.required => {
2050 return Err(GrustError::Schema(format!(
2051 "{context} missing required field '{}'",
2052 field.name
2053 )));
2054 }
2055 None => {}
2056 }
2057 }
2058 Ok(())
2059}
2060
2061fn validate_field_value(
2062 value: &Value,
2063 ty: &FieldType,
2064 context: &str,
2065 field_name: &str,
2066) -> Result<()> {
2067 let matches = match (value, ty) {
2068 (Value::String(_), FieldType::String)
2069 | (Value::Int(_), FieldType::Int)
2070 | (Value::Float(_), FieldType::Float)
2071 | (Value::Bool(_), FieldType::Bool)
2072 | (Value::DateTime(_), FieldType::DateTime)
2073 | (Value::StringArray(_), FieldType::StringArray)
2074 | (Value::IntArray(_), FieldType::IntArray)
2075 | (Value::FloatArray(_), FieldType::FloatArray)
2076 | (_, FieldType::Json) => true,
2077 (Value::String(value), FieldType::DateTime) => is_rfc3339_datetime(value),
2080 _ => false,
2081 };
2082 if matches {
2083 Ok(())
2084 } else {
2085 Err(GrustError::Schema(format!(
2086 "{context} field '{field_name}' expected {ty:?}, got {value:?}"
2087 )))
2088 }
2089}
2090
2091#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2092pub struct Traversal {
2093 pub start: Start,
2094 pub steps: Vec<Step>,
2095 pub limit: Option<u32>,
2096}
2097
2098impl Traversal {
2099 pub fn from_node(id: impl Into<NodeId>) -> Self {
2100 Self {
2101 start: Start::Node(id.into()),
2102 steps: Vec::new(),
2103 limit: None,
2104 }
2105 }
2106
2107 pub fn out(mut self, edge: impl Into<Label>) -> Self {
2108 self.steps.push(Step {
2109 direction: Direction::Out,
2110 edge: Some(edge.into()),
2111 node: None,
2112 });
2113 self
2114 }
2115
2116 pub fn in_(mut self, edge: impl Into<Label>) -> Self {
2117 self.steps.push(Step {
2118 direction: Direction::In,
2119 edge: Some(edge.into()),
2120 node: None,
2121 });
2122 self
2123 }
2124
2125 pub fn both(mut self, edge: impl Into<Label>) -> Self {
2126 self.steps.push(Step {
2127 direction: Direction::Both,
2128 edge: Some(edge.into()),
2129 node: None,
2130 });
2131 self
2132 }
2133
2134 pub fn to(mut self, node: impl Into<Label>) -> Self {
2141 let step = self
2142 .steps
2143 .last_mut()
2144 .expect("Traversal::to() must follow out(), in_(), or both()");
2145 step.node = Some(node.into());
2146 self
2147 }
2148
2149 pub fn limit(mut self, limit: u32) -> Self {
2150 self.limit = Some(limit);
2151 self
2152 }
2153}
2154
2155#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2156pub enum Start {
2157 Node(NodeId),
2158 NodesByLabel(Label),
2159 NodesByProperty {
2160 label: Label,
2161 key: String,
2162 value: Value,
2163 },
2164}
2165
2166#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2167pub struct Step {
2168 pub direction: Direction,
2169 pub edge: Option<Label>,
2170 pub node: Option<Label>,
2171}
2172
2173#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2174pub enum Direction {
2175 Out,
2176 In,
2177 Both,
2178}
2179
2180#[derive(Clone, Debug, Default, PartialEq)]
2181pub struct EdgeQuery {
2182 pub from: Option<NodeId>,
2183 pub to: Option<NodeId>,
2184 pub label: Option<Label>,
2185}
2186
2187#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2197pub enum PutOutcome {
2198 Inserted,
2200 Updated,
2203 Upserted,
2206 Deduped,
2208}
2209
2210impl PutOutcome {
2211 pub fn written(self) -> bool {
2213 !matches!(self, Self::Deduped)
2214 }
2215}
2216
2217#[derive(Clone, Debug, Default, Eq, PartialEq)]
2220pub struct LoadReport {
2221 pub nodes: usize,
2222 pub edges: usize,
2223}
2224
2225#[async_trait]
2226pub trait GraphStore: Send + Sync {
2227 async fn apply_schema(&self, _schema: &GraphSchema) -> Result<()> {
2242 Ok(())
2243 }
2244
2245 fn constraint_capability(&self, _constraint: &GraphConstraint) -> GraphConstraintCapability {
2254 GraphConstraintCapability::MetadataOnly
2255 }
2256
2257 fn native_constraint_capability(
2265 &self,
2266 _constraint: &GraphConstraint,
2267 ) -> GraphNativeConstraintCapability {
2268 GraphNativeConstraintCapability::Unsupported
2269 }
2270
2271 async fn apply_native_constraint(
2279 &self,
2280 request: GraphNativeConstraintRequest,
2281 ) -> Result<GraphNativeConstraintReport> {
2282 match self.native_constraint_capability(&request.constraint) {
2283 GraphNativeConstraintCapability::Unsupported => Err(GrustError::Unsupported(format!(
2284 "backend-native DDL is not supported for graph constraint {:?}",
2285 request.constraint
2286 ))),
2287 GraphNativeConstraintCapability::NativeIndex
2288 | GraphNativeConstraintCapability::NativeConstraint => Err(GrustError::Unsupported(
2289 "backend advertises native graph constraint support but does not implement apply_native_constraint"
2290 .to_string(),
2291 )),
2292 }
2293 }
2294
2295 async fn put_node(&self, node: &Node) -> Result<PutOutcome>;
2301
2302 async fn put_edge(&self, edge: &Edge) -> Result<PutOutcome>;
2308
2309 async fn put_graph(&self, graph: &Graph) -> Result<LoadReport> {
2310 let mut report = LoadReport::default();
2311 for node in &graph.nodes {
2312 if self.put_node(node).await?.written() {
2313 report.nodes += 1;
2314 }
2315 }
2316 for edge in &graph.edges {
2317 if self.put_edge(edge).await?.written() {
2318 report.edges += 1;
2319 }
2320 }
2321 Ok(report)
2322 }
2323
2324 async fn put_typed_graph(&self, schema: &GraphSchema, graph: &Graph) -> Result<LoadReport> {
2325 schema.validate_graph(graph)?;
2326 self.apply_schema(schema).await?;
2327 self.put_graph(graph).await
2328 }
2329
2330 async fn get_node(&self, id: &NodeId) -> Result<Option<Node>>;
2331
2332 async fn get_nodes(&self, ids: &[NodeId]) -> Result<Vec<Node>> {
2339 let mut nodes = Vec::new();
2340 for id in ids {
2341 if let Some(node) = self.get_node(id).await? {
2342 nodes.push(node);
2343 }
2344 }
2345 Ok(nodes)
2346 }
2347
2348 async fn get_edges(&self, query: EdgeQuery) -> Result<Vec<Edge>>;
2349 async fn traverse(&self, traversal: Traversal) -> Result<Vec<Node>>;
2350}
2351
2352#[async_trait]
2353pub trait GraphAdminStore: GraphStore {
2354 async fn bootstrap(&self) -> Result<()> {
2355 Ok(())
2356 }
2357
2358 async fn clear(&self) -> Result<()>;
2359}
2360
2361#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2363pub enum GraphMutation {
2364 UpsertNode(Node),
2365 PatchNode {
2366 id: NodeId,
2367 props: Props,
2368 },
2369 PatchMatchingNodes {
2370 label: Option<Label>,
2371 props: Props,
2372 predicates: Vec<GraphPropertyPredicate>,
2373 patch: Props,
2374 },
2375 UpdateMatchingNodeProperty {
2376 label: Option<Label>,
2377 props: Props,
2378 predicates: Vec<GraphPropertyPredicate>,
2379 target_key: String,
2380 source_key: String,
2381 op: GraphNumericOp,
2382 operand: Value,
2383 },
2384 PatchEdge {
2385 from: NodeId,
2386 label: Label,
2387 to: NodeId,
2388 id: Option<EdgeId>,
2389 props: Props,
2390 },
2391 PatchMatchingEdges {
2392 relationship: GraphRelationshipMatch,
2393 patch: Props,
2394 },
2395 UpdateMatchingEdgeProperty {
2396 relationship: GraphRelationshipMatch,
2397 target_key: String,
2398 source_key: String,
2399 op: GraphNumericOp,
2400 operand: Value,
2401 },
2402 RemoveNodeProps {
2403 id: NodeId,
2404 keys: Vec<String>,
2405 },
2406 RemoveMatchingNodeProps {
2407 label: Option<Label>,
2408 props: Props,
2409 predicates: Vec<GraphPropertyPredicate>,
2410 keys: Vec<String>,
2411 },
2412 RemoveEdgeProps {
2413 from: NodeId,
2414 label: Label,
2415 to: NodeId,
2416 id: Option<EdgeId>,
2417 keys: Vec<String>,
2418 },
2419 RemoveMatchingEdgeProps {
2420 relationship: GraphRelationshipMatch,
2421 keys: Vec<String>,
2422 },
2423 DeleteMatchingNodes {
2424 label: Option<Label>,
2425 props: Props,
2426 predicates: Vec<GraphPropertyPredicate>,
2427 },
2428 DeleteNode(NodeId),
2429 UpsertEdge(Edge),
2430 UpsertEdgesFromNodeMatches {
2431 kind: GraphMutationPlanKind,
2432 from: GraphNodeMatch,
2433 to: GraphNodeMatch,
2434 label: Label,
2435 props: Props,
2436 edge_id_policy: GraphRowEdgeIdPolicy,
2437 },
2438 DeleteEdge {
2439 from: NodeId,
2440 label: Label,
2441 to: NodeId,
2442 },
2443 DeleteMatchingEdges {
2444 relationship: GraphRelationshipMatch,
2445 },
2446}
2447
2448#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2449pub enum GraphMutationAtomicity {
2450 OrderedNonAtomic,
2451 Transactional,
2452}
2453
2454#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2455pub enum GraphMutationPlanKind {
2456 Create,
2457 Merge,
2458}
2459
2460#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2461pub enum GraphRowEdgeIdPolicy {
2462 ExplicitOnly,
2463 GenerateForCreate,
2464 GenerateForCreateAndMerge,
2465}
2466
2467impl Default for GraphRowEdgeIdPolicy {
2468 fn default() -> Self {
2469 Self::ExplicitOnly
2470 }
2471}
2472
2473#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2474pub enum GraphMutationCardinality {
2475 SingleIdentity,
2476 BoundedMany,
2477 UnboundedMany,
2478}
2479
2480pub fn generated_row_edge_id(from: &NodeId, label: &Label, to: &NodeId, props: &Props) -> EdgeId {
2481 const FNV_OFFSET: u64 = 0xcbf29ce484222325;
2482 const FNV_PRIME: u64 = 0x100000001b3;
2483
2484 fn write_part(hash: &mut u64, value: &str) {
2485 for byte in value.as_bytes() {
2486 *hash ^= u64::from(*byte);
2487 *hash = hash.wrapping_mul(FNV_PRIME);
2488 }
2489 *hash ^= 0xff;
2490 *hash = hash.wrapping_mul(FNV_PRIME);
2491 }
2492
2493 let mut hash = FNV_OFFSET;
2494 write_part(&mut hash, from.as_str());
2495 write_part(&mut hash, label.as_str());
2496 write_part(&mut hash, to.as_str());
2497 for (key, value) in props {
2498 write_part(&mut hash, key);
2499 write_part(&mut hash, &value.to_json().to_string());
2500 }
2501 EdgeId::new(format!("edge-{hash:016x}"))
2502}
2503
2504#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2505pub enum GraphNumericOp {
2506 Add,
2507 Subtract,
2508 Multiply,
2509 Divide,
2510}
2511
2512pub fn evaluate_numeric_update(
2513 current: &Value,
2514 op: GraphNumericOp,
2515 operand: &Value,
2516) -> Result<Value> {
2517 match (current, operand) {
2518 (Value::Int(lhs), Value::Int(rhs)) if op != GraphNumericOp::Divide => {
2519 let value = match op {
2520 GraphNumericOp::Add => lhs.checked_add(*rhs),
2521 GraphNumericOp::Subtract => lhs.checked_sub(*rhs),
2522 GraphNumericOp::Multiply => lhs.checked_mul(*rhs),
2523 GraphNumericOp::Divide => unreachable!("division handled as floating point"),
2524 }
2525 .ok_or_else(|| GrustError::CypherExecution("numeric expression overflow".into()))?;
2526 Ok(Value::Int(value))
2527 }
2528 (Value::Int(lhs), Value::Int(rhs)) => numeric_float_result(*lhs as f64, op, *rhs as f64),
2529 (Value::Int(lhs), Value::Float(rhs)) => numeric_float_result(*lhs as f64, op, *rhs),
2530 (Value::Float(lhs), Value::Int(rhs)) => numeric_float_result(*lhs, op, *rhs as f64),
2531 (Value::Float(lhs), Value::Float(rhs)) => numeric_float_result(*lhs, op, *rhs),
2532 (Value::Null, _) | (_, Value::Null) => Err(GrustError::CypherExecution(
2533 "numeric expression cannot read null values".into(),
2534 )),
2535 _ => Err(GrustError::CypherExecution(
2536 "numeric expression requires integer or float values".into(),
2537 )),
2538 }
2539}
2540
2541fn numeric_float_result(lhs: f64, op: GraphNumericOp, rhs: f64) -> Result<Value> {
2542 let value = match op {
2543 GraphNumericOp::Add => lhs + rhs,
2544 GraphNumericOp::Subtract => lhs - rhs,
2545 GraphNumericOp::Multiply => lhs * rhs,
2546 GraphNumericOp::Divide => {
2547 if rhs == 0.0 {
2548 return Err(GrustError::CypherExecution(
2549 "numeric expression division by zero".into(),
2550 ));
2551 }
2552 lhs / rhs
2553 }
2554 };
2555 if value.is_finite() {
2556 Ok(Value::Float(value))
2557 } else {
2558 Err(GrustError::CypherExecution(
2559 "numeric expression produced a non-finite float".into(),
2560 ))
2561 }
2562}
2563
2564#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2565pub enum GraphPredicateOp {
2566 Equal,
2567 NotEqual,
2568 IsNull,
2569 IsNotNull,
2570 StartsWith,
2571 NotStartsWith,
2572 StartsWithAny,
2573 NotStartsWithAny,
2574 EndsWith,
2575 NotEndsWith,
2576 EndsWithAny,
2577 NotEndsWithAny,
2578 Contains,
2579 NotContains,
2580 ContainsAny,
2581 NotContainsAny,
2582 In,
2583 NotIn,
2584 GreaterThan,
2585 GreaterThanOrEqual,
2586 LessThan,
2587 LessThanOrEqual,
2588}
2589
2590#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2591pub struct GraphPropertyPredicate {
2592 pub key: String,
2593 pub op: GraphPredicateOp,
2594 pub value: Value,
2595}
2596
2597impl GraphPropertyPredicate {
2598 pub fn matches(&self, actual: Option<&Value>) -> bool {
2599 if matches!(self.op, GraphPredicateOp::IsNull) {
2600 return actual.is_none_or(|value| matches!(value, Value::Null));
2601 }
2602 let Some(actual) = actual else {
2603 return false;
2604 };
2605 match self.op {
2606 GraphPredicateOp::Equal => actual == &self.value,
2607 GraphPredicateOp::NotEqual => actual != &self.value,
2608 GraphPredicateOp::IsNull => matches!(actual, Value::Null),
2609 GraphPredicateOp::IsNotNull => !matches!(actual, Value::Null),
2610 GraphPredicateOp::StartsWith => string_predicate_values(actual, &self.value)
2611 .is_some_and(|(actual, needle)| actual.starts_with(needle)),
2612 GraphPredicateOp::NotStartsWith => string_predicate_values(actual, &self.value)
2613 .is_some_and(|(actual, needle)| !actual.starts_with(needle)),
2614 GraphPredicateOp::StartsWithAny => string_list_predicate_values(actual, &self.value)
2615 .is_some_and(|(actual, needles)| {
2616 needles.iter().any(|needle| actual.starts_with(needle))
2617 }),
2618 GraphPredicateOp::NotStartsWithAny => string_list_predicate_values(actual, &self.value)
2619 .is_some_and(|(actual, needles)| {
2620 needles.iter().all(|needle| !actual.starts_with(needle))
2621 }),
2622 GraphPredicateOp::EndsWith => string_predicate_values(actual, &self.value)
2623 .is_some_and(|(actual, needle)| actual.ends_with(needle)),
2624 GraphPredicateOp::NotEndsWith => string_predicate_values(actual, &self.value)
2625 .is_some_and(|(actual, needle)| !actual.ends_with(needle)),
2626 GraphPredicateOp::EndsWithAny => string_list_predicate_values(actual, &self.value)
2627 .is_some_and(|(actual, needles)| {
2628 needles.iter().any(|needle| actual.ends_with(needle))
2629 }),
2630 GraphPredicateOp::NotEndsWithAny => string_list_predicate_values(actual, &self.value)
2631 .is_some_and(|(actual, needles)| {
2632 needles.iter().all(|needle| !actual.ends_with(needle))
2633 }),
2634 GraphPredicateOp::Contains => string_predicate_values(actual, &self.value)
2635 .is_some_and(|(actual, needle)| actual.contains(needle)),
2636 GraphPredicateOp::NotContains => string_predicate_values(actual, &self.value)
2637 .is_some_and(|(actual, needle)| !actual.contains(needle)),
2638 GraphPredicateOp::ContainsAny => string_list_predicate_values(actual, &self.value)
2639 .is_some_and(|(actual, needles)| {
2640 needles.iter().any(|needle| actual.contains(needle))
2641 }),
2642 GraphPredicateOp::NotContainsAny => string_list_predicate_values(actual, &self.value)
2643 .is_some_and(|(actual, needles)| {
2644 needles.iter().all(|needle| !actual.contains(needle))
2645 }),
2646 GraphPredicateOp::In => list_predicate_values(&self.value)
2647 .is_some_and(|values| values.iter().any(|value| actual == value)),
2648 GraphPredicateOp::NotIn => list_predicate_values(&self.value)
2649 .is_some_and(|values| values.iter().all(|value| actual != value)),
2650 GraphPredicateOp::GreaterThan
2651 | GraphPredicateOp::GreaterThanOrEqual
2652 | GraphPredicateOp::LessThan
2653 | GraphPredicateOp::LessThanOrEqual => compare_ordered_values(actual, &self.value)
2654 .is_some_and(|ordering| match self.op {
2655 GraphPredicateOp::GreaterThan => ordering.is_gt(),
2656 GraphPredicateOp::GreaterThanOrEqual => ordering.is_gt() || ordering.is_eq(),
2657 GraphPredicateOp::LessThan => ordering.is_lt(),
2658 GraphPredicateOp::LessThanOrEqual => ordering.is_lt() || ordering.is_eq(),
2659 GraphPredicateOp::Equal
2660 | GraphPredicateOp::NotEqual
2661 | GraphPredicateOp::IsNull
2662 | GraphPredicateOp::IsNotNull
2663 | GraphPredicateOp::StartsWith
2664 | GraphPredicateOp::NotStartsWith
2665 | GraphPredicateOp::StartsWithAny
2666 | GraphPredicateOp::NotStartsWithAny
2667 | GraphPredicateOp::EndsWith
2668 | GraphPredicateOp::NotEndsWith
2669 | GraphPredicateOp::EndsWithAny
2670 | GraphPredicateOp::NotEndsWithAny
2671 | GraphPredicateOp::Contains
2672 | GraphPredicateOp::NotContains
2673 | GraphPredicateOp::ContainsAny
2674 | GraphPredicateOp::NotContainsAny
2675 | GraphPredicateOp::In
2676 | GraphPredicateOp::NotIn => unreachable!(),
2677 }),
2678 }
2679 }
2680}
2681
2682fn string_predicate_values<'a>(actual: &'a Value, value: &'a Value) -> Option<(&'a str, &'a str)> {
2683 match (actual, value) {
2684 (Value::String(actual), Value::String(needle)) => Some((actual.as_str(), needle.as_str())),
2685 _ => None,
2686 }
2687}
2688
2689fn string_list_predicate_values<'a>(
2690 actual: &'a Value,
2691 value: &'a Value,
2692) -> Option<(&'a str, Vec<&'a str>)> {
2693 match (actual, value) {
2694 (Value::String(actual), Value::StringArray(needles)) => Some((
2695 actual.as_str(),
2696 needles.iter().map(String::as_str).collect(),
2697 )),
2698 _ => None,
2699 }
2700}
2701
2702fn list_predicate_values(value: &Value) -> Option<Vec<Value>> {
2703 match value {
2704 Value::StringArray(values) => Some(values.iter().map(Value::from).collect()),
2705 Value::IntArray(values) => Some(values.iter().copied().map(Value::Int).collect()),
2706 Value::FloatArray(values) => Some(values.iter().copied().map(Value::Float).collect()),
2707 Value::Json(serde_json::Value::Array(values)) => values
2708 .iter()
2709 .map(|value| match value {
2710 serde_json::Value::Bool(value) => Some(Value::Bool(*value)),
2711 serde_json::Value::Number(value) => value
2712 .as_i64()
2713 .map(Value::Int)
2714 .or_else(|| value.as_f64().map(Value::Float)),
2715 serde_json::Value::String(value) => Some(Value::from(value)),
2716 serde_json::Value::Null
2717 | serde_json::Value::Array(_)
2718 | serde_json::Value::Object(_) => None,
2719 })
2720 .collect(),
2721 _ => None,
2722 }
2723}
2724
2725fn compare_ordered_values(lhs: &Value, rhs: &Value) -> Option<std::cmp::Ordering> {
2726 match (lhs, rhs) {
2727 (Value::Int(lhs), Value::Int(rhs)) => Some(lhs.cmp(rhs)),
2728 (Value::Int(lhs), Value::Float(rhs)) => (*lhs as f64).partial_cmp(rhs),
2729 (Value::Float(lhs), Value::Int(rhs)) => lhs.partial_cmp(&(*rhs as f64)),
2730 (Value::Float(lhs), Value::Float(rhs)) => lhs.partial_cmp(rhs),
2731 (Value::String(lhs), Value::String(rhs)) => Some(lhs.cmp(rhs)),
2732 _ => None,
2733 }
2734}
2735
2736#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
2737pub struct GraphNodeMatch {
2738 pub label: Option<Label>,
2739 pub props: Props,
2740 pub predicates: Vec<GraphPropertyPredicate>,
2741}
2742
2743#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2744pub struct GraphRelationshipMatch {
2745 pub from: GraphNodeMatch,
2746 pub label: Label,
2747 pub to: GraphNodeMatch,
2748 pub id: Option<EdgeId>,
2749 pub props: Props,
2750 pub predicates: Vec<GraphPropertyPredicate>,
2751}
2752
2753#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2754pub enum GraphMutationPlanOp {
2755 UpsertNode {
2756 kind: GraphMutationPlanKind,
2757 node: Node,
2758 },
2759 PatchNode {
2760 id: NodeId,
2761 props: Props,
2762 },
2763 PatchMatchingNodes {
2764 label: Option<Label>,
2765 props: Props,
2766 predicates: Vec<GraphPropertyPredicate>,
2767 patch: Props,
2768 cardinality: GraphMutationCardinality,
2769 },
2770 UpdateMatchingNodeProperty {
2771 label: Option<Label>,
2772 props: Props,
2773 predicates: Vec<GraphPropertyPredicate>,
2774 target_key: String,
2775 source_key: String,
2776 op: GraphNumericOp,
2777 operand: Value,
2778 cardinality: GraphMutationCardinality,
2779 },
2780 PatchEdge {
2781 from: NodeId,
2782 label: Label,
2783 to: NodeId,
2784 id: Option<EdgeId>,
2785 props: Props,
2786 },
2787 PatchMatchingEdges {
2788 relationship: GraphRelationshipMatch,
2789 patch: Props,
2790 cardinality: GraphMutationCardinality,
2791 },
2792 UpdateMatchingEdgeProperty {
2793 relationship: GraphRelationshipMatch,
2794 target_key: String,
2795 source_key: String,
2796 op: GraphNumericOp,
2797 operand: Value,
2798 cardinality: GraphMutationCardinality,
2799 },
2800 RemoveNodeProps {
2801 id: NodeId,
2802 keys: Vec<String>,
2803 },
2804 RemoveMatchingNodeProps {
2805 label: Option<Label>,
2806 props: Props,
2807 predicates: Vec<GraphPropertyPredicate>,
2808 keys: Vec<String>,
2809 cardinality: GraphMutationCardinality,
2810 },
2811 RemoveEdgeProps {
2812 from: NodeId,
2813 label: Label,
2814 to: NodeId,
2815 id: Option<EdgeId>,
2816 keys: Vec<String>,
2817 },
2818 RemoveMatchingEdgeProps {
2819 relationship: GraphRelationshipMatch,
2820 keys: Vec<String>,
2821 cardinality: GraphMutationCardinality,
2822 },
2823 DeleteMatchingNodes {
2824 label: Option<Label>,
2825 props: Props,
2826 predicates: Vec<GraphPropertyPredicate>,
2827 cardinality: GraphMutationCardinality,
2828 },
2829 UpsertEdge {
2830 kind: GraphMutationPlanKind,
2831 edge: Edge,
2832 },
2833 UpsertEdgesFromNodeMatches {
2834 kind: GraphMutationPlanKind,
2835 from: GraphNodeMatch,
2836 to: GraphNodeMatch,
2837 label: Label,
2838 props: Props,
2839 edge_id_policy: GraphRowEdgeIdPolicy,
2840 cardinality: GraphMutationCardinality,
2841 },
2842 DeleteNode(NodeId),
2843 DeleteEdge {
2844 from: NodeId,
2845 label: Label,
2846 to: NodeId,
2847 },
2848 DeleteMatchingEdges {
2849 relationship: GraphRelationshipMatch,
2850 cardinality: GraphMutationCardinality,
2851 },
2852}
2853
2854#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
2855pub struct GraphMutationPlan {
2856 pub operations: Vec<GraphMutationPlanOp>,
2857}
2858
2859impl GraphMutationPlan {
2860 pub fn new(operations: Vec<GraphMutationPlanOp>) -> Self {
2861 Self { operations }
2862 }
2863
2864 pub fn push(&mut self, operation: GraphMutationPlanOp) {
2865 self.operations.push(operation);
2866 }
2867
2868 pub fn report(&self) -> GraphMutationReport {
2869 let mut report = GraphMutationReport::default();
2870 for operation in &self.operations {
2871 report.record(operation);
2872 }
2873 report
2874 }
2875
2876 pub fn into_mutations(self) -> Vec<GraphMutation> {
2877 self.operations
2878 .into_iter()
2879 .map(GraphMutation::from)
2880 .collect()
2881 }
2882}
2883
2884#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
2893pub struct GraphMutationReport {
2894 pub creates: usize,
2895 pub merges: usize,
2896 pub deletes: usize,
2897 pub patches: usize,
2898 pub property_removes: usize,
2899 pub matched_rows: usize,
2900 pub changed_nodes: usize,
2901 pub changed_edges: usize,
2902 pub node_upserts: usize,
2903 pub edge_upserts: usize,
2904 pub node_deletes: usize,
2905 pub edge_deletes: usize,
2906 pub node_patches: usize,
2907 pub edge_patches: usize,
2908 pub node_property_removes: usize,
2909 pub edge_property_removes: usize,
2910 pub node_inserts: usize,
2919 pub node_updates: usize,
2921 pub edge_inserts: usize,
2923 pub edge_updates: usize,
2925}
2926
2927impl GraphMutationReport {
2928 pub fn record(&mut self, operation: &GraphMutationPlanOp) {
2929 match operation {
2930 GraphMutationPlanOp::UpsertNode { kind, .. }
2931 | GraphMutationPlanOp::UpsertEdge { kind, .. } => {
2932 match kind {
2933 GraphMutationPlanKind::Create => self.creates += 1,
2934 GraphMutationPlanKind::Merge => self.merges += 1,
2935 }
2936 match operation {
2937 GraphMutationPlanOp::UpsertNode { .. } => {
2938 self.node_upserts += 1;
2939 self.changed_nodes += 1;
2940 }
2941 GraphMutationPlanOp::UpsertEdge { .. } => {
2942 self.edge_upserts += 1;
2943 self.changed_edges += 1;
2944 }
2945 _ => {}
2946 }
2947 }
2948 GraphMutationPlanOp::UpsertEdgesFromNodeMatches { kind, .. } => match kind {
2949 GraphMutationPlanKind::Create => self.creates += 1,
2950 GraphMutationPlanKind::Merge => self.merges += 1,
2951 },
2952 GraphMutationPlanOp::PatchNode { .. } => {
2953 self.patches += 1;
2954 self.node_patches += 1;
2955 self.changed_nodes += 1;
2956 }
2957 GraphMutationPlanOp::PatchMatchingNodes { .. } => {
2958 self.patches += 1;
2959 }
2960 GraphMutationPlanOp::UpdateMatchingNodeProperty { .. } => {
2961 self.patches += 1;
2962 }
2963 GraphMutationPlanOp::PatchEdge { .. } => {
2964 self.patches += 1;
2965 self.edge_patches += 1;
2966 self.changed_edges += 1;
2967 }
2968 GraphMutationPlanOp::PatchMatchingEdges { .. } => {
2969 self.patches += 1;
2970 }
2971 GraphMutationPlanOp::UpdateMatchingEdgeProperty { .. } => {
2972 self.patches += 1;
2973 }
2974 GraphMutationPlanOp::RemoveNodeProps { .. } => {
2975 self.property_removes += 1;
2976 self.node_property_removes += 1;
2977 self.changed_nodes += 1;
2978 }
2979 GraphMutationPlanOp::RemoveMatchingNodeProps { .. } => {
2980 self.property_removes += 1;
2981 }
2982 GraphMutationPlanOp::RemoveEdgeProps { .. } => {
2983 self.property_removes += 1;
2984 self.edge_property_removes += 1;
2985 self.changed_edges += 1;
2986 }
2987 GraphMutationPlanOp::RemoveMatchingEdgeProps { .. } => {
2988 self.property_removes += 1;
2989 }
2990 GraphMutationPlanOp::DeleteMatchingNodes { .. } => {
2991 self.deletes += 1;
2992 }
2993 GraphMutationPlanOp::DeleteNode(_) => {
2994 self.deletes += 1;
2995 self.node_deletes += 1;
2996 self.changed_nodes += 1;
2997 }
2998 GraphMutationPlanOp::DeleteEdge { .. } => {
2999 self.deletes += 1;
3000 self.edge_deletes += 1;
3001 self.changed_edges += 1;
3002 }
3003 GraphMutationPlanOp::DeleteMatchingEdges { .. } => {
3004 self.deletes += 1;
3005 }
3006 }
3007 }
3008}
3009
3010impl From<GraphMutationPlanOp> for GraphMutation {
3011 fn from(operation: GraphMutationPlanOp) -> Self {
3012 match operation {
3013 GraphMutationPlanOp::UpsertNode { node, .. } => Self::UpsertNode(node),
3014 GraphMutationPlanOp::PatchNode { id, props } => Self::PatchNode { id, props },
3015 GraphMutationPlanOp::PatchMatchingNodes {
3016 label,
3017 props,
3018 predicates,
3019 patch,
3020 ..
3021 } => Self::PatchMatchingNodes {
3022 label,
3023 props,
3024 predicates,
3025 patch,
3026 },
3027 GraphMutationPlanOp::UpdateMatchingNodeProperty {
3028 label,
3029 props,
3030 predicates,
3031 target_key,
3032 source_key,
3033 op,
3034 operand,
3035 ..
3036 } => Self::UpdateMatchingNodeProperty {
3037 label,
3038 props,
3039 predicates,
3040 target_key,
3041 source_key,
3042 op,
3043 operand,
3044 },
3045 GraphMutationPlanOp::PatchEdge {
3046 from,
3047 label,
3048 to,
3049 id,
3050 props,
3051 } => Self::PatchEdge {
3052 from,
3053 label,
3054 to,
3055 id,
3056 props,
3057 },
3058 GraphMutationPlanOp::PatchMatchingEdges {
3059 relationship,
3060 patch,
3061 ..
3062 } => Self::PatchMatchingEdges {
3063 relationship,
3064 patch,
3065 },
3066 GraphMutationPlanOp::UpdateMatchingEdgeProperty {
3067 relationship,
3068 target_key,
3069 source_key,
3070 op,
3071 operand,
3072 ..
3073 } => Self::UpdateMatchingEdgeProperty {
3074 relationship,
3075 target_key,
3076 source_key,
3077 op,
3078 operand,
3079 },
3080 GraphMutationPlanOp::RemoveNodeProps { id, keys } => Self::RemoveNodeProps { id, keys },
3081 GraphMutationPlanOp::RemoveEdgeProps {
3082 from,
3083 label,
3084 to,
3085 id,
3086 keys,
3087 } => Self::RemoveEdgeProps {
3088 from,
3089 label,
3090 to,
3091 id,
3092 keys,
3093 },
3094 GraphMutationPlanOp::RemoveMatchingEdgeProps {
3095 relationship, keys, ..
3096 } => Self::RemoveMatchingEdgeProps { relationship, keys },
3097 GraphMutationPlanOp::RemoveMatchingNodeProps {
3098 label,
3099 props,
3100 predicates,
3101 keys,
3102 ..
3103 } => Self::RemoveMatchingNodeProps {
3104 label,
3105 props,
3106 predicates,
3107 keys,
3108 },
3109 GraphMutationPlanOp::DeleteMatchingNodes {
3110 label,
3111 props,
3112 predicates,
3113 ..
3114 } => Self::DeleteMatchingNodes {
3115 label,
3116 props,
3117 predicates,
3118 },
3119 GraphMutationPlanOp::UpsertEdge { edge, .. } => Self::UpsertEdge(edge),
3120 GraphMutationPlanOp::UpsertEdgesFromNodeMatches {
3121 kind,
3122 from,
3123 to,
3124 label,
3125 props,
3126 edge_id_policy,
3127 ..
3128 } => Self::UpsertEdgesFromNodeMatches {
3129 kind,
3130 from,
3131 to,
3132 label,
3133 props,
3134 edge_id_policy,
3135 },
3136 GraphMutationPlanOp::DeleteNode(id) => Self::DeleteNode(id),
3137 GraphMutationPlanOp::DeleteEdge { from, label, to } => {
3138 Self::DeleteEdge { from, label, to }
3139 }
3140 GraphMutationPlanOp::DeleteMatchingEdges { relationship, .. } => {
3141 Self::DeleteMatchingEdges { relationship }
3142 }
3143 }
3144 }
3145}
3146
3147#[async_trait]
3154pub trait CypherMutationExecutor: GraphMutationStore {
3155 async fn execute_cypher_mutation_plan(
3156 &self,
3157 plan: &GraphMutationPlan,
3158 ) -> Result<GraphMutationReport> {
3159 let mut report = plan.report();
3160 for operation in &plan.operations {
3161 match operation {
3162 GraphMutationPlanOp::DeleteMatchingNodes { .. } => {
3163 return Err(GrustError::CypherExecution(
3164 "matched node deletes require backend-specific query support".to_string(),
3165 ));
3166 }
3167 GraphMutationPlanOp::PatchMatchingNodes { .. } => {
3168 return Err(GrustError::CypherExecution(
3169 "matched node patches require backend-specific query support".to_string(),
3170 ));
3171 }
3172 GraphMutationPlanOp::UpdateMatchingNodeProperty { .. } => {
3173 return Err(GrustError::CypherExecution(
3174 "matched node expression updates require backend-specific query support"
3175 .to_string(),
3176 ));
3177 }
3178 GraphMutationPlanOp::RemoveMatchingNodeProps { .. } => {
3179 return Err(GrustError::CypherExecution(
3180 "matched node property removals require backend-specific query support"
3181 .to_string(),
3182 ));
3183 }
3184 GraphMutationPlanOp::PatchMatchingEdges { .. } => {
3185 return Err(GrustError::CypherExecution(
3186 "matched edge patches require backend-specific query support".to_string(),
3187 ));
3188 }
3189 GraphMutationPlanOp::UpdateMatchingEdgeProperty { .. } => {
3190 return Err(GrustError::CypherExecution(
3191 "matched edge expression updates require backend-specific query support"
3192 .to_string(),
3193 ));
3194 }
3195 GraphMutationPlanOp::RemoveMatchingEdgeProps { .. } => {
3196 return Err(GrustError::CypherExecution(
3197 "matched edge property removals require backend-specific query support"
3198 .to_string(),
3199 ));
3200 }
3201 GraphMutationPlanOp::DeleteMatchingEdges { .. } => {
3202 return Err(GrustError::CypherExecution(
3203 "matched edge deletes require backend-specific query support".to_string(),
3204 ));
3205 }
3206 GraphMutationPlanOp::UpsertEdgesFromNodeMatches { .. } => {
3207 return Err(GrustError::CypherExecution(
3208 "row-producing edge upserts require backend-specific query support"
3209 .to_string(),
3210 ));
3211 }
3212 GraphMutationPlanOp::UpsertNode { node, .. } => {
3213 classify_node_upsert(self.put_node(node).await?, &mut report);
3214 }
3215 GraphMutationPlanOp::UpsertEdge { edge, .. } => {
3216 classify_edge_upsert(self.put_edge(edge).await?, &mut report);
3217 }
3218 _ => {
3219 let mutation = GraphMutation::from(operation.clone());
3220 self.apply_mutations(std::slice::from_ref(&mutation))
3221 .await?;
3222 }
3223 }
3224 }
3225 Ok(report)
3226 }
3227}
3228
3229pub fn classify_node_upsert(outcome: PutOutcome, report: &mut GraphMutationReport) {
3233 match outcome {
3234 PutOutcome::Inserted => report.node_inserts += 1,
3235 PutOutcome::Updated => report.node_updates += 1,
3236 PutOutcome::Upserted | PutOutcome::Deduped => {}
3237 }
3238}
3239
3240pub fn classify_edge_upsert(outcome: PutOutcome, report: &mut GraphMutationReport) {
3243 match outcome {
3244 PutOutcome::Inserted => report.edge_inserts += 1,
3245 PutOutcome::Updated => report.edge_updates += 1,
3246 PutOutcome::Upserted | PutOutcome::Deduped => {}
3247 }
3248}
3249
3250#[async_trait]
3255pub trait GraphMutationStore: GraphStore {
3256 fn mutation_atomicity(&self) -> GraphMutationAtomicity {
3257 GraphMutationAtomicity::OrderedNonAtomic
3258 }
3259
3260 async fn delete_node(&self, id: &NodeId) -> Result<()>;
3262
3263 async fn delete_edge(&self, from: &NodeId, label: &Label, to: &NodeId) -> Result<()>;
3265
3266 async fn apply_mutations(&self, mutations: &[GraphMutation]) -> Result<()> {
3273 for mutation in mutations {
3274 match mutation {
3275 GraphMutation::UpsertNode(node) => {
3276 self.put_node(node).await?;
3277 }
3278 GraphMutation::PatchNode { id, props } => {
3279 if let Some(mut node) = self.get_node(id).await? {
3280 for (key, value) in props {
3281 node.props.insert(key.clone(), value.clone());
3282 }
3283 self.put_node(&node).await?;
3284 }
3285 }
3286 GraphMutation::PatchMatchingNodes { .. } => {
3287 return Err(GrustError::Unsupported(
3288 "matched node patches require backend-specific query support".to_string(),
3289 ));
3290 }
3291 GraphMutation::UpdateMatchingNodeProperty { .. } => {
3292 return Err(GrustError::Unsupported(
3293 "matched node expression updates require backend-specific query support"
3294 .to_string(),
3295 ));
3296 }
3297 GraphMutation::PatchEdge {
3298 from,
3299 label,
3300 to,
3301 id,
3302 props,
3303 } => {
3304 let mut edges = self
3305 .get_edges(EdgeQuery {
3306 from: Some(from.clone()),
3307 to: Some(to.clone()),
3308 label: Some(label.clone()),
3309 })
3310 .await?;
3311 if let Some(id) = id {
3312 edges.retain(|edge| edge.id.as_ref() == Some(id));
3313 }
3314 match edges.len() {
3315 0 => {}
3316 1 => {
3317 let mut edge = edges.remove(0);
3318 for (key, value) in props {
3319 edge.props.insert(key.clone(), value.clone());
3320 }
3321 self.put_edge(&edge).await?;
3322 }
3323 count => {
3324 return Err(GrustError::CypherUnsupportedCardinality(format!(
3325 "edge patch matched {count} edges; add an explicit edge id"
3326 )));
3327 }
3328 }
3329 }
3330 GraphMutation::PatchMatchingEdges { .. } => {
3331 return Err(GrustError::Unsupported(
3332 "matched edge patches require backend-specific query support".to_string(),
3333 ));
3334 }
3335 GraphMutation::UpdateMatchingEdgeProperty { .. } => {
3336 return Err(GrustError::Unsupported(
3337 "matched edge expression updates require backend-specific query support"
3338 .to_string(),
3339 ));
3340 }
3341 GraphMutation::RemoveNodeProps { id, keys } => {
3342 if let Some(mut node) = self.get_node(id).await? {
3343 for key in keys {
3344 node.props.remove(key);
3345 }
3346 self.put_node(&node).await?;
3347 }
3348 }
3349 GraphMutation::RemoveMatchingNodeProps { .. } => {
3350 return Err(GrustError::Unsupported(
3351 "matched node property removal requires backend-specific query support"
3352 .to_string(),
3353 ));
3354 }
3355 GraphMutation::RemoveEdgeProps {
3356 from,
3357 label,
3358 to,
3359 id,
3360 keys,
3361 } => {
3362 let mut edges = self
3363 .get_edges(EdgeQuery {
3364 from: Some(from.clone()),
3365 to: Some(to.clone()),
3366 label: Some(label.clone()),
3367 })
3368 .await?;
3369 if let Some(id) = id {
3370 edges.retain(|edge| edge.id.as_ref() == Some(id));
3371 }
3372 match edges.len() {
3373 0 => {}
3374 1 => {
3375 let mut edge = edges.remove(0);
3376 for key in keys {
3377 edge.props.remove(key);
3378 }
3379 self.put_edge(&edge).await?;
3380 }
3381 count => {
3382 return Err(GrustError::CypherUnsupportedCardinality(format!(
3383 "edge property removal matched {count} edges; add an explicit edge id"
3384 )));
3385 }
3386 }
3387 }
3388 GraphMutation::RemoveMatchingEdgeProps { .. } => {
3389 return Err(GrustError::Unsupported(
3390 "matched edge property removal requires backend-specific query support"
3391 .to_string(),
3392 ));
3393 }
3394 GraphMutation::DeleteMatchingNodes { .. } => {
3395 return Err(GrustError::Unsupported(
3396 "matched node deletes require backend-specific query support".to_string(),
3397 ));
3398 }
3399 GraphMutation::DeleteNode(id) => self.delete_node(id).await?,
3400 GraphMutation::UpsertEdge(edge) => {
3401 self.put_edge(edge).await?;
3402 }
3403 GraphMutation::UpsertEdgesFromNodeMatches { .. } => {
3404 return Err(GrustError::Unsupported(
3405 "row-producing edge upserts require backend-specific query support"
3406 .to_string(),
3407 ));
3408 }
3409 GraphMutation::DeleteEdge { from, label, to } => {
3410 self.delete_edge(from, label, to).await?
3411 }
3412 GraphMutation::DeleteMatchingEdges { .. } => {
3413 return Err(GrustError::Unsupported(
3414 "matched edge deletes require backend-specific query support".to_string(),
3415 ));
3416 }
3417 }
3418 }
3419 Ok(())
3420 }
3421}
3422
3423pub mod prelude {
3424 pub use crate::{
3425 CypherMutationExecutor, Direction, Edge, EdgeId, EdgePolicy, EdgeQuery, EdgeType,
3426 EdgeUniqueness, Field, FieldType, Graph, GraphAdminStore, GraphBuilder, GraphConstraint,
3427 GraphConstraintCapability, GraphIndex, GraphMutation, GraphMutationAtomicity,
3428 GraphMutationCardinality, GraphMutationPlan, GraphMutationPlanKind, GraphMutationPlanOp,
3429 GraphMutationReport, GraphMutationStore, GraphNativeConstraintCapability,
3430 GraphNativeConstraintReport, GraphNativeConstraintRequest, GraphNodeMatch, GraphNumericOp,
3431 GraphPredicateOp, GraphPropertyPredicate, GraphRelationshipMatch, GraphRowEdgeIdPolicy,
3432 GraphSchema, GraphSchemaBuilder, GraphStore, GrustError, Label, LoadReport, Node, NodeId,
3433 NodeType, Props, PutOutcome, Result, RfcDate, Start, Step, Traversal, Value,
3434 classify_edge_upsert, classify_node_upsert, edge_key, evaluate_numeric_update,
3435 generated_row_edge_id, relationship_type, schema_identifier,
3436 };
3437
3438 #[cfg(feature = "typed-garde")]
3439 pub use crate::typed::{TypedEdge, TypedGraphBuilder, TypedNode, garde, props_from_serialize};
3440
3441 #[cfg(feature = "typed-zod-rs")]
3442 pub use crate::typed::{parse_typed_json, parse_typed_json_with, zod_rs};
3443}
3444
3445#[cfg(test)]
3446mod tests;