Skip to main content

linkrs_storage/client/
query_storage.rs

1use super::import_export::{format_csv_value, import_space_impl};
2use super::{CatalogStore, GraphStore, StorageAdmin, StorageAuthOps, StoragePersistenceOps};
3use crate::stats_reader::ColumnStatsReader;
4use crate::{AutoCommitBatchOps, AutoCommitGroupOps, SnapshotHandle};
5use linkrs_core::StorageError;
6use std::io::Write;
7
8/// Minimal combined capability required by the query crate.
9pub trait QueryStorage:
10    GraphStore
11    + CatalogStore
12    + StorageAuthOps
13    + StorageAdmin
14    + StoragePersistenceOps
15    + ColumnStatsReader
16    + AutoCommitBatchOps
17    + AutoCommitGroupOps
18{
19    /// Snapshot handle bound to this storage handle, when the handle is
20    /// bound to an operation context with a pinned read/write snapshot.
21    ///
22    /// Always reports the operation context's read timestamp: storage reads
23    /// execute at the read timestamp, so reporting it tells the truth about
24    /// the observed snapshot. (Write contexts additionally pin a write
25    /// timestamp for visibility ordering, but reads never execute at it;
26    /// reporting it here made read-committed statements look inconsistent
27    /// after a statement refresh moved the read stamp past the write stamp.)
28    /// Unbound handles (raw global storage) return `None`. This lets the
29    /// query layer observe which snapshot a per-query bound handle reads at,
30    /// without reaching into the storage internals.
31    ///
32    /// Snapshot truth lives in the global tracker, so the handle is always
33    /// synthesized from the pinned timestamp (`id = 0`).
34    fn snapshot_handle(&self) -> Option<SnapshotHandle> {
35        let context = self.operation_context()?;
36        Some(SnapshotHandle::new(context.read_timestamp, 0))
37    }
38
39    /// Export the given space to CSV files under `path/<space_name>/`.
40    ///
41    /// Each tag produces a `<tag>.csv` file; each edge type produces a
42    /// `<edge_type>.csv` file.  A `schema.json` metadata file records the
43    /// space, tags, and edge types with their property schemas.
44    fn export_space(&self, space: &str, path: &std::path::Path) -> Result<(), StorageError> {
45        let base = path.join(space);
46        std::fs::create_dir_all(&base)
47            .map_err(|e| StorageError::io_error(format!("Failed to create export dir: {e}")))?;
48
49        // Export schema metadata
50        let tags = self.list_tags(space)?;
51        let edge_types = self.list_edge_types(space)?;
52
53        let schema_meta = serde_json::json!({
54            "space": space,
55            "tags": tags.iter().map(|t| serde_json::json!({
56                "name": t.tag_name,
57                "properties": t.properties.iter().map(|p| serde_json::json!({
58                    "name": p.name,
59                    "type": p.data_type.to_string(),
60                })).collect::<Vec<_>>(),
61            })).collect::<Vec<_>>(),
62            "edge_types": edge_types.iter().map(|e| serde_json::json!({
63                "name": e.edge_type_name,
64                "src_tag": e.src_tag_name,
65                "dst_tag": e.dst_tag_name,
66                "properties": e.properties.iter().map(|p| serde_json::json!({
67                    "name": p.name,
68                    "type": p.data_type.to_string(),
69                })).collect::<Vec<_>>(),
70            })).collect::<Vec<_>>(),
71        });
72
73        let schema_path = base.join("schema.json");
74        let mut schema_file = std::fs::File::create(&schema_path)
75            .map_err(|e| StorageError::io_error(format!("Failed to create schema.json: {e}")))?;
76        schema_file
77            .write_all(
78                serde_json::to_string_pretty(&schema_meta)
79                    .unwrap_or_default()
80                    .as_bytes(),
81            )
82            .map_err(|e| StorageError::io_error(format!("Failed to write schema.json: {e}")))?;
83
84        // Export vertices by tag
85        for tag_info in &tags {
86            let vertices = self.scan_vertices_by_tag(space, &tag_info.tag_name)?;
87            if vertices.is_empty() {
88                continue;
89            }
90
91            // Collect all property keys across all vertices from their tags.
92            let mut prop_keys: Vec<std::sync::Arc<str>> = vertices
93                .iter()
94                .flat_map(|v| v.tag.properties.keys())
95                .cloned()
96                .collect::<std::collections::HashSet<_>>()
97                .into_iter()
98                .collect();
99            prop_keys.sort();
100
101            let csv_path = base.join(format!("{}.csv", tag_info.tag_name));
102            let mut file = std::fs::File::create(&csv_path).map_err(|e| {
103                StorageError::io_error(format!("Failed to create {}.csv: {e}", tag_info.tag_name))
104            })?;
105
106            // Write header
107            let mut header = vec!["vid".to_string()];
108            header.extend(prop_keys.iter().map(|k| k.to_string()));
109            writeln!(file, "{}", header.join(","))
110                .map_err(|e| StorageError::io_error(format!("CSV write error: {e}")))?;
111
112            // Write rows
113            for vertex in &vertices {
114                let mut row = vec![vertex.vid.to_string()];
115                for key in &prop_keys {
116                    let val = vertex
117                        .property_value(key)
118                        .map(|value| format_csv_value(&value))
119                        .unwrap_or_default();
120                    row.push(val);
121                }
122                writeln!(file, "{}", row.join(","))
123                    .map_err(|e| StorageError::io_error(format!("CSV write error: {e}")))?;
124            }
125        }
126
127        // Export edges by type
128        for edge_info in &edge_types {
129            let edges = self.scan_edges_by_type(space, &edge_info.edge_type_name)?;
130            if edges.is_empty() {
131                continue;
132            }
133
134            let mut prop_keys: Vec<std::sync::Arc<str>> = edges
135                .iter()
136                .flat_map(|e| e.props.keys())
137                .cloned()
138                .collect::<std::collections::HashSet<_>>()
139                .into_iter()
140                .collect();
141            prop_keys.sort();
142
143            let csv_path = base.join(format!("{}.csv", edge_info.edge_type_name));
144            let mut file = std::fs::File::create(&csv_path).map_err(|e| {
145                StorageError::io_error(format!(
146                    "Failed to create {}.csv: {e}",
147                    edge_info.edge_type_name
148                ))
149            })?;
150
151            let mut header = vec!["src".to_string(), "dst".to_string(), "ranking".to_string()];
152            header.extend(prop_keys.iter().map(|k| k.to_string()));
153            writeln!(file, "{}", header.join(","))
154                .map_err(|e| StorageError::io_error(format!("CSV write error: {e}")))?;
155
156            for edge in &edges {
157                let mut row = vec![
158                    edge.src.to_string(),
159                    edge.dst.to_string(),
160                    edge.ranking.to_string(),
161                ];
162                for key in &prop_keys {
163                    let val = edge
164                        .props
165                        .get(key.as_ref())
166                        .map(format_csv_value)
167                        .unwrap_or_default();
168                    row.push(val);
169                }
170                writeln!(file, "{}", row.join(","))
171                    .map_err(|e| StorageError::io_error(format!("CSV write error: {e}")))?;
172            }
173        }
174
175        Ok(())
176    }
177
178    /// Import a space from CSV files under `path/<space_name>/`.
179    ///
180    /// Expects a `schema.json` metadata file and `<tag>.csv` / `<edge_type>.csv` data files.
181    fn import_space(&mut self, space: &str, path: &std::path::Path) -> Result<(), StorageError> {
182        import_space_impl(self, space, path)
183    }
184}
185
186impl<T> QueryStorage for T where
187    T: GraphStore
188        + CatalogStore
189        + StorageAuthOps
190        + StorageAdmin
191        + StoragePersistenceOps
192        + ColumnStatsReader
193        + AutoCommitBatchOps
194        + AutoCommitGroupOps
195{
196}