-
Notifications
You must be signed in to change notification settings - Fork 2
Drivers Compatibility
A single view of every @tundralibs/drivers engine — what kind of store it
talks to, which SQL dialect it emits, how it reaches the wire, and which
capabilities it declares. Values are read straight from each engine's
Capabilities object; they are self-describing facts a consumer (norm's
sqlExecutor, application code) can branch on without knowing the concrete
class.
- Kind — SQL (relational), document, or KV (key/value).
-
Dialect — the OQL translator family. The public
engine.Dialectgetter is defined onSQLConnectionEngine— the pool-free base shared bySQLEngine(the pooled SQL engines) and the three pool-free HTTP engines (NeonHttpEngine,TursoEngine,D1Engine), so every SQL engine in the matrix has it. Document/KV engines (MongoEngine,RedisEngine,MemcachedEngine) extend a different, non-SQL base (ConnectionEngine/BaseEngine) that declares noDialectproperty at all —engine.Dialecton one of them is a compile-timeTS2339, not a value that reads asundefinedat runtime, so their cell is—. (MongoEnginestill translates OQL to Mongo operations internally — it just carries no public SQL-dialect getter.) A consumer holding a mixed union of engine types should narrow with'Dialect' in enginerather than reading the property directly — see Branching on capabilities at runtime below. - Transport — how bytes leave the process.
-
Edge-safe — runs on every edge/serverless runtime, including ones with
no socket primitive at all (Vercel Edge), because it uses only global
fetch. Cloudflare Workers and Deno Deploy are partial exceptions to the ❌ column — see Edge / serverless below. - ✅ supported · ❌ not supported · — not applicable (capability is SQL-only; KV/document engines don't declare it).
| Engine | Kind | Dialect | Transport | Edge-safe | Transactions | Prepared statements | Connection pooling | Advisory lock | Referential actions | In-place alter |
|---|---|---|---|---|---|---|---|---|---|---|
PostgresEngine |
SQL | postgres | TCP socket (wire v3, scratch) | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
CockroachEngine (alias)
|
SQL | postgres | TCP socket | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
YugabyteEngine (alias)
|
SQL | postgres | TCP socket | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
AlloyDBEngine (alias)
|
SQL | postgres | TCP socket | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
CitusEngine (alias)
|
SQL | postgres | TCP socket | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
MariaEngine |
SQL | maria |
npm:mariadb (TCP) |
❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
PlanetScaleEngine (alias)
|
SQL | maria |
npm:mariadb (TCP) |
❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
SQLiteEngine |
SQL | sqlite | embedded (in-process) | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ |
MongoEngine |
document | — |
npm:mongodb (TCP) |
❌ | ❌ | ❌ | ❌ | — | — | — |
RedisEngine |
KV | — | TCP (RESP3, scratch) | ❌ | ❌ | ❌ | ✅ | — | — | — |
MemcachedEngine |
KV | — | TCP (text, scratch) | ❌ | ❌ | ❌ | ✅ | — | — | — |
NeonHttpEngine |
SQL | postgres | HTTPS fetch (POST /sql) |
✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
TursoEngine |
SQL | sqlite | HTTPS fetch (POST /v3/pipeline) |
✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
D1Engine |
SQL | sqlite | HTTPS fetch (POST …/query) |
✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
Notes:
-
Aliases reuse a base engine and override only what differs.
CockroachEngineandYugabyteEngineboth extendPostgresEngine: each is a distributed SQL database whose advisory locks are node-local (and, for Yugabyte, version-dependent), unsafe for the cluster-wide mutual exclusion the capability implies — soadvisoryLock: false, everything else stock Postgres.AlloyDBEngine(Google AlloyDB) andCitusEngine(Citus / Azure Cosmos DB for PostgreSQL) also extendPostgresEnginebut are identity aliases with full stock-Postgres capability parity (a distinctEnginevalue for telemetry / discoverability) — AlloyDB is enhanced Postgres and Citus is an extension on Postgres, soadvisoryLockstaystrue(Citus's is coordinator-scoped, which all clients route through). A plainPostgresEnginealso works against both.PlanetScaleEngineextendsMariaEnginefor a standard MySQL-protocol connection to Vitess — which does not enforceFOREIGN KEYconstraints and whoseGET_LOCKis not cluster-wide, so bothreferentialActionsandadvisoryLockarefalse. Byte-compatible backends (Aurora/RDS, AlloyDB, Supabase, TimescaleDB, TiDB, SingleStore) need no alias — point the base engine at them. -
SQLiteEngineis single-connection by design (an embedded, in-process library — no socket, no server), sopooledConnectionsisfalseand there is no server-side advisory lock;ALTERcannot retype a column (inPlaceAlter: false). -
MongoEngine/RedisEngine/MemcachedEnginedeclare only the sharedEngineCapabilities(transactions,preparedStatements,pooledConnections); the SQL-only flags (advisoryLock,referentialActions,inPlaceAlter) don't apply. Mongo and Redis could support transactions (Mongo sessions, RedisMULTI/EXEC) but the surface isn't wired through yet, so both declaretransactions: falsehonestly.
Every value in the matrix above is read straight off engine.Capabilities
(and, for SQL engines, engine.Dialect) — no engine-specific instanceof
checks needed. Capabilities is present on every engine type, so it's
always safe to read; Dialect is not (see the Legend note above), so narrow
with 'Dialect' in engine rather than reading it directly on a value whose
static type might be a non-SQL engine:
import { PostgresEngine } from '@tundralibs/drivers/postgres';
import { RedisEngine } from '@tundralibs/drivers/redis';
type AnyEngine = PostgresEngine | RedisEngine;
/** SQL dialect when the engine has one, `undefined` for document/KV engines. */
function dialectOf(engine: AnyEngine): string | undefined {
// `Dialect` only exists on SQL engines (`SQLConnectionEngine` subclasses,
// covering every SQL row in the matrix above). `'Dialect' in engine` both
// checks its presence and narrows the type — reading `engine.Dialect`
// directly on a `RedisEngine`-typed value is a compile error, not a
// runtime `undefined`.
return 'Dialect' in engine ? engine.Dialect : undefined;
}
const pg = new PostgresEngine('app', { host: 'localhost', database: 'app' });
const cache = new RedisEngine('cache', { host: 'localhost' });
console.log(pg.Capabilities.transactions); // true — safe to call pg.transaction(fn)
console.log(cache.Capabilities.transactions); // false — RedisEngine has no transaction() at all
console.log(dialectOf(pg)); // 'postgres'
console.log(dialectOf(cache)); // undefinedThree edge/HTTP drivers ship today — all pool-free, one request per query, each a thin transport swap over an existing translator:
-
NeonHttpEngine— Postgres-over-HTTP. POSTs each statement tohttps://<host>/sql, reusing the Postgres translator (Dialect = 'postgres') and the shared Postgres text-value decoder, so the emitted SQL and decoded rows are identical toPostgresEngine's. -
TursoEngine— SQLite-over-HTTP (Turso / libSQL Hrana v3). POSTs an[execute, close]pipeline tohttps://<host>/v3/pipeline, reusing the SQLite translator (Dialect = 'sqlite'), the pure Hrana value map, and the shared SQLite error map — so the emitted SQL and mapped errors are identical toSQLiteEngine's, without importing any native SQLite binding. -
D1Engine— SQLite-over-HTTP (Cloudflare D1 REST). POSTs{ sql, params }(positional?) to…/accounts/{accountId}/d1/database/{databaseId}/query, reusing the SQLite translator (Dialect = 'sqlite') and the shared SQLite error map — so the emitted SQL and mapped errors are identical toSQLiteEngine's, without importing any native SQLite binding. Targets D1's REST path (not the in-Workerenv.DBbinding); int64 params are lossy over JSON beyond ±(2^53 − 1).
All three POST over RESTler → the runtime's native global fetch, never opening
a socket (and, for Turso and D1, never loading a native SQLite driver), so they
run unchanged on Cloudflare Workers, Vercel Edge, and Deno Deploy — only the
transport differs from their socket/embedded siblings.
Cloudflare Workers and Deno Deploy are not socket-less, despite the ❌ in
the Edge-safe column above. PostgresEngine, MariaEngine, RedisEngine,
and MemcachedEngine open real TCP connections on both, but not identically:
on Deno Deploy all four connect natively, since it runs the real Deno runtime
and Deno.connect works unchanged. On Workers, PostgresEngine,
RedisEngine and MemcachedEngine connect via @tundralibs/compat's
net.connect(), which runs on cloudflare:sockets — workerd's
outbound-socket primitive — with no nodejs_compat compatibility flag
required; MariaEngine connects too, but by a different path, since it wraps
the third-party mariadb driver directly instead of compat/net — it needs
the nodejs_compat flag to shim node:net underneath that driver. Every one
of the four works the same as on Deno/Bun/Node, as long as the target is
reachable from them. The Edge-safe column tracks fetch-only portability
across every edge runtime, including Vercel Edge, which has no socket
primitive at all; Workers and Deno Deploy are the two edge targets where the
TCP engines also happen to work.
Still planned (see ROADMAP.md → Planned / deferred), a
thin transport swap over its existing translator:
| Driver | Backend | Transport | Reuses translator |
|---|---|---|---|
| PlanetScale-HTTP | PlanetScale | HTTPS (@planetscale/database) |
MariaDB (no FK) |
(The PlanetScaleEngine alias above is the standard MySQL-protocol connection;
the edge HTTP endpoint is a different transport and gets its own engine.)
Foreign-key enforcement over Turso HTTP is server-dependent: TursoEngine
declares referentialActions: true so the translator still emits FK DDL, but
stateless one-shot HTTP holds no PRAGMA foreign_keys = ON, so runtime
enforcement follows the server default (Turso cloud ON; a bare sqld typically
OFF). No pragma is injected. Cloudflare D1, by contrast, enforces foreign
keys by default on every query, so D1Engine's FK constraints are enforced at
runtime without any pragma.
Because each execute() is a standalone HTTP request, there is no session to
carry state across calls. What that means for a higher-level consumer like
norm:
-
KEEPS (all stateless SQL): CRUD, joins, projections,
.encrypt()/.hash(), hashed filters, DDL migrations, RETURNING on INSERT (it's in the translated SQL and the HTTP API returns the rows), LIMIT/OFFSET paging. (Caveat: becausetransactions: false, a multi-statement DDL call is not atomic here — the tx wrapper is skipped, so a partial failure does not roll back statements that already succeeded.) -
LOSES: interactive multi-statement transactions — the only user-visible
loss.
transactions: false, sotransaction()/beginTransaction()reject withUNSUPPORTED_OPERATIONat the base guard (the same seam Mongo uses), before any client is reserved. -
DEGRADES gracefully: advisory locks off (a migrator falls back to a file
lock); no result streaming (norm pages via LIMIT/OFFSET anyway); no
LISTEN/NOTIFY,
SET, temp tables, or session-lifetime prepared statements (norm uses none).
The Neon-Connection-String header carries the database password (it is the
primary authentication mechanism). It lives on the RESTler client's default
headers, never on a thrown error: NeonHttpEngine._wrapDriverError copies only
the safe, query-relevant fields (sqlState, table, column, constraint,
detail, the SQL text), and RESTler redacts the connection-string header in
its own error and log output. An optional bearer token (Neon Authorize / RLS)
is handled the same way.