use std::cell::RefCell;
use std::future::Future;
use asupersync::runtime::{Runtime, RuntimeBuilder};
pub use fsqlite::{FrankenError, Row, SqliteValue};
thread_local! {
static DRIVER: RefCell<Option<Runtime>> = const { RefCell::new(None) };
}
fn drive<T>(future: impl Future<Output = T>) -> T {
let runtime = DRIVER
.with(|slot| slot.borrow_mut().take())
.unwrap_or_else(|| {
RuntimeBuilder::current_thread()
.build()
.expect("failed to build FrankenSQLite sync-bridge runtime")
});
let output = runtime.block_on(future);
DRIVER.with(|slot| {
let mut slot = slot.borrow_mut();
if slot.is_none() {
*slot = Some(runtime);
}
});
output
}
fn schema_stale(err: &FrankenError) -> bool {
matches!(
err,
FrankenError::SchemaChanged
| FrankenError::NoSuchTable { .. }
| FrankenError::NoSuchColumn { .. }
| FrankenError::NoSuchIndex { .. }
)
}
fn retry_busy_recovery<T>(
mut attempt: impl FnMut() -> Result<T, FrankenError>,
) -> Result<T, FrankenError> {
const RETRY_BUDGET: std::time::Duration = std::time::Duration::from_secs(5);
const BACKOFF_CAP: std::time::Duration = std::time::Duration::from_millis(250);
let start = std::time::Instant::now();
let mut backoff = std::time::Duration::from_millis(5);
loop {
match attempt() {
Err(FrankenError::BusyRecovery) if start.elapsed() < RETRY_BUDGET => {
std::thread::sleep(backoff);
backoff = (backoff * 2).min(BACKOFF_CAP);
}
other => return other,
}
}
}
fn retry_transient<T>(
conn: &fsqlite::Connection,
mut attempt: impl FnMut() -> Result<T, FrankenError>,
) -> Result<T, FrankenError> {
const RETRY_BUDGET: std::time::Duration = std::time::Duration::from_secs(5);
const BACKOFF_CAP: std::time::Duration = std::time::Duration::from_millis(250);
let was_autocommit = !conn.in_transaction();
let start = std::time::Instant::now();
let mut backoff = std::time::Duration::from_millis(5);
loop {
match attempt() {
Err(error) => {
let retryable = matches!(error, FrankenError::BusyRecovery)
|| (matches!(error, FrankenError::BusySnapshot { .. })
&& was_autocommit
&& !conn.in_transaction());
if !retryable || start.elapsed() >= RETRY_BUDGET {
return Err(error);
}
std::thread::sleep(backoff);
backoff = (backoff * 2).min(BACKOFF_CAP);
}
ok => return ok,
}
}
}
macro_rules! with_engine_retries {
($conn:expr, $sql:expr, $attempt:expr) => {{
let first = retry_transient(&$conn, || $attempt);
match first {
Err(ref err) if schema_stale(err) => {
let _ = drive($conn.prepare($sql));
retry_transient(&$conn, || $attempt)
}
other => other,
}
}};
}
pub struct Connection {
inner: fsqlite::Connection,
}
impl std::fmt::Debug for Connection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Connection")
.field("path", &self.inner.path())
.finish_non_exhaustive()
}
}
impl Connection {
pub fn open(path: impl Into<String>) -> Result<Self, FrankenError> {
let inner = drive(fsqlite::Connection::open(path))?;
Self::from_inner(inner, true)
}
fn from_inner(inner: fsqlite::Connection, serialized: bool) -> Result<Self, FrankenError> {
let connection = Self { inner };
if !serialized {
return Ok(connection);
}
connection.execute("PRAGMA fsqlite.concurrent_mode = OFF")?;
Ok(connection)
}
#[must_use]
pub const fn as_async(&self) -> &fsqlite::Connection {
&self.inner
}
pub fn execute(&self, sql: &str) -> Result<usize, FrankenError> {
with_engine_retries!(self.inner, sql, drive(self.inner.execute(sql)))
}
pub fn execute_with_params(
&self,
sql: &str,
params: &[SqliteValue],
) -> Result<usize, FrankenError> {
with_engine_retries!(
self.inner,
sql,
drive(self.inner.execute_with_params(sql, params))
)
}
pub fn query(&self, sql: &str) -> Result<Vec<Row>, FrankenError> {
with_engine_retries!(self.inner, sql, drive(self.inner.query(sql)))
}
pub fn query_with_params(
&self,
sql: &str,
params: &[SqliteValue],
) -> Result<Vec<Row>, FrankenError> {
with_engine_retries!(
self.inner,
sql,
drive(self.inner.query_with_params(sql, params))
)
}
pub fn query_row(&self, sql: &str) -> Result<Row, FrankenError> {
with_engine_retries!(self.inner, sql, drive(self.inner.query_row(sql)))
}
pub fn query_row_with_params(
&self,
sql: &str,
params: &[SqliteValue],
) -> Result<Row, FrankenError> {
with_engine_retries!(
self.inner,
sql,
drive(self.inner.query_row_with_params(sql, params))
)
}
pub fn prepare(&self, sql: &str) -> Result<PreparedStatement<'_>, FrankenError> {
Ok(PreparedStatement {
inner: retry_busy_recovery(|| drive(self.inner.prepare(sql)))?,
})
}
#[must_use]
pub fn last_insert_rowid(&self) -> i64 {
self.inner.last_insert_rowid()
}
pub fn close(mut self) -> Result<(), FrankenError> {
drive(self.inner.close_in_place())
}
pub fn close_in_place(&mut self) -> Result<(), FrankenError> {
drive(self.inner.close_in_place())
}
}
impl Drop for Connection {
fn drop(&mut self) {
drive(self.inner.close_best_effort_in_place());
}
}
pub struct PreparedStatement<'conn> {
inner: fsqlite::PreparedStatement<'conn>,
}
impl PreparedStatement<'_> {
#[must_use]
pub fn explain(&self) -> String {
self.inner.explain()
}
pub fn query(&self) -> Result<Vec<Row>, FrankenError> {
drive(self.inner.query())
}
pub fn query_with_params(&self, params: &[SqliteValue]) -> Result<Vec<Row>, FrankenError> {
drive(self.inner.query_with_params(params))
}
pub fn query_row(&self) -> Result<Row, FrankenError> {
drive(self.inner.query_row())
}
pub fn query_row_with_params(&self, params: &[SqliteValue]) -> Result<Row, FrankenError> {
drive(self.inner.query_row_with_params(params))
}
pub fn execute(&self) -> Result<usize, FrankenError> {
drive(self.inner.execute())
}
pub fn execute_with_params(&self, params: &[SqliteValue]) -> Result<usize, FrankenError> {
drive(self.inner.execute_with_params(params))
}
}
pub mod compat {
use super::{Connection, FrankenError, drive};
pub use fsqlite::compat::OpenFlags;
pub fn open_with_flags(path: &str, flags: OpenFlags) -> Result<Connection, FrankenError> {
let serialized = flags.contains(OpenFlags::SQLITE_OPEN_READ_WRITE);
let inner = drive(fsqlite::compat::open_with_flags(path, flags))?;
Connection::from_inner(inner, serialized)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_execute_query_roundtrip() {
let dir = tempfile::tempdir().expect("tempdir");
let db = dir.path().join("bridge.db");
let conn =
Connection::open(db.to_string_lossy().into_owned()).expect("open bridge database");
conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
.expect("create table");
let inserted = conn
.execute_with_params(
"INSERT INTO t (v) VALUES (?1)",
&[SqliteValue::from("hello")],
)
.expect("insert row");
assert_eq!(inserted, 1);
let rows = conn.query("SELECT v FROM t").expect("query rows");
assert_eq!(rows.len(), 1);
let row = conn
.query_row_with_params("SELECT v FROM t WHERE id = ?1", &[SqliteValue::from(1i64)])
.expect("query row");
assert_eq!(row.get(0).and_then(SqliteValue::as_text), Some("hello"));
conn.close().expect("close");
}
#[test]
fn prepared_statement_roundtrip() {
let conn = Connection::open(":memory:").expect("open in-memory database");
conn.execute("CREATE TABLE t (k TEXT)").expect("create");
conn.execute_with_params("INSERT INTO t (k) VALUES (?1)", &[SqliteValue::from("a")])
.expect("insert");
let stmt = conn
.prepare("SELECT count(*) FROM t WHERE k = ?1")
.expect("prepare");
let row = stmt
.query_row_with_params(&[SqliteValue::from("a")])
.expect("query");
assert_eq!(row.get(0).and_then(SqliteValue::as_integer), Some(1));
}
#[test]
fn string_in_list_predicates_match_equality_forms() {
let conn = Connection::open(":memory:").expect("open in-memory database");
conn.execute("CREATE TABLE dependencies (issue_id TEXT, depends_on_id TEXT, type TEXT)")
.expect("create");
for (a, b, t) in [
("i1", "i2", "blocks"),
("i2", "i1", "blocks"),
("i3", "i1", "related"),
("i4", "i1", "waits-for"),
] {
conn.execute_with_params(
"INSERT INTO dependencies (issue_id, depends_on_id, type) VALUES (?1, ?2, ?3)",
&[
SqliteValue::from(a),
SqliteValue::from(b),
SqliteValue::from(t),
],
)
.expect("insert");
}
let in_list = conn
.query(
"SELECT issue_id, depends_on_id FROM dependencies \
WHERE type IN ('blocks', 'conditional-blocks', 'waits-for')",
)
.expect("in-list query");
assert_eq!(in_list.len(), 3, "IN-list must match blocks + waits-for");
let eq = conn
.query("SELECT issue_id FROM dependencies WHERE type = 'blocks'")
.expect("equality query");
assert_eq!(eq.len(), 2, "equality predicate must see both blocks rows");
let or_form = conn
.query(
"SELECT issue_id FROM dependencies \
WHERE type = 'blocks' OR type = 'conditional-blocks' OR type = 'waits-for'",
)
.expect("or query");
assert_eq!(or_form.len(), 3, "OR form must agree with the IN form");
}
#[test]
fn connections_default_to_serialized_engine_mode() {
let conn = Connection::open(":memory:").expect("open in-memory database");
let row = conn
.query_row("PRAGMA fsqlite.concurrent_mode")
.expect("query engine mode");
assert_eq!(row.get(0).and_then(SqliteValue::as_integer), Some(0));
}
#[test]
fn writable_compat_connections_use_serialized_engine_mode() {
let dir = tempfile::tempdir().expect("tempdir");
let db = dir.path().join("compat.db");
let path = db.to_string_lossy().into_owned();
let initial = Connection::open(path.clone()).expect("create compat database");
initial.close().expect("close initial connection");
let conn = compat::open_with_flags(&path, compat::OpenFlags::SQLITE_OPEN_READ_WRITE)
.expect("open writable compat connection");
let row = conn
.query_row("PRAGMA fsqlite.concurrent_mode")
.expect("query compat engine mode");
assert_eq!(row.get(0).and_then(SqliteValue::as_integer), Some(0));
}
#[test]
fn schema_changed_enters_the_stale_schema_retry_path() {
assert!(schema_stale(&FrankenError::SchemaChanged));
}
#[test]
fn reentrant_bridge_calls_build_fresh_runtime() {
let row_count = drive(async {
let conn = Connection::open(":memory:").expect("nested open");
conn.execute("CREATE TABLE t (k INTEGER)")
.expect("nested create");
conn.execute("INSERT INTO t (k) VALUES (1)")
.expect("nested insert");
conn.query("SELECT k FROM t").expect("nested query").len()
});
assert_eq!(row_count, 1);
}
}