sql-middleware
Sql-middleware is a lightweight async wrapper for tokio-postgres, rusqlite, libsql, experimental turso, and tiberius (SQL Server), with deadpool connection pooling (except Turso, which doesn't have deadpool backend yet), and an async api. A slim alternative to SQLx; fewer features, but striving toward a consistent api.
Motivated from trying SQLx, not liking some issue others already noted, and wanting an alternative.
Goals
- Convenience functions for common SQL query patterns
- Keep underlying flexibility of
deadpool - Minimal overhead (ideally, just syntaxs sugar/wrapper fns)
Feature Flags
By default, postgres and sqlite database backends are enabled. You can selectively enable only the backends you need:
# Only include SQLite and LibSQL support
= { = "0", = ["sqlite", "libsql"] }
Available features:
sqlite: Enables SQLite supportpostgres: Enables PostgreSQL supportmssql: Enables SQL Server supportlibsql: Enables LibSQL support (local or remote)turso: Enables Turso (in-process, SQLite-compatible). Experimental. No deadpool support (yet).default: Enables common backends (sqlite, postgres). Enable others as needed.test-utils: Enables test utilities for internal testing
Examples
More examples available in the tests dir, and this is in-use with a tiny little website app, rusty-golf.
Importing
You can use the prelude to import everything you need, or import item by item.
use *;
Get a connection from the pool
Similar api regardless of db backend.
let mut cfg = deadpool_postgres
new;
cfg.dbname = Some;
cfg.host = Some;
cfg.port = Some;
cfg.user = Some;
cfg.password = Some;
let c = ConfigAndPool
new_postgres
.await?;
let conn = MiddlewarePool
get_connection
.await?;
let cfg =
"file::memory:?cache=shared"
.to_string;
// Or file-based:
// let cfg = "./data.db".to_string();
// same api for connection
// sqlite just has fewer required
// config items (no port, etc.)
let c =
new_sqlite
.await?;
let conn = MiddlewarePool
get_connection
.await?;
Note: The SQLite example applies to SQLite, LibSQL, and Turso. Swap the constructor as needed: new_sqlite(path), new_libsql(path), or new_turso(path). For Turso, there’s no deadpool pooling; get_connection creates a fresh connection.
Batch query w/o params
Same api regardless of db backend.
// simple api for batch queries
let ddl_query =
include_str!;
conn.execute_batch.await?;
Parameterized Queries
Consistent API using QueryAndParams. Only the placeholder syntax differs.
// PostgreSQL uses $-style placeholders
let q = new;
// Execute directly with RowValues
conn.execute_dml
.await?;
// SQLite-compatible backends use ? or ?N
let q = new;
// The same for all `sqlite` variants
conn.execute_dml
.await?;
Note: For LibSQL, construct with ConfigAndPool::new_libsql(path). For Turso, use ConfigAndPool::new_turso(path); there is no deadpool pooling for Turso — get_connection creates a new connection.
Queries without parameters
You can issue no-parameter queries directly, the same for PostgreSQL, SQLite, LibSQL, and Turso:
// Either build a QueryAndParams
let query = new_without_params;
let results = conn.execute_select.await?;
// Or pass the SQL string directly
let results2 = conn.execute_select.await?;
Transactions with custom logic
Here, the APIs differ a bit, because the underlying libraries are different. It doesn't appear easy to make these consistent without hiding underlying library capabilities. This is the most similar way to do queries w this middleware if you need custom app logic between transaction() and commit().
See further examples in the tests directory:
// Get db-specific connection
let pg_conn = match &conn ;
// Get client
// (N/A for Turso; no deadpool yet)
begin_transaction
let client: &mut Client
= pg_conn.as_mut;
// Begin transaction
// Turso: `let tx =
// turso::begin_transaction(t).await?;`
let tx = client.transaction.await?;
// could run custom logic between stmts
// Prepare statement
// Turso: `let mut stmt
// = tx.prepare("... ?1, ?2 ...").await?;`
let stmt = tx.prepare.await?;
// Convert params (Postgres)
// Turso/LibSQL not necessary
let converted_params =
?;
// Execute query
// Turso: `tx.execute_prepared(
// &mut stmt, &q.params).await?;`
// LibSQL: `tx.execute_prepared(
// &stmt, &q.params).await?;`
let rows = tx.execute.await?;
// Commit
tx.commit.await?;
// Get SQLite-specific connection
let sqlite_conn = match &conn ;
// Use interact for async tx
let rows = sqlite_conn
.interact
.await?;
Using the AsyncDatabaseExecutor trait
The AsyncDatabaseExecutor trait provides a consistent interface for database operations:
// This works for PostgreSQL, SQLite, LibSQL, and Turso connections
async
Design Documents
- Async Design Decisions - Explains why some functions are marked with
#[allow(clippy::unused_async)]and our async API design philosophy.
Developing and Testing
- Build with defaults (sqlite, postgres):
cargo build - Include Turso backend:
cargo build --features turso - Run tests (defaults):
cargo test - Run with Turso:
cargo test --features turso - Run with LibSQL:
cargo test --features libsql
Release Notes
- 0.1.9 (unreleased): Switched the project license from BSD-2-Clause to MIT and added third-party notice documentation.