MySQL, PostgreSQL, SQL Server, and Hrana protocol proxy for SQLite. Connect your existing apps to SQLite without changing a line of code.
litewire accepts connections from MySQL, PostgreSQL, SQL Server, and Hrana HTTP clients, translates the SQL dialect on the fly, and executes against a SQLite backend. Your app thinks it's talking to a real database server -- it's actually talking to SQLite.
PHP/Rails/Django (pdo_mysql, pdo_pgsql, pdo_sqlsrv)
Hrana HTTP clients
|
v
+---------+
| litewire | <-- MySQL :3306 / PG :5432 / TDS :1433 / Hrana :8080
+----+----+
| SQL translation (MySQL/PG/T-SQL -> SQLite)
| or direct passthrough (Hrana -> SQLite)
v
SQLite
- Zero-config development -- no Docker, no database server, just SQLite
- CI/CD -- spin up a full stack with one process, tear it down when done
- Edge deployments -- single binary, no external dependencies
- Drop-in replacement -- existing MySQL/PG/SQL Server apps work without code changes
litewire is not on crates.io yet; build the CLI from this repository. The
binary is behind the cli feature -- a plain cargo build produces no
executable.
# Build the CLI (MySQL + Hrana frontends are in the default feature set)
cargo install --git https://github.com/ephpm/litewire litewire --features cli
# ...or from a checkout:
cargo build --release --features cli # -> target/release/litewire
# Start with a MySQL frontend
litewire --mysql-listen 127.0.0.1:3306 --db app.db
# Start with all frontends (postgres + tds must also be enabled at build
# time: --features cli,postgres,tds)
litewire --mysql-listen 127.0.0.1:3306 --postgres-listen 127.0.0.1:5432 --tds-listen 127.0.0.1:1433 --hrana-listen 127.0.0.1:8080 --db app.db
# Connect from any MySQL client
mysql -h 127.0.0.1 -P 3306 -e "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"
mysql -h 127.0.0.1 -P 3306 -e "INSERT INTO users (name) VALUES ('Alice')"
mysql -h 127.0.0.1 -P 3306 -e "SELECT * FROM users"
# Or PostgreSQL
psql -h 127.0.0.1 -p 5432 -c "SELECT * FROM users"
# Or SQL Server
sqlcmd -S 127.0.0.1,1433 -Q "SELECT * FROM users"
# Or over the Hrana HTTP protocol (no SQL translation -- native SQLite SQL)
curl -s http://127.0.0.1:8080/v2/pipeline -d '{"requests":[{"type":"execute","stmt":{"sql":"SELECT * FROM users","args":[]}}]}'The Hrana frontend serves a stateless subset of sqld's HTTP API: the
/v2/pipeline endpoint with execute/close requests, plus /health and
/version. There is no baton-based session continuity (multi-request
interactive transactions are not supported over Hrana), no batch/sequence
request types, and no authentication. For single-shot queries and CI-style
workloads it can stand in for sqld with a single process and instant startup:
litewire --hrana-listen 127.0.0.1:8080 --db test.dblitewire is also a Rust crate with a pluggable backend. It is not published on crates.io -- depend on it as a git dependency:
[dependencies]
# mysql, hrana, and backend-rusqlite are on by default;
# add features = ["postgres", "tds"] for the other frontends.
litewire = { git = "https://github.com/ephpm/litewire" }use litewire::{LiteWire, backend::Rusqlite};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let backend = Rusqlite::open("app.db")?;
LiteWire::new(backend)
.mysql("127.0.0.1:3306")
.hrana("127.0.0.1:8080")
.serve()
.await
}With the postgres / tds features enabled, .postgres("127.0.0.1:5432")
and .tds("127.0.0.1:1433") add those frontends to the same builder.
| Backend | Feature flag | Use case |
|---|---|---|
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 engine (Rust rewrite of SQLite, Beta upstream; pinned =0.7.0). 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 via HTTP (per-session Hrana streams plus write admission control), so litewire's wire frontends can sit in front of a sqld server.
SQLite has no GRANT and no per-schema ACL, so the database file is the only
tenant boundary. LiteWire::with_authenticator lets one MySQL listener sit in
front of many of them: the backend is chosen per connection, during the
handshake, and a connection that is not accepted gets no backend at all.
use std::sync::Arc;
use litewire::backend::{AuthRequest, ConnectionAuthenticator, SharedBackend};
use litewire::litewire_mysql::native_password;
struct Tenants { /* username -> (SHA1(SHA1(password)), backend) */ }
impl ConnectionAuthenticator for Tenants {
fn authenticate(&self, req: &AuthRequest<'_>) -> Option<SharedBackend> {
let tenant = self.lookup(req.username)?;
// The username only selects a *candidate*; the password is what
// entitles the client to it.
native_password::verify(&tenant.password_hash, req.salt, req.auth_response)
.then(|| tenant.backend.clone())
}
}
LiteWire::with_authenticator(Arc::new(tenants))
.mysql("127.0.0.1:3306")
.serve()
.awaitThe username in a handshake is a claim, not an identity — a hostile client
will happily type its neighbour's. Selecting a backend from it alone is not
isolation. Bind the choice to something the client cannot forge: either a
per-tenant secret (as above), or req.local_addr when tenants are separated by
OS credentials and genuinely cannot reach each other's listener. See the
litewire_backend::auth module docs for the full contract.
MySQL is the only frontend that can resolve a backend per connection;
serve() refuses to start if Hrana, PostgreSQL, or TDS is also configured
under an authenticator, rather than quietly serving them one shared database.
Tenant sessions cannot reach past their own database file. Every tenant's
database is opened by the same process under the same uid, so nothing at the
filesystem level stops a session from ATTACHing its neighbour's file — which
would make the authenticator's boundary a suggestion. litewire therefore
screens every authenticator-established session at the backend boundary:
ATTACH, DETACH, VACUUM with a target (VACUUM INTO '<path>'), and the
path-bearing or schema-reopening PRAGMAs (data_store_directory,
temp_store_directory, writable_schema) are refused with a clean SQL error,
on every backend, regardless of what the engine underneath would do with them.
The screen sees through EXPLAIN wrappers, schema-qualified and quoted
PRAGMA spellings, and statements hidden behind a ;. Everything else —
including bare VACUUM and ordinary tuning pragmas — passes through
untouched.
Single-tenant deployments (a fixed backend, no authenticator) are deliberately
not screened: ATTACH is legitimate and useful in a single-user embedded
setup. The screen keys off the session being tenant-scoped, never off the
statement alone. See litewire_backend::tenant_screen for the full contract.
litewire translates MySQL and PostgreSQL SQL dialects to SQLite on the fly:
| MySQL / PostgreSQL / T-SQL | SQLite |
|---|---|
AUTO_INCREMENT / SERIAL / IDENTITY(1,1) |
INTEGER (relies on SQLite's rowid alias when combined with PRIMARY KEY) |
NOW() / GETDATE() |
datetime('now') |
ON DUPLICATE KEY UPDATE |
ON CONFLICT DO UPDATE |
SHOW TABLES / sys.tables |
SELECT name FROM sqlite_master WHERE type='table' |
DESCRIBE table / sp_columns |
PRAGMA table_info(table) |
INFORMATION_SCHEMA.* |
sqlite_master + PRAGMA queries |
TRUE / FALSE |
1 / 0 |
TOP n |
LIMIT n |
ISNULL(a, b) |
IFNULL(a, b) |
SET NAMES utf8mb4 / SET NOCOUNT ON |
No-op |
Backtick / [bracket] quoting |
Passed through or converted |
See docs/sql-translation.md for the full translation reference -- every rewrite, every emulated metadata query, and what is rejected -- and docs/architecture.md for the architecture.
The MySQL frontend is exercised end-to-end by an in-process test suite
(crates/litewire/tests/mysql_e2e.rs) that drives the wire protocol via
mysql_async -- CRUD, prepared statements, transactions (START TRANSACTION /
BEGIN / COMMIT / ROLLBACK), LAST_INSERT_ID(), SHOW TABLES,
DESCRIBE, INFORMATION_SCHEMA probes, SET NAMES / SET autocommit, and
the metadata queries used at connection setup.
The PostgreSQL and TDS frontends are wire-compatible enough for basic CRUD
against psql / sqlcmd and the extended-query flow used by pdo_pgsql /
pdo_sqlsrv; the TDS frontend is experimental -- authentication is
simplified, the type coverage is a subset (BigInt / Float8 / NVARCHAR /
VarBinary), and the SSL handshake is not implemented. Real SQL Server tools
(SSMS, sqlcmd with encryption) will not connect until those land.
Anywhere you would normally point at MySQL/PG/SQL Server -- PHP PDO drivers,
mysql / psql / sqlcmd CLIs, DBeaver, pgAdmin -- should work for standard
CRUD workloads. SQL_CALC_FOUND_ROWS + SELECT FOUND_ROWS() (WordPress
pagination) is emulated at the session layer. Anything that depends on
server-side features SQLite doesn't have (stored procedures, LOCK TABLES
isolation, row-level locking semantics, dollar-quoted PL/pgSQL bodies, etc.)
will not.
- Single-writer: SQLite is single-writer. Concurrent writes are serialized.
- No stored procedures: SQLite doesn't support them.
- No replication built-in: Use sqld/libSQL for replication, litewire is the protocol layer only.
- Translation coverage: Not every MySQL/PG/T-SQL construct is translatable. Unsupported constructs return a clear error.
See docs/architecture.md for the full design.
MIT