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#

DatabaseAdapterStatusNotes
PostgreSQL 14+ (tested)None (default)FullNative target. See the note below.
AlloyDBalloydb (no-op)FullGoogle's PG storage engine
TimescaleDBtimescale (no-op)FullPG extension, hypertables introspect fine
NeonNoneFullUse turbine-orm/serverless for HTTP driver
SupabaseNoneFullStandard Postgres, connect directly
YugabyteDByugabytedbExperimentalUntested. No YugabyteDB has ever been run against Turbine, see below
CockroachDBcockroachdbExperimentalMeasured 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 / nested with relations
  • Advisory locks for migrations
  • Standard information_schema introspection
  • 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 generate introspects hypertables the same as regular tables
  • findMany with orderBy: { 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 untyped db.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 param

YugabyteDB#

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_object nested relations
  • Correlated subqueries
  • Parameterized queries and pipeline batching
  • Transactions with SAVEPOINT (nested transactions)
  • information_schema for introspection
  • pg_indexes, pg_type, pg_enum
  • FOR UPDATE / FOR SHARE row-level locking
  • All WHERE operators (LIKE, ILIKE, IN, arrays, JSON)

Known differences#

AreaExpected difference (unverified)Impact if true
Advisory locksPer-node, not cluster-wideUse yugabytedb adapter for migrations. Worth checking whether pg_try_advisory_lock() returns true on two concurrent sessions, which is what CockroachDB does
SequencesMay have gaps under concurrent insertsCosmetic only, IDs are still unique
pg_class.reltuplesMay be stale or zero on new tablesStudio's sidebar row estimates may lag
Index typesHash indexes not supportedGIN, GiST, B-tree all work
NULL ordering in json_aggUnknownWorth 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#

AreaPostgreSQLCockroachDB (with adapter)Measured on v26.2.5
Migration lockspg_try_advisory_lock()_turbine_lock table + SELECT FOR UPDATE NOWAITGenuinely needed. pg_try_advisory_lock() exists but does not lock, see the callout below
Statement timeoutset_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 introspectionpg_indexes viewSame view (compatible since v22.1)Correct. pg_indexes returns usable indexdef strings
Row estimatespg_class.reltuplescrdb_internal.table_row_statisticsDoes 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_object nested relations, at two levels of nesting, with per-relation orderBy and limit
  • 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 as hasMany / belongsTo
  • findMany / count / groupBy with _count and _sum
  • create with RETURNING, and update with the atomic { increment } operator
  • Relation filters (some), which compile to EXISTS subqueries
  • $transaction, including a nested transaction via SAVEPOINT
  • $transaction with sessionContext, the RLS option, which this page previously listed as unsupported
  • Parameterized queries (extended query protocol)
  • information_schema, pg_indexes, pg_enum / pg_type for introspection
  • All WHERE operators tried, including ILIKE
  • turbine studio, including its BEGIN READ ONLY read path (see below)
  • turbine migrate deploy and migrate 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#

AreaMeasured behaviour on v26.2.5Impact
NULL orderingInverted from PostgreSQL. ORDER BY col ASC puts NULLs first; PostgreSQL puts them lastNot cosmetic, it changes results. See the callout below
Advisory locksPresent but inert. pg_try_advisory_lock() returns true to every callerConcurrent turbine migrate runs are not serialized. See the callout above
SERIAL columnsConfirmed: column_default is unique_rowid(). Observed ids like 1200048904107917313IDs are unique, but large and non-sequential. Make sure your generated column type is a 64-bit-safe number or string
Default isolationConfirmed serializableMore retryable errors under contention
pg_class.reltuplesAlways NULL, before and after ANALYZE, on a table with exactly 1000 rowsStudio'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_internalRestricted: 42501 Access to crdb_internal and system is restricted, even as rootThe adapter's row-estimate override cannot work as written
LISTEN / NOTIFYGenuinely unsupported. LISTEN is a 42601 syntax error; pg_notify() is 42883 unknown functiondb.$listen and db.$notify fail with a raw DatabaseError, not a typed UnsupportedFeatureError. See the capability callout
ivfflat vector index42601 unrecognized access method: ivfflatUse hnsw, which works
Pipeline batching, streaming, flatten strategyUntestedUnverified 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 claimMeasured 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 TABLEENABLE 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:

  1. json_agg and json_build_object
  2. Correlated subqueries
  3. $1, $2, ... parameterized queries
  4. information_schema.tables and information_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.