Queries

This guide assumes familiarity with:

A query describes the rows that Drizzle Pulse keeps in sync, for example “all orders” or “the orders of one driver”. You define queries once, pass them to the runtime, and then create live collections and CDC event streams for them.

Pulse tables

A source table is an ordinary Drizzle table whose row changes you want to follow. Wrap it with pulse(table) to get a Pulse table.

import { integer, pgTable, serial, text } from 'drizzle-orm/pg-core';
import { pulse } from 'drizzle-pulse';

export const orders = pgTable('orders', {
  id: serial('id').primaryKey(),
  driverId: integer('driver_id'),
  status: text('status', { enum: ['requested', 'accepted', 'completed', 'cancelled'] }).notNull(),
  price: integer('price').notNull(),
});

export const ordersPulse = pulse(orders);

A Pulse table is itself a query that matches every row of the table. Pass it to the runtime as is to follow all orders:

const runtime = createRuntime({
  queries: { allOrders: ordersPulse },
  // ...
});

Wrap each table once and build all queries of that table from the same Pulse table. Each method call returns a new query and leaves the Pulse table unchanged, so one Pulse table can be the start of many queries.

A source table must have exactly one primary key column. Drizzle Pulse rejects a composite primary key. The primary key type must be one of smallint, integer, bigint, smallserial, serial, bigserial, text, varchar, char, or uuid.

Filters

To choose rows, call .query() with a function. The function gets a context ctx and returns a where clause, which you build with ctx.query(). A query reads one table.

export const acceptedOrders = ordersPulse.query((ctx) => ctx.query({ status: 'accepted' }));

The where clause uses the same object syntax as the where filters of Drizzle relational queries, with a subset of their operators. Each key is a column key from the table definition, such as driverId. The database column name, such as driver_id, is a type error. Each value is either a value to compare for equality or an object with operators. A row must match all columns in the object, so Drizzle Pulse combines them with AND.

export const bigOpenOrders = ordersPulse.query((ctx) =>
  ctx.query({
    price: { gte: 100 },
    status: { in: ['requested', 'accepted'] },
    driverId: { isNotNull: true },
  })
);

The supported operators are eq, ne, gt, gte, lt, lte, in, isNull, and isNotNull. To combine conditions, use AND, OR, and NOT. An object with AND, OR, or NOT cannot also have column keys:

export const urgentOrders = ordersPulse.query((ctx) =>
  ctx.query({
    OR: [
      { status: 'requested' },
      { AND: [{ status: 'accepted' }, { NOT: { driverId: { isNotNull: true } } }] },
    ],
  })
);

If the function returns null, the query matches every row.

Arguments

Often the rows depend on a value that is known only at runtime, such as the current user or a status that the client picks. Call .args(schema) to give a query arguments. The schema can come from any library that supports Standard Schema. This example uses zod:

import { z } from 'zod';

export const ordersByStatus = ordersPulse
  .args(z.object({ status: z.enum(['requested', 'accepted', 'completed', 'cancelled']) }))
  .query((ctx) => ctx.query({ status: ctx.args.status }));

When you create a live collection or subscribe to a CDC event stream for this query, you pass the arguments, for example { status: 'accepted' }. Drizzle Pulse validates them with the schema and throws an error with the schema’s messages if they are invalid.

IMPORTANT

The schema must validate synchronously. A schema with asynchronous checks, such as a zod schema with an async refinement, throws an error when you use the query.

Call .args() before .query(). Calling .args() after .query() throws an error, and so does calling .query() twice in one chain. Without .args(), ctx.args is empty.

Values from a request

You pass all queries to createRuntime() when your application starts. A query that you create later, for example inside a request handler, is not known to the runtime. To get rows for a value from the request, such as the current user, give the query an argument and pass the value when you create the live collection:

// queries.ts: defined once, next to your schema.
export const ordersByDriver = ordersPulse
  .args(z.object({ driverId: z.number().int() }))
  .query((ctx) => ctx.query({ driverId: ctx.args.driverId }));
// In a request handler.
const orders = await runtime.client.ordersByDriver({ driverId: req.user.id });

The schema validates each value before the where function reads it. A value that a client sends, such as a query string parameter, reaches the where clause only through ctx.args.

Columns and order

Call .columns() to choose the columns that a query returns. Set columns to true to include only those columns, or set columns to false to exclude them and keep the rest. Rows from the query contain only the selected columns.

export const orderStatuses = ordersPulse.columns({ id: true, status: true });

If a query uses .columns(), its where clause can use only the selected columns. If a query filters on another column, you cannot create a live collection for it.

Call .order('asc') or .order('desc') to sort the rows of a live collection by primary key. The default is 'asc'.

export const newestOrders = ordersPulse.order('desc');

You can chain .columns(), .args(), .order(), and .query() in one definition. Each call returns a new query and leaves the original unchanged, so you can build several queries from one base:

export const ordersByStatusSummary = ordersPulse
  .columns({ id: true, status: true })
  .args(z.object({ status: z.enum(['requested', 'accepted', 'completed', 'cancelled']) }))
  .order('desc')
  .query((ctx) => ctx.query({ status: ctx.args.status }));

Next steps