Skip to main content

linkrs_wire/
batch.rs

1//! Batch import contract DTOs (HTTP `/batch` endpoints).
2//!
3//! Previously mirrored between `api/server/batch/types.rs` and
4//! `cli/client/{batch,request_types,response_types}.rs`. `BatchItem` uses
5//! internally-tagged representation (`{"type": "vertex", ...}`), the shape
6//! the CLI already emitted.
7
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::fmt;
11
12/// Batch Task ID
13pub type BatchId = String;
14
15/// Batch Task Status
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
17#[serde(rename_all = "snake_case")]
18pub enum BatchStatus {
19    /// Created
20    Created,
21    /// Running
22    Running,
23    /// Completed
24    Completed,
25    /// Failed
26    Failed,
27    /// Cancelled
28    Cancelled,
29}
30
31impl fmt::Display for BatchStatus {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        let name = match self {
34            BatchStatus::Created => "Created",
35            BatchStatus::Running => "Running",
36            BatchStatus::Completed => "Completed",
37            BatchStatus::Failed => "Failed",
38            BatchStatus::Cancelled => "Cancelled",
39        };
40        write!(f, "{}", name)
41    }
42}
43
44/// Batch task type
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
46#[serde(rename_all = "snake_case")]
47pub enum BatchType {
48    /// Vertex batch insertion
49    Vertex,
50    /// Edge batch insertion
51    Edge,
52    /// Mixed batch insertion
53    Mixed,
54}
55
56impl fmt::Display for BatchType {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        let name = match self {
59            BatchType::Vertex => "vertex",
60            BatchType::Edge => "edge",
61            BatchType::Mixed => "mixed",
62        };
63        write!(f, "{}", name)
64    }
65}
66
67/// Batch item type
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
69#[serde(rename_all = "snake_case")]
70pub enum BatchItemType {
71    /// Vertex item
72    Vertex,
73    /// Edge item
74    Edge,
75    /// Vertex/edge update item
76    Update,
77    /// Vertex/edge delete item
78    Delete,
79}
80
81impl fmt::Display for BatchItemType {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        let name = match self {
84            BatchItemType::Vertex => "vertex",
85            BatchItemType::Edge => "edge",
86            BatchItemType::Update => "update",
87            BatchItemType::Delete => "delete",
88        };
89        write!(f, "{}", name)
90    }
91}
92
93/// Batch data
94#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
95#[serde(tag = "type")]
96pub enum BatchItem {
97    #[serde(rename = "vertex")]
98    Vertex(VertexData),
99    #[serde(rename = "edge")]
100    Edge(EdgeData),
101    #[serde(rename = "update_vertex")]
102    UpdateVertex(VertexData),
103    #[serde(rename = "update_edge")]
104    UpdateEdgeData(UpdateEdgeData),
105    #[serde(rename = "delete_vertex")]
106    DeleteVertex(DeleteVertexData),
107    #[serde(rename = "delete_edge")]
108    DeleteEdgeData(DeleteEdgeData),
109}
110
111/// Vertex data
112#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
113pub struct VertexData {
114    /// Vertex ID
115    pub vid: serde_json::Value,
116    /// Single label tag name (single-label vertices carry exactly one tag)
117    pub tag: String,
118    /// Properties
119    #[serde(default)]
120    pub properties: HashMap<String, serde_json::Value>,
121}
122
123/// Edge data
124#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
125pub struct EdgeData {
126    /// Edge type
127    pub edge_type: String,
128    /// Source vertex ID
129    pub src_vid: serde_json::Value,
130    /// Target vertex ID
131    pub dst_vid: serde_json::Value,
132    /// Properties
133    #[serde(default)]
134    pub properties: HashMap<String, serde_json::Value>,
135}
136
137/// Edge update data (edge identity includes rank, unlike inserts)
138#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
139pub struct UpdateEdgeData {
140    /// Edge type
141    pub edge_type: String,
142    /// Source vertex ID
143    pub src_vid: serde_json::Value,
144    /// Target vertex ID
145    pub dst_vid: serde_json::Value,
146    /// Edge rank within (src, dst, type)
147    #[serde(default)]
148    pub rank: i64,
149    /// Properties
150    #[serde(default)]
151    pub properties: HashMap<String, serde_json::Value>,
152}
153
154/// Vertex delete data
155#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
156pub struct DeleteVertexData {
157    /// Vertex ID
158    pub vid: serde_json::Value,
159    /// Tag names to delete the vertex from (incident edges are removed too)
160    #[serde(default)]
161    pub tag_names: Vec<String>,
162}
163
164/// Edge delete data
165#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
166pub struct DeleteEdgeData {
167    /// Edge type
168    pub edge_type: String,
169    /// Source vertex ID
170    pub src_vid: serde_json::Value,
171    /// Target vertex ID
172    pub dst_vid: serde_json::Value,
173    /// Edge rank within (src, dst, type)
174    #[serde(default)]
175    pub rank: i64,
176}
177
178/// Create batch task request
179#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
180pub struct CreateBatchRequest {
181    /// Space ID
182    pub space_id: u64,
183    /// Batch task type
184    pub batch_type: BatchType,
185    /// Batch size
186    #[serde(default = "default_batch_size")]
187    pub batch_size: usize,
188}
189
190fn default_batch_size() -> usize {
191    1000
192}
193
194/// Create batch task response
195#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
196pub struct CreateBatchResponse {
197    /// Batch task ID
198    pub batch_id: BatchId,
199    /// Task status
200    pub status: BatchStatus,
201    /// Creation time
202    pub created_at: String,
203}
204
205/// Add batch items request
206#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
207pub struct AddBatchItemsRequest {
208    /// Batch item list
209    pub items: Vec<BatchItem>,
210}
211
212/// Add batch items response
213#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
214pub struct AddBatchItemsResponse {
215    /// Number accepted
216    pub accepted: usize,
217    /// Number buffered
218    #[serde(default)]
219    pub buffered: usize,
220    /// Total number buffered
221    #[serde(default)]
222    pub total_buffered: usize,
223}
224
225/// Execute batch task response
226#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
227pub struct ExecuteBatchResponse {
228    /// Batch task ID
229    pub batch_id: BatchId,
230    /// Task status
231    pub status: BatchStatus,
232    /// Execution result
233    pub result: BatchResultData,
234    /// Completion time
235    #[serde(default)]
236    pub completed_at: Option<String>,
237}
238
239/// Batch results data
240#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
241pub struct BatchResultData {
242    /// Number of vertices inserted
243    pub vertices_inserted: usize,
244    /// Number of edges inserted
245    pub edges_inserted: usize,
246    /// Number of vertices updated
247    #[serde(default)]
248    pub vertices_updated: usize,
249    /// Number of edges updated
250    #[serde(default)]
251    pub edges_updated: usize,
252    /// Number of vertices deleted
253    #[serde(default)]
254    pub vertices_deleted: usize,
255    /// Number of edges deleted
256    #[serde(default)]
257    pub edges_deleted: usize,
258    /// Error message
259    #[serde(default, skip_serializing_if = "Vec::is_empty")]
260    pub errors: Vec<BatchErrorData>,
261}
262
263/// Batch error data
264#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
265pub struct BatchErrorData {
266    /// Index where the error occurred
267    pub index: usize,
268    /// Type of the item
269    pub item_type: BatchItemType,
270    /// Error message
271    pub error: String,
272}
273
274/// Batch task status response
275#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
276pub struct BatchStatusResponse {
277    /// Batch task ID
278    pub batch_id: BatchId,
279    /// Task status
280    pub status: BatchStatus,
281    /// Progress information
282    pub progress: BatchProgress,
283    /// Creation time
284    pub created_at: String,
285    /// Update time
286    pub updated_at: String,
287}
288
289/// Progress of batch tasks
290#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
291pub struct BatchProgress {
292    /// Total quantity
293    pub total: usize,
294    /// Number of items processed
295    pub processed: usize,
296    /// Number of successes
297    pub succeeded: usize,
298    /// Number of failures
299    pub failed: usize,
300    /// Number buffered
301    #[serde(default)]
302    pub buffered: usize,
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn batch_item_vertex_roundtrip() {
311        let item = BatchItem::Vertex(VertexData {
312            vid: serde_json::json!("v1"),
313            tag: "person".to_string(),
314            properties: HashMap::from([("name".to_string(), serde_json::json!("Alice"))]),
315        });
316        let json = serde_json::to_string(&item).unwrap();
317        assert!(json.contains(r#""type":"vertex""#), "json: {json}");
318        let back: BatchItem = serde_json::from_str(&json).unwrap();
319        match back {
320            BatchItem::Vertex(v) => {
321                assert_eq!(v.vid, serde_json::json!("v1"));
322                assert_eq!(v.tag, "person");
323                assert_eq!(v.properties.get("name"), Some(&serde_json::json!("Alice")));
324            }
325            other => panic!("expected vertex item, got {other:?}"),
326        }
327    }
328
329    #[test]
330    fn batch_item_edge_roundtrip() {
331        let item = BatchItem::Edge(EdgeData {
332            edge_type: "follows".to_string(),
333            src_vid: serde_json::json!("v1"),
334            dst_vid: serde_json::json!("v2"),
335            properties: HashMap::new(),
336        });
337        let json = serde_json::to_string(&item).unwrap();
338        let back: BatchItem = serde_json::from_str(&json).unwrap();
339        match back {
340            BatchItem::Edge(e) => assert_eq!(e.edge_type, "follows"),
341            other => panic!("expected edge item, got {other:?}"),
342        }
343    }
344
345    #[test]
346    fn batch_status_wire_names() {
347        assert_eq!(
348            serde_json::to_string(&BatchStatus::Completed).unwrap(),
349            r#""completed""#
350        );
351        assert_eq!(
352            serde_json::from_str::<BatchStatus>(r#""running""#).unwrap(),
353            BatchStatus::Running
354        );
355        assert_eq!(
356            serde_json::from_str::<BatchType>(r#""mixed""#).unwrap(),
357            BatchType::Mixed
358        );
359    }
360
361    #[test]
362    fn create_batch_request_roundtrip() {
363        let request = CreateBatchRequest {
364            space_id: 1,
365            batch_type: BatchType::Vertex,
366            batch_size: 100,
367        };
368        let json = serde_json::to_string(&request).unwrap();
369        let back: CreateBatchRequest = serde_json::from_str(&json).unwrap();
370        assert_eq!(back.space_id, 1);
371        assert_eq!(back.batch_type, BatchType::Vertex);
372        assert_eq!(back.batch_size, 100);
373    }
374
375    #[test]
376    fn execute_batch_response_roundtrip() {
377        let response = ExecuteBatchResponse {
378            batch_id: "b1".to_string(),
379            status: BatchStatus::Completed,
380            result: BatchResultData {
381                vertices_inserted: 2,
382                edges_inserted: 0,
383                vertices_updated: 0,
384                edges_updated: 0,
385                vertices_deleted: 0,
386                edges_deleted: 0,
387                errors: Vec::new(),
388            },
389            completed_at: Some("now".to_string()),
390        };
391        let json = serde_json::to_string(&response).unwrap();
392        let back: ExecuteBatchResponse = serde_json::from_str(&json).unwrap();
393        assert_eq!(back.result.vertices_inserted, 2);
394        assert_eq!(back.status, BatchStatus::Completed);
395    }
396}