1use crate::StorageClient;
7use linkrs_core::StorageError;
8use std::fmt::Debug;
9use std::sync::Arc;
10
11#[derive(Clone, Debug)]
13pub struct SyncWrapper<S: StorageClient + Debug> {
14 inner: S,
15 sync_manager: Option<Arc<linkrs_sync::SyncManager>>,
16 enabled: bool,
17 auto_commit_owner: bool,
18}
19
20struct IndexDropSnapshotEntry {
23 space_id: u64,
24 index_name: String,
25 schema_name: String,
26 index_type: String,
27 fields: Vec<String>,
28}
29
30struct IndexDropSnapshot {
31 space_id: Option<u64>,
32 drops: Vec<IndexDropSnapshotEntry>,
33}
34
35impl<S: StorageClient> SyncWrapper<S> {
36 pub fn new(storage: S) -> Self {
38 Self {
39 inner: storage,
40 sync_manager: None,
41 enabled: false,
42 auto_commit_owner: false,
43 }
44 }
45
46 pub fn with_sync_manager(storage: S, sync_manager: Arc<linkrs_sync::SyncManager>) -> Self {
48 let frontier_manager = sync_manager.clone();
49 storage.set_outbox_materialized_lsn_provider(Arc::new(move || {
50 frontier_manager
51 .outbox_materialized_lsn()
52 .map_err(|error| StorageError::db_error(error.to_string()))
53 }));
54 Self {
55 inner: storage,
56 sync_manager: Some(sync_manager),
57 enabled: true,
58 auto_commit_owner: false,
59 }
60 }
61
62 pub fn is_enabled(&self) -> bool {
64 self.enabled
65 }
66
67 pub fn get_sync_manager(&self) -> Option<Arc<linkrs_sync::SyncManager>> {
69 self.sync_manager.clone()
70 }
71
72 pub fn inner(&self) -> &S {
74 &self.inner
75 }
76
77 pub fn inner_mut(&mut self) -> &mut S {
79 &mut self.inner
80 }
81}
82
83impl<S: StorageClient> SyncWrapper<S> {
84 fn get_current_txn_id(&self) -> Option<linkrs_core::types::TransactionId> {
86 self.inner
87 .operation_context()
88 .and_then(|ctx| ctx.transaction_id)
89 }
90 fn commit_transaction_fact(
91 &self,
92 transaction_id: linkrs_core::types::TransactionId,
93 ) -> Result<linkrs_core::types::CommitLsn, StorageError> {
94 self.commit_transaction_fact_with_durability(
95 transaction_id,
96 linkrs_core::types::DurabilityLevel::Sync,
97 )
98 }
99
100 fn commit_transaction_fact_with_durability(
101 &self,
102 transaction_id: linkrs_core::types::TransactionId,
103 durability: linkrs_core::types::DurabilityLevel,
104 ) -> Result<linkrs_core::types::CommitLsn, StorageError> {
105 let intents = match self.sync_manager.as_ref() {
106 Some(manager) => manager
107 .pending_transaction_intents(transaction_id)
108 .map_err(|error| {
109 StorageError::db_error(format!(
110 "Failed to build transaction index intents: {}",
111 error
112 ))
113 })?,
114 None => Vec::new(),
115 };
116 let commit_lsn = match self.inner.commit_staged_writes_with_durability(
117 transaction_id,
118 &intents,
119 durability,
120 ) {
121 Ok(commit_lsn) => commit_lsn,
122 Err(error) => {
123 let _ = self.inner.abort_staged_writes(transaction_id);
127 if let Some(manager) = self.sync_manager.as_ref() {
128 let _ = manager.rollback_transaction_sync(transaction_id);
129 }
130 return Err(error);
131 }
132 };
133 Ok(commit_lsn)
134 }
135
136 fn finalize_commit_fact(
137 &self,
138 transaction_id: linkrs_core::types::TransactionId,
139 commit_lsn: linkrs_core::types::CommitLsn,
140 ) -> Result<(), StorageError> {
141 if let Some(manager) = self.sync_manager.as_ref() {
142 let intents = manager
143 .pending_transaction_intents(transaction_id)
144 .map_err(|error| {
145 StorageError::db_error(format!(
146 "Failed to load committed transaction intents: {}",
147 error
148 ))
149 })?;
150 if let Err(error) =
151 manager.materialize_committed_transaction(transaction_id, commit_lsn, &intents)
152 {
153 log::error!(
154 "Committed transaction {} at {} but outbox materialization is pending recovery: {}",
155 transaction_id,
156 commit_lsn,
157 error
158 );
159 }
160 if let Err(error) = manager.rollback_transaction_sync(transaction_id) {
161 log::warn!(
162 "Committed transaction {} at {} but staging cleanup failed: {}",
163 transaction_id,
164 commit_lsn,
165 error
166 );
167 }
168 manager.clear_transaction_intents(transaction_id);
169 if let Err(error) = manager.retry_outbox_sync() {
170 log::debug!(
171 "Committed transaction {} at {}; target delivery will retry: {}",
172 transaction_id,
173 commit_lsn,
174 error
175 );
176 }
177 }
178 Ok(())
179 }
180
181 fn commit_auto_transaction(
182 &self,
183 ) -> Result<Option<linkrs_core::types::CommitLsn>, StorageError> {
184 let Some(context) = self.inner.operation_context() else {
185 return Ok(None);
186 };
187 if (!self.auto_commit_owner && !context.auto_commit) || context.read_only {
188 return Ok(None);
189 }
190 let transaction_id = context.transaction_id.ok_or_else(|| {
191 StorageError::db_error("Auto-commit write has no transaction ID".to_string())
192 })?;
193 let commit_lsn = self.commit_transaction_fact(transaction_id)?;
194 self.finalize_commit_fact(transaction_id, commit_lsn)?;
201 Ok(Some(commit_lsn))
202 }
203
204 fn abort_transaction_fact(
205 &self,
206 transaction_id: linkrs_core::types::TransactionId,
207 ) -> Result<(), StorageError> {
208 self.inner.abort_staged_writes(transaction_id)?;
209 if let Some(manager) = self.sync_manager.as_ref() {
210 manager
211 .rollback_transaction_sync(transaction_id)
212 .map_err(|error| {
213 StorageError::db_error(format!(
214 "Failed to discard transaction index intents: {}",
215 error
216 ))
217 })?;
218 }
219 Ok(())
220 }
221
222 fn stage_index_create(
223 &self,
224 index: &linkrs_core::types::Index,
225 index_type: &str,
226 ) -> Result<(), StorageError> {
227 if !self.enabled {
228 return Ok(());
229 }
230 let Some(sync_manager) = self.get_sync_manager() else {
231 return Ok(());
232 };
233 let transaction_id = self.get_current_txn_id().ok_or_else(|| {
234 StorageError::db_error(
235 "Synchronized schema changes require an operation transaction context".to_string(),
236 )
237 })?;
238 let fields = index
239 .fields
240 .iter()
241 .map(|field| (field.name.clone(), field.value_type.clone()))
242 .collect::<Vec<_>>();
243 sync_manager
244 .on_index_create(
245 transaction_id,
246 linkrs_sync::manager::IndexCreateRequest {
247 space_id: index.space_id,
248 index_name: index.name.clone(),
249 schema_name: index.schema_name.clone(),
250 index_type: index_type.to_string(),
251 fields,
252 properties: index.properties.clone(),
253 },
254 )
255 .map_err(|error| {
256 StorageError::db_error(format!("Failed to stage index creation intent: {error}"))
257 })
258 }
259
260 fn validate_schema_sync_context(&self) -> Result<(), StorageError> {
261 if self.enabled && self.get_sync_manager().is_some() && self.get_current_txn_id().is_none()
262 {
263 return Err(StorageError::db_error(
264 "Synchronized schema changes require an operation transaction context".to_string(),
265 ));
266 }
267 Ok(())
268 }
269
270 fn stage_index_drop(
271 &self,
272 space_id: u64,
273 index_name: &str,
274 schema_name: &str,
275 index_type: &str,
276 fields: &[String],
277 ) -> Result<(), StorageError> {
278 if !self.enabled {
279 return Ok(());
280 }
281 let Some(sync_manager) = self.get_sync_manager() else {
282 return Ok(());
283 };
284 let transaction_id = self.get_current_txn_id().ok_or_else(|| {
285 StorageError::db_error(
286 "Synchronized schema changes require an operation transaction context".to_string(),
287 )
288 })?;
289 sync_manager
290 .on_index_drop(
291 transaction_id,
292 space_id,
293 index_name,
294 schema_name,
295 index_type,
296 fields,
297 )
298 .map_err(|error| {
299 StorageError::db_error(format!("Failed to stage index drop intent: {error}"))
300 })
301 }
302
303 fn stage_snapshot_drops(&self, snapshot: &IndexDropSnapshot) -> Result<(), StorageError> {
305 for drop in &snapshot.drops {
306 self.stage_index_drop(
307 drop.space_id,
308 &drop.index_name,
309 &drop.schema_name,
310 &drop.index_type,
311 &drop.fields,
312 )?;
313 }
314 Ok(())
315 }
316
317 fn stage_space_drop(&self, space_id: u64) -> Result<(), StorageError> {
318 if !self.enabled {
319 return Ok(());
320 }
321 let Some(sync_manager) = self.get_sync_manager() else {
322 return Ok(());
323 };
324 let transaction_id = self.get_current_txn_id().ok_or_else(|| {
325 StorageError::db_error(
326 "Synchronized schema changes require an operation transaction context".to_string(),
327 )
328 })?;
329 sync_manager
330 .on_space_drop(transaction_id, space_id)
331 .map_err(|error| {
332 StorageError::db_error(format!("Failed to stage space drop intent: {error}"))
333 })
334 }
335
336 fn stage_tag_drop(&self, space_id: u64, tag_name: &str) -> Result<(), StorageError> {
337 if !self.enabled {
338 return Ok(());
339 }
340 let Some(sync_manager) = self.get_sync_manager() else {
341 return Ok(());
342 };
343 let transaction_id = self.get_current_txn_id().ok_or_else(|| {
344 StorageError::db_error(
345 "Synchronized schema changes require an operation transaction context".to_string(),
346 )
347 })?;
348 sync_manager
349 .on_tag_drop(transaction_id, space_id, tag_name)
350 .map_err(|error| {
351 StorageError::db_error(format!("Failed to stage tag drop intent: {error}"))
352 })
353 }
354
355 fn drop_or_clear_space(&mut self, space: &str, is_clear: bool) -> Result<bool, StorageError> {
360 let snapshot = self.snapshot_tag_indexes(space, None);
361 let space_id = snapshot.space_id;
362 self.validate_schema_sync_context()?;
363 let result = if is_clear {
364 self.inner.clear_space(space)?
365 } else {
366 self.inner.drop_space(space)?
367 };
368 if result {
369 if let Err(error) = self.stage_snapshot_drops(&snapshot) {
370 if let Some(transaction_id) = self.get_current_txn_id() {
371 let _ = self.abort_transaction_fact(transaction_id);
372 }
373 return Err(error);
374 }
375 if let Some(sid) = space_id {
376 if let Err(error) = self.stage_space_drop(sid) {
377 if let Some(transaction_id) = self.get_current_txn_id() {
378 let _ = self.abort_transaction_fact(transaction_id);
379 }
380 return Err(error);
381 }
382 }
383 }
384 self.commit_auto_transaction()?;
385 Ok(result)
386 }
387
388 fn snapshot_tag_indexes(&self, space: &str, schema_filter: Option<&str>) -> IndexDropSnapshot {
392 let space_id = self.inner.get_space_id(space).ok();
393 let mut drops = Vec::new();
394 if let Some(sid) = space_id {
395 let mut push_index = |index: &linkrs_core::types::Index, index_type: &str| {
396 if schema_filter.is_some_and(|f| f != index.schema_name) {
397 return;
398 }
399 drops.push(IndexDropSnapshotEntry {
400 space_id: sid,
401 index_name: index.name.clone(),
402 schema_name: index.schema_name.clone(),
403 index_type: index_type.to_string(),
404 fields: index
405 .fields
406 .iter()
407 .map(|field| field.name.clone())
408 .collect(),
409 });
410 };
411 if let Ok(indexes) = self.inner.list_tag_indexes(space) {
412 for index in &indexes {
413 push_index(index, "tag");
414 }
415 }
416 if let Ok(indexes) = self.inner.list_edge_indexes(space) {
417 for index in &indexes {
418 push_index(index, "edge");
419 }
420 }
421 }
422 IndexDropSnapshot { space_id, drops }
423 }
424}
425
426pub mod admin;
427pub mod operation;
428pub mod persistence;
429pub mod reader;
430pub mod schema;
431pub mod transaction;
432pub mod undo;
433mod write;
434mod write_edge;
435mod write_vertex;
436
437#[cfg(test)]
438mod tests;