Turbine

The turbine-orm/powdb module

turbine-orm/powdb runs the Turbine API on PowDB, a single-node database with its own query language, PowQL. Turbine generates PowQL instead of SQL, so findMany, with, where and create read the same as on PostgreSQL.

Install#

Install the driver for the transport you use. Both are optional peer dependencies.

npm install turbine-orm @zvndev/powdb-embedded   # in-process, no server
npm install turbine-orm @zvndev/powdb-client     # a running powdb-server

The embedded addon ships binaries for macOS on Apple silicon and Linux (glibc) on x64 and arm64. On other platforms, use the networked client.

Connect#

import { schemaDefToMetadata } from 'turbine-orm';
import { powqlSchemaDDL, turbinePowDB } from 'turbine-orm/powdb';
import { schema } from './schema.js'; // defineSchema({ ... })
 
const SCHEMA = schemaDefToMetadata(schema);
 
// Embedded: opens a data directory in-process
const db = await turbinePowDB({ embedded: './data' }, SCHEMA);
 
// Networked: a running powdb-server
const remote = await turbinePowDB('powdb://127.0.0.1:7070', SCHEMA);
 
// Create the tables
for (const statement of powqlSchemaDDL(SCHEMA)) await db.rawUnsafe(statement);
 
const user = await db.table('users').findUnique({ where: { id: 1 }, with: { posts: true } });

turbinePowDB(target, schema, options?) resolves to a TurbineClient. target is one of:

TargetTransport
{ embedded: dir, syncMode?, memoryLimit?, readonly? }Embedded
'powdb://host:port'Networked
{ host, port, ... } (PowdbConnOptions)Networked
A @zvndev/powdb-client Pool, or a PowdbPoolNetworked, your pool

The networked transport needs PowDB 0.7.0 or later and checks the server version on connect. Pass a data directory for embedded, not an open Database handle. disconnect() closes what the factory opened and leaves a pool you passed in open.

Options#

options accepts every client option except the connection settings and replicas, plus:

OptionDefaultWhat it does
relationLoadStrategyone statement on 0.18+'batched' runs one lookup per relation. Below 0.18, 'join' uses server-side joins for eligible relations.
connectionLimit10Pool size for a networked pool the factory builds.
transactionQueueTimeoutMs30000How long a $transaction waits for the write lock before TimeoutError (TURBINE_E002). 0 waits forever.
retryStaleReadsfalseNetworked only. Retry a first-statement read once when an idle socket has gone stale. Writes are never retried.
readonlyfalseRefuse writes locally with ReadOnlyError (TURBINE_E018).
assumeEngineVersionprobedEngine version for feature gating when it cannot be probed.

transactionQueueTimeoutMs, retryStaleReads and readonly are ignored when you pass a PowdbPool: set them on its constructor.

The embedded target takes three more fields:

FieldDefaultWhat it does
syncMode'full''full' fsyncs every commit. 'normal' fsyncs off the commit path: faster writes, but an OS crash or power loss can lose the last interval. 'off' is for test data.
memoryLimitnonePer-query memory budget in bytes.
readonlyfalseOpen read-only for snapshot serving. Cannot be combined with syncMode.

Schema#

PowDB schemas are code-first: defineSchema, then schemaDefToMetadata. powqlSchemaDDL(SCHEMA) returns the PowQL statements that create the tables.

  • Columns map to PowQL str, int, float, bool and json. A Date is stored as integer microseconds.
  • A generated integer primary key uses PowDB's auto id. A defaulted string primary key gets a client-side UUID.
  • An enum column stores its label. A value that is not a label throws ValidationError, and gt / lt compare in declaration order. Ordering by an enum throws TURBINE_E017.
  • Array and binary columns throw ValidationError when the DDL is built. A composite index or composite unique key, other than the primary key, throws TURBINE_E017.

To bootstrap from an existing database, introspectPowdbDatabase reads the live catalog:

import { capabilitiesFromVersion, introspectPowdbDatabase } from 'turbine-orm/powdb';
 
const meta = await introspectPowdbDatabase(
  async (powql) => ({ rows: await db.rawUnsafe(powql) }),
  { capabilities: capabilitiesFromVersion('0.20.0') }, // your engine's version
);

The primary key is a guess. Relations come only from declared entity links, and only when you pass capabilities for PowDB 0.19.1+. defineSchema remains the complete path.

What works#

  • findMany, findFirst, findUnique and the OrThrow forms, count, aggregate, groupBy and explain().
  • Every write, including composite-key upsert, atomic update operators and optimisticLock.
  • with for every relation kind, many-to-many included.
  • Relation filters (some, every, none, is, isNot) and nested writes in create / update.
  • where operators with SQL NULL semantics, including NOT, and mode: 'insensitive' on in / notIn.
  • JSON path filters, ordering and grouping.
  • Global filters, on relations and relation filters too.

Transactions#

PowDB has one writer and no savepoints.

  • Concurrent $transaction calls queue and run one at a time, bounded by transactionQueueTimeoutMs.
  • A nested tx.$transaction throws TURBINE_E017.
  • Calling db.$transaction from inside another transaction's callback throws TURBINE_E017 instead of deadlocking. Use tx.

Limits#

These throw UnsupportedFeatureError (TURBINE_E017):

  • pgvector, LISTEN/NOTIFY, RLS sessionContext, full-text search, array filters, _percentile and distinct.
  • findManyStream, findManyStreamBatches and cursor pagination. Page with limit and offset.
  • Row locks (lock), $transaction({ readOnly: true }), update expressions (expr), and createMany with onConflict or skipDuplicates.
  • JSON contains, pathless JSON equals, contains with % or _ in the value, and nulls: 'first'.
  • Comparisons, ordering, aggregates and arithmetic on a numeric / decimal column, which PowDB stores as text.
  • Composite-key relation filters, nested reads and many-to-many junctions.
  • col() column references in a filter, and nested writes inside createMany (use create).
  • A relation filter that matches more than 10,000 distinct keys.

createMany also requires every row to name the same fields, or it throws ValidationError.

Version-gated features#

Turbine checks the engine version on connect. Below a feature's minimum, the feature is refused with an error that names the version you need, rather than sent as PowQL the engine cannot parse.

FeatureMinimum PowDBOn an older engine
introspectPowdbDatabase0.10TURBINE_E017
json columns, JSON path filters, ordering and grouping0.12TURBINE_E017
Doc-field indexes0.13TURBINE_E017
relationLoadStrategy: 'join'0.13TURBINE_E017 per query; a client default falls back to lookups
Read-only snapshots0.14ConnectionError
One-statement with0.18Falls back to batched lookups, same results
Relations from entity links in introspectPowdbDatabase0.19.1No relations
Comparisons on a PowDB-native datetime column, per-field _count of a nullable column0.20TURBINE_E017

assumeEngineVersion overrides the probe. Setting it higher than the real engine replaces the clear TURBINE_E017 with a PowQL parse error.

Errors#

PowDB failures map to the typed errors: a unique violation is UniqueConstraintError, a lost or stale connection is ConnectionError (TURBINE_E004) with the original as .cause, and a timeout is TimeoutError. wrapPowdbError(err) applies the same mapping to errors from your own driver calls.

See also#