1use super::core::EdgeStore;
9use crate::edge::FreezeFeasibility;
10use linkrs_core::types::{EdgeId, Timestamp};
11use linkrs_core::StorageResult;
12
13impl EdgeStore {
14 pub fn freeze_feasibility(&self, outgoing: bool, gid: usize) -> FreezeFeasibility {
19 if outgoing {
20 self.out_csr.freeze_feasibility(gid)
21 } else {
22 self.in_csr.freeze_feasibility(gid)
23 }
24 }
25
26 pub fn freeze_group(
33 &mut self,
34 outgoing: bool,
35 gid: usize,
36 cutoff: Timestamp,
37 reserve_ratio: f32,
38 ) -> StorageResult<u64> {
39 let packed = if outgoing {
40 let shards = &mut self.out_csr;
41 let mvcc = &mut self.mvcc;
42 shards.freeze_group(
43 gid,
44 cutoff,
45 reserve_ratio,
46 &mut |edge_id: EdgeId, delete_ts: Timestamp| {
47 mvcc.record_deletion(edge_id, delete_ts);
48 },
49 )?
50 } else {
51 let shards = &mut self.in_csr;
52 let mvcc = &mut self.mvcc;
53 shards.freeze_group(
54 gid,
55 cutoff,
56 reserve_ratio,
57 &mut |edge_id: EdgeId, delete_ts: Timestamp| {
58 mvcc.record_deletion(edge_id, delete_ts);
59 },
60 )?
61 };
62 let drift = self.audit_copy_drift();
63 if !drift.is_empty() {
64 return Err(linkrs_core::StorageError::data_corruption(format!(
65 "freeze_group drift after pack: {}",
66 drift.join("; ")
67 )));
68 }
69 Ok(packed)
70 }
71
72 pub fn unfreeze_group(&mut self, outgoing: bool, gid: usize) -> StorageResult<u64> {
74 if outgoing {
75 self.out_csr.unfreeze_group(gid)
76 } else {
77 self.in_csr.unfreeze_group(gid)
78 }
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::edge::{EdgeRecord, EdgeSchema, EdgeStrategy, RecordForm};
86 use crate::types::StoragePropertyDef;
87 use linkrs_core::types::{DataType, Timestamp, VertexId};
88 use linkrs_core::Value;
89
90 use super::super::config::EdgeTableConfig;
91
92 fn frozen_test_schema() -> EdgeSchema {
93 EdgeSchema {
94 label_id: 0,
95 label_name: "knows".into(),
96 src_label: 0,
97 dst_label: 0,
98 properties: vec![StoragePropertyDef {
99 name: "weight".into(),
100 data_type: DataType::Double,
101 nullable: false,
102 default_value: Some(Value::Double(0.0)),
103 }],
104 oe_strategy: EdgeStrategy::Multiple,
105 ie_strategy: EdgeStrategy::Multiple,
106 schema_version: 1,
107 record_form: RecordForm::default(),
108 }
109 }
110
111 fn sample_table() -> EdgeStore {
112 let mut table =
113 EdgeStore::with_config(frozen_test_schema(), EdgeTableConfig::default()).unwrap();
114 for src in 0..8u32 {
115 for k in 0..2u32 {
116 table
117 .insert_edge(
118 src,
119 src + k + 1,
120 0,
121 &[("weight".into(), Value::Double(1.0))],
122 100,
123 )
124 .unwrap();
125 }
126 }
127 assert!(table.delete_edge(0, 1, 0, 200).unwrap());
128 table
129 }
130
131 fn snapshot(table: &EdgeStore, ts: Timestamp) -> Vec<Vec<VertexId>> {
132 (0..8u32)
133 .map(|src| {
134 table
135 .out_edges(src, ts)
136 .into_iter()
137 .map(|edge| edge.dst_vid)
138 .collect()
139 })
140 .collect()
141 }
142
143 #[test]
144 fn freeze_keeps_reads_and_rejects_writes() {
145 let mut table = sample_table();
146 let before_freeze = snapshot(&table, 300);
147 let before_old = snapshot(&table, 150);
148
149 let out_packed = table.freeze_group(true, 0, Timestamp::MAX, 0.0).unwrap();
150 let in_packed = table.freeze_group(false, 0, Timestamp::MAX, 0.0).unwrap();
151 assert_eq!(out_packed, 15);
152 assert_eq!(in_packed, 15);
153
154 assert_eq!(snapshot(&table, 300), before_freeze);
155 assert_eq!(snapshot(&table, 150), before_old);
156 assert!(table.has_edge(0, 2, 0, 300));
157 assert!(!table.has_edge(0, 1, 0, 300));
158 assert!(table.has_edge(0, 1, 0, 150));
159
160 assert!(table.insert_edge(1, 9, 0, &[], 400).is_err());
161 assert!(table.delete_edge(1, 2, 0, 400).is_err());
162
163 assert_eq!(table.unfreeze_group(true, 0).unwrap(), 15);
164 assert_eq!(table.unfreeze_group(false, 0).unwrap(), 15);
165 assert_eq!(snapshot(&table, 300), before_freeze);
166 table.insert_edge(1, 9, 0, &[], 400).unwrap();
167 assert!(table.has_edge(1, 9, 0, 400));
168 }
169
170 #[test]
171 fn freeze_guards_bad_states() {
172 let mut table = sample_table();
173 assert!(table.freeze_group(true, 41, Timestamp::MAX, 0.0).is_err());
174 assert!(table.unfreeze_group(true, 0).is_err());
175 table.freeze_group(true, 0, Timestamp::MAX, 0.0).unwrap();
176 assert!(table.freeze_group(true, 0, Timestamp::MAX, 0.0).is_err());
177 }
178
179 #[test]
180 fn valued_bundled_freeze_preserves_values() {
181 use crate::edge::RecordFormPreference;
182 let config = EdgeTableConfig {
183 record_form: RecordFormPreference::Bundled,
184 ..Default::default()
185 };
186 let mut bundled =
187 EdgeStore::with_config(frozen_test_schema(), config).expect("bundled table builds");
188 assert_eq!(bundled.schema().record_form, RecordForm::Bundled);
189 bundled
190 .insert_edge(0, 1, 0, &[("weight".into(), Value::Double(1.0))], 100)
191 .expect("valued insert");
192 assert!(bundled.freeze_feasibility(true, 0).is_ready());
193 assert!(bundled.freeze_feasibility(false, 0).is_ready());
194
195 let before: Vec<EdgeRecord> =
196 crate::edge::edge_table::iterator::EdgeTableScanIterator::new(&bundled, 200).collect();
197 assert_eq!(before.len(), 1);
198 bundled.freeze_group(true, 0, Timestamp::MAX, 0.0).unwrap();
199 bundled.freeze_group(false, 0, Timestamp::MAX, 0.0).unwrap();
200 assert!(bundled.out_csr.is_frozen(0));
201 assert!(bundled.in_csr.is_frozen(0));
202
203 let frozen: Vec<EdgeRecord> =
205 crate::edge::edge_table::iterator::EdgeTableScanIterator::new(&bundled, 200).collect();
206 assert_eq!(frozen.len(), 1);
207 assert_eq!(frozen[0].properties, before[0].properties);
208 assert_eq!(
209 bundled
210 .get_edge(0, 1, 0, 200)
211 .expect("frozen edge readable")
212 .properties,
213 vec![("weight".into(), Value::Double(1.0))]
214 );
215
216 assert!(bundled.insert_edge(1, 9, 0, &[], 400).is_err());
219 bundled.unfreeze_group(true, 0).unwrap();
220 bundled.unfreeze_group(false, 0).unwrap();
221 let after: Vec<EdgeRecord> =
222 crate::edge::edge_table::iterator::EdgeTableScanIterator::new(&bundled, 200).collect();
223 assert_eq!(after.len(), 1);
224 assert_eq!(after[0].properties, before[0].properties);
225 bundled
226 .insert_edge(1, 9, 0, &[("weight".into(), Value::Double(2.0))], 400)
227 .expect("writes resume after unfreeze");
228 assert_eq!(
229 bundled
230 .get_edge(1, 9, 0, 500)
231 .expect("new edge readable")
232 .properties,
233 vec![("weight".into(), Value::Double(2.0))]
234 );
235 }
236
237 #[test]
238 fn row_ordering_contract_mutable_unordered_frozen_sorted() {
239 use crate::edge::{EdgeSchema, EdgeStrategy, RecordForm};
240 use crate::types::StoragePropertyDef;
241 use linkrs_core::types::DataType;
242
243 let schema = EdgeSchema {
244 label_id: 0,
245 label_name: "ordered".into(),
246 src_label: 0,
247 dst_label: 0,
248 properties: vec![StoragePropertyDef {
249 name: "weight".into(),
250 data_type: DataType::Double,
251 nullable: false,
252 default_value: Some(Value::Double(0.0)),
253 }],
254 oe_strategy: EdgeStrategy::Multiple,
255 ie_strategy: EdgeStrategy::Multiple,
256 schema_version: 1,
257 record_form: RecordForm::default(),
258 };
259 let mut table = EdgeStore::with_config(schema, EdgeTableConfig::default()).unwrap();
260 for dst in [3u32, 1, 2] {
261 table
262 .insert_edge(0, dst, 0, &[], 100)
263 .expect("insert out of key order");
264 }
265 let mutable_order: Vec<i64> = table
268 .out_edges(0, 200)
269 .iter()
270 .map(|e| e.dst_vid.as_int64().unwrap_or(-1))
271 .collect();
272 assert_eq!(mutable_order, vec![3, 1, 2]);
273
274 table.freeze_group(true, 0, Timestamp::MAX, 0.0).unwrap();
276 let frozen_order: Vec<i64> = table
277 .out_edges(0, 200)
278 .iter()
279 .map(|e| e.dst_vid.as_int64().unwrap_or(-1))
280 .collect();
281 assert_eq!(frozen_order, vec![1, 2, 3]);
282
283 table.unfreeze_group(true, 0).unwrap();
286 let mut restored: Vec<i64> = table
287 .out_edges(0, 200)
288 .iter()
289 .map(|e| e.dst_vid.as_int64().unwrap_or(-1))
290 .collect();
291 restored.sort_unstable();
292 assert_eq!(restored, vec![1, 2, 3]);
293 }
294}