Database Compatibility
Turbine is built for PostgreSQL. Because it generates standard SQL (parameterized queries, json_agg, correlated subqueries), it also works with databases that speak the PostgreSQL wire protocol, and this page is about those databases, the ones that run on Turbine's default Postgres path (CockroachDB, YugabyteDB, AlloyDB, TimescaleDB, Neon, Supabase, and friends).
Looking for SQLite, MySQL, or SQL Server? Those are first-class non-Postgres engines with their own SQL dialects, not wire-compatible Postgres, see Database Engines for setup and the per-engine capability matrix.
Turbine's internal Dialect seam routes query generation, DML, schema DDL, migration-tracking SQL, transactions, streaming, introspection, and TypeScript type mapping through a single contract, quoting, placeholders, JSON aggregation, DML (RETURNING, UNNEST, ON CONFLICT), column types, CREATE TABLE, indexes, and _turbine_migrations statements all flow through it. That seam is what makes the dedicated SQLite / MySQL / SQL Server engines possible without compromising the Postgres path. The PG-wire-compatible databases below need none of that machinery, they reuse the Postgres dialect directly, with at most a thin adapter for migration locking or catalog quirks.
Compatibility Matrix#
| Database | Adapter | Status | Notes |
|---|---|---|---|
| PostgreSQL 14+ (tested) | None (default) | Full | Native target. See the note below. |
| AlloyDB | alloydb (no-op) | Full | Google's PG storage engine |
| TimescaleDB | timescale (no-op) | Full | PG extension, hypertables introspect fine |
| Neon | None | Full | Use turbine-orm/serverless for HTTP driver |
| Supabase | None | Full | Standard Postgres, connect directly |
| YugabyteDB | yugabytedb | Experimental | Untested. No YugabyteDB has ever been run against Turbine, see below |
| CockroachDB | cockroachdb | Experimental | Measured once against v26.2.5, see below. Several core behaviours differ from PostgreSQL |
What "14+ (tested)" means. CI runs the full integration suite against PostgreSQL 14, 15, 16 and 17 on every change, so 14 is the oldest version anything is actually verified on, and that is the supported floor. It is not a hard technical floor: nothing in Turbine's introspection queries or generated SQL is known to require a feature newer than PostgreSQL 12, so a 12 or 13 server may well work fine. It is simply untested, so nothing is promised. If you run one and hit a problem, that is a bug report worth filing rather than a documented limitation.
AlloyDB#
AlloyDB is Google Cloud's PostgreSQL-compatible database service. Under the hood it is PostgreSQL with a custom columnar storage engine, wire protocol, system catalogs, and SQL dialect are identical.
No adapter is needed. Every Turbine feature works out of the box:
json_agg/ nestedwithrelations- Advisory locks for migrations
- Standard
information_schemaintrospection - Transactions, pipelines, streaming
Connection#
import { TurbineClient } from 'turbine-orm';
import { SCHEMA } from './generated/turbine/metadata.js';
// AlloyDB uses standard PG connection strings
const db = new TurbineClient({
connectionString: process.env.ALLOYDB_URL,
// AlloyDB requires SSL in production
ssl: { rejectUnauthorized: true },
}, SCHEMA);If you want to be explicit about the target database:
import { alloydb } from 'turbine-orm/adapters';
// turbine.config.ts
export default {
url: process.env.ALLOYDB_URL,
adapter: alloydb, // purely documentation, no behavior changes
};TimescaleDB#
TimescaleDB is a PostgreSQL extension that adds hypertables (time-series-optimized tables), continuous aggregates, and compression policies. Because it's an extension (not a fork), the underlying database is standard PostgreSQL.
No adapter is needed. Hypertables introspect as regular tables via information_schema. All Turbine features work identically.
Connection#
import { TurbineClient } from 'turbine-orm';
import { SCHEMA } from './generated/turbine/metadata.js';
// Timescale Cloud uses standard PG connection strings
const db = new TurbineClient({
connectionString: process.env.TIMESCALE_URL,
ssl: { rejectUnauthorized: true },
}, SCHEMA);Hypertable considerations#
npx turbine generateintrospects hypertables the same as regular tablesfindManywithorderBy: { time: 'desc' }benefits from Timescale's chunk exclusion- Continuous aggregates are not introspected (they're materialized views, not tables)
- If you need to query a continuous aggregate, drop to raw SQL with the typed
db.sql`...`escape hatch (or untypeddb.raw`...`):
type DailyViews = { day: Date; views: number };
const rows = await db.sql<DailyViews>`
SELECT day, views FROM daily_page_views
WHERE site_id = ${siteId}
ORDER BY day DESC
LIMIT 30
`;
// ^? DailyViews[] , every ${value} becomes a $N paramYugabyteDB#
YugabyteDB is a distributed SQL database that reuses the PostgreSQL query layer. It supports json_agg, subqueries, transactions, and most of pg_catalog.
Turbine's query generation targets PostgreSQL and YugabyteDB accepts it, so the query surface is expected to behave the same. Stated precisely, because the difference matters: Turbine has no YugabyteDB test suite, so "works identically" is an expectation from the shared query layer, not a measured result. Treat the sections below as the differences we know about, not as a complete list.
The one we do know: advisory locks (pg_try_advisory_lock) are scoped per-tserver node, not cluster-wide. In a multi-node deployment, two concurrent turbine migrate commands routed to different nodes could both acquire the "same" lock.
Use the adapter for safe distributed migrations#
import { yugabytedb } from 'turbine-orm/adapters';
// turbine.config.ts
export default {
url: process.env.YUGABYTE_URL,
adapter: yugabytedb,
};The YugabyteDB adapter object replaces advisory locks with a _turbine_lock table using SELECT ... FOR UPDATE NOWAIT. Because YugabyteDB's row locks are distributed (backed by Raft consensus), that would provide a true cluster-wide mutex. Read that in the conditional, per the callout above: the adapter is written and exported, and the CLI does not pass it to the migration runner, so today turbine migrate still takes the per-node advisory lock whatever you set here.
Connection#
import { TurbineClient } from 'turbine-orm';
import { SCHEMA } from './generated/turbine/metadata.js';
// YugabyteDB uses standard PG connection strings
const db = new TurbineClient({
connectionString: 'postgresql://yugabyte:yugabyte@localhost:5433/mydb',
}, SCHEMA);What works identically#
json_agg+json_build_objectnested relations- Correlated subqueries
- Parameterized queries and pipeline batching
- Transactions with
SAVEPOINT(nested transactions) information_schemafor introspectionpg_indexes,pg_type,pg_enumFOR UPDATE/FOR SHARErow-level locking- All WHERE operators (LIKE, ILIKE, IN, arrays, JSON)
Known differences#
| Area | Expected difference (unverified) | Impact if true |
|---|---|---|
| Advisory locks | Per-node, not cluster-wide | Use yugabytedb adapter for migrations. Worth checking whether pg_try_advisory_lock() returns true on two concurrent sessions, which is what CockroachDB does |
| Sequences | May have gaps under concurrent inserts | Cosmetic only, IDs are still unique |
pg_class.reltuples | May be stale or zero on new tables | Studio's sidebar row estimates may lag |
| Index types | Hash indexes not supported | GIN, GiST, B-tree all work |
NULL ordering in json_agg | Unknown | Worth checking first. On CockroachDB this is inverted from PostgreSQL and silently changes which rows an ordered, limited relation returns |
CockroachDB#
CockroachDB is a distributed SQL database that speaks the PostgreSQL wire protocol.
Everything in this section was measured against CockroachDB CCL v26.2.5 (single node, --insecure, arm64), driving real Turbine code rather than hand-written SQL wherever possible. That matters twice over. First, these behaviours change between CockroachDB majors, so a claim verified on v26.2.5 is not a claim about v23. Second, the version is the only reason you should trust any of it: there is still no CockroachDB suite in CI, so this is one run at one point in time, not a standing guarantee.
The measurement corrected more than it confirmed. Four claims that this page previously stated as limitations are false on v26.2.5, and one behaviour it called cosmetic silently returns different rows. Both are called out below.
Use the adapter#
import { cockroachdb } from 'turbine-orm/adapters';
// turbine.config.ts
export default {
url: process.env.COCKROACH_URL,
adapter: cockroachdb,
};What the adapter does#
| Area | PostgreSQL | CockroachDB (with adapter) | Measured on v26.2.5 |
|---|---|---|---|
| Migration locks | pg_try_advisory_lock() | _turbine_lock table + SELECT FOR UPDATE NOWAIT | Genuinely needed. pg_try_advisory_lock() exists but does not lock, see the callout below |
| Statement timeout | set_config('statement_timeout', …, true) | set_config('transaction_timeout', …, true) (v23.1+) | Not needed. statement_timeout works and is enforced. The override is harmless but redundant |
| Index introspection | pg_indexes view | Same view (compatible since v22.1) | Correct. pg_indexes returns usable indexdef strings |
| Row estimates | pg_class.reltuples | crdb_internal.table_row_statistics | Does not work. The override SQL fails with 42501 Access to crdb_internal and system is restricted, even as root |
Every row above describes what the adapter object implements. None of it reaches you through turbine.config.ts today, because the CLI never reads the adapter key: see the warning in the YugabyteDB section. Read this table as the shape of the fix, and plan around the PostgreSQL behaviour in the meantime.
Note the last row: even once the config wiring lands, the row-estimate override as currently written would not work on v26.2.5, because crdb_internal is no longer readable by default. That is an adapter bug rather than a documentation gap, and it is not fixed by this page.
Connection#
import { TurbineClient } from 'turbine-orm';
import { SCHEMA } from './generated/turbine/metadata.js';
const db = new TurbineClient({
connectionString: process.env.COCKROACH_URL,
ssl: { rejectUnauthorized: true }, // CockroachDB Cloud requires SSL
}, SCHEMA);What works, measured on v26.2.5#
Each of these was exercised through Turbine itself against a live server:
json_agg+json_build_objectnested relations, at two levels of nesting, with per-relationorderByandlimit- Correlated subqueries (the core of Turbine's single-query strategy)
introspect(): tables, primary keys, columns, indexes, and foreign-key relations, which came back correctly classified ashasMany/belongsTofindMany/count/groupBywith_countand_sumcreatewithRETURNING, andupdatewith the atomic{ increment }operator- Relation filters (
some), which compile toEXISTSsubqueries $transaction, including a nested transaction viaSAVEPOINT$transactionwithsessionContext, the RLS option, which this page previously listed as unsupported- Parameterized queries (extended query protocol)
information_schema,pg_indexes,pg_enum/pg_typefor introspection- All WHERE operators tried, including
ILIKE turbine studio, including itsBEGIN READ ONLYread path (see below)turbine migrate deployandmigrate status, with the lock caveat above
Transactions default to SERIALIZABLE (confirmed: SHOW transaction_isolation returns serializable), so expect more retryable serialization failures under contention than on PostgreSQL's READ COMMITTED.
Not tried, and therefore unverified: pipeline batching, streaming / cursors, $listen aside from the failure below, and the flatten relation strategy.
Known limitations#
| Area | Measured behaviour on v26.2.5 | Impact |
|---|---|---|
| NULL ordering | Inverted from PostgreSQL. ORDER BY col ASC puts NULLs first; PostgreSQL puts them last | Not cosmetic, it changes results. See the callout below |
| Advisory locks | Present but inert. pg_try_advisory_lock() returns true to every caller | Concurrent turbine migrate runs are not serialized. See the callout above |
SERIAL columns | Confirmed: column_default is unique_rowid(). Observed ids like 1200048904107917313 | IDs are unique, but large and non-sequential. Make sure your generated column type is a 64-bit-safe number or string |
| Default isolation | Confirmed serializable | More retryable errors under contention |
pg_class.reltuples | Always NULL, before and after ANALYZE, on a table with exactly 1000 rows | Studio's schema sidebar shows estimatedRows: 0 for every table. The Data tab is unaffected: it runs a real COUNT(*) and reported the correct total |
crdb_internal | Restricted: 42501 Access to crdb_internal and system is restricted, even as root | The adapter's row-estimate override cannot work as written |
| LISTEN / NOTIFY | Genuinely unsupported. LISTEN is a 42601 syntax error; pg_notify() is 42883 unknown function | db.$listen and db.$notify fail with a raw DatabaseError, not a typed UnsupportedFeatureError. See the capability callout |
ivfflat vector index | 42601 unrecognized access method: ivfflat | Use hnsw, which works |
Pipeline batching, streaming, flatten strategy | Untested | Unverified in either direction |
Four claims this page used to make that are false on v26.2.5
These were asserted, never measured, and the measurement refuted them. They are listed rather than quietly deleted, because if you read this page before today you may have planned around them:
| Former claim | Measured reality |
|---|---|
"BEGIN READ ONLY is rejected, so turbine studio does not work" | False. BEGIN READ ONLY is accepted, and so is the whole Studio read sequence. turbine studio was launched against v26.2.5 and served the UI, the schema tab, the Data tab (rows, COUNT(*) totals, and ILIKE search) and the Query tab (nested json_agg relations) correctly |
"statement_timeout is not supported" | False. set_config('statement_timeout', '250ms', true) cancelled a pg_sleep(3) after 250ms with SQLSTATE 57014. SHOW statement_timeout reflects the value. transaction_timeout also works |
"RLS sessionContext is not supported and fails at the driver" | False. $transaction(..., { sessionContext: … }) succeeded, set_config('app.tenant', …, true) works, and ALTER TABLE … ENABLE ROW LEVEL SECURITY plus CREATE POLICY were both accepted |
| "pgvector is not supported" | Mostly false. CREATE EXTENSION vector succeeded; a VECTOR(3) column, the <->, <=> and <#> operators, and an hnsw index all work. Only ivfflat is missing |
Serverless with CockroachDB#
CockroachDB Serverless exposes a standard PostgreSQL connection string. Use it directly with turbineHttp:
import { turbineHttp } from 'turbine-orm/serverless';
import { Pool } from 'pg';
import { SCHEMA } from './generated/turbine/metadata';
const pool = new Pool({
connectionString: process.env.COCKROACH_URL,
ssl: { rejectUnauthorized: true },
});
export const db = turbineHttp(pool, SCHEMA);Other PG-Compatible Databases#
If your database speaks the PostgreSQL wire protocol and supports:
json_aggandjson_build_object- Correlated subqueries
$1, $2, ...parameterized queriesinformation_schema.tablesandinformation_schema.columns
Then Turbine will likely work out of the box. Connect using the standard pg driver and run npx turbine generate to verify introspection works.
If you encounter issues with a specific PG-compatible database, open an issue, adapters are straightforward to add.