parent
1821a0f116
commit
8efdcab812
@ -0,0 +1,15 @@ |
||||
{ |
||||
"permissions": { |
||||
"allow": [ |
||||
"Bash(echo:*)", |
||||
"Bash(git ls-files:*)", |
||||
"Bash(cat:*)", |
||||
"Bash(cargo:*)", |
||||
"Bash(ls:*)", |
||||
"Bash(tree:*)", |
||||
"Bash(find:*)" |
||||
], |
||||
"deny": [], |
||||
"ask": [] |
||||
} |
||||
} |
||||
@ -0,0 +1,258 @@ |
||||
# `sqlite` |
||||
|
||||
The `sqlite` module provides access to [SQLite](https://sqlite.org/) databases. |
||||
SQLite is compiled into the binary, so no system library is required. The module |
||||
handles type conversion and parameter binding; it is not an ORM. You open a |
||||
connection and call methods on it. |
||||
|
||||
```lua |
||||
local con = sqlite.connect(":memory:") |
||||
|
||||
con:execute("CREATE TABLE people (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)") |
||||
con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Alice", 30}) |
||||
|
||||
for _, row in ipairs(con:query("SELECT * FROM people")) do |
||||
print(row.id, row.name, row.age) |
||||
end |
||||
``` |
||||
|
||||
## Connecting |
||||
|
||||
```lua |
||||
local con = sqlite.connect(path) |
||||
``` |
||||
|
||||
`path` is a filename (`"data.sqlite"`), created if it does not exist, or the special |
||||
string `":memory:"` for a private in-memory database that vanishes when closed. The |
||||
returned connection is an object you call methods on with `:` syntax. |
||||
|
||||
The connection closes automatically when it is garbage-collected. Call |
||||
[`con:close()`](#conclose) to release it eagerly. |
||||
|
||||
## Queries |
||||
|
||||
### `con:execute(sql [, params])` |
||||
|
||||
Run a statement that changes data (`INSERT`, `UPDATE`, `DELETE`, `CREATE`, …). |
||||
Returns the number of rows changed as an integer. |
||||
|
||||
```lua |
||||
local changed = con:execute("UPDATE people SET age = age + 1 WHERE name = ?", {"Alice"}) |
||||
-- changed == 1 |
||||
``` |
||||
|
||||
### `con:query(sql [, params])` |
||||
|
||||
Run a `SELECT` and return **all** matching rows as an array of tables. Each row is a |
||||
table keyed by column name. An empty result is an empty array (`#rows == 0`). |
||||
|
||||
```lua |
||||
local rows = con:query("SELECT id, name FROM people WHERE age > ?", {18}) |
||||
for _, row in ipairs(rows) do |
||||
print(row.id, row.name) |
||||
end |
||||
``` |
||||
|
||||
### `con:queryOne(sql [, params])` |
||||
|
||||
Like `query`, but returns the **first** row as a table, or `nil` if nothing matched. |
||||
Use it for lookups that return at most one row, such as a fetch by primary key. |
||||
|
||||
```lua |
||||
local row = con:queryOne("SELECT * FROM people WHERE id = ?", {1}) |
||||
if row then |
||||
print(row.name) |
||||
end |
||||
``` |
||||
|
||||
## Parameters |
||||
|
||||
Pass values as a `params` table rather than building SQL by string concatenation; |
||||
the values are then bound and escaped by SQLite. The table is bound either |
||||
**positionally** or **by name**, decided by its keys. |
||||
|
||||
### Positional |
||||
|
||||
A plain array binds to `?` placeholders in order. It must be a gapless array starting |
||||
at index 1. |
||||
|
||||
```lua |
||||
con:query("SELECT * FROM people WHERE age > ? AND name <> ?", {18, "Bob"}) |
||||
``` |
||||
|
||||
### Named |
||||
|
||||
A table with string keys binds to named placeholders. In the SQL the placeholder |
||||
carries a sigil (`:name`, `@name`, or `$name`); in the table the key is the bare name |
||||
without it. |
||||
|
||||
```lua |
||||
con:query("SELECT * FROM people WHERE age > :min", {min = 18}) |
||||
con:execute("INSERT INTO people (name, age) VALUES (:name, :age)", {name = "Carol", age = 25}) |
||||
``` |
||||
|
||||
### Rules |
||||
|
||||
- Omitting `params` (or passing `nil`) means the statement takes no parameters. |
||||
- A table may be **all positional** or **all named** — mixing the two is an error. |
||||
- Every named placeholder in the SQL must have a matching key, or the call errors. |
||||
|
||||
## Type mapping |
||||
|
||||
Values convert automatically in both directions. |
||||
|
||||
**Lua → SQLite** (binding parameters): |
||||
|
||||
| Lua | SQLite | |
||||
|--------------|-----------------| |
||||
| `nil` | `NULL` | |
||||
| `utils.NULL` | `NULL` | |
||||
| `boolean` | `INTEGER` (0/1) | |
||||
| integer | `INTEGER` | |
||||
| number | `REAL` | |
||||
| `string` | `TEXT` | |
||||
|
||||
**SQLite → Lua** (reading rows): |
||||
|
||||
| SQLite | Lua | |
||||
|-----------|----------------------| |
||||
| `NULL` | `nil` | |
||||
| `INTEGER` | integer | |
||||
| `REAL` | number | |
||||
| `TEXT` | `string` | |
||||
| `BLOB` | `string` (raw bytes) | |
||||
|
||||
Two consequences worth knowing: |
||||
|
||||
- **Booleans don't round-trip as booleans.** SQLite has no boolean type, so `true` |
||||
is stored as the integer `1` and reads back as `1`, not `true`. Compare against |
||||
`1`/`0`, or store a real `INTEGER` column and interpret it yourself. |
||||
- **`NULL` reads back as an absent key.** A column holding `NULL` simply isn't set on |
||||
the row table, so `row.col` is `nil` — exactly as if the key were present and `nil`. |
||||
To *bind* a SQL `NULL`, pass `utils.NULL`: a literal Lua `nil` cannot |
||||
live inside a table, so it can never reach a parameter slot. |
||||
|
||||
```lua |
||||
con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Dave", utils.NULL}) |
||||
local row = con:queryOne("SELECT age FROM people WHERE name = ?", {"Dave"}) |
||||
print(row.age) --> nil |
||||
``` |
||||
|
||||
## Metadata |
||||
|
||||
### `con:lastInsertRowid()` |
||||
|
||||
The rowid of the most recent successful `INSERT` on this connection, as an integer. |
||||
For a table with an `INTEGER PRIMARY KEY`, this is that key. |
||||
|
||||
```lua |
||||
con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Eve", 40}) |
||||
local id = con:lastInsertRowid() |
||||
``` |
||||
|
||||
### `con:changes()` |
||||
|
||||
The number of rows changed by the most recent `INSERT`/`UPDATE`/`DELETE`. This is the |
||||
same number `execute` returns, but you can query it separately at any time. |
||||
|
||||
## Transactions |
||||
|
||||
### `con:transaction(fn)` |
||||
|
||||
Run `fn` inside a transaction. If it returns normally the transaction is **committed**; |
||||
if it raises an error the transaction is **rolled back** and the original error is |
||||
re-raised unchanged. |
||||
|
||||
```lua |
||||
con:transaction(function() |
||||
con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Frank", 50}) |
||||
con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Grace", 60}) |
||||
end) |
||||
-- Both inserts committed together; if either had thrown, neither would persist. |
||||
``` |
||||
|
||||
### Manual control |
||||
|
||||
For flows that don't fit a single callback, drive the transaction yourself: |
||||
|
||||
```lua |
||||
con:begin() |
||||
local ok, err = pcall(function() |
||||
con:execute("DELETE FROM people WHERE age > ?", {100}) |
||||
end) |
||||
if ok then con:commit() else con:rollback() end |
||||
``` |
||||
|
||||
| Method | Effect | |
||||
|-------------------|---------------------------------| |
||||
| `con:begin()` | Start a transaction (`BEGIN`). | |
||||
| `con:commit()` | Commit it (`COMMIT`). | |
||||
| `con:rollback()` | Discard it (`ROLLBACK`). | |
||||
|
||||
You can mix both styles in the same program. |
||||
|
||||
## Closing |
||||
|
||||
### `con:close()` |
||||
|
||||
Release the connection and its underlying database handle. Any further call on the |
||||
connection raises an error. Closing is optional — a connection also closes when |
||||
garbage-collected — but it is the clean way to release a file lock promptly. |
||||
|
||||
```lua |
||||
con:close() |
||||
``` |
||||
|
||||
## Errors |
||||
|
||||
Failures raise Lua errors rather than returning error codes: a bad SQL statement, a |
||||
type that can't be bound, a parameter mismatch, or use of a closed connection all |
||||
`error()` out. Wrap calls in `pcall` or `utils.try` where you want to |
||||
handle failure rather than abort: |
||||
|
||||
```lua |
||||
local rows, err = utils.try(function() |
||||
return con:query("SELECT * FROM nonexistent") |
||||
end) |
||||
if not rows then |
||||
log.error("query failed: " .. tostring(err)) |
||||
end |
||||
``` |
||||
|
||||
## Notes |
||||
|
||||
- **Statements are cached.** Each query is prepared through SQLite's statement cache, |
||||
so repeating the same SQL reuses the compiled statement. There is no separate |
||||
prepared-statement API to manage. |
||||
- **I/O runs off the event loop.** `execute`, `query`, and `queryOne` run SQLite's |
||||
blocking work on a background thread, so a slow query does not stall other async |
||||
work (timers, `os.sleep`, networking) in the same script. |
||||
|
||||
## Full example |
||||
|
||||
```lua |
||||
local con = sqlite.connect(":memory:") |
||||
|
||||
con:execute([[ |
||||
CREATE TABLE tasks ( |
||||
id INTEGER PRIMARY KEY, |
||||
title TEXT NOT NULL, |
||||
done INTEGER NOT NULL DEFAULT 0 |
||||
) |
||||
]]) |
||||
|
||||
con:transaction(function() |
||||
for _, title in ipairs({"write docs", "review code", "update changelog"}) do |
||||
con:execute("INSERT INTO tasks (title) VALUES (:title)", {title = title}) |
||||
end |
||||
end) |
||||
|
||||
con:execute("UPDATE tasks SET done = 1 WHERE title = ?", {"write docs"}) |
||||
|
||||
local pending = con:query("SELECT id, title FROM tasks WHERE done = 0 ORDER BY id") |
||||
for _, task in ipairs(pending) do |
||||
print(task.id, task.title) |
||||
end |
||||
|
||||
con:close() |
||||
``` |
||||
@ -0,0 +1,40 @@ |
||||
-- Lua-side of the sqlite module. |
||||
-- The connection methods execute/query/queryOne/lastInsertRowid/changes/close |
||||
-- are provided by Rust. This file adds the transaction helpers on top of the |
||||
-- same connection so users can write con:transaction(fn), con:begin(), etc. |
||||
-- |
||||
-- Rust passes in `Connection`: the shared method table that every connection's |
||||
-- metatable indexes into (mlua hides the metatable itself from Lua). |
||||
|
||||
return function(Connection) |
||||
--- Begin a transaction. |
||||
function Connection:begin() |
||||
self:execute("BEGIN") |
||||
end |
||||
|
||||
--- Commit the current transaction. |
||||
function Connection:commit() |
||||
self:execute("COMMIT") |
||||
end |
||||
|
||||
--- Roll back the current transaction. |
||||
function Connection:rollback() |
||||
self:execute("ROLLBACK") |
||||
end |
||||
|
||||
--- Run `fn` inside a transaction: commit on success, roll back and re-raise |
||||
--- on error. The original error is re-raised unchanged (level 0). |
||||
function Connection:transaction(fn) |
||||
if type(fn) ~= "function" then |
||||
error("sqlite: transaction requires a function argument (got " .. type(fn) .. ")", 2) |
||||
end |
||||
self:begin() |
||||
local ok, err = pcall(fn) |
||||
if ok then |
||||
self:commit() |
||||
else |
||||
self:rollback() |
||||
error(err, 0) |
||||
end |
||||
end |
||||
end |
||||
@ -0,0 +1,111 @@ |
||||
-- Exercise of the sqlite stdlib module. |
||||
|
||||
local function assert_eq(actual, expected, msg) |
||||
if actual ~= expected then |
||||
error(string.format("%s: expected %s, got %s", |
||||
msg or "assertion failed", tostring(expected), tostring(actual)), 2) |
||||
end |
||||
end |
||||
|
||||
print("=== connect (in-memory) ===") |
||||
local con = sqlite.connect(":memory:") |
||||
|
||||
print("=== CREATE TABLE / INSERT ===") |
||||
con:execute([[ |
||||
CREATE TABLE people ( |
||||
id INTEGER PRIMARY KEY, |
||||
name TEXT, |
||||
age INTEGER, |
||||
score REAL, |
||||
vip INTEGER, |
||||
notes TEXT |
||||
) |
||||
]]) |
||||
|
||||
-- Positional params, every supported type. |
||||
local changed = con:execute( |
||||
"INSERT INTO people (name, age, score, vip, notes) VALUES (?, ?, ?, ?, ?)", |
||||
{"Alice", 30, 9.5, true, "first"} |
||||
) |
||||
assert_eq(changed, 1, "execute returns rows changed") |
||||
assert_eq(con:changes(), 1, "changes() after insert") |
||||
local first_id = con:lastInsertRowid() |
||||
assert_eq(first_id, 1, "lastInsertRowid") |
||||
|
||||
-- Named params (mix of :name in SQL). |
||||
con:execute( |
||||
"INSERT INTO people (name, age, score, vip, notes) VALUES (:name, :age, :score, :vip, :notes)", |
||||
{name = "Bob", age = 42, score = 7.25, vip = false, notes = utils.NULL} |
||||
) |
||||
assert_eq(con:lastInsertRowid(), 2, "lastInsertRowid after second insert") |
||||
|
||||
print("=== SELECT (query) ===") |
||||
local rows = con:query("SELECT * FROM people ORDER BY id") |
||||
assert_eq(#rows, 2, "query returns 2 rows") |
||||
assert_eq(rows[1].name, "Alice", "row 1 name") |
||||
assert_eq(rows[1].age, 30, "row 1 age (integer round-trip)") |
||||
assert_eq(rows[1].score, 9.5, "row 1 score (real round-trip)") |
||||
assert_eq(rows[1].vip, 1, "row 1 vip (boolean stored as 1)") |
||||
assert_eq(rows[1].notes, "first", "row 1 notes (text round-trip)") |
||||
-- A SQL NULL comes back as nil (absent key). |
||||
assert_eq(rows[2].notes, nil, "row 2 notes is nil (NULL round-trip)") |
||||
|
||||
print("=== queryOne ===") |
||||
local bob = con:queryOne("SELECT * FROM people WHERE name = ?", {"Bob"}) |
||||
assert(bob ~= nil, "queryOne found Bob") |
||||
assert_eq(bob.age, 42, "queryOne Bob age") |
||||
|
||||
local missing = con:queryOne("SELECT * FROM people WHERE name = ?", {"Nobody"}) |
||||
assert_eq(missing, nil, "queryOne returns nil for no match") |
||||
|
||||
print("=== UPDATE / DELETE ===") |
||||
local n = con:execute("UPDATE people SET age = age + 1 WHERE name = :who", {who = "Alice"}) |
||||
assert_eq(n, 1, "update affected 1 row") |
||||
assert_eq(con:queryOne("SELECT age FROM people WHERE name = ?", {"Alice"}).age, 31, "age incremented") |
||||
|
||||
n = con:execute("DELETE FROM people WHERE name = ?", {"Bob"}) |
||||
assert_eq(n, 1, "delete affected 1 row") |
||||
assert_eq(#con:query("SELECT * FROM people"), 1, "one row left after delete") |
||||
|
||||
print("=== transaction(fn) — commit ===") |
||||
con:transaction(function() |
||||
con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Carol", 25}) |
||||
con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Dave", 28}) |
||||
end) |
||||
assert_eq(#con:query("SELECT * FROM people"), 3, "two rows committed") |
||||
|
||||
print("=== transaction(fn) — rollback on error ===") |
||||
local ok, err = pcall(function() |
||||
con:transaction(function() |
||||
con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Eve", 99}) |
||||
error("boom") |
||||
end) |
||||
end) |
||||
assert_eq(ok, false, "transaction propagated the error") |
||||
assert(tostring(err):find("boom"), "original error message preserved") |
||||
assert_eq(#con:query("SELECT * FROM people"), 3, "failed transaction rolled back") |
||||
|
||||
print("=== manual begin / commit ===") |
||||
con:begin() |
||||
con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Frank", 50}) |
||||
con:commit() |
||||
assert_eq(#con:query("SELECT * FROM people"), 4, "manual commit persisted") |
||||
|
||||
print("=== manual begin / rollback ===") |
||||
con:begin() |
||||
con:execute("INSERT INTO people (name, age) VALUES (?, ?)", {"Grace", 60}) |
||||
con:rollback() |
||||
assert_eq(#con:query("SELECT * FROM people"), 4, "manual rollback discarded") |
||||
|
||||
print("=== mixed param table is rejected ===") |
||||
local mixed_ok = pcall(function() |
||||
con:query("SELECT 1 WHERE 1 = ?", {1, key = "x"}) |
||||
end) |
||||
assert_eq(mixed_ok, false, "mixed positional/named params error") |
||||
|
||||
print("=== close ===") |
||||
con:close() |
||||
local closed_ok = pcall(function() con:query("SELECT 1") end) |
||||
assert_eq(closed_ok, false, "use after close errors") |
||||
|
||||
print("\nAll sqlite checks passed.") |
||||
@ -0,0 +1,352 @@ |
||||
use std::sync::{Arc, Mutex}; |
||||
|
||||
use mlua::prelude::LuaResult; |
||||
use mlua::{Lua, Table as LuaTable, UserData, UserDataMethods, Value as LuaValue}; |
||||
use rusqlite::types::Value; |
||||
|
||||
use super::utils::is_null; |
||||
|
||||
const SQLITE_LUA: &str = include_str!("../../lua/stdlib/sqlite.lua"); |
||||
|
||||
/// Shared, lockable handle to a SQLite connection.
|
||||
///
|
||||
/// `Option` allows an explicit `close()` (set to `None`); any later use then
|
||||
/// reports a clear "connection is closed" error. `Arc<Mutex<>>` is required
|
||||
/// because mlua's async `UserData` must be `Send + Sync + 'static`, and it lets
|
||||
/// the blocking SQLite work run on a `spawn_blocking` thread.
|
||||
type ConnHandle = Arc<Mutex<Option<rusqlite::Connection>>>; |
||||
|
||||
struct SqliteConn(ConnHandle); |
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Errors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Error type carried back across the `spawn_blocking` boundary. Unlike
|
||||
/// `mlua::Error` it is trivially `Send + Sync`, so it travels cleanly between
|
||||
/// threads before being surfaced to Lua via `mlua::Error::external`.
|
||||
#[derive(Debug)] |
||||
enum DbError { |
||||
Closed, |
||||
Sqlite(rusqlite::Error), |
||||
Other(String), |
||||
} |
||||
|
||||
impl std::fmt::Display for DbError { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
match self { |
||||
DbError::Closed => write!(f, "sqlite: connection is closed"), |
||||
DbError::Sqlite(e) => write!(f, "sqlite: {e}"), |
||||
DbError::Other(s) => write!(f, "{s}"), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl std::error::Error for DbError {} |
||||
|
||||
impl From<rusqlite::Error> for DbError { |
||||
fn from(e: rusqlite::Error) -> Self { |
||||
DbError::Sqlite(e) |
||||
} |
||||
} |
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parameter binding
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Parameters bound to a statement, already converted to a `Send` form so they
|
||||
/// can cross into the blocking thread.
|
||||
enum BoundParams { |
||||
None, |
||||
/// Positional `?` parameters, in order.
|
||||
Positional(Vec<Value>), |
||||
/// Named `:name` / `@name` / `$name` parameters (sigil stripped from the key).
|
||||
Named(Vec<(String, Value)>), |
||||
} |
||||
|
||||
/// Convert an optional Lua params table into `BoundParams`.
|
||||
///
|
||||
/// A table with any string key is treated as named; a table with only integer
|
||||
/// keys is positional. Mixing the two is an error, as is a non-contiguous
|
||||
/// positional array. Runs on the Lua side (before `spawn_blocking`) because it
|
||||
/// touches non-`Send` Lua values.
|
||||
fn convert_params(params: Option<LuaTable>) -> LuaResult<BoundParams> { |
||||
let table = match params { |
||||
None => return Ok(BoundParams::None), |
||||
Some(t) => t, |
||||
}; |
||||
|
||||
let mut positional: Vec<(i64, Value)> = Vec::new(); |
||||
let mut named: Vec<(String, Value)> = Vec::new(); |
||||
|
||||
for pair in table.pairs::<LuaValue, LuaValue>() { |
||||
let (key, value) = pair?; |
||||
let value = lua_value_to_sqlite(value)?; |
||||
match key { |
||||
LuaValue::Integer(i) => positional.push((i, value)), |
||||
LuaValue::Number(n) if n.fract() == 0.0 => positional.push((n as i64, value)), |
||||
LuaValue::String(s) => named.push((s.to_str()?.to_owned(), value)), |
||||
other => { |
||||
return Err(mlua::Error::external(format!( |
||||
"sqlite: unsupported parameter key of type '{}'", |
||||
other.type_name() |
||||
))) |
||||
} |
||||
} |
||||
} |
||||
|
||||
match (positional.is_empty(), named.is_empty()) { |
||||
(true, true) => Ok(BoundParams::None), |
||||
(false, false) => Err(mlua::Error::external( |
||||
"sqlite: parameter table mixes positional (array) and named keys", |
||||
)), |
||||
(true, false) => Ok(BoundParams::Named(named)), |
||||
(false, true) => { |
||||
// Require a gapless 1..=n sequence so the `?` placeholders line up.
|
||||
positional.sort_by_key(|(i, _)| *i); |
||||
for (offset, (index, _)) in positional.iter().enumerate() { |
||||
if *index != offset as i64 + 1 { |
||||
return Err(mlua::Error::external( |
||||
"sqlite: positional parameters must form a contiguous array starting at index 1", |
||||
)); |
||||
} |
||||
} |
||||
Ok(BoundParams::Positional( |
||||
positional.into_iter().map(|(_, v)| v).collect(), |
||||
)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Map a Lua value to the SQLite value it binds as.
|
||||
fn lua_value_to_sqlite(value: LuaValue) -> LuaResult<Value> { |
||||
// `utils.NULL` is the only practical way to bind a SQL NULL, since a literal
|
||||
// `nil` cannot live inside a Lua table.
|
||||
if is_null(&value) { |
||||
return Ok(Value::Null); |
||||
} |
||||
match value { |
||||
LuaValue::Nil => Ok(Value::Null), |
||||
LuaValue::Boolean(b) => Ok(Value::Integer(b as i64)), |
||||
LuaValue::Integer(i) => Ok(Value::Integer(i)), |
||||
LuaValue::Number(f) => Ok(Value::Real(f)), |
||||
LuaValue::String(s) => Ok(Value::Text(s.to_str()?.to_owned())), |
||||
other => Err(mlua::Error::external(format!( |
||||
"sqlite: cannot bind value of type '{}'", |
||||
other.type_name() |
||||
))), |
||||
} |
||||
} |
||||
|
||||
/// Bind already-converted parameters onto a prepared statement.
|
||||
fn bind_params(stmt: &mut rusqlite::CachedStatement, params: &BoundParams) -> Result<(), DbError> { |
||||
match params { |
||||
BoundParams::None => Ok(()), |
||||
BoundParams::Positional(values) => { |
||||
let expected = stmt.parameter_count(); |
||||
if values.len() != expected { |
||||
return Err(DbError::Other(format!( |
||||
"sqlite: statement expects {expected} positional parameter(s) but {} were given", |
||||
values.len() |
||||
))); |
||||
} |
||||
for (offset, value) in values.iter().enumerate() { |
||||
stmt.raw_bind_parameter(offset + 1, value)?; |
||||
} |
||||
Ok(()) |
||||
} |
||||
BoundParams::Named(map) => { |
||||
for index in 1..=stmt.parameter_count() { |
||||
// `parameter_name` returns the placeholder with its sigil, e.g. ":id";
|
||||
// `None` means an anonymous `?`, which can't be filled by name.
|
||||
let name = stmt |
||||
.parameter_name(index) |
||||
.ok_or_else(|| { |
||||
DbError::Other(format!( |
||||
"sqlite: statement has a positional placeholder at position {index} but named parameters were given" |
||||
)) |
||||
})? |
||||
.to_string(); |
||||
let key = &name[1..]; // strip the leading sigil
|
||||
let value = map |
||||
.iter() |
||||
.find(|(k, _)| k == key) |
||||
.map(|(_, v)| v) |
||||
.ok_or_else(|| { |
||||
DbError::Other(format!("sqlite: missing value for named parameter '{name}'")) |
||||
})?; |
||||
stmt.raw_bind_parameter(index, value)?; |
||||
} |
||||
Ok(()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SQLite value -> Lua
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn sqlite_value_to_lua(lua: &Lua, value: Value) -> LuaResult<LuaValue> { |
||||
Ok(match value { |
||||
Value::Null => LuaValue::Nil, |
||||
Value::Integer(i) => LuaValue::Integer(i), |
||||
Value::Real(f) => LuaValue::Number(f), |
||||
Value::Text(s) => LuaValue::String(lua.create_string(&s)?), |
||||
Value::Blob(b) => LuaValue::String(lua.create_string(&b)?), |
||||
}) |
||||
} |
||||
|
||||
/// One database row as (column name, value) pairs.
|
||||
type Record = Vec<(String, Value)>; |
||||
|
||||
fn record_to_lua(lua: &Lua, record: Record) -> LuaResult<LuaTable> { |
||||
let table = lua.create_table_with_capacity(0, record.len())?; |
||||
for (name, value) in record { |
||||
// A SQL NULL maps to Lua nil; setting a key to nil leaves it absent,
|
||||
// which reads back as nil — exactly what we want.
|
||||
if matches!(value, Value::Null) { |
||||
continue; |
||||
} |
||||
table.raw_set(name, sqlite_value_to_lua(lua, value)?)?; |
||||
} |
||||
Ok(table) |
||||
} |
||||
|
||||
fn rows_to_lua(lua: &Lua, rows: Vec<Record>) -> LuaResult<LuaTable> { |
||||
let array = lua.create_table_with_capacity(rows.len(), 0)?; |
||||
for (i, record) in rows.into_iter().enumerate() { |
||||
array.raw_set(i + 1, record_to_lua(lua, record)?)?; |
||||
} |
||||
Ok(array) |
||||
} |
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Blocking workers (run on the tokio blocking pool)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Run an INSERT/UPDATE/DELETE-style statement, returning rows changed.
|
||||
async fn run_execute(handle: ConnHandle, sql: String, params: BoundParams) -> Result<usize, DbError> { |
||||
tokio::task::spawn_blocking(move || -> Result<usize, DbError> { |
||||
let guard = handle.lock().unwrap_or_else(|e| e.into_inner()); |
||||
let conn = guard.as_ref().ok_or(DbError::Closed)?; |
||||
let mut stmt = conn.prepare_cached(&sql)?; |
||||
bind_params(&mut stmt, ¶ms)?; |
||||
Ok(stmt.raw_execute()?) |
||||
}) |
||||
.await |
||||
.unwrap_or_else(|e| Err(DbError::Other(format!("sqlite: background task failed: {e}")))) |
||||
} |
||||
|
||||
/// Run a SELECT-style statement, materializing every row.
|
||||
async fn run_query(handle: ConnHandle, sql: String, params: BoundParams) -> Result<Vec<Record>, DbError> { |
||||
tokio::task::spawn_blocking(move || -> Result<Vec<Record>, DbError> { |
||||
let guard = handle.lock().unwrap_or_else(|e| e.into_inner()); |
||||
let conn = guard.as_ref().ok_or(DbError::Closed)?; |
||||
let mut stmt = conn.prepare_cached(&sql)?; |
||||
// Column names must be captured before the mutable borrows below.
|
||||
let columns: Vec<String> = stmt.column_names().into_iter().map(String::from).collect(); |
||||
bind_params(&mut stmt, ¶ms)?; |
||||
|
||||
let mut out = Vec::new(); |
||||
let mut rows = stmt.raw_query(); |
||||
while let Some(row) = rows.next()? { |
||||
let mut record = Vec::with_capacity(columns.len()); |
||||
for (i, name) in columns.iter().enumerate() { |
||||
record.push((name.clone(), row.get::<_, Value>(i)?)); |
||||
} |
||||
out.push(record); |
||||
} |
||||
Ok(out) |
||||
}) |
||||
.await |
||||
.unwrap_or_else(|e| Err(DbError::Other(format!("sqlite: background task failed: {e}")))) |
||||
} |
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UserData methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
impl UserData for SqliteConn { |
||||
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) { |
||||
// I/O-bound methods run on a blocking thread so SQLite never stalls the
|
||||
// tokio event loop.
|
||||
methods.add_async_method("execute", |_, this, (sql, params): (String, Option<LuaTable>)| { |
||||
let handle = this.0.clone(); |
||||
let bound = convert_params(params); |
||||
async move { |
||||
let changed = run_execute(handle, sql, bound?).await.map_err(mlua::Error::external)?; |
||||
Ok(changed as i64) |
||||
} |
||||
}); |
||||
|
||||
methods.add_async_method("query", |lua, this, (sql, params): (String, Option<LuaTable>)| { |
||||
let handle = this.0.clone(); |
||||
let bound = convert_params(params); |
||||
async move { |
||||
let rows = run_query(handle, sql, bound?).await.map_err(mlua::Error::external)?; |
||||
rows_to_lua(&lua, rows) |
||||
} |
||||
}); |
||||
|
||||
methods.add_async_method("queryOne", |lua, this, (sql, params): (String, Option<LuaTable>)| { |
||||
let handle = this.0.clone(); |
||||
let bound = convert_params(params); |
||||
async move { |
||||
let mut rows = run_query(handle, sql, bound?).await.map_err(mlua::Error::external)?; |
||||
if rows.is_empty() { |
||||
Ok(LuaValue::Nil) |
||||
} else { |
||||
Ok(LuaValue::Table(record_to_lua(&lua, rows.swap_remove(0))?)) |
||||
} |
||||
} |
||||
}); |
||||
|
||||
// These only read in-memory connection state, so they stay synchronous.
|
||||
methods.add_method("lastInsertRowid", |_, this, ()| { |
||||
let guard = this.0.lock().unwrap_or_else(|e| e.into_inner()); |
||||
let conn = guard.as_ref().ok_or_else(|| mlua::Error::external(DbError::Closed))?; |
||||
Ok(conn.last_insert_rowid()) |
||||
}); |
||||
|
||||
methods.add_method("changes", |_, this, ()| { |
||||
let guard = this.0.lock().unwrap_or_else(|e| e.into_inner()); |
||||
let conn = guard.as_ref().ok_or_else(|| mlua::Error::external(DbError::Closed))?; |
||||
Ok(conn.changes() as i64) |
||||
}); |
||||
|
||||
methods.add_method("close", |_, this, ()| { |
||||
// Dropping the connection releases the SQLite handle; later use errors.
|
||||
*this.0.lock().unwrap_or_else(|e| e.into_inner()) = None; |
||||
Ok(()) |
||||
}); |
||||
} |
||||
} |
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Installation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub(super) fn install(lua: &Lua) -> LuaResult<()> { |
||||
let sqlite = lua.create_table()?; |
||||
|
||||
sqlite.set( |
||||
"connect", |
||||
lua.create_function(|_, path: String| { |
||||
let conn = rusqlite::Connection::open(&path).map_err(mlua::Error::external)?; |
||||
Ok(SqliteConn(Arc::new(Mutex::new(Some(conn))))) |
||||
})?, |
||||
)?; |
||||
|
||||
lua.globals().set("sqlite", sqlite)?; |
||||
|
||||
// The Lua layer adds the transaction helpers (begin/commit/rollback/
|
||||
// transaction) onto the connection's shared method table. mlua hides the
|
||||
// metatable from Lua (`__metatable` is set), so we reach the `__index`
|
||||
// method table from Rust and hand it to the Lua chunk. Every connection of
|
||||
// this type shares that one table, so the helpers apply to all of them. A
|
||||
// closed (None) sample handle opens no database and is cheap to discard.
|
||||
let init: mlua::Function = lua.load(SQLITE_LUA).set_name("@[stdlib/sqlite]").eval()?; |
||||
let sample = lua.create_userdata(SqliteConn(Arc::new(Mutex::new(None))))?; |
||||
let methods: LuaTable = sample.metatable()?.get("__index")?; |
||||
init.call::<()>(methods) |
||||
} |
||||
Loading…
Reference in new issue