database_replicator/sqlite/
converter.rs1use anyhow::{Context, Result};
5use rusqlite::Connection;
6use serde_json::Value as JsonValue;
7use std::collections::HashMap;
8
9pub fn sqlite_value_to_json(value: &rusqlite::types::Value) -> Result<JsonValue> {
36 match value {
37 rusqlite::types::Value::Null => Ok(JsonValue::Null),
38
39 rusqlite::types::Value::Integer(i) => Ok(JsonValue::Number((*i).into())),
40
41 rusqlite::types::Value::Real(f) => {
42 if f.is_finite() {
45 serde_json::Number::from_f64(*f)
46 .map(JsonValue::Number)
47 .ok_or_else(|| anyhow::anyhow!("Failed to convert float {} to JSON number", f))
48 } else {
49 Ok(JsonValue::String(f.to_string()))
51 }
52 }
53
54 rusqlite::types::Value::Text(s) => Ok(JsonValue::String(s.clone())),
55
56 rusqlite::types::Value::Blob(b) => {
57 let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, b);
61 Ok(serde_json::json!({
62 "_type": "blob",
63 "data": encoded
64 }))
65 }
66 }
67}
68
69pub fn sqlite_row_to_json(row: HashMap<String, rusqlite::types::Value>) -> Result<JsonValue> {
96 let mut json_obj = serde_json::Map::new();
97
98 for (col_name, value) in row {
99 let json_value = sqlite_value_to_json(&value)
100 .with_context(|| format!("Failed to convert column '{}' to JSON", col_name))?;
101 json_obj.insert(col_name, json_value);
102 }
103
104 Ok(JsonValue::Object(json_obj))
105}
106
107pub fn convert_table_to_jsonb(conn: &Connection, table: &str) -> Result<Vec<(String, JsonValue)>> {
146 crate::jsonb::validate_table_name(table).context("Invalid table name for JSONB conversion")?;
148
149 tracing::info!("Converting SQLite table '{}' to JSONB", table);
150
151 let rows = crate::sqlite::reader::read_table_data(conn, table)
153 .with_context(|| format!("Failed to read data from table '{}'", table))?;
154
155 let id_column = detect_id_column(conn, table)?;
157
158 let mut result = Vec::with_capacity(rows.len());
159
160 for (row_num, row) in rows.into_iter().enumerate() {
161 let id = if let Some(ref id_col) = id_column {
163 match row.get(id_col) {
165 Some(rusqlite::types::Value::Integer(i)) => i.to_string(),
166 Some(rusqlite::types::Value::Text(s)) => s.clone(),
167 Some(rusqlite::types::Value::Real(f)) => f.to_string(),
168 _ => {
169 tracing::warn!(
171 "Row {} in table '{}' has invalid ID type, using row number",
172 row_num + 1,
173 table
174 );
175 (row_num + 1).to_string()
176 }
177 }
178 } else {
179 (row_num + 1).to_string()
182 };
183
184 let json_data = sqlite_row_to_json(row).with_context(|| {
186 format!(
187 "Failed to convert row {} in table '{}' to JSON",
188 row_num + 1,
189 table
190 )
191 })?;
192
193 result.push((id, json_data));
194 }
195
196 tracing::info!(
197 "Converted {} rows from table '{}' to JSONB",
198 result.len(),
199 table
200 );
201
202 Ok(result)
203}
204
205fn detect_id_column(conn: &Connection, table: &str) -> Result<Option<String>> {
210 let query = format!("PRAGMA table_info(\"{}\")", table);
212 let mut stmt = conn
213 .prepare(&query)
214 .with_context(|| format!("Failed to get table info for '{}'", table))?;
215
216 let columns: Vec<String> = stmt
217 .query_map([], |row| row.get::<_, String>(1))
218 .context("Failed to query table columns")?
219 .collect::<Result<Vec<_>, _>>()
220 .context("Failed to collect column names")?;
221
222 let id_candidates = ["id", "rowid", "_id"];
224 for candidate in &id_candidates {
225 if let Some(col) = columns.iter().find(|c| c.to_lowercase() == *candidate) {
226 tracing::debug!("Using column '{}' as ID for table '{}'", col, table);
227 return Ok(Some(col.clone()));
228 }
229 }
230
231 tracing::debug!(
232 "No ID column found for table '{}', will use row number",
233 table
234 );
235 Ok(None)
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use rusqlite::types::Value;
242
243 #[test]
244 fn test_convert_integer() {
245 let value = Value::Integer(42);
246 let json = sqlite_value_to_json(&value).unwrap();
247 assert_eq!(json, serde_json::json!(42));
248 }
249
250 #[test]
251 fn test_convert_real() {
252 let value = Value::Real(42.75);
253 let json = sqlite_value_to_json(&value).unwrap();
254 assert_eq!(json, serde_json::json!(42.75));
255 }
256
257 #[test]
258 fn test_convert_text() {
259 let value = Value::Text("Hello, World!".to_string());
260 let json = sqlite_value_to_json(&value).unwrap();
261 assert_eq!(json, serde_json::json!("Hello, World!"));
262 }
263
264 #[test]
265 fn test_convert_null() {
266 let value = Value::Null;
267 let json = sqlite_value_to_json(&value).unwrap();
268 assert_eq!(json, JsonValue::Null);
269 }
270
271 #[test]
272 fn test_convert_blob() {
273 let blob_data = vec![0x48, 0x65, 0x6c, 0x6c, 0x6f]; let value = Value::Blob(blob_data.clone());
275 let json = sqlite_value_to_json(&value).unwrap();
276
277 assert!(json.is_object());
279 assert_eq!(json["_type"], "blob");
280
281 let encoded = json["data"].as_str().unwrap();
283 let decoded =
284 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded).unwrap();
285 assert_eq!(decoded, blob_data);
286 }
287
288 #[test]
289 fn test_convert_non_finite_float() {
290 let nan_value = Value::Real(f64::NAN);
291 let json = sqlite_value_to_json(&nan_value).unwrap();
292 assert!(json.is_string());
294
295 let inf_value = Value::Real(f64::INFINITY);
296 let json = sqlite_value_to_json(&inf_value).unwrap();
297 assert!(json.is_string());
299 }
300
301 #[test]
302 fn test_sqlite_row_to_json() {
303 let mut row = HashMap::new();
304 row.insert("id".to_string(), Value::Integer(1));
305 row.insert("name".to_string(), Value::Text("Alice".to_string()));
306 row.insert("age".to_string(), Value::Integer(30));
307 row.insert("balance".to_string(), Value::Real(100.50));
308 row.insert("notes".to_string(), Value::Null);
309
310 let json = sqlite_row_to_json(row).unwrap();
311
312 assert_eq!(json["id"], 1);
313 assert_eq!(json["name"], "Alice");
314 assert_eq!(json["age"], 30);
315 assert_eq!(json["balance"], 100.50);
316 assert_eq!(json["notes"], JsonValue::Null);
317 }
318
319 #[test]
320 fn test_convert_table_to_jsonb() {
321 let conn = Connection::open_in_memory().unwrap();
323
324 conn.execute(
326 "CREATE TABLE users (
327 id INTEGER PRIMARY KEY,
328 name TEXT NOT NULL,
329 email TEXT,
330 age INTEGER
331 )",
332 [],
333 )
334 .unwrap();
335
336 conn.execute(
338 "INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'alice@example.com', 30)",
339 [],
340 )
341 .unwrap();
342 conn.execute(
343 "INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'bob@example.com', 25)",
344 [],
345 )
346 .unwrap();
347
348 let result = convert_table_to_jsonb(&conn, "users").unwrap();
350
351 assert_eq!(result.len(), 2);
352
353 let (id1, json1) = &result[0];
355 assert_eq!(id1, "1");
356 assert_eq!(json1["name"], "Alice");
357 assert_eq!(json1["email"], "alice@example.com");
358 assert_eq!(json1["age"], 30);
359
360 let (id2, json2) = &result[1];
362 assert_eq!(id2, "2");
363 assert_eq!(json2["name"], "Bob");
364 }
365
366 #[test]
367 fn test_convert_table_without_id_column() {
368 let conn = Connection::open_in_memory().unwrap();
370
371 conn.execute(
373 "CREATE TABLE logs (
374 timestamp INTEGER,
375 message TEXT
376 )",
377 [],
378 )
379 .unwrap();
380
381 conn.execute(
383 "INSERT INTO logs (timestamp, message) VALUES (12345, 'Test message')",
384 [],
385 )
386 .unwrap();
387
388 let result = convert_table_to_jsonb(&conn, "logs").unwrap();
390
391 assert_eq!(result.len(), 1);
392
393 let (id, json) = &result[0];
395 assert_eq!(id, "1");
396 assert_eq!(json["message"], "Test message");
397 }
398
399 #[test]
400 fn test_convert_table_handles_null_values() {
401 let conn = Connection::open_in_memory().unwrap();
402
403 conn.execute(
404 "CREATE TABLE users (
405 id INTEGER PRIMARY KEY,
406 name TEXT,
407 email TEXT
408 )",
409 [],
410 )
411 .unwrap();
412
413 conn.execute(
415 "INSERT INTO users (id, name, email) VALUES (1, 'Alice', NULL)",
416 [],
417 )
418 .unwrap();
419
420 let result = convert_table_to_jsonb(&conn, "users").unwrap();
421
422 assert_eq!(result.len(), 1);
423 let (_, json) = &result[0];
424 assert_eq!(json["name"], "Alice");
425 assert_eq!(json["email"], JsonValue::Null);
426 }
427
428 #[test]
429 fn test_convert_table_with_blob() {
430 let conn = Connection::open_in_memory().unwrap();
431
432 conn.execute(
433 "CREATE TABLE files (
434 id INTEGER PRIMARY KEY,
435 name TEXT,
436 data BLOB
437 )",
438 [],
439 )
440 .unwrap();
441
442 let blob_data: Vec<u8> = vec![0x01, 0x02, 0x03, 0x04];
444 conn.execute(
445 "INSERT INTO files (id, name, data) VALUES (?1, ?2, ?3)",
446 rusqlite::params![1, "test.bin", &blob_data],
447 )
448 .unwrap();
449
450 let result = convert_table_to_jsonb(&conn, "files").unwrap();
451
452 assert_eq!(result.len(), 1);
453 let (_, json) = &result[0];
454 assert_eq!(json["name"], "test.bin");
455
456 assert!(json["data"].is_object());
458 assert_eq!(json["data"]["_type"], "blob");
459 assert!(json["data"]["data"].is_string());
460 }
461
462 #[test]
463 fn test_detect_id_column_case_insensitive() {
464 let conn = Connection::open_in_memory().unwrap();
465
466 conn.execute("CREATE TABLE test (ID INTEGER PRIMARY KEY, value TEXT)", [])
468 .unwrap();
469
470 let id_col = detect_id_column(&conn, "test").unwrap();
471 assert!(id_col.is_some());
472 assert_eq!(id_col.unwrap().to_lowercase(), "id");
473 }
474
475 #[test]
476 fn test_convert_empty_table() {
477 let conn = Connection::open_in_memory().unwrap();
478
479 conn.execute("CREATE TABLE empty (id INTEGER PRIMARY KEY)", [])
480 .unwrap();
481
482 let result = convert_table_to_jsonb(&conn, "empty").unwrap();
483 assert_eq!(result.len(), 0);
484 }
485}