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 and not liking some issue others already noted.
This middleware performance is about 14% faster than SQlx for at least some SQLite workloads. (That could be my misunderstanding of SQLx rather than an inherent performance difference.) For current evidence, see our benchmark results.
Goals
- Convenience functions for common async SQL query patterns
- Keep underlying flexibility of
deadpoolconnection pooling - 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.
Importing
You can use the prelude to import everything you need, or import item by item.
use *;
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
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. Use execute_batch when you have no parameters to pass.
// simple api for batch queries
let ddl_query =
include_str!;
conn.execute_batch.await?;
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.query.select.await?;
// Or pass the SQL string directly
let results2 = conn.query.select.await?;
Custom logic in between transactions
Here, because the underlying libraries are different, unfortunately, if you need custom app logic between transaction() and commit(), the code becomes a little less DRY.
use Transaction as PgTransaction;
use convert_sql_params;
use PostgresParams;
use ;
use *;
use ;
use Statement as PgStatement;
pub 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 with the
*_with_translation(..., true)constructors (or by togglingtranslate_placeholdersonConfigAndPool) 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?;
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. No deadpool support (yet).default: Enables common backends (sqlite, postgres). Enable others as needed.test-utils: Enables test utilities for internal testing
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
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:10),ConfigAndPool::new_mssql(src/mssql/config.rs:19), andMiddlewarePool::get(src/pool/types.rs:66). We also call out the rationale in Async Design Decisions.#[allow(clippy::useless_conversion)]is used once to satisfyrusqlite::params_from_iter, which requires an iterator type that Clippy would otherwise collapse away (src/sqlite/params.rs:79).#[allow(unreachable_patterns)]guards catch-all branches that only fire when a backend feature is disabled, preventing false positives when matching onMiddlewarePoolConnection(src/pool/connection.rs:102,src/executor.rs:64,src/executor.rs:97,src/executor.rs:130,src/pool/interaction.rs:40,src/pool/interaction.rs:78).#[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:10,src/pool/interaction.rs:51).
Release Notes
- 0.3.0 (unreleased): Defaulted to the fluent query builder for prepared statements (older
execute_select/execute_dmlhelpers onMiddlewarePoolConnectionwere removed), expanded placeholder translation docs and examples, and improved Postgres integer binding to downcast toINT2/INT4when inferred. - 0.1.9 (unreleased): 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).