Skip to main content

sz_orm_graph/
model.rs

1//! # Model — 声明式建模
2//!
3//! GraphNodeModel + GraphRelationModel + GraphPropertyDef
4
5use crate::query::GraphResult;
6
7/// 属性值类型
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum GraphValueType {
10    String,
11    Integer,
12    Float,
13    Boolean,
14    DateTime,
15    Duration,
16}
17
18/// 图属性定义
19#[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/// 关系方向
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum RelationDirection {
51    Outgoing,
52    Incoming,
53    Undirected,
54}
55
56/// 图节点模型(声明式建模)
57#[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    /// 生成 MATCH 子句
78    pub fn match_clause(&self, alias: &str) -> String {
79        format!("MATCH ({}:{})", alias, self.label)
80    }
81
82    /// 生成 CREATE 子句
83    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/// 图关系模型
103#[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    /// 生成 MATCH 关系子句
135    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
168/// 从 GraphResult 提取节点属性
169pub 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}