Backends
Every QuerySet compiles to a backend-neutral QueryPlan. The backend
checks required_features() against BackendCapabilities before any
I/O and returns a typed BackendCapabilityError for anything it cannot
run. Nothing is silently ignored.
The compact matrix is in Backend matrix. This page covers how to connect, dialect notes, and Redis.
Status
Section titled “Status”| Backend | Feature | Execution | Notes |
|---|---|---|---|
| SQLite | default | SqliteBackend |
Row locking, regex, DISTINCT ON, arrays, STDDEV / VARIANCE are capability errors |
| PostgreSQL | postgres |
PgBackend |
Live suite against postgres:17 |
| MySQL 8 | mysql |
MySqlBackend |
8.0.31+; live suite against mysql:8.4 |
| MongoDB | mongodb |
MongoBackend |
5.0+; replica set for transactions; supported QuerySet subset |
| Redis | redis |
RedisStore |
Key/hash/set client. Not a QuerySet backend |
DynamoDB is a design note only; it is not planned for v1.
use siderite_backends::sqlite::SqliteBackend;use siderite::orm::Db;
let db = Db::new(SqliteBackend::connect("sqlite::memory:").await?);Live tests skip themselves when the matching URL variable is unset. Start
the four servers from the root docker-compose.yml. See
Testing.
Canonical storage
Section titled “Canonical storage”PostgreSQL stores every Value natively. SQLite has fewer storage classes,
so the adapter binds and expects the canonical forms from
siderite_orm::types. Decoding accepts both the native and the canonical
form.
| Rust type | PostgreSQL | SQLite | MySQL | MongoDB |
|---|---|---|---|---|
bool |
boolean |
integer 0 / 1 | TINYINT(1) |
boolean |
i16 / i32 / i64 |
smallint / integer / bigint |
integer | SMALLINT / INT / BIGINT |
int64 |
f32 / f64 |
real / double precision |
real | FLOAT / DOUBLE |
double |
Decimal |
numeric |
NUMERIC column; bound as text |
DECIMAL(p,s) |
Decimal128 |
Uuid |
uuid |
text (hyphenated) | CHAR(36) |
Binary subtype 4 |
NaiveDate |
date |
text YYYY-MM-DD |
DATE |
text YYYY-MM-DD |
NaiveTime |
time |
text HH:MM:SS.ffffff |
TIME(6) |
text HH:MM:SS.ffffff |
DateTime<Utc> |
timestamptz |
text RFC3339 (fixed width) | DATETIME(6), UTC session |
BSON datetime (ms; µs truncated) |
serde_json::Value |
jsonb |
text | JSON |
embedded document |
Vec<u8> |
bytea |
blob | BLOB |
Binary |
Decimals on SQLite: a TEXT column would compare "9.99" after "15.00".
With NUMERIC affinity SQLite stores numbers. Very long decimals lose
precision beyond a double.
Numeric results on PostgreSQL: SUM and AVG return numeric; integer
and float types decode a Decimal, so row.get_as::<i64>("total") works
on both backends.
Bulk operations chunk rows to max_params (SQLite 32766, PostgreSQL/MySQL
65535, MongoDB 50000 rows per insert_many).
SQL dialect notes
Section titled “SQL dialect notes”- Placeholders are
$nin PostgreSQL and?in SQLite and MySQL. The same in raw SQL. Parameters are numbered across the whole query, subqueries included. - Identifiers are double-quoted on PostgreSQL/SQLite and backtick-quoted on MySQL.
LIKEpatterns escape%,_, and\and addESCAPE '\'.- SQLite
LIKEis ASCII case-insensitive, so case-sensitivecontains/startswith/endswithcompile toinstrandsubstr. - An empty
INlist compiles to(1=0). An empty needle matches everything. - SQLite needs a
LIMITbeforeOFFSET, so it emitsLIMIT -1 OFFSET n. MySQL emitsLIMIT 18446744073709551615for a bareOFFSET. concattreatsNULLas empty text.- A
NULLvalue is written as the keywordNULL, not bound, so statement text stays distinct for SQLx’s prepared-statement cache. Inraw_sqlaValue::Nullis sent untyped; keepNULLand non-NULLvariants of a raw statement textually different (WHERE x IS NULL) or cast the parameter.
RETURNINGemulation. Generated keys come fromLAST_INSERT_ID()stepped by@@auto_increment_increment, then rows are re-read by key. Other updates and deletes read the affected keysFOR UPDATEinside a transaction. Tables need a primary key; an update withRETURNINGmay not change it.connectpinstime_zone '+00:00', removesNO_BACKSLASH_ESCAPES, and raisesgroup_concat_max_len. A pool given tofrom_poolmust do the same.- Default collation is case-insensitive.
contains/startswith/endswith/ regex are forced case-sensitive. Useutf8mb4_binfor case-sensitive equality. UPDATE .. SETassignments are reordered so every right-hand side sees the old values. A cycle (SET a = b, b = a) isInvalidPlan.- Subqueries with
LIMIT, and subqueries over the target of anUPDATE/DELETE, are wrapped in derived tables. A correlatedEXISTSover the target table still fails with MySQL 1093. - DDL commits implicitly.
TEXTcannot be a primary key: useVARCHAR(191). See Migrations.
Experimental native MySQL
Section titled “Experimental native MySQL”Enable the mysql-native feature on siderite-backends and explicitly use
mysql::native::NativeMySqlBackend. SQLx MySqlBackend remains the default.
The adapter shares its canonical values and trigger-aware RETURNING engine.
use siderite_backends::mysql::native::NativeMySqlBackend;use siderite::orm::Db;
let backend = NativeMySqlBackend::connect(&database_url).await?;let db = Db::new(backend.clone());// Release transaction handles before disconnecting the pool.backend.close().await?;connect_with accepts NativeMySqlOptions: defaults are 10 connections,
100 waiting callers and a 10-second checkout deadline. Startup initializes
all pool slots; warm repeats this after schema setup, before traffic starts.
Clean ORM sessions retain prepared statements. Raw SQL clears table metadata
and retires its session after use; transaction and schema scopes retain that
session until completion. Cancellation and unfinished transactions retire
their sockets, and interrupted handles cannot commit. Writes are never
automatically retried after a transport error or ambiguous commit.
Native URL settings use the pinned driver’s syntax. TLS, server restart, complete migration recovery and matched performance evidence remain release gates. This adapter is experimental and has not replaced the default driver.
MongoDB
Section titled “MongoDB”- Requires MongoDB 5.0+. Transactions need a replica set (a single-node one
is enough). No savepoints, so
bulk_createinside a transaction is a capability error. - A table is a collection. The primary key column (default
id) is stored as_id. Missing keys are generated as consecutivei64values fromsiderite_counters, outside the transaction, so rollbacks leave gaps. - Reads compile to aggregation pipelines. Plans are checked and compiled entirely before any I/O.
- Predicates follow SQL three-valued logic:
= NULLmeansIS NULL. update()/delete()on a queryset with a limit,distinct, or joins becomespk IN (subquery)and is therefore aSubqueriescapability error.- No foreign keys or cascades. Only
_idis unique, plus indexes made withMongoBackend::create_unique_index. - Joins, subqueries, set operations, window functions, row locking,
DISTINCT ON, and array aggregates are capability errors (HTTP 501).
RedisStore wraps a ConnectionManager with an optional key prefix: keys
(get, set with TTL, set_nx, set_nx_px, del, exists, expire, ttl,
incr_by), hashes, sets, get_json / set_json, and atomic MULTI /
EXEC pipelines. It does not implement QuerySet traits;
BackendCapabilities::redis() rejects every relational feature.
- Values are UTF-8; TTLs are milliseconds (
PX/PEXPIRE), rejected below 1 ms.set_nxsets no TTL (set_nx_pxcreates with one in a singleSET NX PX);setwithout a TTL clears an existing one. - A command Redis rejects while queueing aborts the whole batch; a command
failing during
EXECdoes not undo earlier ones. pipelinedoes not prefix keys: passstore.key(..).delete_namespacematchesprefix*, so end prefixes with a separator.- Single server only: no cluster, pub/sub, lists, sorted sets, streams, or Lua.
RedisCache (feature redis) sits on this store. See
Cache.
Raw queries
Section titled “Raw queries”Db::raw_sql returns a QueryResult. Decode with
result.decode::<Model>() (columns by name),
result.decode_values::<(A, B)>() (columns by position), or
result.scalar(). Row::get_as::<T>(column) decodes one value.
Db::raw_execute returns the affected row count. Values are always bound;
never format untrusted input into the SQL text.
MongoDB rejects raw SQL (Feature::RawSql); use
MongoBackend::raw_command.
See also
Section titled “See also”Prefetch parameter admission
Section titled “Prefetch parameter admission”Prefetch constructs each source key’s canonical identity once and uses a hash set to deduplicate keys. Repeated references share loaded objects; target queries are batched, without one query per source object. An explicit prefetch queryset uses its own database’s capabilities. SQL adapters count its full compiled bind parameters before allocating the remaining IN-list budget; NULL literals do not consume placeholders. Exhausted budgets fail before target I/O. Empty target querysets skip parameter counting and I/O.
Backend::read_parameter_count returns an exact compiled read bind count;
SQL extension backends must implement it to support bounded prefetch.
Db::read_parameter_count exposes the same no-I/O diagnostic. Unknown SQL
counts fail explicitly. MongoDB batching uses its document batch capacity.
Sliced target querysets spanning multiple bind batches are rejected because
repeating a limit/offset per batch would change the queryset’s meaning.
SQLx connection initialization
Section titled “SQLx connection initialization”PostgreSQL/MySQL connect_with(url, options) installs mandatory session
settings and replaces options.after_connect. SQLx keeps that callback
private, so a constructor cannot recover it for transparent composition.
Other pool settings, including checkout validation, remain in effect.
Use connect_with_init(url, options, hook) for custom session setup:
use siderite_backends::connection_init::ConnectionInit;use siderite_backends::postgres::PgBackend;use sqlx::postgres::PgPoolOptions;use std::sync::Arc;
let hook: ConnectionInit<sqlx::Postgres> = Arc::new(|conn, _| { Box::pin(async move { sqlx::Executor::execute(conn, "SET application_name = 'my-app'").await?; Ok(()) })});let backend = PgBackend::connect_with_init( &database_url, PgPoolOptions::new(), hook,).await?;The custom hook runs first on every fresh/replacement connection. Required
UTC (and MySQL SQL-mode/GROUP_CONCAT) settings run afterwards, overriding
conflicting values. A hook must leave no transaction or lock open. Its
failure rejects that physical connection; SQLx retries within the pool
acquisition deadline and controls hook error logging. Callback capture
must be safe across concurrent connections. Wrapping an external pool with
from_pool leaves session initialization and reset obligations to its owner.
Native MySQL retains its separate explicit session/lease policy.
Verified network transports
Section titled “Verified network transports”Enable siderite-backends/tls for SQLx/Redis Rustls support. PostgreSQL
uses sslmode=verify-full and a trusted sslrootcert; MySQL uses
ssl-mode=VERIFY_IDENTITY and ssl-ca. Preferred modes do not establish
verified peer identity and can permit plaintext fallback. Native MySQL
already compiles Rustls; its connect_options accepts SslOpts with private
roots while preserving mandatory adapter/pool settings. Redis uses rediss
and a configured Client plus RedisStore::connect_with for custom roots.
MongoDB already enables Rustls; keep certificate/hostname verification on.
SQLite has no network transport. Keep credentials and URLs out of logs.
The repository’s docs/BACKEND_TLS.md documents deployment options and
scripts/test_tls.py owns the local positive/negative verification fixture.
The rsa exception is reviewed in docs/DEPENDENCY_EXCEPTIONS.md: the pinned
SQLx authentication path encrypts with the server’s public key; no framework
private-key operation was found. This rationale does not cover application
RSA operations or substitute for verified TLS.