Skip to main content

mixtape_tools/sqlite/query/
bulk_insert.rs

1//! Bulk insert tool
2
3use crate::prelude::*;
4use crate::sqlite::error::SqliteToolError;
5use crate::sqlite::manager::with_connection;
6use crate::sqlite::types::json_to_sql;
7
8/// Input for bulk insert operation
9#[derive(Debug, Deserialize, JsonSchema)]
10pub struct BulkInsertInput {
11    /// Table name to insert into
12    pub table: String,
13
14    /// Array of records to insert. Each record is an object with column names as keys.
15    pub data: Vec<serde_json::Map<String, serde_json::Value>>,
16
17    /// Number of records to insert per batch (default: 1000)
18    #[serde(default = "default_batch_size")]
19    pub batch_size: usize,
20
21    /// Database file path. If not specified, uses the default database.
22    #[serde(default)]
23    pub db_path: Option<String>,
24}
25
26fn default_batch_size() -> usize {
27    1000
28}
29
30/// Bulk insert result
31#[derive(Debug, Serialize, JsonSchema)]
32struct BulkInsertResult {
33    status: String,
34    total_inserted: usize,
35    batches: usize,
36}
37
38/// Tool for efficiently inserting multiple records (DESTRUCTIVE)
39///
40/// Inserts records in batches using transactions for efficiency.
41/// Each record is an object with column names as keys.
42pub struct BulkInsertTool;
43
44impl Tool for BulkInsertTool {
45    type Input = BulkInsertInput;
46
47    fn name(&self) -> &str {
48        "sqlite_bulk_insert"
49    }
50
51    fn description(&self) -> &str {
52        "Efficiently insert multiple records into a table using batched transactions. Each record is an object with column names as keys."
53    }
54
55    async fn execute(&self, input: Self::Input) -> Result<ToolResult, ToolError> {
56        if input.data.is_empty() {
57            return Ok(ToolResult::Json(serde_json::json!({
58                "status": "success",
59                "total_inserted": 0,
60                "batches": 0,
61                "message": "No data to insert"
62            })));
63        }
64
65        let table = input.table;
66        let data = input.data;
67        let batch_size = input.batch_size.max(1);
68
69        let result = with_connection(input.db_path, move |conn| {
70            // Get column names from first record
71            let columns: Vec<&String> = data[0].keys().collect();
72            if columns.is_empty() {
73                return Err(SqliteToolError::InvalidQuery(
74                    "Records must have at least one column".to_string(),
75                ));
76            }
77
78            // Build INSERT statement
79            let column_names = columns
80                .iter()
81                .map(|c| format!("\"{}\"", c))
82                .collect::<Vec<_>>()
83                .join(", ");
84            let placeholders = columns.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
85
86            let sql = format!(
87                "INSERT INTO \"{}\" ({}) VALUES ({})",
88                table, column_names, placeholders
89            );
90
91            let mut total_inserted = 0;
92            let mut batches = 0;
93
94            // Process in batches
95            for chunk in data.chunks(batch_size) {
96                conn.execute("BEGIN TRANSACTION", [])?;
97
98                for record in chunk {
99                    // Collect values in column order
100                    let values: Vec<Box<dyn rusqlite::ToSql>> = columns
101                        .iter()
102                        .map(|col| {
103                            let value = record.get(*col).unwrap_or(&serde_json::Value::Null);
104                            json_to_sql(value)
105                        })
106                        .collect();
107
108                    let params: Vec<&dyn rusqlite::ToSql> =
109                        values.iter().map(|b| b.as_ref()).collect();
110
111                    conn.execute(&sql, params.as_slice())?;
112                    total_inserted += 1;
113                }
114
115                conn.execute("COMMIT", [])?;
116                batches += 1;
117            }
118
119            Ok(BulkInsertResult {
120                status: "success".to_string(),
121                total_inserted,
122                batches,
123            })
124        })
125        .await?;
126
127        Ok(ToolResult::Json(serde_json::to_value(result)?))
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::sqlite::test_utils::{unwrap_json, TestDatabase};
135
136    #[tokio::test]
137    async fn test_bulk_insert() {
138        let db =
139            TestDatabase::with_schema("CREATE TABLE users (id INTEGER, name TEXT, age INTEGER);")
140                .await;
141
142        // Bulk insert
143        let tool = BulkInsertTool;
144        let mut data = Vec::new();
145        for i in 0..100 {
146            let mut record = serde_json::Map::new();
147            record.insert("id".to_string(), serde_json::json!(i));
148            record.insert("name".to_string(), serde_json::json!(format!("User {}", i)));
149            record.insert("age".to_string(), serde_json::json!(20 + (i % 50)));
150            data.push(record);
151        }
152
153        let input = BulkInsertInput {
154            table: "users".to_string(),
155            data,
156            batch_size: 25,
157            db_path: Some(db.key()),
158        };
159
160        let result = tool.execute(input).await.unwrap();
161        let json = unwrap_json(result);
162
163        assert_eq!(json["total_inserted"].as_i64().unwrap(), 100);
164        assert_eq!(json["batches"].as_i64().unwrap(), 4);
165
166        // Verify data
167        assert_eq!(db.count("users"), 100);
168    }
169
170    #[tokio::test]
171    async fn test_bulk_insert_empty() {
172        let db = TestDatabase::new().await;
173
174        let tool = BulkInsertTool;
175        let input = BulkInsertInput {
176            table: "users".to_string(),
177            data: vec![],
178            batch_size: 1000,
179            db_path: Some(db.key()),
180        };
181
182        let result = tool.execute(input).await.unwrap();
183        let json = unwrap_json(result);
184
185        assert_eq!(json["total_inserted"].as_i64().unwrap(), 0);
186    }
187
188    #[test]
189    fn test_tool_metadata() {
190        let tool = BulkInsertTool;
191        assert_eq!(tool.name(), "sqlite_bulk_insert");
192        assert!(!tool.description().is_empty());
193    }
194}