Database Engines
Turbine is built for PostgreSQL. The same typed findMany / with / where / create API also runs on SQLite, MySQL 8, SQL Server and PowDB, each behind its own import path.
| Engine | Import | Driver to install | Minimum version |
|---|---|---|---|
| PostgreSQL | turbine-orm | none (pg is included) | PostgreSQL 14 |
| SQLite | turbine-orm/sqlite | none (node:sqlite is built in) | Node 22.5 |
| MySQL | turbine-orm/mysql | mysql2 | MySQL 8.0 (not MariaDB) |
| SQL Server | turbine-orm/mssql | mssql | SQL Server 2016 |
| PowDB | turbine-orm/powdb | @zvndev/powdb-embedded or @zvndev/powdb-client | PowDB 0.7 |
The engine drivers are optional peer dependencies, so npm install turbine-orm installs only pg. The non-PostgreSQL engines are Experimental under the stability policy, so a minor release can change them.
Capability matrix#
✗ means the feature throws UnsupportedFeatureError (TURBINE_E017). n/a means the engine has no such option.
| Feature | PostgreSQL | SQLite | MySQL 8 | SQL Server | PowDB |
|---|---|---|---|---|---|
Nested relations (with) | ✓ | ✓ | ✓ | ✓ | ✓ ¹ |
Atomic update operators (increment, ...) | ✓ | ✓ | ✓ | ✓ | ✓ |
Update expressions (expr) | ✓ | ✓ | ✓ | ✓ | ✗ |
Multi-row upsert (createMany onConflict) | ✓ | ✓ | ✓ 8.0.19+ | ✓ | ✗ |
| Optimistic locking | ✓ | ✓ | ✓ | ✓ | ✓ |
$transaction | ✓ | ✓ ² | ✓ | ✓ | ✓ ³ |
Nested tx.$transaction (savepoints) | ✓ | ✓ | ✓ | ✓ | ✗ |
Read-only transactions (readOnly) | ✓ | ✗ | ✓ | ✗ | ✗ |
Row locks (lock) | all modes | ✗ | 'update', 'share' | ✗ | ✗ |
findManyStream | server cursor | in memory ⁴ | in memory ⁴ | in memory ⁴ | ✗ |
Cursor pagination (cursor) | ✓ | ✓ | ✓ | ✓ | ✗ |
mode: 'insensitive' on in / notIn | ✓ | ✗ | ✗ | ✗ | ✓ |
nulls placement in orderBy | ✓ | ✓ | ✗ | ✗ | 'last' only |
explain() | ✓ | ✓ | ✓ | ✗ | ✓ |
distinct / groupBy({ distinctOn }) | ✓ | ✗ | ✗ | ✗ | ✗ |
Percentiles (_percentile) | ✓ | ✗ | ✗ | ✗ | ✗ |
Full-text search filter | ✓ | ✗ | ✗ | ✗ | ✗ |
Array-column filters (has, hasEvery, ...) | ✓ | ✗ | ✗ | ✗ | ✗ |
| pgvector distance and KNN ordering | ✓ | ✗ | ✗ | ✗ | ✗ |
LISTEN/NOTIFY ($listen / $notify) | ✓ | ✗ | ✗ | ✗ | ✗ |
RLS sessionContext | ✓ | ✗ | ✗ | ✗ | ✗ |
Read replicas (replicas) | ✓ | n/a | n/a | n/a | snapshots ⁵ |
turbine CLI | ✓ | n/a | n/a | n/a | n/a |
¹ One PowQL statement on engine 0.18+. Older engines and many-to-many relations use batched lookups, with the same results.
² SQLite has one connection. An open $transaction holds it, and other callers wait up to busyTimeoutMs.
³ PowDB has a single writer. Concurrent $transaction calls queue and run one at a time.
⁴ The whole result loads into memory, then yields in batchSize batches. Only PostgreSQL streams in constant memory.
⁵ Read-only snapshots, routed by hand.
What is actually Postgres-only#
Every row above with ✗ on all other engines is a PostgreSQL feature. Using one elsewhere throws TURBINE_E017, never broken SQL.
Two pieces of tooling also target PostgreSQL only, with no capability error:
- The
turbineCLI:generate/pull,push,migrate,seed,status,studio,mcp,observe,doctorandmigrate-from-prisma. schemaPushandschemaDiff.schemaToSQLbuilds DDL for any engine's dialect and opens no connection.
If pgvector, realtime or RLS is core to your app, stay on PostgreSQL. The other engines suit tests, edge demos and existing MySQL or SQL Server deployments.
Setup#
Outside PostgreSQL you work code-first. Define the schema with defineSchema and turn it into runtime metadata:
// schema.ts
import { defineSchema, schemaDefToMetadata } from 'turbine-orm';
export const schema = defineSchema({
users: {
id: { type: 'serial', primaryKey: true },
email: { type: 'text', unique: true, notNull: true },
},
posts: {
id: { type: 'serial', primaryKey: true },
userId: { type: 'bigint', notNull: true, references: 'users.id' },
title: { type: 'text', notNull: true },
},
});
export const SCHEMA = schemaDefToMetadata(schema);Each factory takes (target, schema, options?) and returns a TurbineClient. options accepts every client option except the connection settings and replicas, plus the engine options listed below. disconnect() closes what the factory opened and leaves a pool you passed in open.
The factories return the base client type, so the examples reach tables with db.table('users').
SQLite#
npm install turbine-ormimport { schemaToSQL } from 'turbine-orm';
import { sqliteDialect, turbineSqlite } from 'turbine-orm/sqlite';
import { schema, SCHEMA } from './schema.js';
// A file path, ':memory:', or an open DatabaseSync handle
const db = turbineSqlite(':memory:', SCHEMA);
for (const statement of schemaToSQL(schema, { dialect: sqliteDialect })) {
await db.rawUnsafe(statement);
}
const user = await db.table('users').findUnique({ where: { id: 1 }, with: { posts: true } });turbineSqlite is synchronous and adds no dependency.
| Option | Default | What it does |
|---|---|---|
wal | true | WAL journal mode for file databases. |
busyTimeoutMs | 5000 | How long a statement waits for a lock or for another caller's open transaction. |
foreignKeys | true | Turns on PRAGMA foreign_keys. |
- A caller still waiting after
busyTimeoutMsgets a retryableSerializationFailureError(TURBINE_E013). Inside a$transactioncallback usetx: callingdbthere throwsTURBINE_E017. - Columns your schema declares
booleanorjsonread back as booleans and parsed values on Node 22.16+ or 23.11+. Older Node returns1/0and JSON text. numericreads as a JS number. Integers pastNumber.MAX_SAFE_INTEGERread as strings.mode: 'insensitive'usesCOLLATE NOCASE, which folds ASCII only.isolationLevelis ignored, since SQLite transactions are serializable.
MySQL 8#
npm install turbine-orm mysql2import { turbineMysql } from 'turbine-orm/mysql';
import { SCHEMA } from './schema.js';
const db = await turbineMysql('mysql://user:pass@localhost:3306/app', SCHEMA);target is a connection string, a mysql2 config object or a mysql2 pool. A pool you pass in must set namedPlaceholders: true. connectionLimit (default 10) sizes a pool the factory builds.
createManyreturns[], because MySQL has noRETURNING. Query the rows again if you need them. WithonConflictit returns the written rows.createreads the new row back by a key it knows: a primary key indata, anAUTO_INCREMENTid, or a unique key indata. Any other server-generated key (a UUID default) throwsTURBINE_E017before the insert, so pass it indata.- The default collation,
utf8mb4_0900_ai_ci, ignores case and accents in=, unique keys,groupByandORDER BY. Use a binary or_as_cscollation where case must count. - An unconstrained
numericis created asDECIMAL(65,30)and reads back padded to 30 decimal places. NaN,Infinityand an Invalid Date throwValidationErrorinstead of storing0.
SQL Server#
npm install turbine-orm mssqlimport { turbineMssql } from 'turbine-orm/mssql';
import { SCHEMA } from './schema.js';
const db = await turbineMssql('mssql://sa:Passw0rd!@localhost:1433/app', SCHEMA);target is a connection string, an mssql config object or an mssql pool. The schema option sets the database schema for introspection and DDL (default dbo).
- A
createManyover 1000 rows or 2098 parameters is split into chunks inside one transaction. numericreads as a JS number, because the driver has no string mode forDECIMAL. AUNIQUEIDENTIFIERreads back in upper case.- A
json/jsonbcolumn is stored asNVARCHAR(MAX)and reads back parsed. Pathless JSONequalsthrowsTURBINE_E017. - Back an upsert's conflict target with a real
UNIQUEindex or primary key.MERGEalone does not stop two concurrent inserts.
PowDB#
PowDB is a single-node database with its own query language, PowQL. It runs in-process (embedded) or as a server (networked), over the same data.
npm install turbine-orm @zvndev/powdb-embedded # in-process, no server
npm install turbine-orm @zvndev/powdb-client # a running powdb-serverimport { powqlSchemaDDL, turbinePowDB } from 'turbine-orm/powdb';
import { SCHEMA } from './schema.js';
const db = await turbinePowDB({ embedded: './data' }, SCHEMA);
// or: await turbinePowDB('powdb://127.0.0.1:7070', SCHEMA);
for (const statement of powqlSchemaDDL(SCHEMA)) await db.rawUnsafe(statement);PowDB has no savepoints, row locks, cursors or bulk upsert, so those features throw TURBINE_E017 (see the matrix). Setup options, version-gated features and the full list of limits are on the PowDB module page.
Read-only snapshots#
PowDB 0.14+ serves a copy of a data directory strictly read-only, to any number of readers:
const replica = await turbinePowDB({ embedded: './snapshot', readonly: true }, SCHEMA);
// or a powdb-server started with --readonly:
const remote = await turbinePowDB('powdb://replica-host:7070', SCHEMA, { readonly: true });Reads work unchanged. A write throws ReadOnlyError (TURBINE_E018) with reason: 'snapshot', so route it to the primary. The data is as fresh as the last snapshot. See Read Replicas.
Differences you will hit#
Some results follow the engine, and no error marks the difference:
| PostgreSQL | SQLite | MySQL 8 | SQL Server | |
|---|---|---|---|---|
String = | case-sensitive | case-sensitive | follows the collation (default ignores case and accents) | follows the collation |
contains / startsWith / endsWith | case-sensitive | ASCII case-insensitive | follows the collation | follows the collation |
NULL in ascending order | last | first | first | first |
numeric / decimal | exact string | JS number | exact string | JS number |
Pathless JSON equals | document equality | TURBINE_E017 | document equality | TURBINE_E017 |
These agree on all four SQL engines:
- A value read inside
withequals the same column read at top level, includingbigint,numericand binary columns. - A zone-less
datereads as UTC midnight. See Zone-less columns. - Integer
dividetruncates, and_avgover an integer column returns the full fraction. create,update,deleteandupsertreturn the full row. MySQL'screateManyis the exception above.
Postgres-compatible databases#
Databases that speak the PostgreSQL wire protocol (Neon, Supabase, AlloyDB, TimescaleDB, YugabyteDB, CockroachDB) are not separate engines. They use the default turbine-orm import, some with a thin adapter. See Database Compatibility.
See also#
- Database Compatibility: PostgreSQL-wire databases and their adapters.
- The
turbine-orm/powdbmodule: PowDB setup, options and limits. - Writing a custom dialect: the
Dialectcontract, for an engine Turbine does not ship. - Serverless & Edge: driver injection on the PostgreSQL path.
- Typed Errors:
UnsupportedFeatureErrorand the full code table.