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 DeleteRelationshipRows {
2447 relationship: GraphRelationshipMatch,
2448 delete_edges: bool,
2449 endpoint_nodes: Vec<GraphRelationshipEndpoint>,
2450 },
2451}
2452
2453#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2454pub enum GraphMutationAtomicity {
2455 OrderedNonAtomic,
2456 Transactional,
2457}
2458
2459#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2460pub enum GraphMutationPlanKind {
2461 Create,
2462 Merge,
2463}
2464
2465#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2466pub enum GraphRowEdgeIdPolicy {
2467 ExplicitOnly,
2468 GenerateForCreate,
2469 GenerateForCreateAndMerge,
2470}
2471
2472#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2473pub enum GraphRelationshipEndpoint {
2474 From,
2475 To,
2476}
2477
2478impl Default for GraphRowEdgeIdPolicy {
2479 fn default() -> Self {
2480 Self::ExplicitOnly
2481 }
2482}
2483
2484#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2485pub enum GraphMutationCardinality {
2486 SingleIdentity,
2487 BoundedMany,
2488 UnboundedMany,
2489}
2490
2491pub fn generated_row_edge_id(from: &NodeId, label: &Label, to: &NodeId, props: &Props) -> EdgeId {
2492 const FNV_OFFSET: u64 = 0xcbf29ce484222325;
2493 const FNV_PRIME: u64 = 0x100000001b3;
2494
2495 fn write_part(hash: &mut u64, value: &str) {
2496 for byte in value.as_bytes() {
2497 *hash ^= u64::from(*byte);
2498 *hash = hash.wrapping_mul(FNV_PRIME);
2499 }
2500 *hash ^= 0xff;
2501 *hash = hash.wrapping_mul(FNV_PRIME);
2502 }
2503
2504 let mut hash = FNV_OFFSET;
2505 write_part(&mut hash, from.as_str());
2506 write_part(&mut hash, label.as_str());
2507 write_part(&mut hash, to.as_str());
2508 for (key, value) in props {
2509 write_part(&mut hash, key);
2510 write_part(&mut hash, &value.to_json().to_string());
2511 }
2512 EdgeId::new(format!("edge-{hash:016x}"))
2513}
2514
2515#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2516pub enum GraphNumericOp {
2517 Add,
2518 Subtract,
2519 Multiply,
2520 Divide,
2521}
2522
2523pub fn evaluate_numeric_update(
2524 current: &Value,
2525 op: GraphNumericOp,
2526 operand: &Value,
2527) -> Result<Value> {
2528 match (current, operand) {
2529 (Value::Int(lhs), Value::Int(rhs)) if op != GraphNumericOp::Divide => {
2530 let value = match op {
2531 GraphNumericOp::Add => lhs.checked_add(*rhs),
2532 GraphNumericOp::Subtract => lhs.checked_sub(*rhs),
2533 GraphNumericOp::Multiply => lhs.checked_mul(*rhs),
2534 GraphNumericOp::Divide => unreachable!("division handled as floating point"),
2535 }
2536 .ok_or_else(|| GrustError::CypherExecution("numeric expression overflow".into()))?;
2537 Ok(Value::Int(value))
2538 }
2539 (Value::Int(lhs), Value::Int(rhs)) => numeric_float_result(*lhs as f64, op, *rhs as f64),
2540 (Value::Int(lhs), Value::Float(rhs)) => numeric_float_result(*lhs as f64, op, *rhs),
2541 (Value::Float(lhs), Value::Int(rhs)) => numeric_float_result(*lhs, op, *rhs as f64),
2542 (Value::Float(lhs), Value::Float(rhs)) => numeric_float_result(*lhs, op, *rhs),
2543 (Value::Null, _) | (_, Value::Null) => Err(GrustError::CypherExecution(
2544 "numeric expression cannot read null values".into(),
2545 )),
2546 _ => Err(GrustError::CypherExecution(
2547 "numeric expression requires integer or float values".into(),
2548 )),
2549 }
2550}
2551
2552fn numeric_float_result(lhs: f64, op: GraphNumericOp, rhs: f64) -> Result<Value> {
2553 let value = match op {
2554 GraphNumericOp::Add => lhs + rhs,
2555 GraphNumericOp::Subtract => lhs - rhs,
2556 GraphNumericOp::Multiply => lhs * rhs,
2557 GraphNumericOp::Divide => {
2558 if rhs == 0.0 {
2559 return Err(GrustError::CypherExecution(
2560 "numeric expression division by zero".into(),
2561 ));
2562 }
2563 lhs / rhs
2564 }
2565 };
2566 if value.is_finite() {
2567 Ok(Value::Float(value))
2568 } else {
2569 Err(GrustError::CypherExecution(
2570 "numeric expression produced a non-finite float".into(),
2571 ))
2572 }
2573}
2574
2575#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
2576pub enum GraphPredicateOp {
2577 Equal,
2578 NotEqual,
2579 IsNull,
2580 IsNotNull,
2581 StartsWith,
2582 NotStartsWith,
2583 StartsWithAny,
2584 NotStartsWithAny,
2585 EndsWith,
2586 NotEndsWith,
2587 EndsWithAny,
2588 NotEndsWithAny,
2589 Contains,
2590 NotContains,
2591 ContainsAny,
2592 NotContainsAny,
2593 In,
2594 NotIn,
2595 GreaterThan,
2596 GreaterThanOrEqual,
2597 LessThan,
2598 LessThanOrEqual,
2599}
2600
2601#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2602pub struct GraphPropertyPredicate {
2603 pub key: String,
2604 pub op: GraphPredicateOp,
2605 pub value: Value,
2606}
2607
2608impl GraphPropertyPredicate {
2609 pub fn matches(&self, actual: Option<&Value>) -> bool {
2610 if matches!(self.op, GraphPredicateOp::IsNull) {
2611 return actual.is_none_or(|value| matches!(value, Value::Null));
2612 }
2613 let Some(actual) = actual else {
2614 return false;
2615 };
2616 match self.op {
2617 GraphPredicateOp::Equal => actual == &self.value,
2618 GraphPredicateOp::NotEqual => actual != &self.value,
2619 GraphPredicateOp::IsNull => matches!(actual, Value::Null),
2620 GraphPredicateOp::IsNotNull => !matches!(actual, Value::Null),
2621 GraphPredicateOp::StartsWith => string_predicate_values(actual, &self.value)
2622 .is_some_and(|(actual, needle)| actual.starts_with(needle)),
2623 GraphPredicateOp::NotStartsWith => string_predicate_values(actual, &self.value)
2624 .is_some_and(|(actual, needle)| !actual.starts_with(needle)),
2625 GraphPredicateOp::StartsWithAny => string_list_predicate_values(actual, &self.value)
2626 .is_some_and(|(actual, needles)| {
2627 needles.iter().any(|needle| actual.starts_with(needle))
2628 }),
2629 GraphPredicateOp::NotStartsWithAny => string_list_predicate_values(actual, &self.value)
2630 .is_some_and(|(actual, needles)| {
2631 needles.iter().all(|needle| !actual.starts_with(needle))
2632 }),
2633 GraphPredicateOp::EndsWith => string_predicate_values(actual, &self.value)
2634 .is_some_and(|(actual, needle)| actual.ends_with(needle)),
2635 GraphPredicateOp::NotEndsWith => string_predicate_values(actual, &self.value)
2636 .is_some_and(|(actual, needle)| !actual.ends_with(needle)),
2637 GraphPredicateOp::EndsWithAny => string_list_predicate_values(actual, &self.value)
2638 .is_some_and(|(actual, needles)| {
2639 needles.iter().any(|needle| actual.ends_with(needle))
2640 }),
2641 GraphPredicateOp::NotEndsWithAny => string_list_predicate_values(actual, &self.value)
2642 .is_some_and(|(actual, needles)| {
2643 needles.iter().all(|needle| !actual.ends_with(needle))
2644 }),
2645 GraphPredicateOp::Contains => string_predicate_values(actual, &self.value)
2646 .is_some_and(|(actual, needle)| actual.contains(needle)),
2647 GraphPredicateOp::NotContains => string_predicate_values(actual, &self.value)
2648 .is_some_and(|(actual, needle)| !actual.contains(needle)),
2649 GraphPredicateOp::ContainsAny => string_list_predicate_values(actual, &self.value)
2650 .is_some_and(|(actual, needles)| {
2651 needles.iter().any(|needle| actual.contains(needle))
2652 }),
2653 GraphPredicateOp::NotContainsAny => string_list_predicate_values(actual, &self.value)
2654 .is_some_and(|(actual, needles)| {
2655 needles.iter().all(|needle| !actual.contains(needle))
2656 }),
2657 GraphPredicateOp::In => list_predicate_values(&self.value)
2658 .is_some_and(|values| values.iter().any(|value| actual == value)),
2659 GraphPredicateOp::NotIn => list_predicate_values(&self.value)
2660 .is_some_and(|values| values.iter().all(|value| actual != value)),
2661 GraphPredicateOp::GreaterThan
2662 | GraphPredicateOp::GreaterThanOrEqual
2663 | GraphPredicateOp::LessThan
2664 | GraphPredicateOp::LessThanOrEqual => compare_ordered_values(actual, &self.value)
2665 .is_some_and(|ordering| match self.op {
2666 GraphPredicateOp::GreaterThan => ordering.is_gt(),
2667 GraphPredicateOp::GreaterThanOrEqual => ordering.is_gt() || ordering.is_eq(),
2668 GraphPredicateOp::LessThan => ordering.is_lt(),
2669 GraphPredicateOp::LessThanOrEqual => ordering.is_lt() || ordering.is_eq(),
2670 GraphPredicateOp::Equal
2671 | GraphPredicateOp::NotEqual
2672 | GraphPredicateOp::IsNull
2673 | GraphPredicateOp::IsNotNull
2674 | GraphPredicateOp::StartsWith
2675 | GraphPredicateOp::NotStartsWith
2676 | GraphPredicateOp::StartsWithAny
2677 | GraphPredicateOp::NotStartsWithAny
2678 | GraphPredicateOp::EndsWith
2679 | GraphPredicateOp::NotEndsWith
2680 | GraphPredicateOp::EndsWithAny
2681 | GraphPredicateOp::NotEndsWithAny
2682 | GraphPredicateOp::Contains
2683 | GraphPredicateOp::NotContains
2684 | GraphPredicateOp::ContainsAny
2685 | GraphPredicateOp::NotContainsAny
2686 | GraphPredicateOp::In
2687 | GraphPredicateOp::NotIn => unreachable!(),
2688 }),
2689 }
2690 }
2691}
2692
2693fn string_predicate_values<'a>(actual: &'a Value, value: &'a Value) -> Option<(&'a str, &'a str)> {
2694 match (actual, value) {
2695 (Value::String(actual), Value::String(needle)) => Some((actual.as_str(), needle.as_str())),
2696 _ => None,
2697 }
2698}
2699
2700fn string_list_predicate_values<'a>(
2701 actual: &'a Value,
2702 value: &'a Value,
2703) -> Option<(&'a str, Vec<&'a str>)> {
2704 match (actual, value) {
2705 (Value::String(actual), Value::StringArray(needles)) => Some((
2706 actual.as_str(),
2707 needles.iter().map(String::as_str).collect(),
2708 )),
2709 _ => None,
2710 }
2711}
2712
2713fn list_predicate_values(value: &Value) -> Option<Vec<Value>> {
2714 match value {
2715 Value::StringArray(values) => Some(values.iter().map(Value::from).collect()),
2716 Value::IntArray(values) => Some(values.iter().copied().map(Value::Int).collect()),
2717 Value::FloatArray(values) => Some(values.iter().copied().map(Value::Float).collect()),
2718 Value::Json(serde_json::Value::Array(values)) => values
2719 .iter()
2720 .map(|value| match value {
2721 serde_json::Value::Bool(value) => Some(Value::Bool(*value)),
2722 serde_json::Value::Number(value) => value
2723 .as_i64()
2724 .map(Value::Int)
2725 .or_else(|| value.as_f64().map(Value::Float)),
2726 serde_json::Value::String(value) => Some(Value::from(value)),
2727 serde_json::Value::Null
2728 | serde_json::Value::Array(_)
2729 | serde_json::Value::Object(_) => None,
2730 })
2731 .collect(),
2732 _ => None,
2733 }
2734}
2735
2736fn compare_ordered_values(lhs: &Value, rhs: &Value) -> Option<std::cmp::Ordering> {
2737 match (lhs, rhs) {
2738 (Value::Int(lhs), Value::Int(rhs)) => Some(lhs.cmp(rhs)),
2739 (Value::Int(lhs), Value::Float(rhs)) => (*lhs as f64).partial_cmp(rhs),
2740 (Value::Float(lhs), Value::Int(rhs)) => lhs.partial_cmp(&(*rhs as f64)),
2741 (Value::Float(lhs), Value::Float(rhs)) => lhs.partial_cmp(rhs),
2742 (Value::String(lhs), Value::String(rhs)) => Some(lhs.cmp(rhs)),
2743 _ => None,
2744 }
2745}
2746
2747#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
2748pub struct GraphNodeMatch {
2749 pub label: Option<Label>,
2750 pub props: Props,
2751 pub predicates: Vec<GraphPropertyPredicate>,
2752}
2753
2754#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2755pub struct GraphRelationshipMatch {
2756 pub from: GraphNodeMatch,
2757 pub label: Label,
2758 pub to: GraphNodeMatch,
2759 pub id: Option<EdgeId>,
2760 pub props: Props,
2761 pub predicates: Vec<GraphPropertyPredicate>,
2762}
2763
2764#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2765pub enum GraphMutationPlanOp {
2766 UpsertNode {
2767 kind: GraphMutationPlanKind,
2768 node: Node,
2769 },
2770 PatchNode {
2771 id: NodeId,
2772 props: Props,
2773 },
2774 PatchMatchingNodes {
2775 label: Option<Label>,
2776 props: Props,
2777 predicates: Vec<GraphPropertyPredicate>,
2778 patch: Props,
2779 cardinality: GraphMutationCardinality,
2780 },
2781 UpdateMatchingNodeProperty {
2782 label: Option<Label>,
2783 props: Props,
2784 predicates: Vec<GraphPropertyPredicate>,
2785 target_key: String,
2786 source_key: String,
2787 op: GraphNumericOp,
2788 operand: Value,
2789 cardinality: GraphMutationCardinality,
2790 },
2791 PatchEdge {
2792 from: NodeId,
2793 label: Label,
2794 to: NodeId,
2795 id: Option<EdgeId>,
2796 props: Props,
2797 },
2798 PatchMatchingEdges {
2799 relationship: GraphRelationshipMatch,
2800 patch: Props,
2801 cardinality: GraphMutationCardinality,
2802 },
2803 UpdateMatchingEdgeProperty {
2804 relationship: GraphRelationshipMatch,
2805 target_key: String,
2806 source_key: String,
2807 op: GraphNumericOp,
2808 operand: Value,
2809 cardinality: GraphMutationCardinality,
2810 },
2811 RemoveNodeProps {
2812 id: NodeId,
2813 keys: Vec<String>,
2814 },
2815 RemoveMatchingNodeProps {
2816 label: Option<Label>,
2817 props: Props,
2818 predicates: Vec<GraphPropertyPredicate>,
2819 keys: Vec<String>,
2820 cardinality: GraphMutationCardinality,
2821 },
2822 RemoveEdgeProps {
2823 from: NodeId,
2824 label: Label,
2825 to: NodeId,
2826 id: Option<EdgeId>,
2827 keys: Vec<String>,
2828 },
2829 RemoveMatchingEdgeProps {
2830 relationship: GraphRelationshipMatch,
2831 keys: Vec<String>,
2832 cardinality: GraphMutationCardinality,
2833 },
2834 DeleteMatchingNodes {
2835 label: Option<Label>,
2836 props: Props,
2837 predicates: Vec<GraphPropertyPredicate>,
2838 cardinality: GraphMutationCardinality,
2839 },
2840 UpsertEdge {
2841 kind: GraphMutationPlanKind,
2842 edge: Edge,
2843 },
2844 UpsertEdgesFromNodeMatches {
2845 kind: GraphMutationPlanKind,
2846 from: GraphNodeMatch,
2847 to: GraphNodeMatch,
2848 label: Label,
2849 props: Props,
2850 edge_id_policy: GraphRowEdgeIdPolicy,
2851 cardinality: GraphMutationCardinality,
2852 },
2853 DeleteNode(NodeId),
2854 DeleteEdge {
2855 from: NodeId,
2856 label: Label,
2857 to: NodeId,
2858 },
2859 DeleteMatchingEdges {
2860 relationship: GraphRelationshipMatch,
2861 cardinality: GraphMutationCardinality,
2862 },
2863 DeleteRelationshipRows {
2864 relationship: GraphRelationshipMatch,
2865 delete_edges: bool,
2866 endpoint_nodes: Vec<GraphRelationshipEndpoint>,
2867 target_count: usize,
2868 cardinality: GraphMutationCardinality,
2869 },
2870}
2871
2872#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
2873pub struct GraphMutationPlan {
2874 pub operations: Vec<GraphMutationPlanOp>,
2875}
2876
2877impl GraphMutationPlan {
2878 pub fn new(operations: Vec<GraphMutationPlanOp>) -> Self {
2879 Self { operations }
2880 }
2881
2882 pub fn push(&mut self, operation: GraphMutationPlanOp) {
2883 self.operations.push(operation);
2884 }
2885
2886 pub fn report(&self) -> GraphMutationReport {
2887 let mut report = GraphMutationReport::default();
2888 for operation in &self.operations {
2889 report.record(operation);
2890 }
2891 report
2892 }
2893
2894 pub fn into_mutations(self) -> Vec<GraphMutation> {
2895 self.operations
2896 .into_iter()
2897 .map(GraphMutation::from)
2898 .collect()
2899 }
2900}
2901
2902#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
2911pub struct GraphMutationReport {
2912 pub creates: usize,
2913 pub merges: usize,
2914 pub deletes: usize,
2915 pub patches: usize,
2916 pub property_removes: usize,
2917 pub matched_rows: usize,
2918 pub changed_nodes: usize,
2919 pub changed_edges: usize,
2920 pub node_upserts: usize,
2921 pub edge_upserts: usize,
2922 pub node_deletes: usize,
2923 pub edge_deletes: usize,
2924 pub node_patches: usize,
2925 pub edge_patches: usize,
2926 pub node_property_removes: usize,
2927 pub edge_property_removes: usize,
2928 pub node_inserts: usize,
2937 pub node_updates: usize,
2939 pub edge_inserts: usize,
2941 pub edge_updates: usize,
2943}
2944
2945impl GraphMutationReport {
2946 pub fn record(&mut self, operation: &GraphMutationPlanOp) {
2947 match operation {
2948 GraphMutationPlanOp::UpsertNode { kind, .. }
2949 | GraphMutationPlanOp::UpsertEdge { kind, .. } => {
2950 match kind {
2951 GraphMutationPlanKind::Create => self.creates += 1,
2952 GraphMutationPlanKind::Merge => self.merges += 1,
2953 }
2954 match operation {
2955 GraphMutationPlanOp::UpsertNode { .. } => {
2956 self.node_upserts += 1;
2957 self.changed_nodes += 1;
2958 }
2959 GraphMutationPlanOp::UpsertEdge { .. } => {
2960 self.edge_upserts += 1;
2961 self.changed_edges += 1;
2962 }
2963 _ => {}
2964 }
2965 }
2966 GraphMutationPlanOp::UpsertEdgesFromNodeMatches { kind, .. } => match kind {
2967 GraphMutationPlanKind::Create => self.creates += 1,
2968 GraphMutationPlanKind::Merge => self.merges += 1,
2969 },
2970 GraphMutationPlanOp::PatchNode { .. } => {
2971 self.patches += 1;
2972 self.node_patches += 1;
2973 self.changed_nodes += 1;
2974 }
2975 GraphMutationPlanOp::PatchMatchingNodes { .. } => {
2976 self.patches += 1;
2977 }
2978 GraphMutationPlanOp::UpdateMatchingNodeProperty { .. } => {
2979 self.patches += 1;
2980 }
2981 GraphMutationPlanOp::PatchEdge { .. } => {
2982 self.patches += 1;
2983 self.edge_patches += 1;
2984 self.changed_edges += 1;
2985 }
2986 GraphMutationPlanOp::PatchMatchingEdges { .. } => {
2987 self.patches += 1;
2988 }
2989 GraphMutationPlanOp::UpdateMatchingEdgeProperty { .. } => {
2990 self.patches += 1;
2991 }
2992 GraphMutationPlanOp::RemoveNodeProps { .. } => {
2993 self.property_removes += 1;
2994 self.node_property_removes += 1;
2995 self.changed_nodes += 1;
2996 }
2997 GraphMutationPlanOp::RemoveMatchingNodeProps { .. } => {
2998 self.property_removes += 1;
2999 }
3000 GraphMutationPlanOp::RemoveEdgeProps { .. } => {
3001 self.property_removes += 1;
3002 self.edge_property_removes += 1;
3003 self.changed_edges += 1;
3004 }
3005 GraphMutationPlanOp::RemoveMatchingEdgeProps { .. } => {
3006 self.property_removes += 1;
3007 }
3008 GraphMutationPlanOp::DeleteMatchingNodes { .. } => {
3009 self.deletes += 1;
3010 }
3011 GraphMutationPlanOp::DeleteNode(_) => {
3012 self.deletes += 1;
3013 self.node_deletes += 1;
3014 self.changed_nodes += 1;
3015 }
3016 GraphMutationPlanOp::DeleteEdge { .. } => {
3017 self.deletes += 1;
3018 self.edge_deletes += 1;
3019 self.changed_edges += 1;
3020 }
3021 GraphMutationPlanOp::DeleteMatchingEdges { .. } => {
3022 self.deletes += 1;
3023 }
3024 GraphMutationPlanOp::DeleteRelationshipRows { target_count, .. } => {
3025 self.deletes += *target_count;
3026 }
3027 }
3028 }
3029}
3030
3031impl From<GraphMutationPlanOp> for GraphMutation {
3032 fn from(operation: GraphMutationPlanOp) -> Self {
3033 match operation {
3034 GraphMutationPlanOp::UpsertNode { node, .. } => Self::UpsertNode(node),
3035 GraphMutationPlanOp::PatchNode { id, props } => Self::PatchNode { id, props },
3036 GraphMutationPlanOp::PatchMatchingNodes {
3037 label,
3038 props,
3039 predicates,
3040 patch,
3041 ..
3042 } => Self::PatchMatchingNodes {
3043 label,
3044 props,
3045 predicates,
3046 patch,
3047 },
3048 GraphMutationPlanOp::UpdateMatchingNodeProperty {
3049 label,
3050 props,
3051 predicates,
3052 target_key,
3053 source_key,
3054 op,
3055 operand,
3056 ..
3057 } => Self::UpdateMatchingNodeProperty {
3058 label,
3059 props,
3060 predicates,
3061 target_key,
3062 source_key,
3063 op,
3064 operand,
3065 },
3066 GraphMutationPlanOp::PatchEdge {
3067 from,
3068 label,
3069 to,
3070 id,
3071 props,
3072 } => Self::PatchEdge {
3073 from,
3074 label,
3075 to,
3076 id,
3077 props,
3078 },
3079 GraphMutationPlanOp::PatchMatchingEdges {
3080 relationship,
3081 patch,
3082 ..
3083 } => Self::PatchMatchingEdges {
3084 relationship,
3085 patch,
3086 },
3087 GraphMutationPlanOp::UpdateMatchingEdgeProperty {
3088 relationship,
3089 target_key,
3090 source_key,
3091 op,
3092 operand,
3093 ..
3094 } => Self::UpdateMatchingEdgeProperty {
3095 relationship,
3096 target_key,
3097 source_key,
3098 op,
3099 operand,
3100 },
3101 GraphMutationPlanOp::RemoveNodeProps { id, keys } => Self::RemoveNodeProps { id, keys },
3102 GraphMutationPlanOp::RemoveEdgeProps {
3103 from,
3104 label,
3105 to,
3106 id,
3107 keys,
3108 } => Self::RemoveEdgeProps {
3109 from,
3110 label,
3111 to,
3112 id,
3113 keys,
3114 },
3115 GraphMutationPlanOp::RemoveMatchingEdgeProps {
3116 relationship, keys, ..
3117 } => Self::RemoveMatchingEdgeProps { relationship, keys },
3118 GraphMutationPlanOp::RemoveMatchingNodeProps {
3119 label,
3120 props,
3121 predicates,
3122 keys,
3123 ..
3124 } => Self::RemoveMatchingNodeProps {
3125 label,
3126 props,
3127 predicates,
3128 keys,
3129 },
3130 GraphMutationPlanOp::DeleteMatchingNodes {
3131 label,
3132 props,
3133 predicates,
3134 ..
3135 } => Self::DeleteMatchingNodes {
3136 label,
3137 props,
3138 predicates,
3139 },
3140 GraphMutationPlanOp::UpsertEdge { edge, .. } => Self::UpsertEdge(edge),
3141 GraphMutationPlanOp::UpsertEdgesFromNodeMatches {
3142 kind,
3143 from,
3144 to,
3145 label,
3146 props,
3147 edge_id_policy,
3148 ..
3149 } => Self::UpsertEdgesFromNodeMatches {
3150 kind,
3151 from,
3152 to,
3153 label,
3154 props,
3155 edge_id_policy,
3156 },
3157 GraphMutationPlanOp::DeleteNode(id) => Self::DeleteNode(id),
3158 GraphMutationPlanOp::DeleteEdge { from, label, to } => {
3159 Self::DeleteEdge { from, label, to }
3160 }
3161 GraphMutationPlanOp::DeleteMatchingEdges { relationship, .. } => {
3162 Self::DeleteMatchingEdges { relationship }
3163 }
3164 GraphMutationPlanOp::DeleteRelationshipRows {
3165 relationship,
3166 delete_edges,
3167 endpoint_nodes,
3168 ..
3169 } => Self::DeleteRelationshipRows {
3170 relationship,
3171 delete_edges,
3172 endpoint_nodes,
3173 },
3174 }
3175 }
3176}
3177
3178#[async_trait]
3185pub trait CypherMutationExecutor: GraphMutationStore {
3186 async fn execute_cypher_mutation_plan(
3187 &self,
3188 plan: &GraphMutationPlan,
3189 ) -> Result<GraphMutationReport> {
3190 let mut report = plan.report();
3191 for operation in &plan.operations {
3192 match operation {
3193 GraphMutationPlanOp::DeleteMatchingNodes { .. } => {
3194 return Err(GrustError::CypherExecution(
3195 "matched node deletes require backend-specific query support".to_string(),
3196 ));
3197 }
3198 GraphMutationPlanOp::PatchMatchingNodes { .. } => {
3199 return Err(GrustError::CypherExecution(
3200 "matched node patches require backend-specific query support".to_string(),
3201 ));
3202 }
3203 GraphMutationPlanOp::UpdateMatchingNodeProperty { .. } => {
3204 return Err(GrustError::CypherExecution(
3205 "matched node expression updates require backend-specific query support"
3206 .to_string(),
3207 ));
3208 }
3209 GraphMutationPlanOp::RemoveMatchingNodeProps { .. } => {
3210 return Err(GrustError::CypherExecution(
3211 "matched node property removals require backend-specific query support"
3212 .to_string(),
3213 ));
3214 }
3215 GraphMutationPlanOp::PatchMatchingEdges { .. } => {
3216 return Err(GrustError::CypherExecution(
3217 "matched edge patches require backend-specific query support".to_string(),
3218 ));
3219 }
3220 GraphMutationPlanOp::UpdateMatchingEdgeProperty { .. } => {
3221 return Err(GrustError::CypherExecution(
3222 "matched edge expression updates require backend-specific query support"
3223 .to_string(),
3224 ));
3225 }
3226 GraphMutationPlanOp::RemoveMatchingEdgeProps { .. } => {
3227 return Err(GrustError::CypherExecution(
3228 "matched edge property removals require backend-specific query support"
3229 .to_string(),
3230 ));
3231 }
3232 GraphMutationPlanOp::DeleteMatchingEdges { .. } => {
3233 return Err(GrustError::CypherExecution(
3234 "matched edge deletes require backend-specific query support".to_string(),
3235 ));
3236 }
3237 GraphMutationPlanOp::UpsertEdgesFromNodeMatches { .. } => {
3238 return Err(GrustError::CypherExecution(
3239 "row-producing edge upserts require backend-specific query support"
3240 .to_string(),
3241 ));
3242 }
3243 GraphMutationPlanOp::UpsertNode { node, .. } => {
3244 classify_node_upsert(self.put_node(node).await?, &mut report);
3245 }
3246 GraphMutationPlanOp::UpsertEdge { edge, .. } => {
3247 classify_edge_upsert(self.put_edge(edge).await?, &mut report);
3248 }
3249 _ => {
3250 let mutation = GraphMutation::from(operation.clone());
3251 self.apply_mutations(std::slice::from_ref(&mutation))
3252 .await?;
3253 }
3254 }
3255 }
3256 Ok(report)
3257 }
3258}
3259
3260pub fn classify_node_upsert(outcome: PutOutcome, report: &mut GraphMutationReport) {
3264 match outcome {
3265 PutOutcome::Inserted => report.node_inserts += 1,
3266 PutOutcome::Updated => report.node_updates += 1,
3267 PutOutcome::Upserted | PutOutcome::Deduped => {}
3268 }
3269}
3270
3271pub fn classify_edge_upsert(outcome: PutOutcome, report: &mut GraphMutationReport) {
3274 match outcome {
3275 PutOutcome::Inserted => report.edge_inserts += 1,
3276 PutOutcome::Updated => report.edge_updates += 1,
3277 PutOutcome::Upserted | PutOutcome::Deduped => {}
3278 }
3279}
3280
3281#[async_trait]
3286pub trait GraphMutationStore: GraphStore {
3287 fn mutation_atomicity(&self) -> GraphMutationAtomicity {
3288 GraphMutationAtomicity::OrderedNonAtomic
3289 }
3290
3291 async fn delete_node(&self, id: &NodeId) -> Result<()>;
3293
3294 async fn delete_edge(&self, from: &NodeId, label: &Label, to: &NodeId) -> Result<()>;
3296
3297 async fn apply_mutations(&self, mutations: &[GraphMutation]) -> Result<()> {
3304 for mutation in mutations {
3305 match mutation {
3306 GraphMutation::UpsertNode(node) => {
3307 self.put_node(node).await?;
3308 }
3309 GraphMutation::PatchNode { id, props } => {
3310 if let Some(mut node) = self.get_node(id).await? {
3311 for (key, value) in props {
3312 node.props.insert(key.clone(), value.clone());
3313 }
3314 self.put_node(&node).await?;
3315 }
3316 }
3317 GraphMutation::PatchMatchingNodes { .. } => {
3318 return Err(GrustError::Unsupported(
3319 "matched node patches require backend-specific query support".to_string(),
3320 ));
3321 }
3322 GraphMutation::UpdateMatchingNodeProperty { .. } => {
3323 return Err(GrustError::Unsupported(
3324 "matched node expression updates require backend-specific query support"
3325 .to_string(),
3326 ));
3327 }
3328 GraphMutation::PatchEdge {
3329 from,
3330 label,
3331 to,
3332 id,
3333 props,
3334 } => {
3335 let mut edges = self
3336 .get_edges(EdgeQuery {
3337 from: Some(from.clone()),
3338 to: Some(to.clone()),
3339 label: Some(label.clone()),
3340 })
3341 .await?;
3342 if let Some(id) = id {
3343 edges.retain(|edge| edge.id.as_ref() == Some(id));
3344 }
3345 match edges.len() {
3346 0 => {}
3347 1 => {
3348 let mut edge = edges.remove(0);
3349 for (key, value) in props {
3350 edge.props.insert(key.clone(), value.clone());
3351 }
3352 self.put_edge(&edge).await?;
3353 }
3354 count => {
3355 return Err(GrustError::CypherUnsupportedCardinality(format!(
3356 "edge patch matched {count} edges; add an explicit edge id"
3357 )));
3358 }
3359 }
3360 }
3361 GraphMutation::PatchMatchingEdges { .. } => {
3362 return Err(GrustError::Unsupported(
3363 "matched edge patches require backend-specific query support".to_string(),
3364 ));
3365 }
3366 GraphMutation::UpdateMatchingEdgeProperty { .. } => {
3367 return Err(GrustError::Unsupported(
3368 "matched edge expression updates require backend-specific query support"
3369 .to_string(),
3370 ));
3371 }
3372 GraphMutation::RemoveNodeProps { id, keys } => {
3373 if let Some(mut node) = self.get_node(id).await? {
3374 for key in keys {
3375 node.props.remove(key);
3376 }
3377 self.put_node(&node).await?;
3378 }
3379 }
3380 GraphMutation::RemoveMatchingNodeProps { .. } => {
3381 return Err(GrustError::Unsupported(
3382 "matched node property removal requires backend-specific query support"
3383 .to_string(),
3384 ));
3385 }
3386 GraphMutation::RemoveEdgeProps {
3387 from,
3388 label,
3389 to,
3390 id,
3391 keys,
3392 } => {
3393 let mut edges = self
3394 .get_edges(EdgeQuery {
3395 from: Some(from.clone()),
3396 to: Some(to.clone()),
3397 label: Some(label.clone()),
3398 })
3399 .await?;
3400 if let Some(id) = id {
3401 edges.retain(|edge| edge.id.as_ref() == Some(id));
3402 }
3403 match edges.len() {
3404 0 => {}
3405 1 => {
3406 let mut edge = edges.remove(0);
3407 for key in keys {
3408 edge.props.remove(key);
3409 }
3410 self.put_edge(&edge).await?;
3411 }
3412 count => {
3413 return Err(GrustError::CypherUnsupportedCardinality(format!(
3414 "edge property removal matched {count} edges; add an explicit edge id"
3415 )));
3416 }
3417 }
3418 }
3419 GraphMutation::RemoveMatchingEdgeProps { .. } => {
3420 return Err(GrustError::Unsupported(
3421 "matched edge property removal requires backend-specific query support"
3422 .to_string(),
3423 ));
3424 }
3425 GraphMutation::DeleteMatchingNodes { .. } => {
3426 return Err(GrustError::Unsupported(
3427 "matched node deletes require backend-specific query support".to_string(),
3428 ));
3429 }
3430 GraphMutation::DeleteNode(id) => self.delete_node(id).await?,
3431 GraphMutation::UpsertEdge(edge) => {
3432 self.put_edge(edge).await?;
3433 }
3434 GraphMutation::UpsertEdgesFromNodeMatches { .. } => {
3435 return Err(GrustError::Unsupported(
3436 "row-producing edge upserts require backend-specific query support"
3437 .to_string(),
3438 ));
3439 }
3440 GraphMutation::DeleteEdge { from, label, to } => {
3441 self.delete_edge(from, label, to).await?
3442 }
3443 GraphMutation::DeleteMatchingEdges { .. } => {
3444 return Err(GrustError::Unsupported(
3445 "matched edge deletes require backend-specific query support".to_string(),
3446 ));
3447 }
3448 GraphMutation::DeleteRelationshipRows { .. } => {
3449 return Err(GrustError::Unsupported(
3450 "relationship-row deletes require backend-specific query support"
3451 .to_string(),
3452 ));
3453 }
3454 }
3455 }
3456 Ok(())
3457 }
3458}
3459
3460pub mod prelude {
3461 pub use crate::{
3462 CypherMutationExecutor, Direction, Edge, EdgeId, EdgePolicy, EdgeQuery, EdgeType,
3463 EdgeUniqueness, Field, FieldType, Graph, GraphAdminStore, GraphBuilder, GraphConstraint,
3464 GraphConstraintCapability, GraphIndex, GraphMutation, GraphMutationAtomicity,
3465 GraphMutationCardinality, GraphMutationPlan, GraphMutationPlanKind, GraphMutationPlanOp,
3466 GraphMutationReport, GraphMutationStore, GraphNativeConstraintCapability,
3467 GraphNativeConstraintReport, GraphNativeConstraintRequest, GraphNodeMatch, GraphNumericOp,
3468 GraphPredicateOp, GraphPropertyPredicate, GraphRelationshipEndpoint,
3469 GraphRelationshipMatch, GraphRowEdgeIdPolicy, GraphSchema, GraphSchemaBuilder, GraphStore,
3470 GrustError, Label, LoadReport, Node, NodeId, NodeType, Props, PutOutcome, Result, RfcDate,
3471 Start, Step, Traversal, Value, classify_edge_upsert, classify_node_upsert, edge_key,
3472 evaluate_numeric_update, generated_row_edge_id, relationship_type, schema_identifier,
3473 };
3474
3475 #[cfg(feature = "typed-garde")]
3476 pub use crate::typed::{TypedEdge, TypedGraphBuilder, TypedNode, garde, props_from_serialize};
3477
3478 #[cfg(feature = "typed-zod-rs")]
3479 pub use crate::typed::{parse_typed_json, parse_typed_json_with, zod_rs};
3480}
3481
3482#[cfg(test)]
3483mod tests;