mixtape_tools/sqlite/query/
bulk_insert.rs1use crate::prelude::*;
4use crate::sqlite::error::SqliteToolError;
5use crate::sqlite::manager::with_connection;
6use crate::sqlite::types::json_to_sql;
7
8#[derive(Debug, Deserialize, JsonSchema)]
10pub struct BulkInsertInput {
11 pub table: String,
13
14 pub data: Vec<serde_json::Map<String, serde_json::Value>>,
16
17 #[serde(default = "default_batch_size")]
19 pub batch_size: usize,
20
21 #[serde(default)]
23 pub db_path: Option<String>,
24}
25
26fn default_batch_size() -> usize {
27 1000
28}
29
30#[derive(Debug, Serialize, JsonSchema)]
32struct BulkInsertResult {
33 status: String,
34 total_inserted: usize,
35 batches: usize,
36}
37
38pub 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 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 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 for chunk in data.chunks(batch_size) {
96 conn.execute("BEGIN TRANSACTION", [])?;
97
98 for record in chunk {
99 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 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 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}