Turbine

Client API

What the client does around every query: timeouts, middleware, query plans, and the options it is created with.

Per-query timeout#

Every method takes timeout in milliseconds, or 0 for none.

import { TimeoutError } from 'turbine-orm';
 
try {
  await db.orders.update({ where: { id }, data: { status: 'shipped' }, timeout: 2_000 });
} catch (err) {
  if (err instanceof TimeoutError && err.outcome === 'not-executed') {
    // Nothing changed: safe to retry.
  }
}

A TimeoutError (E002) carries outcome. 'not-executed' means nothing changed, for example when the PostgreSQL server confirms Turbine's cancel. 'unknown' means a write may have applied, which is the answer on other engines; check before retrying.

Inside $transaction on PostgreSQL, a timed-out statement rolls the transaction back, and $transaction rejects with TransactionRolledBackError (E019) even if you catch the timeout.

Middleware#

db.$use(async (params, next) => {
  const start = Date.now();
  const result = await next(params);
  console.log(`${params.model}.${params.action} took ${Date.now() - start}ms`);
  return result;
});

Middleware can read params, time the call and transform the result. It cannot change the query: editing params.args makes next() throw a TypeError. Put tenant and soft-delete filters in a global filter.

explain#

const plan = await db.posts.explain({ where: { orgId: 7 }, orderBy: { createdAt: 'desc' } });
// ['Sort  (cost=…)', '  ->  Index Scan using posts_org_id_idx on posts …', …]

Returns the plan for the statement findMany(args) would run, as lines. SQLite uses EXPLAIN QUERY PLAN, PowDB its own explain, and SQL Server throws E017. Match node names, not layout.

Client configuration#

turbine() and new TurbineClient() take a TurbineConfig. Quick Start and Serverless cover the connection fields.

const db = turbine({ connectionString: process.env.DATABASE_URL, poolSize: 20 });
OptionDefaultWhat it does
poolSize10Maximum connections
idleTimeoutMs30000Close an idle connection
connectionTimeoutMs5000Give up acquiring a connection (E004)
maxLifetimeSeconds1800Replace an old connection when idle
preparedStatementstrueNamed statements; false for external pools and poolers
statementTimeoutMsunsetPostgreSQL statement_timeout per statement
streamIdleTimeoutMs300000Close an abandoned stream
sqlCache / sqlCacheSizetrue / 1000Per-table SQL template cache
implicitPkOrderingfalseOrder unordered pages by primary key
planCacheModeunsetPostgreSQL plan_cache_mode
utcTimestampstrueZone-less columns as UTC
temporalInfinity'preserve'infinity as Infinity, or 'null'
sqlCommentunsetsqlcommenter tags
loggerconsoleLog output; {} silences it
diagnosticsfrom NODE_ENVAdvice and self-checks
autoToOneJoinMaxRows1000Row ceiling for the 'auto' to-one rule
  • max, idleTimeoutMillis and connectionTimeoutMillis work as aliases.
  • An unknown key logs unknown-config-key with the nearest option, and never throws.
  • Behind a transaction pooler Turbine does not recognize, set preparedStatements: false, or queries fail with prepared statement "..." does not exist.
  • Each connection keeps every named statement it parses. Build select, omit and with from user input only through a fixed allowlist.

Client escape hatches#

MemberWhat it is for
db.table<T>(name)Query a table missing from your generated types
db.pool, db.schemaThe underlying pool and schema metadata
db.statsPool gauges: totalCount, idleCount, waitingCount
db.transaction(fn)A raw pg.PoolClient transaction for hand-written SQL
db.disconnect()Close subscriptions and the pools Turbine owns
db.$ping()Round-trip time in milliseconds
db.$on(event, fn)'query', 'poolError' or 'connectionLost' events
db.<table>.cacheStats()SQL cache hits, misses and hitRate
const audit = db.table<{ id: number; action: string; at: Date }>('audit_log');
const recent = await audit.findMany({ orderBy: { at: 'desc' }, limit: 20 });

See also#