Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ the commit that merges this changelog.

### Changed

- Turso engine bumped `=0.7.2` → `=0.8.2` (ephpm#535). No litewire API
change. Database files are compatible in both directions (0.7.2 files,
including one with an uncheckpointed WAL, open under 0.8.2 and the reverse;
the `turso_cdc` log schema stays "v2"). Behaviour changes from upstream:
`PRAGMA journal_mode` changes are now refused on a connection with CDC
capture on (previously accepted, after which capture silently stopped), and
the `turso` binding's `execute_batch` now fails instead of running while a
result set from the same connection is still open.
- Declared MSRV corrected from 1.85 to **1.88** and now verified by a CI job.
1.85 was never true with the locked dependency graph: the Turso engine's
`aristo` dependency requires rustc 1.88.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,5 @@ in CI.
implemented.
- Dependencies are added deliberately, with a comment in the workspace
`Cargo.toml` explaining why that crate (see the existing entries). The
`turso` crate is pinned exactly (`=0.7.0`) — never bump it via
`turso` crate is pinned exactly (`=0.8.2`) — never bump it via
`cargo update`.
55 changes: 30 additions & 25 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 22 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,29 @@ sqlparser = { version = "0.57", features = ["serde", "visitor"] }
rusqlite = { version = "0.32", features = ["bundled", "column_decltype", "functions"] }
# Turso Database — the ground-up Rust rewrite of SQLite
# (github.com/tursodatabase/turso). Pinned exactly: the engine is Beta and
# API/behavior move between releases. 0.7.2 is the latest stable release on
# the 0.7 line (July 2026); 0.8.x is pre-release only and deliberately not
# taken. Bump deliberately, never via `cargo update`.
# API/behavior move between releases. Bump deliberately, never via
# `cargo update`.
#
# 0.7.0 -> 0.7.2 picks up tursodatabase/turso#7855 ("close vtab cursors
# before transaction finalization", merged 2026-07-16, first released in
# 0.7.2): a vdbe fix for pragma-backed table-valued functions where a vtab
# cursor left open across transaction finalization could misbehave. 0.7.0
# predates it.
turso = { version = "=0.7.2", default-features = false }
# 0.7.0 -> 0.7.2 picked up tursodatabase/turso#7855 ("close vtab cursors
# before transaction finalization").
#
# 0.7.2 -> 0.8.2 (ephpm#535; 0.8.2 is the latest stable, October 2026).
# Changes that reach litewire or its embedders:
# - `PRAGMA journal_mode` changes are refused on a connection with CDC
# capture on (0.7.2 accepted them and capture stopped silently); pinned by
# tests/cdc_replication.rs.
# - The Rust binding gates each connection: `Connection::execute_batch` (and
# the new `batch`/`transactional_batch`) need exclusive use and fail with
# "connection is busy with another operation" while a `Rows` from the same
# connection (or a clone of it) is still being read; a `query`/`execute`
# started during a batch fails with "connection is busy executing a
# batch". It errors rather than waits. litewire calls none of these.
# - `TransactionBehavior` gained a `Concurrent` variant; `Builder::with_io`
# takes `impl Into<IoBackend>`.
# - File format and the CDC log schema (`turso_cdc`, version "v2") are
# unchanged: 0.7.2 files, including an uncheckpointed WAL, open under
# 0.8.2 and 0.8.2 files open under 0.7.2 (checked with a two-binary probe).
turso = { version = "=0.8.2", default-features = false }

# HTTP (Hrana)
axum = "0.8"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ and `.tds("127.0.0.1:1433")` add those frontends to the same builder.
|---------|-------------|----------|
| `Rusqlite` | `backend-rusqlite` | Direct in-process SQLite |
| `HranaClient` | `backend-hrana-client` | Remote SQLite via the Hrana HTTP protocol (sqld / Turso) |
| `Turso` | `turso` | **Experimental** — [Turso Database](https://github.com/tursodatabase/turso) engine (Rust rewrite of SQLite, Beta upstream; pinned `=0.7.2`). See `crates/litewire-turso` docs for limitations (no `VACUUM`, no multi-process access) |
| `Turso` | `turso` | **Experimental** — [Turso Database](https://github.com/tursodatabase/turso) engine (Rust rewrite of SQLite, Beta upstream; pinned `=0.8.2`). See `crates/litewire-turso` docs for limitations (no `VACUUM`, no multi-process access) |
| Custom | implement `Backend` trait | Bring your own |

The `HranaClient` backend connects to [sqld](https://github.com/tursodatabase/libsql) via HTTP (per-session Hrana streams plus write admission control), so litewire's wire frontends can sit in front of a sqld server.
Expand Down
2 changes: 1 addition & 1 deletion crates/litewire-turso/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ turso = { workspace = true }
litewire-backend = { workspace = true, features = ["rusqlite"] }
tokio = { workspace = true, features = ["full"] }
# Temp files for CDC replication tests that need two independent
# file-backed databases (Turso 0.7.2's shared-memory `:memory:` isn't
# file-backed databases (Turso's shared-memory `:memory:` isn't
# suitable for primary/replica pairs).
tempfile = "3"
7 changes: 4 additions & 3 deletions crates/litewire-turso/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
//!
//! # Status
//!
//! The Turso engine is **Beta** (pinned here at `turso = "=0.7.0"`, the
//! first non-pre release of the 0.7 line, July 2026). Upstream does not
//! The Turso engine is **Beta** (pinned here at `turso = "=0.8.2"`,
//! October 2026; see the workspace `Cargo.toml` for what each bump
//! changed). Upstream does not
//! yet position it as a production SQLite replacement. This backend exists
//! to gather evidence (benchmarks, file-format round-trips, crash-recovery
//! smokes) behind an explicit opt-in — it is not a default anywhere.
Expand Down Expand Up @@ -40,7 +41,7 @@
//! same data. (No temp-file workaround is needed here, unlike the rusqlite
//! backend, because connections derive from a single engine instance.)
//!
//! # Known-unsupported operations (Turso 0.7.0)
//! # Known-unsupported operations (Turso 0.7.0, unchanged through 0.8.2)
//!
//! Returned as clear [`BackendError`]s rather than silent misbehavior:
//!
Expand Down
54 changes: 54 additions & 0 deletions crates/litewire-turso/tests/cdc_replication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,3 +621,57 @@ async fn apply_batch_reports_an_empty_batch_instead_of_panicking() {
.expect_err("an empty batch is malformed, not a silent no-op");
assert!(err.to_string().contains("empty batch"), "got: {err}");
}

/// A `journal_mode` change on a capturing connection must fail loudly, and
/// capture must still be on afterwards.
///
/// Turso supports only `wal` and `mvcc`; any other mode is a silent no-op,
/// so `mvcc` is the one real change. Turso 0.7.2 accepted WAL -> MVCC on a
/// capturing connection (this test fails there), and upstream documents
/// that capture then stops silently while `capture_data_changes_conn` still
/// reports it on — later writes commit without reaching `turso_cdc`, so they
/// never replicate. 0.8.0 rejects the change instead (upstream "core: reject
/// journal_mode changes while CDC capture is active"). This pins that: a
/// regression would be an unreplicated write, which nothing else notices.
#[tokio::test]
async fn journal_mode_change_on_a_capturing_connection_is_refused() {
let (primary, _p) = temp_factory().await;
let p_conn = primary.raw_connection().unwrap();
enable_cdc(&p_conn).await.unwrap();
p_conn
.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)", ())
.await
.unwrap();

// `query`, not `execute`: the pragma returns the resulting mode as a
// row, which `execute` reports as "unexpected row" on any engine.
let attempt = async {
let mut rows = p_conn.query("PRAGMA journal_mode = mvcc", ()).await?;
while rows.next().await?.is_some() {}
Ok::<_, turso::Error>(())
};
let err = attempt
.await
.expect_err("changing journal_mode while capturing must be refused");
assert!(
err.to_string().contains("CDC capture is active"),
"unexpected error: {err}"
);

// Capture is still live: the next write reaches the log.
p_conn
.execute("INSERT INTO t VALUES (1, 'after')", ())
.await
.unwrap();
let mut tailer = CdcTailer::new(&primary, 0);
let rows: Vec<CdcRow> = drain_batches(&mut tailer)
.await
.into_iter()
.flat_map(|b| b.rows)
.collect();
assert!(
rows.iter()
.any(|r| r.table_name.as_deref() == Some("t") && r.change_type == 1),
"the INSERT after the refused PRAGMA must be captured; got {rows:?}"
);
}
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ Three implementations ship:
sqld's single-writer lock is queued on this side instead of thrashed.
* **`Turso`** (`crates/litewire-turso`, feature `turso`) — **experimental**
in-process backend on the Turso Database engine (the Rust rewrite of SQLite,
Beta upstream, pinned `=0.7.0`). Async-native (no `spawn_blocking`). Known
Beta upstream, pinned `=0.8.2`). Async-native (no `spawn_blocking`). Known
unsupported: `VACUUM`, `ATTACH`/`DETACH`, multi-process access to one file,
byte-exact non-UTF-8 `TEXT` round-trips. It also exposes an experimental CDC
tail/apply API (`crates/litewire-turso/src/cdc.rs`) for external replication
Expand Down
Loading