Skip to main content

linkrs_api/api_core/
bulk_insert.rs

1//! Bulk Insert API - Core Layer
2//!
3//! Provides transport layer-independent bulk insert capabilities.
4//! The buffering operation is shared by the embedded layer and the network
5//! service layer.
6//!
7//! Contract: this is a data-level write path. Items go straight to
8//! `storage.batch_insert_vertices` / `batch_insert_edges`, bypassing the
9//! statement pipeline and the `TransactionManager`; secondary-index
10//! synchronization is handled by the storage SyncWrapper. Statement-level
11//! batch execution belongs to `QueryApi::execute_batch` and must not be
12//! mixed with this path.
13
14use crate::CoreResult;
15use linkrs_core::{Edge, Vertex};
16use linkrs_storage::StorageClient;
17
18/// Bulk insert configuration
19#[derive(Debug, Clone)]
20pub struct BulkInsertConfig {
21    /// Number of buffered items before an automatic flush
22    pub batch_size: usize,
23    /// Whether to flush automatically when the buffer is full
24    pub auto_flush: bool,
25    /// Whether to continue after a failed chunk
26    pub continue_on_error: bool,
27    /// Maximum number of recorded errors (None means unlimited)
28    pub max_errors: Option<usize>,
29}
30
31impl Default for BulkInsertConfig {
32    fn default() -> Self {
33        Self {
34            batch_size: 1000,
35            auto_flush: true,
36            continue_on_error: true,
37            max_errors: Some(100),
38        }
39    }
40}
41
42impl BulkInsertConfig {
43    /// Create default configuration
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Set the buffer size that triggers a flush
49    pub fn with_batch_size(mut self, size: usize) -> Self {
50        self.batch_size = size.max(1);
51        self
52    }
53
54    /// Set automatic flushing
55    pub fn with_auto_flush(mut self, auto_flush: bool) -> Self {
56        self.auto_flush = auto_flush;
57        self
58    }
59
60    /// Set continue-on-error
61    pub fn with_continue_on_error(mut self, continue_on_error: bool) -> Self {
62        self.continue_on_error = continue_on_error;
63        self
64    }
65
66    /// Set the maximum number of recorded errors
67    pub fn with_max_errors(mut self, max_errors: Option<usize>) -> Self {
68        self.max_errors = max_errors;
69        self
70    }
71}
72
73/// Item buffered for one bulk insert
74#[derive(Debug, Clone)]
75pub enum BulkInsertItem {
76    /// Vertex to insert
77    Vertex(Vertex),
78    /// Edge to insert
79    Edge(Edge),
80}
81
82/// Bulk insert result
83#[derive(Debug, Clone, Default)]
84pub struct BulkInsertResult {
85    /// Number of vertices inserted
86    pub vertices_inserted: usize,
87    /// Number of edges inserted
88    pub edges_inserted: usize,
89    /// Number of failed chunks
90    pub failed_count: usize,
91    /// Errors recorded for failed chunks
92    pub errors: Vec<BulkInsertError>,
93}
94
95/// Error recorded for one failed chunk of a bulk insert
96#[derive(Debug, Clone)]
97pub struct BulkInsertError {
98    /// Chunk index inside the bulk insert
99    pub index: usize,
100    /// Chunk item type
101    pub item_type: BulkInsertItemType,
102    /// Error message
103    pub message: String,
104}
105
106/// Chunk item type of a bulk insert
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum BulkInsertItemType {
109    /// Vertex chunk
110    Vertex,
111    /// Edge chunk
112    Edge,
113}
114
115/// Core bulk insert operation
116///
117/// Holds the buffered items and maps them onto the storage batch insert
118/// entry points. Used by both the embedded layer and the network service
119/// layer.
120#[derive(Debug)]
121pub struct BulkInsertOperation {
122    items: Vec<BulkInsertItem>,
123    config: BulkInsertConfig,
124}
125
126impl BulkInsertOperation {
127    /// Create a new bulk insert operation
128    pub fn new(config: BulkInsertConfig) -> Self {
129        Self {
130            items: Vec::with_capacity(config.batch_size),
131            config,
132        }
133    }
134
135    /// Buffer a vertex
136    pub fn add_vertex(&mut self, vertex: Vertex) {
137        self.items.push(BulkInsertItem::Vertex(vertex));
138    }
139
140    /// Buffer an edge
141    pub fn add_edge(&mut self, edge: Edge) {
142        self.items.push(BulkInsertItem::Edge(edge));
143    }
144
145    /// Buffer multiple items
146    pub fn add_items(&mut self, items: Vec<BulkInsertItem>) {
147        self.items.extend(items);
148    }
149
150    /// Number of buffered items
151    pub fn len(&self) -> usize {
152        self.items.len()
153    }
154
155    /// Whether the buffer is empty
156    pub fn is_empty(&self) -> bool {
157        self.items.is_empty()
158    }
159
160    /// Whether the buffer reached the configured flush size
161    pub fn should_flush(&self) -> bool {
162        self.config.auto_flush && self.items.len() >= self.config.batch_size
163    }
164
165    /// Configured flush size
166    pub fn batch_size(&self) -> usize {
167        self.config.batch_size
168    }
169
170    /// Drop all buffered items
171    pub fn clear(&mut self) {
172        self.items.clear();
173    }
174
175    /// Take all buffered items, leaving the buffer empty
176    pub fn take_items(&mut self) -> Vec<BulkInsertItem> {
177        std::mem::take(&mut self.items)
178    }
179
180    /// Insert every buffered item through one storage call per item kind
181    ///
182    /// # Parameters
183    /// - `storage`: storage client
184    /// - `space_name`: graph space name
185    ///
186    /// # Returns
187    /// Bulk insert result; per-chunk failures are recorded in the result
188    /// instead of failing the whole call.
189    pub fn execute_sync<S: StorageClient>(
190        &mut self,
191        storage: &mut S,
192        space_name: &str,
193    ) -> CoreResult<BulkInsertResult> {
194        let items = self.take_items();
195        Self::execute_items_sync(storage, space_name, items, &self.config)
196    }
197
198    fn execute_items_sync<S: StorageClient>(
199        storage: &mut S,
200        space_name: &str,
201        items: Vec<BulkInsertItem>,
202        config: &BulkInsertConfig,
203    ) -> CoreResult<BulkInsertResult> {
204        let mut result = BulkInsertResult::default();
205        let mut vertices = Vec::new();
206        let mut edges = Vec::new();
207
208        for item in items {
209            match item {
210                BulkInsertItem::Vertex(v) => vertices.push(v),
211                BulkInsertItem::Edge(e) => edges.push(e),
212            }
213        }
214
215        if !vertices.is_empty() {
216            let vertex_count = vertices.len();
217            match storage.batch_insert_vertices(space_name, vertices) {
218                Ok(_) => {
219                    result.vertices_inserted = vertex_count;
220                }
221                Err(e) => {
222                    let error = BulkInsertError {
223                        index: 0,
224                        item_type: BulkInsertItemType::Vertex,
225                        message: format!("Failed to insert vertices: {}", e),
226                    };
227                    result.errors.push(error);
228                    result.failed_count += 1;
229                    if !config.continue_on_error {
230                        return Ok(result);
231                    }
232                }
233            }
234        }
235
236        if !edges.is_empty() {
237            let edge_count = edges.len();
238            match storage.batch_insert_edges(space_name, edges) {
239                Ok(()) => {
240                    result.edges_inserted = edge_count;
241                }
242                Err(e) => {
243                    let error = BulkInsertError {
244                        index: 0,
245                        item_type: BulkInsertItemType::Edge,
246                        message: format!("Failed to insert edges: {}", e),
247                    };
248                    result.errors.push(error);
249                    result.failed_count += 1;
250                }
251            }
252        }
253
254        Ok(result)
255    }
256}
257
258/// Builder for [`BulkInsertOperation`]
259#[derive(Debug)]
260pub struct BulkInsertOperationBuilder {
261    config: BulkInsertConfig,
262}
263
264impl BulkInsertOperationBuilder {
265    /// Create a new builder
266    pub fn new() -> Self {
267        Self {
268            config: BulkInsertConfig::default(),
269        }
270    }
271
272    /// Set the buffer size that triggers a flush
273    pub fn batch_size(mut self, size: usize) -> Self {
274        self.config.batch_size = size;
275        self
276    }
277
278    /// Set automatic flushing
279    pub fn auto_flush(mut self, auto_flush: bool) -> Self {
280        self.config.auto_flush = auto_flush;
281        self
282    }
283
284    /// Set continue-on-error
285    pub fn continue_on_error(mut self, continue_on_error: bool) -> Self {
286        self.config.continue_on_error = continue_on_error;
287        self
288    }
289
290    /// Build the bulk insert operation
291    pub fn build(self) -> BulkInsertOperation {
292        BulkInsertOperation::new(self.config)
293    }
294}
295
296impl Default for BulkInsertOperationBuilder {
297    fn default() -> Self {
298        Self::new()
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn bulk_insert_config_default() {
308        let config = BulkInsertConfig::default();
309        assert_eq!(config.batch_size, 1000);
310        assert!(config.auto_flush);
311        assert!(config.continue_on_error);
312    }
313
314    #[test]
315    fn bulk_insert_config_builder() {
316        let config = BulkInsertConfig::new()
317            .with_batch_size(500)
318            .with_auto_flush(false)
319            .with_continue_on_error(true);
320
321        assert_eq!(config.batch_size, 500);
322        assert!(!config.auto_flush);
323        assert!(config.continue_on_error);
324    }
325
326    #[test]
327    fn bulk_insert_operation_add_items() {
328        let mut operation = BulkInsertOperation::new(BulkInsertConfig::default());
329
330        let vertex = Vertex::new(
331            linkrs_core::types::VertexId::try_from_int64(1).expect("test vertex id"),
332            linkrs_core::Tag::new("test".into(), std::collections::HashMap::new()),
333        );
334        operation.add_vertex(vertex);
335
336        assert_eq!(operation.len(), 1);
337        assert!(!operation.is_empty());
338    }
339
340    #[test]
341    fn bulk_insert_operation_should_flush() {
342        let config = BulkInsertConfig::new().with_batch_size(2);
343        let mut operation = BulkInsertOperation::new(config);
344
345        assert!(!operation.should_flush());
346
347        let vertex = Vertex::new(
348            linkrs_core::types::VertexId::try_from_int64(1).expect("test vertex id"),
349            linkrs_core::Tag::new("test".into(), std::collections::HashMap::new()),
350        );
351        operation.add_vertex(vertex.clone());
352        operation.add_vertex(vertex);
353
354        assert!(operation.should_flush());
355    }
356}