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#
| 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, see below |
| CockroachDB | cockroachdb | Experimental | Measured 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 generateintrospects hypertables the same as regular tablesfindManywithorderBy: { 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 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, 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)#
| Area | Expected difference | Impact if true |
|---|---|---|
| Advisory locks | Per-node, not cluster-wide | Use the yugabytedb adapter once wired. Check 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 | Check 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#
| 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 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_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, 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):set_config('app.tenant', …, true)works, andALTER TABLE … ENABLE ROW LEVEL SECURITYplusCREATE POLICYwere both accepted- Statement timeouts:
set_config('statement_timeout', '250ms', true)cancelled apg_sleep(3)after 250ms with SQLSTATE57014;transaction_timeoutalso works - pgvector:
CREATE EXTENSION vector, aVECTOR(3)column, the<->,<=>and<#>operators, and anhnswindex all work (ivfflatdoes not, see below) - 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 pathturbine 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 | 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 | 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 | 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 | 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 |
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:
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 hit issues with a specific PG-compatible database, open an issue; adapters are straightforward to add.