1use linkrs_core::{DataType, Value};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum SchemaObjectType {
13 Vertex,
14 Edge,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub enum ChangeDetails {
20 PropertyAdded {
22 name: String,
23 data_type: DataType,
24 nullable: bool,
25 default_value: Option<Value>,
26 },
27
28 PropertyRemoved { name: String, data_type: DataType },
30
31 PropertyRenamed { old_name: String, new_name: String },
33
34 PropertyTypeModified {
36 name: String,
37 old_type: DataType,
38 new_type: DataType,
39 },
40
41 PropertyNullabilityChanged {
43 name: String,
44 was_nullable: bool,
45 now_nullable: bool,
46 },
47
48 PropertyDefaultValueChanged {
50 name: String,
51 old_default: Option<Value>,
52 new_default: Option<Value>,
53 },
54
55 PrimaryKeyChanged {
57 old_property: String,
58 new_property: String,
59 },
60}
61
62impl ChangeDetails {
63 pub fn is_breaking(&self) -> bool {
65 matches!(
66 self,
67 ChangeDetails::PropertyRemoved { .. }
68 | ChangeDetails::PropertyTypeModified { .. }
69 | ChangeDetails::PrimaryKeyChanged { .. }
70 )
71 }
72
73 pub fn description(&self) -> String {
75 match self {
76 ChangeDetails::PropertyAdded { name, .. } => {
77 format!("Added property '{}'", name)
78 }
79 ChangeDetails::PropertyRemoved { name, .. } => {
80 format!("Removed property '{}'", name)
81 }
82 ChangeDetails::PropertyRenamed { old_name, new_name } => {
83 format!("Renamed property '{}' to '{}'", old_name, new_name)
84 }
85 ChangeDetails::PropertyTypeModified {
86 name,
87 old_type,
88 new_type,
89 } => {
90 format!(
91 "Modified property '{}' type from {:?} to {:?}",
92 name, old_type, new_type
93 )
94 }
95 ChangeDetails::PropertyNullabilityChanged {
96 name,
97 was_nullable,
98 now_nullable,
99 } => {
100 format!(
101 "Changed property '{}' nullability from {} to {}",
102 name, was_nullable, now_nullable
103 )
104 }
105 ChangeDetails::PropertyDefaultValueChanged {
106 name,
107 old_default,
108 new_default,
109 } => {
110 format!(
111 "Changed property '{}' default value from {:?} to {:?}",
112 name, old_default, new_default
113 )
114 }
115 ChangeDetails::PrimaryKeyChanged {
116 old_property,
117 new_property,
118 } => {
119 format!(
120 "Changed primary key from '{}' to '{}'",
121 old_property, new_property
122 )
123 }
124 }
125 }
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct PropertyChange {
131 pub version: u64,
133 pub timestamp_ms: u64,
135 pub details: ChangeDetails,
137}
138
139impl PropertyChange {
140 pub fn new(
145 version: u64,
146 _object_type: SchemaObjectType,
147 _label_id: u32,
148 _label_name: String,
149 details: ChangeDetails,
150 ) -> Self {
151 let timestamp_ms = std::time::SystemTime::now()
152 .duration_since(std::time::UNIX_EPOCH)
153 .map(|d| d.as_millis() as u64)
154 .unwrap_or(0);
155
156 Self {
157 version,
158 timestamp_ms,
159 details,
160 }
161 }
162
163 pub fn is_breaking(&self) -> bool {
165 self.details.is_breaking()
166 }
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct ChangeLog {
172 pub object_type: SchemaObjectType,
174 pub label_id: u32,
176 pub label_name: String,
178 pub changes: HashMap<u64, Vec<PropertyChange>>,
180}
181
182impl ChangeLog {
183 pub fn new(object_type: SchemaObjectType, label_id: u32, label_name: String) -> Self {
185 Self {
186 object_type,
187 label_id,
188 label_name,
189 changes: HashMap::new(),
190 }
191 }
192
193 pub fn add_change(&mut self, change: PropertyChange) {
195 self.changes.entry(change.version).or_default().push(change);
196 }
197
198 pub fn get_version_changes(&self, version: u64) -> Option<&Vec<PropertyChange>> {
200 self.changes.get(&version)
201 }
202
203 pub fn get_versions(&self) -> Vec<u64> {
205 let mut versions: Vec<_> = self.changes.keys().copied().collect();
206 versions.sort_unstable();
207 versions
208 }
209
210 pub fn latest_version(&self) -> Option<u64> {
212 self.changes.keys().max().copied()
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_property_change_creation() {
222 let change = PropertyChange::new(
223 1,
224 SchemaObjectType::Vertex,
225 1,
226 "User".to_string(),
227 ChangeDetails::PropertyAdded {
228 name: "email".to_string(),
229 data_type: DataType::String,
230 nullable: false,
231 default_value: None,
232 },
233 );
234
235 assert_eq!(change.version, 1);
236 assert!(!change.is_breaking());
237 }
238
239 #[test]
240 fn test_breaking_changes() {
241 let breaking_change = PropertyChange::new(
242 1,
243 SchemaObjectType::Vertex,
244 1,
245 "User".to_string(),
246 ChangeDetails::PropertyRemoved {
247 name: "old_field".to_string(),
248 data_type: DataType::String,
249 },
250 );
251
252 assert!(breaking_change.is_breaking());
253 }
254
255 #[test]
256 fn test_changelog_operations() {
257 let mut log = ChangeLog::new(SchemaObjectType::Vertex, 1, "User".to_string());
258
259 let change1 = PropertyChange::new(
260 1,
261 SchemaObjectType::Vertex,
262 1,
263 "User".to_string(),
264 ChangeDetails::PropertyAdded {
265 name: "email".to_string(),
266 data_type: DataType::String,
267 nullable: false,
268 default_value: None,
269 },
270 );
271
272 log.add_change(change1);
273
274 assert_eq!(log.get_versions(), vec![1]);
275 assert_eq!(log.latest_version(), Some(1));
276 assert!(log.get_version_changes(1).is_some());
277 }
278}