Backends: PostgreSQL, MySQL, Oracle, SQL Server & SQLite¶
Yara ORM selects a database backend by connection URL. The same model and queryset
code runs unchanged across backends — only the URL you pass to YaraOrm.init() differs.
from yara_orm import YaraOrm
await YaraOrm.init("postgres://user:pass@localhost/db") # PostgreSQL (tokio-postgres)
await YaraOrm.init("mysql://user:pass@localhost/db") # MySQL/MariaDB (mysql_async)
await YaraOrm.init("oracle://user:pass@localhost:1521/FREEPDB1") # Oracle (oracle-rs)
await YaraOrm.init("mssql://user:pass@localhost:1433/db") # SQL Server (tiberius)
await YaraOrm.init("sqlite:///path/to/app.db") # SQLite (rusqlite)
PostgreSQL¶
The PostgreSQL backend is built on tokio-postgres with a deadpool connection pool.
- Async, pooled connections kept warm for steady-state latency.
- Prepared-statement caching (
prepare_cached) per pooled connection — on by default, disable withstatement_cache_size=0(see below). - Case-insensitive lookups (
icontains,istartswith, …) use SQLILIKE. - Column and table
description=values become SQLCOMMENTs. - A column of a type the engine cannot decode (e.g.
interval,moneyin raw SQL) raises a clearOperationalErrornaming the column — cast it to text in the query — instead of silently reading back asNone.
URL schemes
Both postgres:// and postgresql:// style URLs are accepted, including
user:password@host:port/dbname and standard query parameters.
Pool and statement-cache tuning¶
A few pool/cache knobs ride along as URL query parameters. They are consumed by
the engine and stripped from the URL before the driver parses it, so they sit
alongside ordinary driver parameters (e.g. sslmode):
await YaraOrm.init(
"postgres://user:pass@host/db"
"?max_size=32&min_size=4&statement_cache_size=0&sslmode=require"
)
| Parameter | Default | Effect |
|---|---|---|
max_size |
16 |
Maximum pooled connections. |
min_size |
0 |
Connections pre-warmed at startup (best effort — the pool keeps no hard minimum). |
statement_cache_size |
nonzero | 0 disables per-connection prepared-statement caching. |
Standard driver parameters pass straight through to tokio-postgres, so
application_name and server settings work via the URL too — handy when migrating
from Tortoise's application_name / server_settings credentials:
await YaraOrm.init(
"postgres://user:pass@host/db"
"?application_name=my-service"
# server_settings via the libpq `options` param (one `-c key=value` each):
"&options=-c%20search_path%3Dmyschema%20-c%20timezone%3DUTC"
)
application_name shows up in pg_stat_activity; the options settings (e.g.
search_path, timezone) are applied on every pooled connection.
PgBouncer / transaction pooling
The PostgreSQL backend caches prepared statements per connection by default.
Behind a transaction-pooling proxy such as PgBouncer, set
statement_cache_size=0 so each statement is prepared and used within a
single pooled checkout — otherwise the proxy can route a Bind to a backend
that never saw the Parse. A non-numeric value (e.g. max_size=lots) raises
a ValueError at init() rather than being silently ignored.
These parameters apply to SQLite too (max_size/min_size/statement_cache_size);
in-memory databases always pin a single connection regardless of max_size.
MySQL¶
The MySQL backend is built on the pure-Rust mysql_async driver and its own
connection pool. It targets MySQL 8.x and also speaks the MariaDB protocol;
driver-qualified schemes (mysql+aiomysql://, mariadb://, ...) are
normalised automatically.
- The same
max_size/min_size/statement_cache_sizeURL parameters as the other backends; everything else passes through to the driver (e.g.require_ssl=truefor TLS, served by rustls — no system OpenSSL needed). On MySQL,min_sizealso bounds the idle connections the pool retains (the driver closes idle connections beyond it); it defaults tomax_sizeso pooled statements never pay a reconnect handshake. - Every session is pinned to UTC and to
ANSI_QUOTES, so portable raw SQL with double-quoted identifiers runs unchanged. String literals must use single quotes (everything the ORM emits already does). - No
INSERT ... RETURNING: new auto-increment primary keys come from the driver-reported last-insert id (single inserts andbulk_create, which backfills a batch from its first id under the default consecutiveinnodb_autoinc_lock_mode).Meta.fetch_db_defaultsis honoured with a follow-upSELECTby primary key. - Upserts render
INSERT IGNORE(ignore_conflicts) and the 8.4-safeINSERT ... AS new ON DUPLICATE KEY UPDATE(update_fields); MySQL matches against any unique key, so an expliciton_conflicttarget is ignored. - Case semantics: the default utf8mb4 collation makes
LIKEcase-insensitive, soicontains/iexact/... use plainLIKEwhile the case-sensitive lookups useLIKE BINARY. Regex lookups renderREGEXP_LIKE(col, ?, 'c')(or'i'). __searchrendersMATCH ... AGAINST; the column needs a FULLTEXT index — declareIndex(fields=["col"], using="fulltext")on the model.- Aware datetimes are stored as their UTC instant in a naive
DATETIME(6)column and read back naive (aware UTC underuse_tz=True).CHAR(36)uuid columns are reconstructed touuid.UUIDon read. - JSON columns cannot be indexed directly on MySQL; a JSON
Index(e.g. a PostgreSQL GIN declaration) is dropped like the other PostgreSQL-only index options.
Oracle¶
Beta
The Oracle backend is beta: the ORM surface is complete — the same model
code, URL and query API as every other backend, and the shared cross-backend
suite passes — and it correctly handles CRUD, relations, migrations,
aggregations, nested transactions/savepoints, IntegrityError and
result sets of any size.
It is not yet marked stable for one reason: it rides on the young pure-Rust
oracle-rs 0.1.x driver. Making the wire protocol reliable required
several fixes that yara-orm carries as a pinned fork
(each proposed upstream); until those land in a published release and the few
remaining gaps close, treat production use with
care and pin your yara-orm version.
The Oracle backend is built on the pure-Rust oracle-rs driver — a native
implementation of Oracle's TNS protocol, with no OCI, ODPI-C or Instant
Client, so the Python wheels stay self-contained. Connections are pooled with
deadpool. It targets Oracle Database 23ai (tested against
gvenzl/oracle-free).
How it maps¶
- Connection. The same
max_size/min_size/statement_cache_sizeURL parameters as the other backends;require_ssl=trueopts into TLS (rustls — no system OpenSSL). Every session is pinned to UTC. - Primary keys. Auto-increment pks use
GENERATED BY DEFAULT ON NULL AS IDENTITYcolumns; the new pk is read back with aRETURNING ... INTOOUT bind (run inside a PL/SQL block, which the driver handles reliably). - Types.
NUMBER(p[,s])for the integer / decimal /bool(NUMBER(1)) family,FLOATfor floats,VARCHAR2/CLOBfor text,VARCHAR2(36)for uuids (reconstructed touuid.UUIDon read),TIMESTAMP(6)for datetimes,DATE,CLOBfor JSON,BLOBfor bytes. Aware datetimes are stored as their UTC instant in a naiveTIMESTAMPand read back naive (aware UTC underuse_tz=True), like MySQL. - Queries. Row slicing renders the SQL-standard
OFFSET ... ROWS FETCH NEXT ... ROWS ONLY; case-insensitive lookups fold both operands withUPPER()(Oracle has noILIKE); regex usesREGEXP_LIKE(col, ?, 'c'|'i'); random ordering usesDBMS_RANDOM.VALUE;GROUP BYlists every selected column (Oracle's strict rule). - Upserts.
ignore_conflicts/update_fieldsupserts (and the m2m join-table insert) render aMERGE. Anignore_conflictsupsert needs an expliciton_conflict=[...]target present in the inserted columns. - Identifiers are quoted lower-case (
"author","age"), consistent with the other backends. Because Oracle folds unquoted identifiers to upper-case, hand-written raw SQL against Oracle must quote its column and table names.
The driver fork¶
yara-orm pins a fork of oracle-rs 0.1.7 that fixes three wire-protocol bugs in
the stock crate; each is proposed upstream:
| Fix | Without it | Upstream |
|---|---|---|
Negotiate END_OF_RESPONSE + read multi-packet responses |
connection desyncs; every query capped at ~100 rows | stiang/oracle-rs#14 |
| Terminate long-form bind data with a zero-length chunk | any string/bytes value over ~252 bytes drops the connection | stiang/oracle-rs#15 |
| Frame marker packets with a 4-byte length in large-SDU mode | constraint violations drop the connection instead of raising ORA- errors |
stiang/oracle-rs#16 |
The fork keeps the pure-Rust / self-contained-wheel guarantee (no OCI/ODPI-C). When these merge and ship, the pin can move back to the crates.io release.
Remaining limitations¶
These are why the backend is beta rather than stable; their tests are skipped:
- Values larger than the server's max
VARCHAR2/RAW(32 767 with extended strings, otherwise 4000) needCLOB/LONGbinding, which is not yet implemented — so a very largeTextField/JSONField/BinaryFieldvalue cannot be inserted (smaller values work). - Custom per-transaction isolation levels are unavailable:
SET TRANSACTION ISOLATION LEVELdrops the connection, so the session default governs. __search(Oracle Text) and JSON__containslookups are not implemented.bulk_createinserts one row per statement (Oracle has no multi-rowVALUES), so large bulk loads are slower than on the other backends.
Everything else in the shared cross-backend suite passes — including AddField /
AlterField migrations, which run against a live Oracle server in CI.
Microsoft SQL Server¶
Beta
The SQL Server backend is beta: the ORM surface is complete — the same
model code, URL and query API as every other backend — and the shared
cross-backend suite (CRUD, relations, aggregations, upserts, transactions
and savepoints, IntegrityError) runs against a live SQL Server 2022 in
CI with no backend-specific skips. It is beta rather than stable because it
landed most recently and a few surfaces are not yet covered (see
remaining limitations). Pin your yara-orm version
for production use.
The SQL Server backend is built on the pure-Rust tiberius TDS driver — no
ODBC, native client or Instant Client, so the Python wheels stay self-contained
(the same invariant as the PostgreSQL/MySQL stacks). Connections are pooled with
deadpool. It targets SQL Server 2017+ and Azure SQL. Both mssql://
and sqlserver:// URLs connect.
How it maps¶
- Connection. The same
max_size/min_size/statement_cache_sizeURL parameters as the other backends;require_ssl=trueopts into TLS (rustls — no system OpenSSL). Aware datetimes are stored UTC-naive inDATETIME2and read back as aware UTC underuse_tz=True(SQL Server'sDATETIMEOFFSETis avoided, as MySQL/SQLite avoid theirs). - Primary keys. Auto-increment pks use
IDENTITY(1,1)columns; the generated value is read back with a batchedSELECT SCOPE_IDENTITY()(T-SQL has noRETURNING, andOUTPUTcannot be a statement suffix — so, like MySQL, it uses the no-RETURNINGpath). An explicit pk value brackets the INSERT withSET IDENTITY_INSERT ON/OFF. - Types.
BIGINT/INT/SMALLINTfor integers,BITfor booleans,NVARCHAR(n)/NVARCHAR(MAX)for text,UNIQUEIDENTIFIER(native GUID) for uuids,DATETIME2(6)for datetimes,DATE/TIME(6),DECIMAL(p,s),FLOAT(53),NVARCHAR(MAX)for JSON,VARBINARY(MAX)for bytes (bound through an explicitCASTso SQL Server accepts it). - Identifiers are
[bracket]-quoted and bind parameters are@P1,@P2, … - Queries. Row slicing renders
OFFSET ... ROWS FETCH NEXT ... ROWS ONLY(which SQL Server requires anORDER BYfor — a stable placeholder ordering is supplied when a query has none); case-insensitive lookups use plainLIKE(default collations are case-insensitive) while case-sensitive lookups fold a binaryCOLLATE; string concatenation usesCONCAT; random ordering usesNEWID();DATEPART/JSON_VALUEback the date-part and JSON lookups. - Upserts.
ignore_conflicts/update_fieldsupserts (and the m2m join-table insert) render aMERGE, which must name its match columns — anignore_conflictsupsert needs an expliciton_conflict=[...]target.
Remaining limitations¶
These are why the backend is beta rather than stable:
- Regular-expression lookups (
__regex/__iregex) raiseUnSupportedError— SQL Server has noREGEXPoperator. SELECT ... FOR UPDATEhas no T-SQL statement suffix (locking is done with table hints), so the row-lock clause is dropped, as on SQLite.- Altering a column's
DEFAULTin a migration raisesUnSupportedError: SQL Server stores defaults as auto-named constraints a migration cannot target portably — change a default with hand-writtenRunSQL. Every other migration operation (AddField,AlterFieldtype/nullability, renames viasp_rename, index drops) renders to T-SQL;AddField/AlterFieldrun against a live server in the cross-backend suite.
SQLite¶
The SQLite backend is built on rusqlite (bundled SQLite). Statements run inline on
the async runtime (long-running work like BEGIN under contention and migration scripts
hops to a blocking thread), and the async bridge itself can be removed entirely with the
opt-in sync fast path.
- Rich types (UUID, JSON, datetime, decimal) are mapped onto SQLite's storage classes and reconstructed on read from the declared column type — so your models behave identically.
-
Datetimes are stored as text in one canonical layout: naive values as
YYYY-MM-DD HH:MM:SS.ffffff, timezone-aware values normalised to UTC asYYYY-MM-DD HH:MM:SS.ffffff+00:00— so naive and aware rows in one column compare and sort chronologically. Rows written by older versions (RFC 3339T-separated text) still decode.Upgrading a SQLite database with aware datetimes written by ≤ 1.9
Old aware rows use a
Tseparator, so they no longer compare correctly against newly written rows or bound query parameters (SQLite compares datetime text lexicographically). Rewrite each affected column once after upgrading — this preserves the stored precision:Naive-only columns (the default) need no rewrite.
- Case-insensitive lookups use
LIKE(SQLite'sLIKEis already case-insensitive for ASCII), sinceILIKEis PostgreSQL-only. This is handled for you by the dialect. - Foreign keys are enforced.
PRAGMA foreign_keys=ONis applied to every pooled connection, soon_deleteactions (CASCADE / SET NULL / RESTRICT) and referential integrity behave the same as on PostgreSQL. File databases also run in WAL mode with a 5-second busy timeout. - Transactions begin with
BEGIN IMMEDIATE, taking the write lock up front so concurrent read-then-write transactions queue on the busy timeout instead of failing instantly withdatabase is locked. - URL query parameters are validated:
sqlite://app.db?mode=memoryandsqlite://app.db?sync_fast_path=1are supported, and an unrecognised parameter raisesValueErrorinstead of being read as part of the file name.
- Case-insensitive lookups use
Opt-in synchronous fast path (sync_fast_path=1)¶
For microsecond-statement workloads, the per-query asyncio bridge (scheduling the statement on the runtime, waking the event loop, resuming the task) costs far more than the SQLite work itself. Opting in with:
makes every statement run synchronously on the calling thread (with the
GIL released) and return an already-completed awaitable — your code still
awaits everything exactly as before, but each query is ~7× faster
(~6µs instead of ~40µs per point query). sync_fast_path=0 / off keep the
default async bridge; any other value raises ValueError. The flag is
SQLite-only — a postgres URL carrying it is rejected at init().
Two things stay async regardless: BEGIN (it can queue behind competing
write transactions for up to the 5s busy timeout) and execute_script
(arbitrary migration SQL can run for seconds).
Semantics you are opting into
- The event loop is blocked for the duration of each statement. Great for tests, scripts, benchmarks and low-contention apps where every statement is microseconds; wrong for anything that runs large table scans or contended writes — a write parked on the 5s busy timeout stalls all tasks on the loop, not just the caller.
awaitmay no longer be a scheduling point. Awaiting a completed awaitable resumes immediately without yielding to the event loop, so task interleaving/fairness changes. Code must not rely onawait Model.get(...)giving other tasks a turn (insertawait asyncio.sleep(0)where you need a guaranteed yield).- Exception behaviour is unchanged: errors are stored and raised at the
await, exactly like the async path.
When to choose which
SQLite is ideal for tests, local development, embedded apps and small services; PostgreSQL and MySQL for concurrent, production workloads. Because the model layer is identical, you can develop against SQLite and deploy on PostgreSQL.
Mixing backends¶
Each named connection has its own backend, so a single app can talk to PostgreSQL, MySQL and SQLite databases at once. See Multiple databases.
await YaraOrm.init("postgres://localhost/primary") # default
await YaraOrm.add_connection("cache", "sqlite:///cache.db")
Adding a new backend¶
The backend abstraction is intentionally a two-seam extension point:
- A Rust
Backendtrait implementation (connection, execution, value conversion) plus a scheme match inrust/src/backend/mod.rs. - A
BaseDialectsubclass inpython/yara_orm/dialects.pythat renders SQL for the new database, registered viaregister_dialect(name, DialectClass).
The model and queryset layers never change. See Architecture for the full picture.
See also¶
- Migrations — backend-portable schema changes.
- Performance — PostgreSQL, MySQL and SQLite benchmark results.