zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Schema and ORM

Set OperationsSupported

Set operations combine result sets from multiple queries — UNION, INTERSECT, and EXCEPT. Batch executes multiple statements in a single round-trip. zmdb's query compiler exposes both primitives directly, giving you full control over SQL generation.

UNION / UNION ALL#

Combine rows from two or more SELECT statements. Use union for distinct rows, unionAll to keep duplicates.

import { createQueryCompiler, trustedTable } from '@zmdb/sql';
import { postgres } from '@zmdb/postgres';

const compiler = createQueryCompiler(postgres);

const query1 = compiler.selectFrom(trustedTable('users')).select(['id', 'name']).where('active', '=', true).compile();

const query2 = compiler.selectFrom(trustedTable('archived_users')).select(['id', 'name']).compile();

import { setOperation } from '@zmdb/sql/set-ops';

const combined = setOperation('union', [query1, query2], postgres);

// combined.text => SELECT ... UNION SELECT ...
// combined.parameters => [...]

INTERSECT & EXCEPT#

INTERSECT returns rows present in both queries. EXCEPT returns rows from the first query that aren't in the second.

import { setOperation } from '@zmdb/sql/set-ops';

// Active users who have placed orders
const activeWithOrders = setOperation('intersect', [activeUsersQuery, ordersQuery], 'postgres');

// Users who have never ordered
const neverOrdered = setOperation('except', [allUsersQuery, ordersQuery], 'postgres');
📝 Note

All queries in a set operation must have the same column count and compatible types. The query compiler doesn't validate this — your database will reject mismatched unions.

Batch Execution#

When you need to run multiple independent statements in one database round-trip, use batch. This is useful for bulk inserts, multi-table updates, or running migrations.

import { createQueryCompiler, trustedTable } from '@zmdb/sql';
import { batch } from '@zmdb/sql/set-ops';
import { postgres } from '@zmdb/postgres';

const compiler = createQueryCompiler(postgres);

const stmt1 = compiler.insertInto(trustedTable('users')).values({ name: 'Alice', email: 'alice@example.com' }).compile();

const stmt2 = compiler.insertInto(trustedTable('users')).values({ name: 'Bob', email: 'bob@example.com' }).compile();

const batchHandle = batch([stmt1, stmt2]);

// Execute against your driver
const results = await batchHandle.execute(async statements => {
  // Run all statements in a single transaction or call
  return driver.executeMulti(statements);
});

Generated SQL (parameterized):

INSERT INTO "users" ("name", "email") VALUES ($1, $2);
INSERT INTO "users" ("name", "email") VALUES ($3, $4);
-- Parameters: ['Alice', 'alice@example.com', 'Bob', 'bob@example.com']

Parameter Renumbering#

The query compiler automatically renumbers positional parameters ($1, $2, ...) when combining queries. This ensures parameters remain valid across the combined statement.

import { trustedTable } from '@zmdb/sql';

// Two queries with overlapping parameter positions
const q1 = compiler.selectFrom(trustedTable('orders')).where('user_id', '=', 1).compile();
const q2 = compiler.selectFrom(trustedTable('products')).where('category_id', '=', 2).compile();

// After union, q1's $1 stays $1, q2's $1 becomes $3
const combined = setOperation('union', [q1, q2], 'postgres');
// combined.parameters => [1, 2] (assuming q2 had one param)
⚠️ Warning

Batch does NOT guarantee transaction semantics by default. Wrap in a transaction if you need atomicity.

---

See also: Query Compiler · Repository · Migrations