1use crate::query::GraphResult;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum GraphValueType {
10 String,
11 Integer,
12 Float,
13 Boolean,
14 DateTime,
15 Duration,
16}
17
18#[derive(Debug, Clone)]
20pub struct GraphPropertyDef {
21 pub name: String,
22 pub value_type: GraphValueType,
23 pub nullable: bool,
24 pub default: Option<serde_json::Value>,
25}
26
27impl GraphPropertyDef {
28 pub fn new(name: &str, value_type: GraphValueType) -> Self {
29 Self {
30 name: name.to_string(),
31 value_type,
32 nullable: false,
33 default: None,
34 }
35 }
36
37 pub fn nullable(mut self) -> Self {
38 self.nullable = true;
39 self
40 }
41
42 pub fn with_default(mut self, value: serde_json::Value) -> Self {
43 self.default = Some(value);
44 self
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum RelationDirection {
51 Outgoing,
52 Incoming,
53 Undirected,
54}
55
56#[derive(Debug, Clone)]
58pub struct GraphNodeModel {
59 pub label: String,
60 pub properties: Vec<GraphPropertyDef>,
61}
62
63impl GraphNodeModel {
64 pub fn new(label: &str) -> Self {
65 Self {
66 label: label.to_string(),
67 properties: Vec::new(),
68 }
69 }
70
71 pub fn property(mut self, name: &str, value_type: GraphValueType) -> Self {
72 self.properties
73 .push(GraphPropertyDef::new(name, value_type));
74 self
75 }
76
77 pub fn match_clause(&self, alias: &str) -> String {
79 format!("MATCH ({}:{})", alias, self.label)
80 }
81
82 pub fn create_clause(&self, alias: &str) -> String {
84 let prop_names: Vec<String> = self
85 .properties
86 .iter()
87 .map(|p| format!("{}: ${}", p.name, p.name))
88 .collect();
89 if prop_names.is_empty() {
90 format!("CREATE ({}:{})", alias, self.label)
91 } else {
92 format!(
93 "CREATE ({}:{} {{{}}})",
94 alias,
95 self.label,
96 prop_names.join(", ")
97 )
98 }
99 }
100}
101
102#[derive(Debug, Clone)]
104pub struct GraphRelationModel {
105 pub rel_type: String,
106 pub direction: RelationDirection,
107 pub from_label: String,
108 pub to_label: String,
109 pub properties: Vec<GraphPropertyDef>,
110}
111
112impl GraphRelationModel {
113 pub fn new(rel_type: &str, from_label: &str, to_label: &str) -> Self {
114 Self {
115 rel_type: rel_type.to_string(),
116 direction: RelationDirection::Outgoing,
117 from_label: from_label.to_string(),
118 to_label: to_label.to_string(),
119 properties: Vec::new(),
120 }
121 }
122
123 pub fn direction(mut self, dir: RelationDirection) -> Self {
124 self.direction = dir;
125 self
126 }
127
128 pub fn property(mut self, name: &str, value_type: GraphValueType) -> Self {
129 self.properties
130 .push(GraphPropertyDef::new(name, value_type));
131 self
132 }
133
134 pub fn match_clause(&self, from: &str, rel: &str, to: &str) -> String {
136 match self.direction {
137 RelationDirection::Outgoing => format!(
138 "MATCH ({from}:{from_label})-[{rel}:{rel_type}]->({to}:{to_label})",
139 from = from,
140 from_label = self.from_label,
141 rel = rel,
142 rel_type = self.rel_type,
143 to = to,
144 to_label = self.to_label
145 ),
146 RelationDirection::Incoming => format!(
147 "MATCH ({from}:{from_label})<-[{rel}:{rel_type}]-({to}:{to_label})",
148 from = from,
149 from_label = self.from_label,
150 rel = rel,
151 rel_type = self.rel_type,
152 to = to,
153 to_label = self.to_label
154 ),
155 RelationDirection::Undirected => format!(
156 "MATCH ({from}:{from_label})-[{rel}:{rel_type}]-({to}:{to_label})",
157 from = from,
158 from_label = self.from_label,
159 rel = rel,
160 rel_type = self.rel_type,
161 to = to,
162 to_label = self.to_label
163 ),
164 }
165 }
166}
167
168pub fn extract_node_properties(result: &GraphResult) -> Option<&serde_json::Value> {
170 result.as_node().map(|n| &n.properties)
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::query::{GraphNode, GraphResult};
177
178 #[test]
179 fn test_graph_property_def_builders() {
180 let prop = GraphPropertyDef::new("name", GraphValueType::String)
181 .nullable()
182 .with_default(serde_json::json!("unknown"));
183 assert_eq!(prop.name, "name");
184 assert!(prop.nullable);
185 assert_eq!(prop.default, Some(serde_json::json!("unknown")));
186
187 let non_nullable = GraphPropertyDef::new("age", GraphValueType::Integer);
188 assert!(!non_nullable.nullable);
189 assert!(non_nullable.default.is_none());
190 }
191
192 #[test]
193 fn test_node_model_match_clause() {
194 let model = GraphNodeModel::new("Person")
195 .property("name", GraphValueType::String)
196 .property("age", GraphValueType::Integer);
197 let clause = model.match_clause("n");
198 assert_eq!(clause, "MATCH (n:Person)");
199 }
200
201 #[test]
202 fn test_node_model_create_clause_with_props() {
203 let model = GraphNodeModel::new("Person")
204 .property("name", GraphValueType::String)
205 .property("age", GraphValueType::Integer);
206 let clause = model.create_clause("n");
207 assert!(clause.contains("CREATE (n:Person"));
208 assert!(clause.contains("name: $name"));
209 assert!(clause.contains("age: $age"));
210 }
211
212 #[test]
213 fn test_node_model_create_clause_no_props() {
214 let model = GraphNodeModel::new("EmptyLabel");
215 let clause = model.create_clause("n");
216 assert_eq!(clause, "CREATE (n:EmptyLabel)");
217 }
218
219 #[test]
220 fn test_relation_model_match_outgoing() {
221 let model = GraphRelationModel::new("KNOWS", "Person", "Person");
222 let clause = model.match_clause("a", "r", "b");
223 assert!(clause.contains("MATCH (a:Person)-[r:KNOWS]->(b:Person)"));
224 }
225
226 #[test]
227 fn test_relation_model_match_incoming_and_undirected() {
228 let incoming = GraphRelationModel::new("KNOWS", "Person", "Person")
229 .direction(RelationDirection::Incoming);
230 let clause = incoming.match_clause("a", "r", "b");
231 assert!(clause.contains("<-[r:KNOWS]-"));
232
233 let undirected = GraphRelationModel::new("KNOWS", "Person", "Person")
234 .direction(RelationDirection::Undirected);
235 let clause = undirected.match_clause("a", "r", "b");
236 assert!(clause.contains("-[r:KNOWS]-"));
237 assert!(!clause.contains("->"));
238 assert!(!clause.contains("<-"));
239 }
240
241 #[test]
242 fn test_extract_node_properties() {
243 let node = GraphNode {
244 id: "1".into(),
245 labels: vec![],
246 properties: serde_json::json!({"name": "Alice"}),
247 };
248 let result = GraphResult::Node { node };
249 let props = extract_node_properties(&result);
250 assert!(props.is_some());
251 assert_eq!(props.unwrap()["name"], serde_json::json!("Alice"));
252
253 let scalar = GraphResult::Scalar {
254 value: serde_json::json!(42),
255 };
256 assert!(extract_node_properties(&scalar).is_none());
257 }
258}