API Reference

Every query method, operator, and API surface Turbine exposes. Generated clients attach db.<table> accessors with the full API below — the examples use db.users and db.posts but any introspected table works identically.

findMany

Returns an array of rows matching the query. Supports where, with, orderBy, take / limit, skip, select, and omit.

const users = await db.users.findMany({
  where: { role: 'admin', orgId: 1 },
  orderBy: { createdAt: 'desc' },
  take: 20,
  skip: 0,
});

With nested relations:

const users = await db.users.findMany({
  where: { orgId: 1 },
  with: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: 'desc' },
      limit: 5,
      with: { comments: true },
    },
  },
});

The entire object graph resolves in one SQL statement. users[0].posts[0].comments[0] is fully typed.

select / omit

Pick or drop columns at the query level. Either one — not both.

// Only return id and email
const users = await db.users.findMany({
  select: { id: true, email: true },
});
 
// Return everything except the password_hash column
const users = await db.users.findMany({
  omit: { passwordHash: true },
});

findUnique / findUniqueOrThrow

Look up a row by a unique column (primary key or any unique constraint).

const user = await db.users.findUnique({
  where: { id: 42 },
  with: { posts: true },
});
// user is User | null
 
const user2 = await db.users.findUniqueOrThrow({
  where: { email: 'alice@example.com' },
});
// Throws NotFoundError (TURBINE_E001) if not found

Composite primary keys are passed as an object matching the keys:

const row = await db.memberships.findUnique({
  where: { userId: 1, orgId: 2 },
});

findFirst / findFirstOrThrow

First matching row by the given where + orderBy. Non-unique lookups.

const post = await db.posts.findFirst({
  where: { authorId: 42, published: true },
  orderBy: { createdAt: 'desc' },
});

create

Insert a single row. Returns the full row (including generated columns).

const newUser = await db.users.create({
  data: {
    email: 'alice@example.com',
    name: 'Alice',
    orgId: 1,
  },
});

createMany

Batch insert with a single INSERT ... UNNEST(...) — not N separate inserts.

const users = await db.users.createMany({
  data: [
    { email: 'a@b.com', name: 'A', orgId: 1 },
    { email: 'b@b.com', name: 'B', orgId: 1 },
    { email: 'c@b.com', name: 'C', orgId: 1 },
  ],
});

update / updateMany

Update a single row by unique key, or every row matching a where.

await db.users.update({
  where: { id: 42 },
  data: { name: 'Alice Updated' },
});
 
await db.users.updateMany({
  where: { role: 'guest' },
  data: { role: 'member' },
});

Atomic update operators

For race-safe counter updates, pass an operator object instead of a literal. Turbine generates col = col + $n style SQL so concurrent updates can't lose writes.

await db.posts.update({
  where: { id: 1 },
  data: {
    viewCount: { increment: 1 },
    likesCount: { decrement: 1 },
    score: { multiply: 2 },
    rank: { divide: 2 },
    title: { set: 'New title' }, // explicit set — same as a literal
  },
});

Supported operators on numeric columns: set, increment, decrement, multiply, divide.

delete / deleteMany

await db.users.delete({ where: { id: 42 } });
 
await db.users.deleteMany({
  where: { createdAt: { lt: thirtyDaysAgo } },
});

Empty where (or where: {}) throws ValidationError — Turbine blocks accidental mass deletes. Pass { allowFullTableScan: true } if you really mean it.

upsert

Insert a row or update it if a row matching the where clause already exists. Uses PostgreSQL's INSERT ... ON CONFLICT ... DO UPDATE under the hood — atomic, no race conditions.

The where keys determine the conflict target (must be unique or primary key columns). If no matching row exists, create is inserted. If a row matches, update is applied.

const user = await db.users.upsert({
  where: { email: 'alice@example.com' },
  create: { email: 'alice@example.com', name: 'Alice', orgId: 1 },
  update: { name: 'Alice Updated' },
});

Returns the full row (via RETURNING *) whether it was inserted or updated.

// Upsert with a composite key
const membership = await db.memberships.upsert({
  where: { userId: 1, orgId: 2 },
  create: { userId: 1, orgId: 2, role: 'member' },
  update: { role: 'admin' },
});

count

const total = await db.users.count();
const admins = await db.users.count({ where: { role: 'admin' } });

aggregate

const stats = await db.posts.aggregate({
  where: { orgId: 1 },
  _sum: { viewCount: true },
  _avg: { score: true },
  _max: { createdAt: true },
  _count: true,
});

groupBy

Group rows by one or more columns and compute aggregations per group. Similar to SQL GROUP BY with aggregate functions.

const postsByUser = await db.posts.groupBy({
  by: ['userId'],
  _count: true,
  _sum: { viewCount: true },
  _avg: { score: true },
});
// [{ userId: 1, _count: 12, _sum: { viewCount: 4800 }, _avg: { score: 4.2 } }, ...]

Filtering groups

Pass a where clause to filter rows before grouping:

const activePostsByOrg = await db.posts.groupBy({
  by: ['orgId'],
  where: { published: true },
  _count: true,
  _max: { createdAt: true },
  orderBy: { _count: 'desc' },
});

Multiple group-by columns

const breakdown = await db.posts.groupBy({
  by: ['orgId', 'published'],
  _count: true,
  _min: { createdAt: true },
  _max: { createdAt: true },
});

Supported aggregate functions: _count, _sum, _avg, _min, _max. When _count is true (or omitted), each group includes a _count field with the number of rows in that group.

findManyStream

Stream rows using a PostgreSQL server-side cursor. Constant memory, works on any number of rows, and supports nested with clauses inside the stream.

for await (const user of db.users.findManyStream({
  where: { orgId: 1 },
  orderBy: { id: 'asc' },
  batchSize: 1000,       // internal FETCH batch size (default: 1000)
  with: { posts: true }, // nested relations work inside the stream
})) {
  process.stdout.write(`${user.email}\n`);
}

Under the hood Turbine issues a speculative LIMIT batchSize + 1 first fetch — if the result fits in one batch, rows are yielded directly without DECLARE CURSOR overhead. Larger result sets escalate to a cursor. Safe to break early; the cursor and connection are cleaned up deterministically.

WHERE operators

Every operator composes freely with AND, OR, NOT, and relation filters.

Equality

OperatorDescriptionExample
literalImplicit equalitywhere: { email: 'a@b.com' }
equalsExplicit equalitywhere: { email: { equals: 'a@b.com' } }
notInequality (or not: null for IS NOT NULL)where: { role: { not: 'admin' } }

Sets

OperatorDescriptionExample
inMatch any value in the arraywhere: { id: { in: [1, 2, 3] } }
notInMatch none of the valueswhere: { role: { notIn: ['banned', 'spam'] } }

Comparison

OperatorDescriptionExample
gtGreater thanwhere: { score: { gt: 100 } }
gteGreater than or equalwhere: { score: { gte: 100 } }
ltLess thanwhere: { score: { lt: 100 } }
lteLess than or equalwhere: { score: { lte: 100 } }

String

OperatorDescriptionExample
containsSubstring match (LIKE %v%)where: { title: { contains: 'sql' } }
startsWithPrefix match (LIKE v%)where: { email: { startsWith: 'admin@' } }
endsWithSuffix match (LIKE %v)where: { email: { endsWith: '@acme.com' } }
mode: 'insensitive'Switch any string operator to ILIKEwhere: { title: { contains: 'SQL', mode: 'insensitive' } }

LIKE wildcards in user input (%, _, \) are escaped automatically.

Array columns

OperatorDescriptionExample
hasArray contains elementwhere: { tags: { has: 'sql' } }
hasEveryArray contains every elementwhere: { tags: { hasEvery: ['sql', 'pg'] } }
hasSomeArray contains at least one elementwhere: { tags: { hasSome: ['sql', 'mysql'] } }

Relation filters

Filter parent rows by predicates on their child rows.

OperatorDescriptionExample
someAt least one related row matcheswhere: { posts: { some: { published: true } } }
everyEvery related row matcheswhere: { posts: { every: { published: true } } }
noneNo related row matcheswhere: { posts: { none: { published: false } } }

Combinators

where: {
  AND: [{ orgId: 1 }, { role: 'admin' }],
  OR: [{ role: 'owner' }, { role: 'admin' }],
  NOT: { deletedAt: { not: null } },
}

Nested with

Relations in a with clause accept their own where, orderBy, limit, select / omit, and nested with.

const users = await db.users.findMany({
  with: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: 'desc' },
      limit: 10,
      select: { id: true, title: true, createdAt: true },
      with: {
        comments: {
          where: { deletedAt: null },
          orderBy: { createdAt: 'asc' },
          limit: 50,
        },
      },
    },
  },
});

Nesting depth is capped at 10 — beyond that, Turbine throws CircularRelationError (TURBINE_E007) with the full relation path.

Transactions

await db.$transaction(async (tx) => {
  const user = await tx.users.create({
    data: { email: 'new@example.com', name: 'New', orgId: 1 },
  });
  await tx.posts.create({
    data: { userId: user.id, orgId: 1, title: 'Hello', content: '...' },
  });
});

tx has the same typed table accessors as db. Nested $transaction calls create SAVEPOINTs automatically.

Isolation levels + timeout

await db.$transaction(
  async (tx) => {
    // ...
  },
  {
    isolationLevel: 'Serializable',
    timeout: 5000, // ms — destroys the connection on expiry
  },
);

Supported isolation levels: 'ReadCommitted', 'RepeatableRead', 'Serializable'.

Pipeline

Run N independent queries in a single database round-trip using the PostgreSQL extended-query pipeline protocol.

const [user, postCount, recentPosts] = await db.pipeline([
  db.users.buildFindUnique({ where: { id: 1 } }),
  db.posts.buildCount({ where: { orgId: 1 } }),
  db.posts.buildFindMany({ where: { userId: 1 }, limit: 5 }),
]);

The build* methods return DeferredQuery objects — generated SQL + params + a transform — without executing. pipeline() sends every statement in one TCP flush (parse/bind/execute/sync) and runs each transform on the result.

Non-transactional pipelines

By default, pipelines run inside a transaction — any failure rolls back the batch. Opt out for error-isolated execution:

try {
  await db.pipeline(queries, { transactional: false });
} catch (err) {
  if (err instanceof PipelineError) {
    // err.results is a per-query [{status:'ok', value} | {status:'error', error}] array
    // err.failedIndex / err.failedTag identify the first failure
  }
}

HTTP / serverless pools that don't support the pipeline protocol fall back to sequential execution automatically. Probe at runtime with db.pipelineSupported().

Middleware

db.$use(fn) registers a middleware that runs around every query. It can log, time, mutate arguments, or transform results — but it cannot rewrite the SQL.

// Query timing
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;
});
 
// Soft-delete filter
db.$use(async (params, next) => {
  if (params.action === 'findMany' || params.action === 'findUnique') {
    params.args.where = { ...params.args.where, deletedAt: null };
  }
  return next(params);
});

Raw SQL

When you need something the query builder doesn't expose (full-text search, window functions, WITH RECURSIVE, etc.):

const stats = await db.raw<{ day: Date; count: number }>`
  SELECT DATE_TRUNC('day', created_at) AS day, COUNT(*)::int AS count
  FROM posts WHERE org_id = ${orgId}
  GROUP BY day ORDER BY day
`;

Parameters from ${} interpolations become $1, $2, ... — never string-interpolated into the SQL text.

See also