Skip to main content

linkrs_storage/engine/
graph_storage.rs

1//! Storage Interface Implementation
2//!
3//! Implements the StorageClient trait for the storage engine.
4//! This module acts as an adapter layer between the high-level StorageClient API
5//! and the low-level storage engine.
6
7mod commit_api;
8pub mod context;
9mod cursor_impl;
10mod index_engine;
11mod index_manager;
12mod ops;
13mod persistence;
14mod persistence_api;
15mod reader;
16mod reader_api;
17mod schema_api;
18mod schema_engine;
19mod schema_writer;
20mod serial;
21mod stats_reader_impl;
22pub mod ttl;
23pub(crate) mod ttl_purge;
24mod writer;
25mod writer_api;
26
27pub use context::{AutoCommitBatchWindow, GraphStorageContext, WriteGateStats};
28pub use serial::SerialKey;
29
30use std::path::PathBuf;
31use std::sync::Arc;
32
33use crate::engine::background_freeze::{BackgroundFreezeManager, FreezeStats};
34use crate::engine::PersistenceConfig;
35use crate::index::key_codec::KeyBuilder;
36use crate::index::types::IndexIdentity;
37use crate::index::IndexGcConfig;
38use crate::{
39    StorageAdmin, StorageAuthOps, StorageGcOps, StorageOperationContext,
40    StorageOperationContextOps, StorageRecoveryOps, StorageSchemaContextOps, StorageStats,
41    StorageSyncContextOps, StorageTtlPurgeOps,
42};
43use linkrs_core::metadata::{IndexMetadataManager, SchemaManager};
44use linkrs_core::types::{
45    CommitLsn, PasswordInfo, SnapshotTimestamp, UserAlterInfo, UserInfo, VertexId,
46};
47use linkrs_core::{Edge, RoleType, StorageError, StorageResult, Value};
48use linkrs_metrics::StatsManager;
49
50#[derive(Clone)]
51pub struct GraphStorage {
52    ctx: Arc<GraphStorageContext>,
53}
54
55impl std::fmt::Debug for GraphStorage {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        f.debug_struct("GraphStorage")
58            .field("work_dir", &self.ctx.work_dir())
59            .field("db_path", &self.ctx.db_path())
60            .finish()
61    }
62}
63
64impl crate::stats_reader::ColumnStatsReader for GraphStorage {
65    fn vertex_column_stats(
66        &self,
67        space: &str,
68        tag: &str,
69        column: &str,
70    ) -> Option<std::sync::Arc<crate::stats_reader::ColumnStatsSnapshot>> {
71        stats_reader_impl::vertex_column_stats(&self.ctx, space, tag, column)
72    }
73
74    fn edge_column_stats(
75        &self,
76        space: &str,
77        edge_type: &str,
78        column: &str,
79    ) -> Option<std::sync::Arc<crate::stats_reader::ColumnStatsSnapshot>> {
80        stats_reader_impl::edge_column_stats(&self.ctx, space, edge_type, column)
81    }
82
83    fn vertex_table_stats(
84        &self,
85        space: &str,
86        tag: &str,
87    ) -> Option<std::sync::Arc<crate::stats_reader::TableCardinalitySnapshot>> {
88        stats_reader_impl::vertex_table_stats(&self.ctx, space, tag)
89    }
90
91    fn edge_table_stats(
92        &self,
93        space: &str,
94        edge_type: &str,
95    ) -> Option<std::sync::Arc<crate::stats_reader::TableCardinalitySnapshot>> {
96        stats_reader_impl::edge_table_stats(&self.ctx, space, edge_type)
97    }
98
99    fn stats_epoch(&self) -> u64 {
100        // The MVCC write timestamp is monotonic and bumps on every write
101        // allocation, making it a cheap data-version stamp for the
102        // optimizer's statistics cache.
103        self.ctx.version_manager().write_timestamp()
104    }
105}
106
107impl GraphStorage {
108    /// Return the MVCC manager used by this storage instance.
109    pub fn version_manager(&self) -> Arc<linkrs_transaction::VersionManager> {
110        self.ctx.version_manager().clone()
111    }
112
113    /// Cumulative auto-commit write-gate admission statistics (see
114    /// [`context::WriteGateStats`]).
115    pub fn write_gate_stats(&self) -> context::WriteGateStats {
116        self.ctx.write_gate_stats()
117    }
118
119    /// Recommended caller-side chunk size for very large edge batches.
120    ///
121    /// One batch is one atomic unit holding the table lock for the whole
122    /// apply; batches up to this size are the measured regime (see
123    /// `edge_group_commit_bench`). Larger fanouts split via
124    /// [`Self::chunk_edges_for_commit`] with one chunk per atomic commit.
125    pub const RECOMMENDED_BATCH_CHUNK_EDGES: usize = writer::RECOMMENDED_BATCH_CHUNK_EDGES;
126
127    /// Split an edge batch into caller-side chunks of at most
128    /// [`Self::RECOMMENDED_BATCH_CHUNK_EDGES`] edges.
129    ///
130    /// Each chunk commits atomically on its own through `batch_insert_edges`,
131    /// keeping the per-commit lock-hold bounded under the single-writer
132    /// discipline.
133    pub fn chunk_edges_for_commit(edges: &[Edge]) -> Vec<&[Edge]> {
134        writer::chunk_edges_for_commit(edges)
135    }
136
137    /// Outbox pending depth for backpressure-aware clients.
138    pub fn outbox_pending(&self) -> u64 {
139        self.ctx.outbox_pending()
140    }
141
142    pub fn begin_auto_commit_batch(&self) -> StorageResult<Arc<context::AutoCommitBatchWindow>> {
143        self.ctx.begin_auto_commit_batch()
144    }
145
146    pub fn bind_auto_commit_statement(
147        &self,
148        window: &Arc<context::AutoCommitBatchWindow>,
149    ) -> StorageResult<Self> {
150        Ok(Self {
151            ctx: Arc::new(window.bind_statement()?),
152        })
153    }
154
155    pub fn finalize_auto_commit_batch(
156        &self,
157        window: &context::AutoCommitBatchWindow,
158    ) -> StorageResult<()> {
159        window.finalize()
160    }
161
162    pub fn begin_auto_commit_group(&self) -> StorageResult<Arc<context::AutoCommitBatchWindow>> {
163        self.ctx.begin_auto_commit_group()
164    }
165
166    pub fn finalize_auto_commit_group(
167        &self,
168        window: &context::AutoCommitBatchWindow,
169    ) -> StorageResult<()> {
170        window.finalize_group()
171    }
172
173    fn commit_auto_if_needed(&self) -> StorageResult<()> {
174        let Some(context) = self.ctx.operation_context() else {
175            return Ok(());
176        };
177        if !context.auto_commit || context.read_only {
178            return Ok(());
179        }
180        if self.ctx.is_group_bound() {
181            return Ok(());
182        }
183        let transaction_id = context.transaction_id.ok_or_else(|| {
184            StorageError::db_error("Auto-commit storage context has no transaction ID".to_string())
185        })?;
186        self.ctx.commit_staged_writes(transaction_id, &[])?;
187        Ok(())
188    }
189
190    pub fn new() -> StorageResult<Self> {
191        Ok(Self {
192            ctx: Arc::new(GraphStorageContext::new()),
193        })
194    }
195
196    /// Create with a custom property graph configuration.
197    pub fn new_with_config(
198        config: crate::engine::config::PropertyGraphConfig,
199    ) -> StorageResult<Self> {
200        Ok(Self {
201            ctx: Arc::new(GraphStorageContext::new_with_config(config)?),
202        })
203    }
204
205    pub fn new_with_path(path: PathBuf) -> StorageResult<Self> {
206        GraphStorageContext::new_with_path(path).map(|ctx| Self { ctx: Arc::new(ctx) })
207    }
208
209    /// Open a persistent storage instance and load the on-disk state.
210    ///
211    /// This is the entry point for production usage. It loads the persisted
212    /// data first and then replays any remaining WAL entries if recovery is needed.
213    pub fn open(path: PathBuf) -> StorageResult<Self> {
214        let config = PersistenceConfig::for_work_dir(&path);
215        Self::open_with_persistence_config(path, config)
216    }
217
218    /// Open persistent storage with an explicit property graph configuration.
219    pub fn open_with_config(
220        path: PathBuf,
221        property_config: crate::engine::config::PropertyGraphConfig,
222    ) -> StorageResult<Self> {
223        let config =
224            PersistenceConfig::for_work_dir(&path).with_property_graph_config(property_config);
225        Self::open_with_persistence_config(path, config)
226    }
227
228    /// Open persistent storage using a fully specified persistence contract.
229    ///
230    /// Recovery errors propagate: a manifest-listed file that is missing
231    /// or corrupt refuses the open instead of serving a partially loaded
232    /// table whose global IDs silently lost siblings.
233    pub fn open_with_persistence_config(
234        path: PathBuf,
235        config: PersistenceConfig,
236    ) -> StorageResult<Self> {
237        let storage = Self::new_with_persistence(path, config)?;
238        let _recovery_stats = persistence::initialize_with_recovery(&storage.ctx)?;
239        Ok(storage)
240    }
241
242    pub fn new_with_persistence(path: PathBuf, config: PersistenceConfig) -> StorageResult<Self> {
243        GraphStorageContext::new_with_persistence(path, config)
244            .map(|ctx| Self { ctx: Arc::new(ctx) })
245    }
246
247    pub fn open_with_persistence(
248        path: PathBuf,
249        enable_wal: bool,
250        sync_policy: Option<linkrs_transaction::wal::SyncPolicy>,
251    ) -> StorageResult<Self> {
252        let mut config = PersistenceConfig::for_work_dir(&path);
253        config.enable_wal = enable_wal;
254        config.sync_policy = sync_policy;
255        let storage = Self::new_with_persistence(path, config)?;
256        let _recovery_stats = persistence::initialize_with_recovery(&storage.ctx)?;
257        Ok(storage)
258    }
259
260    pub fn with_index_gc(mut self, config: IndexGcConfig) -> Self {
261        let new_ctx = Arc::new((*self.ctx).clone().with_index_gc(config));
262        self.ctx = new_ctx;
263        self
264    }
265
266    /// number of staged-WAL entries held for in-flight transactions.
267    /// Auto-commit statements commit/remove their staged entries on write;
268    /// an unbounded value indicates a lifecycle leak.
269    pub fn staged_wal_len(&self) -> usize {
270        self.ctx.staged_wal_len()
271    }
272
273    /// total number of retired index generations awaiting reclamation.
274    pub fn retired_generation_count(&self) -> usize {
275        self.ctx
276            .index_data_manager()
277            .read()
278            .retired_generation_count()
279    }
280
281    pub fn with_vertex_gc(mut self, config: crate::vertex::VertexGcConfig) -> Self {
282        let new_ctx = Arc::new((*self.ctx).clone().with_vertex_gc(config));
283        self.ctx = new_ctx;
284        self
285    }
286
287    /// Set the StatsManager for recording MVCC metrics.
288    ///
289    /// This injects the stats manager into the GraphStorageContext,
290    /// which will then automatically pass it to all EdgeTable instances
291    /// for automatic metrics recording.
292    pub fn set_stats_manager(mut self, stats: Arc<StatsManager>) -> Self {
293        let mut ctx = (*self.ctx).clone();
294        ctx.set_stats_manager(stats);
295        self.ctx = Arc::new(ctx);
296        self
297    }
298
299    pub fn is_persistence_enabled(&self) -> bool {
300        self.ctx.is_persistence_enabled()
301    }
302
303    pub fn with_background_freeze(mut self) -> Self {
304        let freeze_config = self.ctx.get_freeze_config_full();
305        let manager = Arc::new(BackgroundFreezeManager::from_config(freeze_config));
306        let new_ctx = (*self.ctx)
307            .clone()
308            .with_background_freeze(Arc::clone(&manager));
309        self.ctx = Arc::new(new_ctx);
310        self
311    }
312
313    pub fn get_freeze_stats(&self) -> Option<FreezeStats> {
314        self.ctx.get_freeze_stats()
315    }
316
317    /// Return current and peak memory usage by storage ownership category.
318    pub fn resource_snapshot(&self) -> crate::ResourceSnapshot {
319        self.ctx.resource_snapshot()
320    }
321
322    /// Return WAL durability positions and sync counters when WAL is enabled.
323    pub fn wal_metrics(&self) -> Option<crate::WalMetrics> {
324        self.ctx.wal_metrics()
325    }
326
327    /// Check whether another active snapshot may be registered.
328    pub fn check_snapshot_admission(&self) -> StorageResult<()> {
329        self.ctx.check_snapshot_admission()
330    }
331
332    pub fn trigger_background_freeze(&self) -> StorageResult<()> {
333        self.ctx.trigger_background_freeze()
334    }
335
336    /// Run the background maintenance pass synchronously: automatic vertex
337    /// compaction (ID hole reclamation, governed by
338    /// `PropertyGraphConfig::auto_compact`) followed by delta freeze.
339    /// Mainly for tests and explicit operator invocation.
340    pub fn trigger_background_maintenance(&self) -> StorageResult<()> {
341        self.ctx.trigger_background_maintenance()
342    }
343
344    /// Remove old published checkpoints while retaining the newest recovery points.
345    pub fn cleanup_old_checkpoints(&self, max_checkpoints: usize) -> StorageResult<usize> {
346        let persistence = self
347            .ctx
348            .persistence()
349            .as_ref()
350            .ok_or_else(|| StorageError::not_supported("Persistence is not enabled"))?;
351        persistence.read().cleanup_old_checkpoints(max_checkpoints)
352    }
353
354    /// Split one persistent native index at an ordered-key boundary.
355    ///
356    /// The operation records a real MVCC snapshot and WAL start position,
357    /// builds the new shard layout, then reads the committed WAL intents after
358    /// the final publish barrier before installing the new generation.
359    pub fn split_native_index(
360        &self,
361        space: &str,
362        index_name: &str,
363        boundary: Vec<u8>,
364    ) -> StorageResult<()> {
365        let space_id = self.ctx.schema_manager().get_space_id(space)?;
366        let index = self
367            .ctx
368            .index_metadata_manager()
369            .get_tag_index(space_id, index_name)?
370            .or(self
371                .ctx
372                .index_metadata_manager()
373                .get_edge_index(space_id, index_name)?)
374            .ok_or_else(|| StorageError::not_found(format!("Index {index_name} not found")))?;
375        // Hold the rebuild gate from snapshot acquisition through publication.
376        // Index writers take its read side before resolving their active
377        // generation, so no writer can land in the old generation after this
378        // split snapshot.
379        let rebuild_gate = self.ctx.index_data_manager().read().rebuild_gate();
380        let _rebuild_guard = rebuild_gate.write();
381        let snapshot_timestamp = SnapshotTimestamp::new(self.ctx.get_read_timestamp().max(1));
382        let start_lsn = {
383            let current = index_manager::current_wal_lsn(&self.ctx);
384            if current == CommitLsn::ZERO {
385                CommitLsn::new(1)
386            } else {
387                current
388            }
389        };
390        let wal_context = Arc::clone(&self.ctx);
391        let wal_index = index.clone();
392        let result = self.ctx.index_data_manager().write().split_native_index(
393            IndexIdentity {
394                space_id,
395                index_id: index.id,
396            },
397            boundary,
398            snapshot_timestamp,
399            start_lsn,
400            {
401                let wal_context = Arc::clone(&wal_context);
402                move || {
403                    let current = index_manager::current_wal_lsn(&wal_context);
404                    Ok(if current < start_lsn {
405                        start_lsn
406                    } else {
407                        current
408                    })
409                }
410            },
411            move |from_lsn, to_lsn| {
412                index_manager::wal_intents_for_index(
413                    &wal_context,
414                    space_id,
415                    &wal_index,
416                    from_lsn,
417                    to_lsn,
418                )
419            },
420        );
421        if let Some(stats) = self.ctx.stats_manager() {
422            stats.record_split(result.is_ok());
423            if result.is_err() {
424                stats.record_fence_failure();
425            }
426        }
427        result
428    }
429
430    /// Split a native index at the beginning of one ordered property value.
431    pub fn split_native_index_at_value(
432        &self,
433        space: &str,
434        index_name: &str,
435        value: &Value,
436    ) -> StorageResult<()> {
437        let space_id = self.ctx.schema_manager().get_space_id(space)?;
438        let index = self
439            .ctx
440            .index_metadata_manager()
441            .get_tag_index(space_id, index_name)?
442            .or(self
443                .ctx
444                .index_metadata_manager()
445                .get_edge_index(space_id, index_name)?)
446            .ok_or_else(|| StorageError::not_found(format!("Index {index_name} not found")))?;
447        let boundary = match index.index_type {
448            linkrs_core::types::IndexType::TagIndex => {
449                KeyBuilder::build_vertex_index_value_prefix(space_id, index_name, value)?.0
450            }
451            linkrs_core::types::IndexType::EdgeIndex => {
452                KeyBuilder::build_edge_index_value_prefix(space_id, index_name, value)?.0
453            }
454        };
455        self.split_native_index(space, index_name, boundary)
456    }
457}
458
459impl crate::AutoCommitBatchOps for GraphStorage {
460    fn begin_auto_commit_batch(&self) -> StorageResult<Arc<context::AutoCommitBatchWindow>> {
461        self.ctx.begin_auto_commit_batch()
462    }
463
464    fn bind_auto_commit_statement(
465        &self,
466        window: &Arc<context::AutoCommitBatchWindow>,
467    ) -> StorageResult<Self> {
468        GraphStorage::bind_auto_commit_statement(self, window)
469    }
470
471    fn finalize_auto_commit_batch(
472        &self,
473        window: &context::AutoCommitBatchWindow,
474    ) -> StorageResult<()> {
475        GraphStorage::finalize_auto_commit_batch(self, window)
476    }
477
478    fn bind_auto_commit_writer(
479        &self,
480        window: &Arc<context::AutoCommitBatchWindow>,
481    ) -> StorageResult<Box<dyn crate::StorageWriter + '_>> {
482        let bound = window.bind_statement()?;
483        Ok(Box::new(GraphStorage {
484            ctx: Arc::new(bound),
485        }))
486    }
487}
488
489impl crate::AutoCommitGroupOps for GraphStorage {
490    fn begin_auto_commit_group(&self) -> StorageResult<Arc<context::AutoCommitBatchWindow>> {
491        self.ctx.begin_auto_commit_group()
492    }
493
494    fn finalize_auto_commit_group(
495        &self,
496        window: &context::AutoCommitBatchWindow,
497    ) -> StorageResult<()> {
498        window.finalize_group()
499    }
500
501    fn rollback_auto_commit_group(
502        &self,
503        window: &context::AutoCommitBatchWindow,
504    ) -> StorageResult<()> {
505        window.rollback_group()
506    }
507}
508
509impl StorageAuthOps for GraphStorage {
510    fn change_password(&mut self, info: &PasswordInfo) -> Result<bool, StorageError> {
511        self.ctx.check_write_admission()?;
512        ops::change_password(&self.ctx, info)
513    }
514
515    fn create_user(&mut self, info: &UserInfo) -> Result<bool, StorageError> {
516        self.ctx.check_write_admission()?;
517        ops::create_user(&self.ctx, info)
518    }
519
520    fn alter_user(&mut self, info: &UserAlterInfo) -> Result<bool, StorageError> {
521        self.ctx.check_write_admission()?;
522        ops::alter_user(&self.ctx, info)
523    }
524
525    fn drop_user(&mut self, username: &str) -> Result<bool, StorageError> {
526        self.ctx.check_write_admission()?;
527        ops::drop_user(&self.ctx, username)
528    }
529
530    fn user_exists(&self, username: &str) -> bool {
531        self.ctx.user_storage().user_exists(username)
532    }
533
534    fn list_users(&self) -> Vec<String> {
535        self.ctx.user_storage().list_users()
536    }
537
538    fn get_user(&self, username: &str) -> Option<UserInfo> {
539        self.ctx.user_storage().get_user(username)
540    }
541
542    fn update_last_login(&self, username: &str) -> Result<bool, StorageError> {
543        self.ctx.user_storage().update_last_login(username)
544    }
545
546    fn list_user_roles(&self, username: &str) -> Vec<(i64, RoleType)> {
547        self.ctx
548            .user_storage()
549            .get_user_roles(username)
550            .into_iter()
551            .collect()
552    }
553
554    fn list_all_user_roles(&self) -> Vec<(String, Vec<(i64, RoleType)>)> {
555        self.ctx
556            .user_storage()
557            .list_all_roles()
558            .into_iter()
559            .map(|(user, roles)| {
560                let mut entries: Vec<(i64, RoleType)> = roles.into_iter().collect();
561                entries.sort_by_key(|(space_id, _)| *space_id);
562                (user, entries)
563            })
564            .collect()
565    }
566
567    fn grant_role(
568        &mut self,
569        username: &str,
570        space_id: i64,
571        role: RoleType,
572    ) -> Result<bool, StorageError> {
573        self.ctx.check_write_admission()?;
574        ops::grant_role(&self.ctx, username, space_id, role)
575    }
576
577    fn revoke_role(&mut self, username: &str, space_id: i64) -> Result<bool, StorageError> {
578        self.ctx.check_write_admission()?;
579        ops::revoke_role(&self.ctx, username, space_id)
580    }
581}
582
583impl StorageAdmin for GraphStorage {
584    fn load_from_disk(&mut self) -> Result<(), StorageError> {
585        persistence::load_from_disk(&self.ctx)
586    }
587
588    fn save_to_disk(&self) -> Result<(), StorageError> {
589        persistence::save_to_disk(&self.ctx)
590    }
591
592    fn get_storage_stats(&self) -> StorageStats {
593        ops::get_storage_stats(&self.ctx)
594    }
595
596    fn find_dangling_edges(&self, space: &str) -> Result<Vec<Edge>, StorageError> {
597        ops::find_dangling_edges(&self.ctx, space)
598    }
599
600    fn repair_dangling_edges(&mut self, space: &str) -> Result<usize, StorageError> {
601        ops::repair_dangling_edges(&self.ctx, space)
602    }
603
604    fn purge_expired(
605        &self,
606        space: &str,
607        target: Option<crate::PurgeTarget<'_>>,
608    ) -> Result<Vec<crate::TtlPurgeEntry>, StorageError> {
609        GraphStorage::purge_expired(self, space, target)
610    }
611
612    fn get_db_path(&self) -> &str {
613        self.ctx.db_path()
614    }
615}
616
617impl StorageSchemaContextOps for GraphStorage {
618    fn get_schema_manager(&self) -> Option<Arc<SchemaManager>> {
619        Some(self.ctx.schema_manager().clone())
620    }
621
622    fn get_index_metadata_manager(&self) -> Option<Arc<dyn IndexMetadataManager>> {
623        Some(self.ctx.index_metadata_manager().clone())
624    }
625}
626
627impl StorageOperationContextOps for GraphStorage {
628    fn bind_auto_commit_context(&self) -> StorageResult<Self> {
629        Ok(Self {
630            ctx: Arc::new(self.ctx.with_auto_commit_context()?),
631        })
632    }
633
634    fn bind_read_operation_context(&self) -> StorageResult<Self> {
635        Ok(Self {
636            ctx: Arc::new(self.ctx.with_read_operation_context()?),
637        })
638    }
639
640    fn bind_operation_context(&self, context: StorageOperationContext) -> Self {
641        Self {
642            ctx: Arc::new(self.ctx.with_operation_context(context)),
643        }
644    }
645
646    fn operation_context(&self) -> Option<Arc<StorageOperationContext>> {
647        self.ctx.operation_context()
648    }
649
650    fn finalize_operation(&self, committed: bool) -> linkrs_core::StorageResult<()> {
651        self.ctx.finalize_operation(committed)
652    }
653}
654
655impl StorageSyncContextOps for GraphStorage {
656    fn get_sync_manager(&self) -> Option<Arc<linkrs_sync::SyncManager>> {
657        None
658    }
659}
660
661impl StorageRecoveryOps for GraphStorage {
662    fn needs_recovery(&self) -> bool {
663        persistence::needs_recovery(&self.ctx)
664    }
665
666    fn recover_from_wal(&self) -> StorageResult<linkrs_transaction::wal::recovery::RecoveryStats> {
667        persistence::recover_from_wal(&self.ctx)
668    }
669
670    fn recover_from_wal_with_config(
671        &self,
672        config: linkrs_transaction::wal::recovery::RecoveryConfig,
673    ) -> StorageResult<linkrs_transaction::wal::recovery::RecoveryStats> {
674        persistence::recover_from_wal_with_config(&self.ctx, config)
675    }
676
677    fn init_with_recovery(
678        &self,
679    ) -> StorageResult<Option<linkrs_transaction::wal::recovery::RecoveryStats>> {
680        persistence::initialize_with_recovery(&self.ctx)
681    }
682}
683
684impl StorageGcOps for GraphStorage {
685    fn is_index_gc_running(&self) -> bool {
686        self.ctx.is_index_gc_running()
687    }
688
689    fn start_index_gc(&self) -> Option<crate::thread_pool::BackgroundTaskHandle> {
690        self.ctx.start_index_gc()
691    }
692
693    fn stop_index_gc(&self) {
694        self.ctx.stop_index_gc();
695    }
696}
697
698impl StorageTtlPurgeOps for GraphStorage {
699    fn start_ttl_purge(&self, interval: std::time::Duration) {
700        self.ctx.start_ttl_purge(interval);
701    }
702
703    fn stop_ttl_purge(&self) {
704        self.ctx.stop_ttl_purge();
705    }
706
707    fn is_ttl_purge_running(&self) -> bool {
708        self.ctx.is_ttl_purge_running()
709    }
710}
711
712/// Direct vertex GC controls (outside StorageGcOps trait).
713impl GraphStorage {
714    pub fn is_vertex_gc_running(&self) -> bool {
715        self.ctx.is_vertex_gc_running()
716    }
717
718    pub fn start_vertex_gc(&self) -> Option<crate::thread_pool::BackgroundTaskHandle> {
719        self.ctx.start_vertex_gc()
720    }
721
722    pub fn stop_vertex_gc(&self) {
723        self.ctx.stop_vertex_gc();
724    }
725
726    /// Register a schema-change observer (single shared registry:
727    /// one subscription receives both table/space DDL and index DDL).
728    pub fn register_schema_callback(
729        &self,
730        callback: linkrs_core::metadata::SchemaChangeCallback,
731    ) -> linkrs_core::event_dispatch::SubscriptionId {
732        self.ctx.register_schema_callback(callback)
733    }
734
735    /// Remove a previously registered schema-change observer.
736    pub fn unregister_schema_callback(
737        &self,
738        id: linkrs_core::event_dispatch::SubscriptionId,
739    ) -> bool {
740        self.ctx.unregister_schema_callback(id)
741    }
742
743    /// Number of registered schema-change observers (diagnostics).
744    pub fn schema_callback_count(&self) -> usize {
745        self.ctx.schema_callback_count()
746    }
747
748    /// Shared schema-event registry for the central `HookBus`.
749    pub fn shared_schema_callbacks(
750        &self,
751    ) -> Arc<
752        linkrs_core::event_dispatch::EventSubscriptions<linkrs_core::metadata::SchemaChangeEvent>,
753    > {
754        self.ctx.shared_schema_callbacks()
755    }
756
757    /// Register a storage-lifecycle observer on the persistence coordinator.
758    ///
759    /// Returns `None` when no persistence coordinator is attached (e.g.
760    /// pure in-memory mode); otherwise returns the subscription id, which
761    /// can be passed to `unregister_storage_callback`.
762    pub fn register_storage_callback(
763        &self,
764        callback: crate::engine::persistence_coordinator::StorageEventCallback,
765    ) -> Option<linkrs_core::event_dispatch::SubscriptionId> {
766        self.ctx
767            .persistence()
768            .as_ref()
769            .map(|persistence| persistence.read().register_storage_callback(callback))
770    }
771
772    /// Register a filtered storage-lifecycle observer (only invoked when
773    /// `filter` returns true). Returns `None` without a coordinator.
774    pub fn register_storage_callback_filtered(
775        &self,
776        callback: crate::engine::persistence_coordinator::StorageEventCallback,
777        filter: linkrs_core::event_dispatch::EventFilter<
778            crate::engine::persistence_coordinator::StorageEvent,
779        >,
780    ) -> Option<linkrs_core::event_dispatch::SubscriptionId> {
781        self.ctx.persistence().as_ref().map(|persistence| {
782            persistence
783                .read()
784                .register_storage_callback_filtered(callback, filter)
785        })
786    }
787
788    /// Remove a previously registered storage-lifecycle observer.
789    /// Returns false when unknown or without a coordinator.
790    pub fn unregister_storage_callback(
791        &self,
792        id: linkrs_core::event_dispatch::SubscriptionId,
793    ) -> bool {
794        if let Some(persistence) = self.ctx.persistence() {
795            persistence.read().unregister_storage_callback(id)
796        } else {
797            false
798        }
799    }
800
801    /// Number of registered storage-lifecycle observers (diagnostics).
802    pub fn storage_callback_count(&self) -> usize {
803        if let Some(persistence) = self.ctx.persistence() {
804            persistence.read().storage_callback_count()
805        } else {
806            0
807        }
808    }
809
810    /// Shared storage-event registry for the central `HookBus`.
811    /// Returns `None` without an attached persistence coordinator.
812    pub fn shared_storage_callbacks(
813        &self,
814    ) -> Option<
815        Arc<
816            linkrs_core::event_dispatch::EventSubscriptions<
817                crate::engine::persistence_coordinator::StorageEvent,
818            >,
819        >,
820    > {
821        self.ctx
822            .persistence()
823            .as_ref()
824            .map(|persistence| persistence.read().shared_storage_callbacks())
825    }
826
827    /// Batch delete vertices by external string IDs.
828    pub fn batch_delete_vertices(
829        &self,
830        label: linkrs_core::types::LabelId,
831        external_ids: &[&str],
832        ts: linkrs_core::types::Timestamp,
833    ) -> linkrs_core::StorageResult<usize> {
834        self.ctx.batch_delete_vertices(label, external_ids, ts)
835    }
836
837    /// Batch delete vertices by external i64 IDs.
838    pub fn batch_delete_vertices_by_i64(
839        &self,
840        label: linkrs_core::types::LabelId,
841        external_ids: &[i64],
842        ts: linkrs_core::types::Timestamp,
843    ) -> linkrs_core::StorageResult<usize> {
844        self.ctx
845            .batch_delete_vertices_by_i64(label, external_ids, ts)
846    }
847
848    /// Batch-delete multiple vertices together with all their incident edges.
849    pub fn batch_delete_vertices_with_edges(
850        &self,
851        space: &str,
852        tag: &str,
853        ids: &[VertexId],
854    ) -> linkrs_core::StorageResult<usize> {
855        self.ctx.check_write_admission()?;
856        crate::engine::graph_storage::writer::batch_delete_vertices_with_edges(
857            &self.ctx, space, tag, ids,
858        )
859    }
860}
861
862impl crate::client::StorageSnapshotOps for GraphStorage {
863    fn get_freeze_stats(&self) -> Option<FreezeStats> {
864        self.ctx.get_freeze_stats()
865    }
866
867    fn trigger_background_freeze(&self) -> StorageResult<()> {
868        self.ctx.trigger_background_freeze()
869    }
870}
871
872impl linkrs_transaction::UndoTarget for GraphStorage {
873    fn delete_vertex_type(
874        &self,
875        label: linkrs_core::types::LabelId,
876    ) -> linkrs_transaction::undo_log::UndoLogResult<()> {
877        linkrs_core::types::UndoTarget::delete_vertex_type(&*self.ctx, label)
878    }
879
880    fn delete_edge_type(
881        &self,
882        edge_key: linkrs_core::types::EdgeKey,
883    ) -> linkrs_transaction::undo_log::UndoLogResult<()> {
884        linkrs_core::types::UndoTarget::delete_edge_type(&*self.ctx, edge_key)
885    }
886
887    fn delete_vertex(
888        &self,
889        vertex: linkrs_core::types::VertexIdentifier,
890        ts: linkrs_transaction::wal::Timestamp,
891    ) -> linkrs_transaction::undo_log::UndoLogResult<()> {
892        linkrs_core::types::UndoTarget::delete_vertex(&*self.ctx, vertex, ts)
893    }
894
895    fn delete_edge(
896        &self,
897        edge_ctx: linkrs_core::types::EdgeDeletionContext,
898    ) -> linkrs_transaction::undo_log::UndoLogResult<()> {
899        linkrs_core::types::UndoTarget::delete_edge(&*self.ctx, edge_ctx)
900    }
901
902    fn restore_edge(
903        &self,
904        edge: linkrs_core::types::EdgeIdentifier,
905        properties: Vec<(std::sync::Arc<str>, linkrs_core::Value)>,
906        ts: linkrs_transaction::wal::Timestamp,
907    ) -> linkrs_transaction::undo_log::UndoLogResult<()> {
908        linkrs_core::types::UndoTarget::restore_edge(&*self.ctx, edge, properties, ts)
909    }
910
911    fn undo_update_edge_property(
912        &self,
913        edge_id: linkrs_core::types::EdgeIdentifier,
914        col_id: linkrs_core::types::ColumnId,
915        value: linkrs_core::Value,
916        ts: linkrs_transaction::wal::Timestamp,
917    ) -> linkrs_transaction::undo_log::UndoLogResult<()> {
918        linkrs_core::types::UndoTarget::undo_update_edge_property(
919            &*self.ctx, edge_id, col_id, value, ts,
920        )
921    }
922
923    fn revert_delete_edge(
924        &self,
925        edge_ctx: linkrs_core::types::EdgeDeletionContext,
926    ) -> linkrs_transaction::undo_log::UndoLogResult<()> {
927        linkrs_core::types::UndoTarget::revert_delete_edge(&*self.ctx, edge_ctx)
928    }
929
930    fn revert_delete_vertex_properties(
931        &self,
932        label_name: &str,
933        prop_names: &[Arc<str>],
934    ) -> linkrs_transaction::undo_log::UndoLogResult<()> {
935        linkrs_core::types::UndoTarget::revert_delete_vertex_properties(
936            &*self.ctx, label_name, prop_names,
937        )
938    }
939
940    fn revert_delete_edge_properties(
941        &self,
942        src_label: &str,
943        dst_label: &str,
944        edge_label: &str,
945        prop_names: &[Arc<str>],
946    ) -> linkrs_transaction::undo_log::UndoLogResult<()> {
947        linkrs_core::types::UndoTarget::revert_delete_edge_properties(
948            &*self.ctx, src_label, dst_label, edge_label, prop_names,
949        )
950    }
951
952    fn revert_delete_vertex_label(
953        &self,
954        label_name: &str,
955    ) -> linkrs_transaction::undo_log::UndoLogResult<()> {
956        linkrs_core::types::UndoTarget::revert_delete_vertex_label(&*self.ctx, label_name)
957    }
958
959    fn revert_delete_edge_label(
960        &self,
961        src_label: &str,
962        dst_label: &str,
963        edge_label: &str,
964    ) -> linkrs_transaction::undo_log::UndoLogResult<()> {
965        linkrs_core::types::UndoTarget::revert_delete_edge_label(
966            &*self.ctx, src_label, dst_label, edge_label,
967        )
968    }
969
970    fn revert_rename_vertex_properties(
971        &self,
972        label: &str,
973        current_names: &[Arc<str>],
974        original_names: &[Arc<str>],
975    ) -> linkrs_transaction::undo_log::UndoLogResult<()> {
976        linkrs_core::types::UndoTarget::revert_rename_vertex_properties(
977            &*self.ctx,
978            label,
979            current_names,
980            original_names,
981        )
982    }
983
984    fn revert_rename_edge_properties(
985        &self,
986        src_label: &str,
987        dst_label: &str,
988        edge_label: &str,
989        current_names: &[Arc<str>],
990        original_names: &[Arc<str>],
991    ) -> linkrs_transaction::undo_log::UndoLogResult<()> {
992        linkrs_core::types::UndoTarget::revert_rename_edge_properties(
993            &*self.ctx,
994            src_label,
995            dst_label,
996            edge_label,
997            current_names,
998            original_names,
999        )
1000    }
1001
1002    fn staged_write_mark(
1003        &self,
1004        txn_id: linkrs_core::types::TransactionId,
1005    ) -> Option<linkrs_core::types::StagedWriteMark> {
1006        linkrs_core::types::UndoTarget::staged_write_mark(&*self.ctx, txn_id)
1007    }
1008
1009    fn rollback_staged_writes(
1010        &self,
1011        txn_id: linkrs_core::types::TransactionId,
1012        mark: linkrs_core::types::StagedWriteMark,
1013    ) -> linkrs_transaction::undo_log::UndoLogResult<()> {
1014        linkrs_core::types::UndoTarget::rollback_staged_writes(&*self.ctx, txn_id, mark)
1015    }
1016}
1017
1018#[cfg(test)]
1019mod tests;