Skip to main content

linkrs_storage/schema/
change.rs

1//! Schema change event tracking and history management
2//!
3//! This module provides the foundation for recording schema modifications,
4//! enabling version history tracking and compatibility analysis.
5
6use linkrs_core::{DataType, Value};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Type identifier for change tracking - distinguishes between vertex and edge schema changes
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum SchemaObjectType {
13    Vertex,
14    Edge,
15}
16
17/// Detailed information about schema modifications
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub enum ChangeDetails {
20    /// Property added: (property_name, data_type, nullable, default_value)
21    PropertyAdded {
22        name: String,
23        data_type: DataType,
24        nullable: bool,
25        default_value: Option<Value>,
26    },
27
28    /// Property removed: property_name
29    PropertyRemoved { name: String, data_type: DataType },
30
31    /// Property renamed: (old_name, new_name)
32    PropertyRenamed { old_name: String, new_name: String },
33
34    /// Property type modified: (property_name, old_type, new_type)
35    PropertyTypeModified {
36        name: String,
37        old_type: DataType,
38        new_type: DataType,
39    },
40
41    /// Property nullability changed
42    PropertyNullabilityChanged {
43        name: String,
44        was_nullable: bool,
45        now_nullable: bool,
46    },
47
48    /// Property default value changed
49    PropertyDefaultValueChanged {
50        name: String,
51        old_default: Option<Value>,
52        new_default: Option<Value>,
53    },
54
55    /// Primary key changed (for vertices)
56    PrimaryKeyChanged {
57        old_property: String,
58        new_property: String,
59    },
60}
61
62impl ChangeDetails {
63    /// Check if this change is breaking (destructive)
64    pub fn is_breaking(&self) -> bool {
65        matches!(
66            self,
67            ChangeDetails::PropertyRemoved { .. }
68                | ChangeDetails::PropertyTypeModified { .. }
69                | ChangeDetails::PrimaryKeyChanged { .. }
70        )
71    }
72
73    /// Get a human-readable description of the change
74    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/// Property change event - represents a single property schema modification
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct PropertyChange {
131    /// Version at which this change occurred
132    pub version: u64,
133    /// Timestamp in milliseconds when the change was made
134    pub timestamp_ms: u64,
135    /// Detailed information about the change
136    pub details: ChangeDetails,
137}
138
139impl PropertyChange {
140    /// Create a new property change event
141    ///
142    /// Note: object_type, label_id, and label_name are stored in ChangeLog container,
143    /// so we don't duplicate them here to avoid data inconsistency.
144    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    /// Check if this change is breaking
164    pub fn is_breaking(&self) -> bool {
165        self.details.is_breaking()
166    }
167}
168
169/// Batch of schema changes for a single label
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct ChangeLog {
172    /// Object type (vertex or edge)
173    pub object_type: SchemaObjectType,
174    /// Label ID
175    pub label_id: u32,
176    /// Label name
177    pub label_name: String,
178    /// All changes for this label, indexed by version
179    pub changes: HashMap<u64, Vec<PropertyChange>>,
180}
181
182impl ChangeLog {
183    /// Create a new change log
184    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    /// Add a change to the log
194    pub fn add_change(&mut self, change: PropertyChange) {
195        self.changes.entry(change.version).or_default().push(change);
196    }
197
198    /// Get changes for a specific version
199    pub fn get_version_changes(&self, version: u64) -> Option<&Vec<PropertyChange>> {
200        self.changes.get(&version)
201    }
202
203    /// Get all versions in order
204    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    /// Get the latest version
211    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}