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. This page is about those databases: the ones that run on Turbine's default Postgres path (CockroachDB, YugabyteDB, AlloyDB, TimescaleDB, Neon, Supabase, and friends). They need no dialect; they reuse the Postgres one directly, with at most a thin adapter for migration locking or catalog quirks.

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.

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, see below
CockroachDBcockroachdbExperimentalMeasured once against v26.2.5, see below

What "14+ (tested)" means. CI runs the full integration suite against PostgreSQL 14, 15, 16 and 17 on every change, so 14 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 work, it is simply untested. If you run one and hit a problem, that is a bug report worth filing.

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 relations, advisory locks for migrations, 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 is an extension, not a fork, the underlying database is standard PostgreSQL.

No adapter is needed. Hypertables introspect as regular tables via information_schema.

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 are materialized views, not tables)
  • 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, so the query surface is expected to behave the same. Turbine has no YugabyteDB test suite: nothing in this section has been run against a live server, so treat it as a list of things to check on your own cluster, and please report what you find.

The one difference documented by Yugabyte itself: 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, once the CLI wiring above lands.

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);

Expected differences (unverified)#

AreaExpected differenceImpact if true
Advisory locksPer-node, not cluster-wideUse the yugabytedb adapter once wired. Check 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_aggUnknownCheck 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 wherever possible. These behaviours change between CockroachDB majors, and there is no CockroachDB suite in CI, so this is one run at one point in time, not a standing guarantee.

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 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 YugabyteDB callout above). And note the last row: even once the wiring lands, the row-estimate override as written cannot work on v26.2.5, because crdb_internal is no longer readable by default. That is an adapter bug, not a documentation gap.

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, 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): set_config('app.tenant', …, true) works, and ALTER TABLE … ENABLE ROW LEVEL SECURITY plus CREATE POLICY were both accepted
  • Statement timeouts: set_config('statement_timeout', '250ms', true) cancelled a pg_sleep(3) after 250ms with SQLSTATE 57014; transaction_timeout also works
  • pgvector: CREATE EXTENSION vector, a VECTOR(3) column, the <->, <=> and <#> operators, and an hnsw index all work (ivfflat does not, see below)
  • 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
  • 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 lastIt 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 columnscolumn_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 isolationserializableMore 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 / NOTIFYUnsupported. 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

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.js';
 
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 hit issues with a specific PG-compatible database, open an issue; adapters are straightforward to add.