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 supportturso: Enables Turso (in-process, SQLite-compatible). Experimental. Uses direct handles (no pool backend yet).default: Enables common backends (sqlite, postgres). Enable others as needed.
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, 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, 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; for SQLite the connection is rewrapped automatically by the transaction handle so you can keep using the same MiddlewarePoolConnection afterward. For Turso, begin_transaction takes &mut turso::Connection to enforce compile-time prevention of nested transactions.
use *;
use ;
use ;
use ;
pub async
async
Using the query builder in helpers
// This works for PostgreSQL, SQLite, 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
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 - 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).