sql-middleware
Sql-middleware is a lightweight async wrapper for tokio-postgres, rusqlite, turso, and tiberius (SQL Server), with bb8-backed pools for Postgres/SQLite (and Turso handles) plus deadpool pools for SQL Server. A slim alternative to SQLx; fewer features, but striving toward a consistent api.
Motivated from trying SQLx and not liking some issue others already noted.
Current benches vs. SQL are about 29% faster on the single-row SQLite lookup benchmark, and about 32% faster on the multithread pool checkout/parallel SELECT benchmark. See benchmark results. (Keep in mind this performance difference is one data point and there may be other reasons to use SQLx.) Of note, however, rusqlite (without a connection pool) is by far the fastest, at roughly 1.x msec avg per operation for single-row lookups. You may not need a connection pool or this middleware if you're designing your application to use a single database backend and its likely inevitable you will you pay some performance hit using a middleware over raw backend use.
Goals
- Convenience functions for common async SQL query patterns
- Keep underlying flexibility of connection pooling (
bb8for Postgres/SQLite,deadpoolwhere available) - Minimal overhead (ideally, just syntax sugar/wrapper fns)
- See Benchmarks for details on performance testing.
Examples
More examples available in tests. Also in-use with a tiny little personal website app, rusty-golf.
Feature Flags
By default, postgres and sqlite database backends are enabled. You can selectively enable only the backends you need:
# Only include SQLite and Turso support
= { = "0", = ["sqlite", "turso"] }
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. Uses direct handles (no pool backend yet).default: Enables common backends (sqlite, postgres). Enable others as needed.test-utils: Enables test utilities for internal testing
Deprecated backend:
libsqlis now deprecated in favor of the Turso backend. Thelibsqlfeature is still available today for legacy needs, but support will eventually be removed. Prefer thetursofeature for new work and plan to migrate existing consumers before the next major release.
Parameterized queries for reading or changing data
QueryAndParams gives you a single API for both reads and writes through the query builder. The query builder optionally supports same SQL regardless of backend, even with different parameter placeholders ($1 or ?1, with some limitations). Here is an example that supports PostgreSQL, SQLite, LibSQL, or Turso without duplicating logic.
use NaiveDateTime;
use *;
pub async
Multi-database support without copy/pasting query logic
An example using multiple different backends (sqlite, postgres, turso). Notice the need to not repeat the query logic regardless of backend connection type.
use *;
pub async
Batch query w/o params
Same API regardless of db backend. Full setup, including imports and pool creation. See test8 for compile-ready example.
use ConfigAndPool;
use execute_batch;
async
You can also pass a backend transaction to keep manual control of commit/rollback:
use ;
use execute_batch;
async
Queries without parameters
You can issue no-parameter queries directly, the same for PostgreSQL, SQLite, LibSQL, and Turso:
async
Custom logic in between transactions
Here, because the underlying libraries are different, the snippets can get chatty. You can still tuck the commit/rollback and dispatch logic behind a couple helpers to avoid repeating the same block across backends. commit()/rollback() return a TxOutcome, which carries the rewrapped pooled connection for SQLite (translation flag included) so you can keep using the same MiddlewarePoolConnection afterward.
use *;
use ;
use ;
use ;
use ;
pub async
async
Using the query builder in helpers
// This works for PostgreSQL, SQLite, LibSQL, and Turso connections
async
Further examples
See further examples in the tests directory:
- SQLite test example, SQLite bench example 1, SQLite bench example 2
- Turso test example, Turso bench example 1
- PostgreSQL test example
- LibSQL test example
Placeholder Translation
- Default off. Enable at pool creation via backend options/builders (e.g.,
PostgresOptions::new(cfg).with_translation(true)orConfigAndPool::sqlite_builder(path).translation(true)) to translate SQLite-style?1to Postgres$1(or the inverse) automatically for parameterised calls. - Override per call via the query builder:
.translation(TranslationMode::ForceOff | ForceOn)or.options(...). - Manual path:
translate_placeholders(sql, PlaceholderStyle::{Postgres, Sqlite}, enabled)to reuse translated SQL with your own prepare/execute flow. - Limitations: Translation runs only when parameters are non-empty and skips quoted strings, identifiers, comments, and dollar-quoted blocks; MSSQL is left untouched. Basically, don't rely on this to try to translate
?Xto$Xin complicated, per-dialect specific stuff (like$tag$...$tag$in postgres, this translation is meant to cover 90% of use cases). - More design notes and edge cases live in documentation of the feature.
use *;
let mut conn = config_and_pool.get_connection.await?;
let rows = conn
.query
.translation
.params
.select
.await?;
Developing and Testing
- Build with defaults (sqlite, postgres):
cargo build - Include Turso backend:
cargo build --features turso - Run tests (defaults):
cargo testorcargo nextest run- Notice that
test4_traitdoes have hard-coded testing postgres connection strings. I can't get codex to work with postgres embedded anymore, so when working on this test w codex I've hardcoded those values so I can work around it's lack of network connectivity. You'll have to change them if you want that test to compile in your environment.
- Notice that
- Run with Turso:
cargo test --features turso - Run with LibSQL:
cargo test --features libsql - See also: API test coverage for a map of the public surface to current tests.
Our use of [allow(...)]s
#[allow(clippy::unused_async)]keeps public constructors async so the signature stays consistent even when the current body has no awaits. You’ll see this onConfigAndPool::new_postgres(src/postgres/config.rs),ConfigAndPool::new_mssql(src/mssql/config.rs), andMiddlewarePool::get(src/pool/types.rs). We also call out the rationale in Async Design Decisions.#[allow(clippy::manual_async_fn)]lives on the typed trait impls and re-exports because we exposeimpl Future-returning trait methods withoutasync-trait, requiring manual async blocks. We intentionally skipasync-traitto avoid the boxed futures and blanketSendbounds it injects; sticking withimpl Futurekeeps these adapters zero-alloc and aligned to the concrete backend lifetimes. You’ll see it acrosssrc/typed/traits.rs, the typed backend impls (src/typed/impl_{sqlite,turso,postgres}.rs,src/postgres/typed/core.rs,src/turso/typed/core.rs), and theAny*wrappers (src/typed/any/ops.rs,src/typed/any/queryable.rs).#[allow(unreachable_patterns)]guards catch-all branches that only fire when a backend feature is disabled, preventing false positives when matching onMiddlewarePoolConnectionor the typed wrappers (src/executor/dispatch.rs,src/executor/targets.rs,src/pool/connection/mod.rs,src/pool/interaction.rs,src/typed/any/ops.rs,src/typed/any/queryable.rs).#[allow(unused_variables)]appears around the interaction helpers because the higher-order functions take arguments that are only needed for certain backend combinations (src/pool/interaction.rs).#[allow(unused_imports)]sits on re-exports in the SQLite module to keep the public API visible while some submodules are feature-gated (src/sqlite/mod.rs).#[allow(dead_code)]and#[allow(clippy::too_many_arguments)]are present in the SQL Server backend while we keep the API surface and wiring ready even when the feature is off (src/mssql/{executor.rs,params.rs,config.rs}).
Release Notes
See also the changelog.
- 0.4.0: Introduced the typed API (
typedmodule withAnyIdle/AnyTx, backend wrappers, andTxOutcome) so query/execute flows work consistently across pooled connections and transactions, and swapped Postgres/SQLite pooling to bb8 with new builders and placeholder-translation options to support that API. SQLite now runs on a pooled rusqlite worker with safer transaction semantics, LibSQL is deprecated in favor of Turso, and docs/tests/benches were expanded to cover the new flows. - 0.3.0: Defaulted to the fluent query builder for prepared statements (older
execute_select/execute_dmlhelpers onMiddlewarePoolConnectionwere removed), expanded placeholder translation docs and examples, switched pool constructors to backend options + builders (instead of per-feature constructor permutations), and improved Postgres integer binding to downcast toINT2/INT4when inferred. - 0.1.9: Switched the project license from BSD-2-Clause to MIT, added third-party notice documentation, and introduced optional placeholder translation (pool defaults + per-call
QueryOptions).