Skip to main content

scirs2_io/database/
sqlite.rs

1//! SQLite database implementation — Pure Rust via `oxisql-sqlite-compat` (Limbo engine).
2//!
3//! # Architecture: sync/async bridge
4//!
5//! The `DatabaseConnection` trait used in scirs2-io is **synchronous**, while
6//! `oxisql-sqlite-compat` (and the underlying Limbo engine) is fully async (tokio).
7//!
8//! The bridge function `run_sync` resolves this impedance mismatch:
9//! - If a tokio runtime is already active (`Handle::try_current()` succeeds) it
10//!   uses `block_in_place` + `Handle::block_on` so that the current thread can
11//!   block without parking the entire executor.
12//! - Otherwise it spins up a fresh single-threaded `Runtime` for the call.
13//!
14//! # Limbo 0.0.22 caveats
15//!
16//! - **ROLLBACK**: not implemented by Limbo; `SqliteTransaction::rollback()`
17//!   returns an error.  This file does not call `rollback()` — `commit()` is
18//!   the only transaction completion path used here.
19//! - **Affected-row count**: retrieved via `SELECT changes()` internally by
20//!   `oxisql-sqlite-compat`, adding one round-trip per DML.
21
22use crate::database::{
23    ColumnDef, DataType, DatabaseConfig, DatabaseConnection, Index, QueryBuilder, QueryType,
24    ResultSet, TableSchema,
25};
26use crate::error::{IoError, Result};
27use oxisql_core::{Connection as OxiConnection, Row as OxiRow, ToSqlValue, Transaction, Value};
28use oxisql_sqlite_compat::SqliteConnection;
29use scirs2_core::ndarray::ArrayView2;
30use std::future::Future;
31use std::sync::Mutex;
32
33// ── sync/async bridge ──────────────────────────────────────────────────────────
34
35/// Run an async future to completion on the current thread, blocking it.
36///
37/// Works both inside an existing tokio multi-threaded runtime (via
38/// `block_in_place`) and outside any runtime (via a freshly created
39/// single-threaded `Runtime`).
40fn run_sync<F, T, E>(fut: F) -> std::result::Result<T, E>
41where
42    F: Future<Output = std::result::Result<T, E>>,
43{
44    match tokio::runtime::Handle::try_current() {
45        Ok(handle) => {
46            // We are inside an existing async runtime. Use block_in_place so
47            // that the current tokio worker thread is temporarily allowed to
48            // perform blocking work without stalling the scheduler.
49            tokio::task::block_in_place(|| handle.block_on(fut))
50        }
51        Err(_) => {
52            // No runtime active — create a minimal one for this call.
53            let rt = tokio::runtime::Builder::new_current_thread()
54                .enable_all()
55                .build()
56                .map_err(|_| {
57                    // This path is only reachable if tokio runtime creation
58                    // fails (extremely rare). We can't propagate IoError here
59                    // because the error type is generic E, so we have to panic.
60                    // In practice this should never happen.
61                    panic!("scirs2-io sqlite: failed to create tokio runtime for sync bridge")
62                })
63                .expect("tokio runtime creation cannot fail in practice");
64            rt.block_on(fut)
65        }
66    }
67}
68
69/// Convert an [`oxisql_core::OxiSqlError`] into our [`IoError`].
70fn oxi_err(e: oxisql_core::OxiSqlError) -> IoError {
71    IoError::DatabaseError(e.to_string())
72}
73
74/// Convert a single [`oxisql_core::Value`] into a [`serde_json::Value`].
75fn oxi_value_to_json(v: &Value) -> serde_json::Value {
76    match v {
77        Value::Null => serde_json::Value::Null,
78        Value::Bool(b) => serde_json::json!(*b),
79        Value::I64(n) => serde_json::json!(*n),
80        Value::F64(f) => serde_json::json!(*f),
81        Value::Text(s) => serde_json::json!(s),
82        Value::Blob(b) => serde_json::json!(crate::encoding_utils::base64_encode(b)),
83        Value::Timestamp(ts) => serde_json::json!(*ts),
84        Value::Date(d) => serde_json::json!(*d),
85        Value::Time(t) => serde_json::json!(*t),
86        Value::Uuid(u) => serde_json::json!(format!("{v}")),
87        Value::Json(j) => serde_json::from_str(j).unwrap_or_else(|_| serde_json::json!(j)),
88        Value::Decimal(d) => serde_json::json!(d),
89        Value::Array(arr) => serde_json::Value::Array(arr.iter().map(oxi_value_to_json).collect()),
90        // A typed array carries the same element values as a plain array plus a
91        // nominal element type; the type annotation has no JSON representation,
92        // so serialize the elements as a JSON array.
93        Value::TypedArray { values, .. } => {
94            serde_json::Value::Array(values.iter().map(oxi_value_to_json).collect())
95        }
96    }
97}
98
99/// Convert a [`serde_json::Value`] parameter into a boxed [`ToSqlValue`].
100fn json_param_to_oxi(p: &serde_json::Value) -> Value {
101    match p {
102        serde_json::Value::String(s) => Value::Text(s.clone()),
103        serde_json::Value::Number(n) => {
104            if let Some(i) = n.as_i64() {
105                Value::I64(i)
106            } else {
107                Value::F64(n.as_f64().unwrap_or(0.0))
108            }
109        }
110        serde_json::Value::Bool(b) => Value::Bool(*b),
111        serde_json::Value::Null => Value::Null,
112        serde_json::Value::Array(_) | serde_json::Value::Object(_) => Value::Text(p.to_string()),
113    }
114}
115
116// ── SQLiteConnection ──────────────────────────────────────────────────────────
117
118/// SQLite connection wrapper backed by `oxisql-sqlite-compat` (Pure Rust / Limbo).
119pub struct SQLiteConnection {
120    config: DatabaseConfig,
121    /// The underlying async Limbo connection, wrapped in `Mutex` so that the
122    /// synchronous `DatabaseConnection` methods can access it from any thread.
123    connection: Option<Mutex<SqliteConnection>>,
124}
125
126impl SQLiteConnection {
127    /// Open or create the SQLite database at the path given in `config.database`.
128    ///
129    /// Use `":memory:"` for an ephemeral in-memory database.
130    pub fn new(config: &DatabaseConfig) -> Result<Self> {
131        let conn = run_sync(SqliteConnection::open(&config.database)).map_err(oxi_err)?;
132
133        // Enable foreign key constraints (best-effort; ignore error on failure).
134        let _ = run_sync(conn.execute("PRAGMA foreign_keys = ON", &[]));
135
136        Ok(Self {
137            config: config.clone(),
138            connection: Some(Mutex::new(conn)),
139        })
140    }
141
142    /// Obtain a lock on the inner connection, returning an error if the
143    /// connection was never initialised.
144    fn with_conn<F, T>(&self, f: F) -> Result<T>
145    where
146        F: FnOnce(&SqliteConnection) -> Result<T>,
147    {
148        let guard = self
149            .connection
150            .as_ref()
151            .ok_or_else(|| IoError::DatabaseError("SQLite connection not initialised".to_string()))?
152            .lock()
153            .map_err(|_| IoError::DatabaseError("SQLite connection mutex poisoned".to_string()))?;
154        f(&guard)
155    }
156}
157
158impl DatabaseConnection for SQLiteConnection {
159    fn query(&self, query: &QueryBuilder) -> Result<ResultSet> {
160        let sql = query.build_sql();
161        let params = &query.values;
162        self.execute_sql(&sql, params)
163    }
164
165    fn execute_sql(&self, sql: &str, params: &[serde_json::Value]) -> Result<ResultSet> {
166        self.with_conn(|conn| {
167            // Convert serde_json params → oxisql Values, then collect refs.
168            let oxi_params: Vec<Value> = params.iter().map(json_param_to_oxi).collect();
169            let param_refs: Vec<&dyn ToSqlValue> =
170                oxi_params.iter().map(|v| v as &dyn ToSqlValue).collect();
171
172            let rows = run_sync(conn.query(sql, &param_refs)).map_err(oxi_err)?;
173
174            // Derive column names from the first row, or an empty vec if no rows.
175            let column_names: Vec<String> = rows
176                .first()
177                .map(|r| r.columns().to_vec())
178                .unwrap_or_default();
179
180            let mut result = ResultSet::new(column_names.clone());
181
182            for row in &rows {
183                let col_count = row.column_count();
184                let mut row_data = Vec::with_capacity(col_count);
185                for i in 0..col_count {
186                    let v = row
187                        .get_by_index(i)
188                        .map(oxi_value_to_json)
189                        .unwrap_or(serde_json::Value::Null);
190                    row_data.push(v);
191                }
192                result.add_row(row_data);
193            }
194
195            Ok(result)
196        })
197    }
198
199    fn insert_array(&self, table: &str, data: ArrayView2<f64>, columns: &[&str]) -> Result<usize> {
200        if columns.len() != data.ncols() {
201            return Err(IoError::ValidationError(
202                "Number of columns doesn't match data dimensions".to_string(),
203            ));
204        }
205
206        self.with_conn(|conn| {
207            let placeholders: Vec<String> =
208                (1..=columns.len()).map(|i| format!("${}", i)).collect();
209            let insert_sql = format!(
210                "INSERT INTO {} ({}) VALUES ({})",
211                table,
212                columns.join(", "),
213                placeholders.join(", ")
214            );
215
216            // Open a transaction for the bulk insert.
217            let mut txn = run_sync(conn.transaction()).map_err(oxi_err)?;
218
219            for row in data.rows() {
220                let row_vals: Vec<Value> = row.iter().map(|&f| Value::F64(f)).collect();
221                let row_refs: Vec<&dyn ToSqlValue> =
222                    row_vals.iter().map(|v| v as &dyn ToSqlValue).collect();
223                run_sync(txn.execute(&insert_sql, &row_refs)).map_err(oxi_err)?;
224            }
225
226            run_sync(txn.commit()).map_err(oxi_err)?;
227            // Any failed insert returns early above, so all rows were inserted.
228            Ok(data.nrows())
229        })
230    }
231
232    fn create_table(&self, table: &str, schema: &TableSchema) -> Result<()> {
233        self.with_conn(|conn| {
234            let column_defs: Vec<String> = schema
235                .columns
236                .iter()
237                .map(|col| {
238                    let sqlite_type = match col.data_type {
239                        DataType::Integer | DataType::BigInt => "INTEGER",
240                        DataType::Float | DataType::Double => "REAL",
241                        DataType::Decimal(_, _) => "REAL",
242                        DataType::Varchar(_) | DataType::Text => "TEXT",
243                        DataType::Boolean => "INTEGER",
244                        DataType::Date | DataType::Timestamp => "TEXT",
245                        DataType::Json => "TEXT",
246                        DataType::Binary => "BLOB",
247                    };
248                    let nullable = if col.nullable { "" } else { " NOT NULL" };
249                    format!("{} {}{}", col.name, sqlite_type, nullable)
250                })
251                .collect();
252
253            let mut create_sql = format!("CREATE TABLE {} (", table);
254            create_sql.push_str(&column_defs.join(", "));
255
256            if let Some(ref pk_cols) = schema.primary_key {
257                create_sql.push_str(&format!(", PRIMARY KEY ({})", pk_cols.join(", ")));
258            }
259            create_sql.push(')');
260
261            run_sync(conn.execute(&create_sql, &[])).map_err(oxi_err)?;
262            Ok(())
263        })
264    }
265
266    fn table_exists(&self, table: &str) -> Result<bool> {
267        self.with_conn(|conn| {
268            let table_val = Value::Text(table.to_string());
269            let params: &[&dyn ToSqlValue] = &[&table_val];
270            let rows = run_sync(conn.query(
271                "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=$1",
272                params,
273            ))
274            .map_err(oxi_err)?;
275
276            let count = rows
277                .first()
278                .and_then(|r| r.get_by_index(0))
279                .and_then(|v| {
280                    if let Value::I64(n) = v {
281                        Some(*n)
282                    } else {
283                        None
284                    }
285                })
286                .unwrap_or(0);
287
288            Ok(count > 0)
289        })
290    }
291
292    fn get_schema(&self, table: &str) -> Result<TableSchema> {
293        self.with_conn(|conn| {
294            // PRAGMA table_info returns: cid, name, type, notnull, dflt_value, pk
295            let sql = format!("PRAGMA table_info({})", table);
296            let rows = run_sync(conn.query(&sql, &[])).map_err(oxi_err)?;
297
298            let mut columns = Vec::new();
299            let mut primary_key: Vec<String> = Vec::new();
300
301            for row in &rows {
302                let name = row
303                    .get_by_index(1)
304                    .and_then(|v| {
305                        if let Value::Text(s) = v {
306                            Some(s.clone())
307                        } else {
308                            None
309                        }
310                    })
311                    .unwrap_or_default();
312
313                let type_str = row
314                    .get_by_index(2)
315                    .and_then(|v| {
316                        if let Value::Text(s) = v {
317                            Some(s.clone())
318                        } else {
319                            None
320                        }
321                    })
322                    .unwrap_or_default();
323
324                let notnull = row
325                    .get_by_index(3)
326                    .and_then(|v| {
327                        if let Value::I64(n) = v {
328                            Some(*n)
329                        } else {
330                            None
331                        }
332                    })
333                    .unwrap_or(0);
334
335                let default_val = row.get_by_index(4).and_then(|v| match v {
336                    Value::Text(s) => Some(serde_json::Value::String(s.clone())),
337                    Value::Null => None,
338                    other => Some(serde_json::json!(format!("{:?}", other))),
339                });
340
341                let pk_flag = row
342                    .get_by_index(5)
343                    .and_then(|v| {
344                        if let Value::I64(n) = v {
345                            Some(*n)
346                        } else {
347                            None
348                        }
349                    })
350                    .unwrap_or(0);
351
352                let data_type = match type_str.to_uppercase().as_str() {
353                    "INTEGER" => DataType::Integer,
354                    "REAL" => DataType::Double,
355                    "TEXT" => DataType::Text,
356                    "BLOB" => DataType::Binary,
357                    _ => DataType::Text,
358                };
359
360                columns.push(ColumnDef {
361                    name: name.clone(),
362                    data_type,
363                    nullable: notnull == 0,
364                    default: default_val,
365                });
366
367                if pk_flag > 0 {
368                    primary_key.push(name);
369                }
370            }
371
372            Ok(TableSchema {
373                name: table.to_string(),
374                columns,
375                primary_key: if primary_key.is_empty() {
376                    None
377                } else {
378                    Some(primary_key)
379                },
380                indexes: Vec::new(),
381            })
382        })
383    }
384}