1use crate::agent::wire_id;
2use crate::error::InvalidError;
3use crate::limits::MAX_GRAPH_NAME_BYTES;
4use crate::query::{Filter, Value};
5use serde::{Deserialize, Serialize};
6
7wire_id!(
8 NodeId
12);
13wire_id!(
14 EdgeId
17);
18
19impl NodeId {
20 pub fn content(label: &str, value: &[u8]) -> Self {
27 Self::from_u128(crate::hashing::content_id(&[label.as_bytes(), &[0], value]))
28 }
29}
30
31impl EdgeId {
32 pub fn content(from: NodeId, edge_type: &str, to: NodeId) -> Self {
36 Self::from_u128(crate::hashing::content_id(&[
37 &from.to_bytes(),
38 &[0],
39 edge_type.as_bytes(),
40 &[0],
41 &to.to_bytes(),
42 ]))
43 }
44}
45
46#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
48#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
49#[serde(rename_all = "snake_case")]
50pub enum EdgeDir {
51 #[default]
53 Out,
54 In,
56 Both,
58}
59
60#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
62#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
63#[serde(rename_all = "snake_case")]
64pub enum GraphReturn {
65 #[default]
67 Nodes,
68 Edges,
70 Paths,
72 Triplets,
74}
75
76#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
80#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
81pub struct Hop {
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub edge_type: Option<String>,
84 #[serde(default, skip_serializing_if = "EdgeDir::is_out")]
85 pub dir: EdgeDir,
86 pub max: u32,
87}
88
89impl EdgeDir {
90 pub fn is_out(&self) -> bool {
92 matches!(self, EdgeDir::Out)
93 }
94}
95
96#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
99#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
100pub enum GraphStart {
101 Ids(Vec<NodeId>),
102 Match(Filter),
103 Nearest { embedding: Vec<f32>, k: u32 },
104}
105
106#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
110#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
111pub struct GraphQuery {
112 pub v: u32,
113 pub graph: String,
114 pub start: GraphStart,
115 #[serde(default, skip_serializing_if = "Vec::is_empty")]
116 pub traverse: Vec<Hop>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub node_filter: Option<Filter>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub edge_filter: Option<Filter>,
121 #[serde(default, skip_serializing_if = "GraphReturn::is_nodes")]
122 pub return_: GraphReturn,
123 pub limit: u32,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub as_of: Option<u64>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub conversation: Option<String>,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub stream: Option<String>,
141}
142
143impl GraphReturn {
144 pub fn is_nodes(&self) -> bool {
146 matches!(self, GraphReturn::Nodes)
147 }
148}
149
150#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
153#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
154pub struct GraphNeighbors {
155 pub v: u32,
156 pub graph: String,
157 pub node: NodeId,
158 #[serde(default, skip_serializing_if = "EdgeDir::is_out")]
159 pub dir: EdgeDir,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub edge_type: Option<String>,
162 pub depth: u32,
163 pub limit: u32,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub as_of: Option<u64>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub conversation: Option<String>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub stream: Option<String>,
179}
180
181#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
191#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
192pub enum SourceRef {
193 Message {
200 stream: u32,
201 topic: u32,
202 partition: u32,
203 offset: u64,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
206 generation: Option<u64>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 conversation: Option<String>,
209 },
210 Kv { namespace: String, key: String },
212 Memory { id: String },
214}
215
216#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
218#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
219pub struct ProducerInfo {
220 pub name: String,
221 pub version: String,
222}
223
224#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
226#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
227pub struct GraphNode {
228 pub id: NodeId,
229 #[serde(default, skip_serializing_if = "Vec::is_empty")]
230 pub labels: Vec<String>,
231 #[serde(default, skip_serializing_if = "Vec::is_empty")]
232 pub attrs: Vec<(String, Value)>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub embedding: Option<Vec<f32>>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub source: Option<SourceRef>,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub producer: Option<ProducerInfo>,
241}
242
243impl GraphNode {
244 pub fn entity(label: impl Into<String>, value: impl Into<String>) -> Self {
250 let label = label.into();
251 let value = value.into();
252 let id = NodeId::content(&label, value.as_bytes());
253 Self {
254 id,
255 labels: vec![label],
256 attrs: vec![("value".to_owned(), Value::from(value))],
257 embedding: None,
258 source: None,
259 producer: None,
260 }
261 }
262}
263
264#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
267#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
268pub struct GraphEdge {
269 pub id: EdgeId,
270 pub from: NodeId,
271 pub to: NodeId,
272 pub edge_type: String,
273 pub weight: f32,
274 #[serde(default, skip_serializing_if = "Vec::is_empty")]
275 pub attrs: Vec<(String, Value)>,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub valid_from: Option<u64>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub valid_to: Option<u64>,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub source: Option<SourceRef>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub producer: Option<ProducerInfo>,
294}
295
296impl GraphEdge {
297 pub fn relate(from: &GraphNode, edge_type: impl Into<String>, to: &GraphNode) -> Self {
301 let edge_type = edge_type.into();
302 Self {
303 id: EdgeId::content(from.id, &edge_type, to.id),
304 from: from.id,
305 to: to.id,
306 edge_type,
307 weight: 1.0,
308 attrs: Vec::new(),
309 valid_from: None,
310 valid_to: None,
311 source: None,
312 producer: None,
313 }
314 }
315
316 pub fn with_source(mut self, source: SourceRef) -> Self {
319 self.source = Some(source);
320 self
321 }
322
323 pub fn valid(mut self, from: Option<u64>, to: Option<u64>) -> Self {
328 self.valid_from = from;
329 self.valid_to = to;
330 self
331 }
332
333 pub fn valid_at(&self, at: u64) -> bool {
337 self.valid_from.is_none_or(|from| at >= from) && self.valid_to.is_none_or(|to| at < to)
338 }
339}
340
341#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
343#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
344pub struct Path {
345 pub nodes: Vec<NodeId>,
346 pub edges: Vec<EdgeId>,
347}
348
349#[derive(Clone, Debug, Default, Serialize, Deserialize)]
352#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
353pub struct GraphResult {
354 #[serde(default, skip_serializing_if = "Vec::is_empty")]
355 pub nodes: Vec<GraphNode>,
356 #[serde(default, skip_serializing_if = "Vec::is_empty")]
357 pub edges: Vec<GraphEdge>,
358 #[serde(default, skip_serializing_if = "Vec::is_empty")]
359 pub paths: Vec<Path>,
360 #[serde(default, skip_serializing_if = "is_false")]
363 pub truncated: bool,
364}
365
366fn is_false(value: &bool) -> bool {
367 !*value
368}
369
370#[derive(Clone, Debug, Default, Serialize, Deserialize)]
373#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
374pub struct GraphUpsert {
375 pub v: u32,
376 pub graph: String,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
378 pub session: Option<crate::agent::SessionRef>,
379 #[serde(default, skip_serializing_if = "Vec::is_empty")]
380 pub nodes: Vec<GraphNode>,
381 #[serde(default, skip_serializing_if = "Vec::is_empty")]
382 pub edges: Vec<GraphEdge>,
383}
384
385#[derive(Clone, Debug, Serialize, Deserialize)]
388#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
389#[non_exhaustive]
390pub enum GraphReply {
391 Ok(GraphResult),
392 Err(GraphError),
393}
394
395#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
397#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
398#[non_exhaustive]
399pub enum GraphError {
400 #[error("graph not supported: {0}")]
401 Unsupported(String),
402 #[error("unauthorized: {0}")]
403 Unauthorized(String),
404 #[error("invalid graph name: {0}")]
406 InvalidName(String),
407 #[error("graph not found: {0}")]
408 NotFound(String),
409 #[error("traversal too large: {what} is {size}, exceeds cap {cap}")]
410 TooLarge {
411 what: String,
412 size: usize,
413 cap: usize,
414 },
415 #[error("graph backend error: {0}")]
416 Backend(String),
417 #[error("temporarily unavailable: {0}")]
418 Unavailable(String),
419 #[error("unsupported graph op version (expected {expected}, got {got})")]
420 Version { expected: u32, got: u32 },
421}
422
423pub fn validate_graph_name(name: &str) -> Result<(), InvalidError> {
427 if name.is_empty() {
428 return Err(InvalidError::new("graph name must not be empty"));
429 }
430 if name.len() > MAX_GRAPH_NAME_BYTES {
431 return Err(InvalidError::new(format!(
432 "graph name is {}B, exceeds cap {MAX_GRAPH_NAME_BYTES}B",
433 name.len()
434 )));
435 }
436 if name.bytes().any(|byte| byte.is_ascii_control()) {
437 return Err(InvalidError::new(
438 "graph name must not contain ASCII control characters",
439 ));
440 }
441 Ok(())
442}
443
444#[cfg(all(test, feature = "cbor"))]
445mod tests {
446 use super::*;
447 use crate::codes::GRAPH_OP_VERSION;
448 use crate::framing::{decode_named, encode_named};
449 use crate::query::CmpOp;
450
451 #[test]
452 fn given_a_graph_query_when_round_tripped_then_should_preserve_traversal() {
453 let query = GraphQuery {
454 v: GRAPH_OP_VERSION,
455 graph: "knowledge".to_owned(),
456 start: GraphStart::Match(Filter::pred("label", CmpOp::Eq, "Person")),
457 traverse: vec![
458 Hop {
459 edge_type: Some("works_at".to_owned()),
460 dir: EdgeDir::Out,
461 max: 1,
462 },
463 Hop {
464 edge_type: Some("located_in".to_owned()),
465 dir: EdgeDir::Out,
466 max: 1,
467 },
468 ],
469 node_filter: None,
470 edge_filter: None,
471 return_: GraphReturn::Paths,
472 limit: 100,
473 as_of: Some(1_900_000_000_000_000),
474 conversation: None,
475 stream: None,
476 };
477 let bytes = encode_named(&query).expect("serializes");
478 let back: GraphQuery = decode_named(&bytes).expect("deserializes");
479 assert_eq!(back.graph, "knowledge");
480 assert_eq!(back.traverse.len(), 2);
481 assert_eq!(back.return_, GraphReturn::Paths);
482 assert_eq!(back.as_of, Some(1_900_000_000_000_000));
483 }
484
485 #[test]
486 fn given_a_graph_result_when_round_tripped_then_should_preserve_nodes_and_edges() {
487 let reply = GraphReply::Ok(GraphResult {
488 nodes: vec![GraphNode {
489 producer: None,
490 id: NodeId::from_u128(1),
491 labels: vec!["Person".to_owned()],
492 attrs: vec![("name".to_owned(), Value::from("Alice"))],
493 embedding: None,
494 source: None,
495 }],
496 edges: vec![GraphEdge {
497 producer: None,
498 id: EdgeId::from_u128(2),
499 from: NodeId::from_u128(1),
500 to: NodeId::from_u128(3),
501 edge_type: "works_at".to_owned(),
502 weight: 1.0,
503 attrs: Vec::new(),
504 valid_from: None,
505 valid_to: None,
506 source: None,
507 }],
508 paths: Vec::new(),
509 truncated: false,
510 });
511 let bytes = encode_named(&reply).expect("serializes");
512 let back: GraphReply = decode_named(&bytes).expect("deserializes");
513 let GraphReply::Ok(result) = back else {
514 panic!("expected Ok");
515 };
516 assert_eq!(result.nodes.len(), 1);
517 assert_eq!(result.edges[0].edge_type, "works_at");
518 }
519
520 #[test]
521 fn given_a_nearest_start_when_round_tripped_then_should_preserve_the_seed() {
522 let query = GraphQuery {
523 v: GRAPH_OP_VERSION,
524 graph: "knowledge".to_owned(),
525 start: GraphStart::Nearest {
526 embedding: vec![0.1, 0.2, 0.3],
527 k: 5,
528 },
529 traverse: Vec::new(),
530 node_filter: None,
531 edge_filter: None,
532 return_: GraphReturn::Nodes,
533 limit: 10,
534 as_of: None,
535 conversation: None,
536 stream: None,
537 };
538 let bytes = encode_named(&query).expect("serializes");
539 let back: GraphQuery = decode_named(&bytes).expect("deserializes");
540 match back.start {
541 GraphStart::Nearest { embedding, k } => {
542 assert_eq!(embedding, vec![0.1, 0.2, 0.3]);
543 assert_eq!(k, 5);
544 }
545 other => panic!("expected Nearest, got {other:?}"),
546 }
547 }
548
549 #[test]
550 fn given_a_node_id_when_round_tripped_through_a_string_then_should_be_equal() {
551 let id = NodeId::from_u128(987_654_321);
552 let parsed: NodeId = id.to_string().parse().expect("a node id parses");
553 assert_eq!(parsed, id);
554 }
555
556 #[test]
557 fn given_the_same_entity_when_addressed_twice_then_should_converge_on_one_node_id() {
558 let a = NodeId::content("Person", b"Alice");
559 let b = NodeId::content("Person", b"Alice");
560 assert_eq!(a, b, "the same entity is one node");
561 assert_ne!(a, NodeId::content("Company", b"Alice"));
563 assert_ne!(a, NodeId::content("Person", b"Bob"));
564 }
565
566 #[test]
567 fn given_the_pinned_entity_when_addressed_then_should_match_the_golden_id() {
568 assert_eq!(
571 NodeId::content("Person", b"Alice").to_string(),
572 "13NCEPHNVFHHGNK9GD3MT0W1AB"
573 );
574 }
575
576 #[test]
577 fn given_two_nodes_when_related_then_should_content_address_the_edge() {
578 let alice = GraphNode::entity("Person", "Alice");
579 let acme = GraphNode::entity("Company", "Acme");
580 let one = GraphEdge::relate(&alice, "works_at", &acme);
581 let two = GraphEdge::relate(&alice, "works_at", &acme);
582 assert_eq!(one.id, two.id, "the same relationship is one edge");
583 assert_eq!(one.from, alice.id);
584 assert_eq!(one.to, acme.id);
585 assert_ne!(one.id, GraphEdge::relate(&acme, "works_at", &alice).id);
587 }
588
589 #[test]
590 fn given_an_edge_validity_window_when_checked_then_should_hold_only_inside_it() {
591 let alice = GraphNode::entity("User", "alice");
592 let pool = GraphNode::entity("Pool", "eu-west");
593 let edge = GraphEdge::relate(&alice, "in_pool", &pool).valid(Some(100), Some(200));
594 assert!(!edge.valid_at(99), "before the window");
595 assert!(edge.valid_at(100), "the lower bound is inclusive");
596 assert!(edge.valid_at(150), "inside the window");
597 assert!(!edge.valid_at(200), "the upper bound is exclusive");
598 let open = GraphEdge::relate(&alice, "in_pool", &pool);
599 assert!(open.valid_at(0) && open.valid_at(u64::MAX));
600 assert_eq!(edge.id, open.id, "validity is not part of edge identity");
601 }
602
603 #[test]
604 fn given_an_edge_without_validity_when_serialized_then_should_omit_the_window() {
605 let edge = GraphEdge::relate(
606 &GraphNode::entity("A", "x"),
607 "rel",
608 &GraphNode::entity("B", "y"),
609 );
610 let json = serde_json::to_string(&edge).expect("serializes");
611 assert!(
612 !json.contains("valid_from") && !json.contains("valid_to"),
613 "an unset window must be omitted so a pre-bitemporal edge is byte-identical: {json}"
614 );
615 }
616
617 #[test]
618 fn given_a_node_without_a_source_when_serialized_then_should_omit_it() {
619 let node = GraphNode::entity("Person", "Alice");
620 let json = serde_json::to_string(&node).expect("serializes");
621 assert!(
622 !json.contains("source"),
623 "an unknown source must be omitted so a pre-provenance node is byte-identical: {json}"
624 );
625 }
626
627 #[test]
628 fn given_a_node_with_a_source_when_round_tripped_then_should_preserve_it_and_keep_identity() {
629 let mut node = GraphNode::entity("Component", "cache");
630 node.source = Some(SourceRef::Message {
631 generation: None,
632 stream: 7,
633 topic: 2,
634 partition: 3,
635 offset: 4096,
636 conversation: None,
637 });
638 let bytes = encode_named(&node).expect("serializes");
639 let back: GraphNode = decode_named(&bytes).expect("deserializes");
640 assert_eq!(back.source, node.source);
641 assert_eq!(
642 back.id,
643 GraphNode::entity("Component", "cache").id,
644 "source is not part of node identity"
645 );
646 }
647
648 #[test]
649 fn given_an_edge_with_a_source_when_round_tripped_then_should_preserve_it_and_keep_identity() {
650 let from = GraphNode::entity("A", "x");
651 let to = GraphNode::entity("B", "y");
652 let edge = GraphEdge::relate(&from, "rel", &to).with_source(SourceRef::Kv {
653 namespace: "ns".to_owned(),
654 key: "k".to_owned(),
655 });
656 let bytes = encode_named(&edge).expect("serializes");
657 let back: GraphEdge = decode_named(&bytes).expect("deserializes");
658 assert_eq!(back.source, edge.source);
659 assert_eq!(
660 back.id,
661 GraphEdge::relate(&from, "rel", &to).id,
662 "source is not part of edge identity"
663 );
664 }
665
666 #[test]
667 fn given_a_source_without_a_conversation_when_serialized_then_should_omit_it() {
668 let source = SourceRef::Message {
669 generation: None,
670 stream: 1,
671 topic: 1,
672 partition: 0,
673 offset: 0,
674 conversation: None,
675 };
676 let json = serde_json::to_string(&source).expect("serializes");
677 assert!(
678 !json.contains("conversation"),
679 "an unset conversation must be omitted so a pre-conversation source stays byte-identical: {json}"
680 );
681 }
682
683 #[test]
684 fn given_a_source_with_a_conversation_when_round_tripped_then_should_preserve_it() {
685 let mut node = GraphNode::entity("Ticket", "7");
686 node.source = Some(SourceRef::Message {
687 generation: None,
688 stream: 4,
689 topic: 6,
690 partition: 2,
691 offset: 99,
692 conversation: Some("01KWM3K3XEP3NP5TN850J17YBP".to_owned()),
693 });
694 let bytes = encode_named(&node).expect("serializes");
695 let back: GraphNode = decode_named(&bytes).expect("deserializes");
696 assert_eq!(
697 back.source, node.source,
698 "the conversation survives the round trip"
699 );
700 assert_eq!(
701 back.id,
702 GraphNode::entity("Ticket", "7").id,
703 "the conversation is provenance, not identity"
704 );
705 }
706
707 #[test]
708 fn given_a_conversation_filter_on_a_traversal_when_round_tripped_then_should_preserve_it() {
709 let query = GraphQuery {
710 v: GRAPH_OP_VERSION,
711 graph: "knowledge".to_owned(),
712 start: GraphStart::Ids(vec![NodeId::from_u128(1)]),
713 traverse: Vec::new(),
714 node_filter: None,
715 edge_filter: None,
716 return_: GraphReturn::Nodes,
717 limit: 10,
718 as_of: None,
719 conversation: Some("01KWM3K3XEP3NP5TN850J17YBP".to_owned()),
720 stream: Some("acme".to_owned()),
721 };
722 let back: GraphQuery =
723 decode_named(&encode_named(&query).expect("serializes")).expect("deserializes");
724 assert_eq!(
725 back.conversation.as_deref(),
726 Some("01KWM3K3XEP3NP5TN850J17YBP")
727 );
728 assert_eq!(back.stream.as_deref(), Some("acme"));
729 let unfiltered = GraphQuery {
731 conversation: None,
732 stream: None,
733 ..query
734 };
735 let json = serde_json::to_string(&unfiltered).expect("serializes");
736 assert!(
737 !json.contains("conversation") && !json.contains("stream"),
738 "an unset filter is omitted: {json}"
739 );
740 }
741
742 #[test]
743 fn given_a_neighbors_read_with_and_without_a_stream_when_round_tripped_then_should_omit_it_only_when_unset()
744 {
745 let request = GraphNeighbors {
746 v: GRAPH_OP_VERSION,
747 graph: "knowledge".to_owned(),
748 node: NodeId::from_u128(1),
749 dir: EdgeDir::Out,
750 edge_type: None,
751 depth: 1,
752 limit: 10,
753 as_of: None,
754 conversation: Some("01KWM3K3XEP3NP5TN850J17YBP".to_owned()),
755 stream: Some("acme".to_owned()),
756 };
757 let back: GraphNeighbors =
758 decode_named(&encode_named(&request).expect("serializes")).expect("deserializes");
759 assert_eq!(back.stream.as_deref(), Some("acme"));
760 let bare = GraphNeighbors {
761 conversation: None,
762 stream: None,
763 ..request
764 };
765 let bytes = encode_named(&bare).expect("serializes");
766 let back: GraphNeighbors = decode_named(&bytes).expect("deserializes");
767 assert_eq!(back.stream, None);
768 assert!(
769 !serde_json::to_string(&bare)
770 .expect("serializes")
771 .contains("stream"),
772 "an unset stream is omitted"
773 );
774 }
775
776 #[test]
777 fn given_a_max_element_reply_with_source_when_encoded_then_should_fit_one_frame() {
778 use crate::limits::{MAX_FRAME_BYTES, MAX_GRAPH_RESULT_ELEMENTS};
779 let source = SourceRef::Message {
780 generation: None,
781 stream: u32::MAX,
782 topic: u32::MAX,
783 partition: u32::MAX,
784 offset: u64::MAX,
785 conversation: Some("7ZZZZZZZZZZZZZZZZZZZZZZZZZ".to_owned()),
786 };
787 let half = (MAX_GRAPH_RESULT_ELEMENTS / 2) as u128;
788 let nodes = (0..half)
789 .map(|i| {
790 let mut node = GraphNode::entity("Component", format!("entity-{i}"));
791 node.source = Some(source.clone());
792 node
793 })
794 .collect();
795 let edges = (0..half)
796 .map(|i| GraphEdge {
797 producer: None,
798 id: EdgeId::from_u128(i),
799 from: NodeId::from_u128(i),
800 to: NodeId::from_u128(i + 1),
801 edge_type: "relates_to".to_owned(),
802 weight: 1.0,
803 attrs: Vec::new(),
804 valid_from: None,
805 valid_to: None,
806 source: Some(source.clone()),
807 })
808 .collect();
809 let reply = GraphReply::Ok(GraphResult {
810 nodes,
811 edges,
812 paths: Vec::new(),
813 truncated: true,
814 });
815 let encoded = encode_named(&reply).expect("serializes");
816 assert!(
817 encoded.len() < MAX_FRAME_BYTES,
818 "a full {MAX_GRAPH_RESULT_ELEMENTS}-element reply with source is {} bytes, over the frame cap {MAX_FRAME_BYTES}",
819 encoded.len()
820 );
821 }
822}