Skip to content

PostgreSQL example

This application keeps ordinary PostgreSQL at the center of the API while exercising the complete pg adapter surface. Boolean selection changes the projected row shape, nullable filters add ordered parameters, and the join only exists when its projected column is requested.

AreaDemonstrated behavior
Read and writeDynamic queries, CTEs, cardinality helpers, inserts, updates, deletes, transactions
Reuse and concurrencyPrepared queries, transactional batches, PostgreSQL pipeline mode
Large results and data transferCursor-backed async iteration, COPY import and export
Production controlsStandard Schema validation, cancellation, deadlines, read routing, redacted observation

Define the database

The pinned container initializes this small schema:

sql
CREATE TYPE account_status AS ENUM ('active', 'suspended');

CREATE TABLE users (
  id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  email text NOT NULL UNIQUE,
  status account_status NOT NULL DEFAULT 'active'
);

CREATE TABLE projects (
  id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  owner_id bigint NOT NULL REFERENCES users(id),
  name text NOT NULL,
  budget numeric(14, 2)
);

INSERT INTO users (id, email, status) VALUES
  (1, 'alice@example.com', 'active'),
  (2, 'bob@example.com', 'suspended');

INSERT INTO projects (owner_id, name, budget) VALUES
  (1, 'Compiler', 12500.50);

The application selects the PostgreSQL grammar, live schema provider, generated output, project, and one shared type policy:

ts
import { defineConfig } from "@typed-sql/core";
import { postgres, typePolicy } from "@typed-sql/postgres";
import { pg } from "@typed-sql/postgres/pg";

export const connectionString = () =>
  process.env.DATABASE_URL ?? "postgresql://typed_sql:typed_sql_examples@127.0.0.1:55441/typed_sql_examples";

export default defineConfig({
  dialect: postgres({ typePolicy }),
  schema: {
    file: "generated/db/schema.json",
    provider: pg({ connectionString, schemas: ["public"], typePolicy }),
  },
  outDir: "generated/db",
  projects: ["tsconfig.json"],
  typePolicy,
});

pg is installed by the example application. It is not a dependency of the grammar package.

Compose native SQL

The function composes only typed-sql primitives. Values remain parameters; selected columns, joins, and predicates remain explicit structural fragments.

ts
import type { QueryParameters, QueryRow } from "@typed-sql/core";
import { sql } from "@typed-sql/postgres";

export interface AccountFilters {
  readonly status?: "active" | "suspended" | null;
  readonly minimumId?: bigint | null;
}

export interface AccountSelect {
  readonly status: boolean;
  readonly projectBudget: boolean;
}

export function accounts<const Select extends AccountSelect>(filters: AccountFilters, select: Select) {
  return sql`
    SELECT
      account.id,
      account.email
      ${select.status ? sql.fragment`, account.status` : sql.empty}
      ${select.projectBudget ? sql.fragment`, project.budget` : sql.empty}
    FROM users AS account
      ${select.projectBudget ? sql.fragment`LEFT JOIN projects AS project ON project.owner_id = account.id` : sql.empty}
    WHERE 1 = 1
      ${filters.status == null ? sql.empty : sql.fragment`AND account.status = ${filters.status}`}
      ${filters.minimumId == null ? sql.empty : sql.fragment`AND account.id >= ${filters.minimumId}`}
    ORDER BY account.id
  `;
}

export const activeAccounts = accounts({ status: "active", minimumId: 1n }, { status: true, projectBudget: true });

export const accountById = (accountId: bigint) => sql`
  SELECT account.id, account.email, account.status
  FROM users AS account
  WHERE account.id = ${accountId}
`;

export const projectsByOwner = (ownerId: bigint) => sql`
  SELECT project.id, project.owner_id, project.name, project.budget
  FROM projects AS project
  WHERE project.owner_id = ${ownerId}
  ORDER BY project.id
`;

export const accountProjectSummary = sql`
  WITH project_totals AS (
    SELECT
      project.owner_id,
      COUNT(*) AS project_count,
      SUM(project.budget) AS total_budget
    FROM projects AS project
    GROUP BY project.owner_id
  )
  SELECT
    account.id,
    account.email,
    account.status,
    project_totals.project_count,
    project_totals.total_budget
  FROM users AS account
  LEFT JOIN project_totals ON project_totals.owner_id = account.id
  ORDER BY account.id
`;

export type ActiveAccount = QueryRow<typeof activeAccounts>;
export type ActiveAccountParameters = QueryParameters<typeof activeAccounts>;

Hover activeAccounts in a configured editor to see its selected row and ordered parameter tuple. Changing projectBudget or status changes the complete query type instead of widening it to a hand-written interface.

Mutations and cardinality

Mutations are ordinary tagged SQL and use PostgreSQL RETURNING when the caller needs a typed row:

ts
import { sql } from "@typed-sql/postgres";

export interface NewAccount {
  readonly id: bigint;
  readonly email: string;
  readonly status: "active" | "suspended";
}

export interface NewProject {
  readonly id: bigint;
  readonly ownerId: bigint;
  readonly name: string;
  readonly budget: string | null;
}

export const insertAccount = (account: NewAccount) => sql`
  INSERT INTO users (id, email, status)
  VALUES (${account.id}, ${account.email}, ${account.status}::account_status)
  RETURNING id, email, status
`;

export const updateAccountStatus = (accountId: bigint, status: NewAccount["status"]) => sql`
  UPDATE users
  SET status = ${status}::account_status
  WHERE id = ${accountId}
  RETURNING id, email, status
`;

export const deleteAccount = (accountId: bigint) => sql`
  DELETE FROM users
  WHERE id = ${accountId}
  RETURNING id, email, status
`;

export const insertProject = (project: NewProject) => sql`
  INSERT INTO projects (id, owner_id, name, budget)
  VALUES (${project.id}, ${project.ownerId}, ${project.name}, ${project.budget})
  RETURNING id, owner_id, name, budget
`;

export const deleteProjectsByOwner = (ownerId: bigint) => sql`
  DELETE FROM projects
  WHERE owner_id = ${ownerId}
`;

/** The plain, one-row INSERT shape required by PostgreSQL COPY FROM. */
export const bulkAccountInsert = (account: NewAccount) => sql`
  INSERT INTO users (id, email, status)
  VALUES (${account.id}, ${account.email}, ${account.status})
`;

The database contract makes expected cardinality explicit instead of returning the same array shape for every operation:

ts
import type { PostgresDatabase } from "@typed-sql/postgres/runtime";
import { accountById, activeAccounts } from "./queries.js";

export const listActiveAccounts = (database: PostgresDatabase) => database.all(activeAccounts);

export const requireAccount = (database: PostgresDatabase, accountId: bigint) => database.one(accountById(accountId));

export const findAccount = (database: PostgresDatabase, accountId: bigint) => database.maybeOne(accountById(accountId));

Transactions keep the same inferred query rows across the callback boundary:

ts
import type { PostgresDatabase } from "@typed-sql/postgres/runtime";
import { insertAccount, insertProject, type NewAccount, type NewProject } from "./mutations.js";

export function createAccountWithProject(
  database: PostgresDatabase,
  account: NewAccount,
  project: Omit<NewProject, "ownerId">,
) {
  return database.transaction(async (transaction) => {
    const insertedAccount = await transaction.one(insertAccount(account));
    const insertedProject = await transaction.one(insertProject({ ...project, ownerId: account.id }));
    return { account: insertedAccount, project: insertedProject };
  });
}

Prepared, batched, and pipelined work

Prepared query factories keep parameter inference at the call site:

ts
import type { PostgresDatabase } from "@typed-sql/postgres/runtime";
import { accountById, projectsByOwner } from "./queries.js";

export function prepareAccountQueries(database: PostgresDatabase) {
  return Object.freeze({
    accountById: database.prepare("example-account-by-id", accountById),
    projectsByOwner: database.prepare("example-projects-by-owner", projectsByOwner),
  });
}

Independent or transactional work uses adapter capabilities rather than a second query language:

ts
import type { PostgresDatabase } from "@typed-sql/postgres/runtime";
import { accountById, projectsByOwner } from "./queries.js";

export async function loadAccountWorkspace(database: PostgresDatabase, accountId: bigint) {
  const [accounts, projects] = await database.batch([accountById(accountId), projectsByOwner(accountId)]);
  return { account: accounts[0], projects };
}
ts
import type { PostgresDatabase } from "@typed-sql/postgres/runtime";
import { accountById, accountProjectSummary } from "./queries.js";

export async function loadIndependentReports(database: PostgresDatabase, accountId: bigint) {
  const [account, summary] = await database.pipeline([accountById(accountId), accountProjectSummary]);
  return { account: account[0], summary };
}

Streaming and COPY

The async iterable is cursor-backed, and collected rows retain QueryRow<typeof activeAccounts>:

ts
import type { QueryRow } from "@typed-sql/core";
import type { PostgresDatabase } from "@typed-sql/postgres/runtime";
import { activeAccounts } from "./queries.js";

export function streamActiveAccounts(database: PostgresDatabase, batchSize = 100) {
  return database.stream(activeAccounts, { batchSize });
}

export async function collectActiveAccounts(database: PostgresDatabase) {
  const accounts: QueryRow<typeof activeAccounts>[] = [];
  for await (const account of streamActiveAccounts(database, 25)) accounts.push(account);
  return accounts;
}

export async function firstActiveAccount(database: PostgresDatabase) {
  for await (const account of streamActiveAccounts(database, 1)) return account;
  return undefined;
}

COPY is exposed by the PostgreSQL adapter as an explicit capability:

ts
import { requireAdapterCapability } from "@typed-sql/core";
import { postgresCopy, sql } from "@typed-sql/postgres";
import type { PostgresDatabase } from "@typed-sql/postgres/runtime";
import { bulkAccountInsert, type NewAccount } from "./mutations.js";

export function importAccounts(database: PostgresDatabase, accounts: Iterable<NewAccount> | AsyncIterable<NewAccount>) {
  const copy = requireAdapterCapability(database, postgresCopy);
  return copy.copyFrom(bulkAccountInsert, accounts, { chunkBytes: 64 * 1024 });
}

export function exportAccountsAsCsv(database: PostgresDatabase) {
  const copy = requireAdapterCapability(database, postgresCopy);
  return copy.copyTo(sql`
    SELECT id, email, status
    FROM users
    ORDER BY id
  `);
}

Validation and production controls

Any Standard Schema-compatible validator can decode the inferred result before it reaches the caller:

ts
import { sql } from "@typed-sql/postgres";
import * as v from "valibot";

export const accountResult = v.object({
  id: v.bigint(),
  email: v.pipe(v.string(), v.email()),
  status: v.picklist(["active", "suspended"]),
});

interface ValidatedAccount {
  readonly id: bigint;
  readonly email: string;
  readonly status: "active" | "suspended";
}

export const validatedAccountById = (accountId: bigint) =>
  sql.validateResult(
    sql<ValidatedAccount>`
      SELECT account.id, account.email, account.status
      FROM users AS account
      WHERE account.id = ${accountId}
    `,
    accountResult,
  );

Cancellation and deadlines operate on driver work, routing uses a validated grammar snapshot, and observation never exposes query text or parameter values:

ts
import { sql } from "@typed-sql/postgres";
import type { PostgresDatabase } from "@typed-sql/postgres/runtime";

const wait = (seconds: number) => sql`SELECT pg_sleep(${seconds})`;

export function waitUntilDeadline(database: PostgresDatabase, seconds: number, timeoutMilliseconds: number) {
  return database.all(wait(seconds), { deadline: Date.now() + timeoutMilliseconds });
}

export function waitUntilAborted(database: PostgresDatabase, seconds: number, signal: AbortSignal) {
  return database.all(wait(seconds), { signal });
}
ts
import type { Database } from "@typed-sql/core";
import { createPostgresRoutedDatabase, postgres, typePolicy } from "@typed-sql/postgres";

export function createAccountRouter(primary: Database, replicas: readonly Database[], snapshot: unknown) {
  return createPostgresRoutedDatabase({
    primary,
    replicas,
    schema: postgres({ typePolicy }).validateSnapshot(snapshot),
    typePolicy,
  });
}
ts
import type { DatabaseObserver, DatabaseOperationEnd, DatabaseOperationStart } from "@typed-sql/core";

export interface OperationLog {
  readonly starts: readonly DatabaseOperationStart[];
  readonly ends: readonly DatabaseOperationEnd[];
  readonly observer: DatabaseObserver;
}

export function createOperationLog(): OperationLog {
  const starts: DatabaseOperationStart[] = [];
  const ends: DatabaseOperationEnd[] = [];
  return {
    starts,
    ends,
    observer: {
      start(operation) {
        starts.push(operation);
        return { end: (completion) => ends.push(completion) };
      },
    },
  };
}

Execute through pg

The adapter consumes the same query object and closes its application-owned pool:

ts
import { typePolicy } from "@typed-sql/postgres";
import { createPgDatabase } from "@typed-sql/postgres/pg";
import { connectionString } from "../typed-sql.config.js";
import { loadAccountWorkspace } from "./batches.js";
import { findAccount } from "./cardinality.js";
import { createOperationLog } from "./observation.js";
import { loadIndependentReports } from "./pipelines.js";
import { prepareAccountQueries } from "./prepared.js";
import { collectActiveAccounts } from "./streams.js";

const pgCursorPackage: string = "pg-cursor";
const pgCopyStreamsPackage: string = "pg-copy-streams";

export async function runPostgresExample() {
  const operationLog = createOperationLog();
  const database = await createPgDatabase({
    connectionString,
    typePolicy,
    observer: operationLog.observer,
    poolConfig: { pipeline: true },
    cursorImporter: () => import(pgCursorPackage),
    copyStreamsImporter: () => import(pgCopyStreamsPackage),
  });

  try {
    const prepared = prepareAccountQueries(database);
    const [account, preparedAccount, workspace, reports, streamedAccounts] = await Promise.all([
      findAccount(database, 1n),
      database.maybeOne(prepared.accountById(2n)),
      loadAccountWorkspace(database, 1n),
      loadIndependentReports(database, 1n),
      collectActiveAccounts(database),
    ]);
    return {
      account,
      preparedAccount,
      workspace,
      reports,
      streamedAccounts,
      observedOperations: operationLog.ends.length,
    };
  } finally {
    await database.close();
  }
}

The real Poku suite creates actual rows and verifies queries, CTEs, prepared statements, transactions, pipeline mode, cursor streaming, COPY, cancellation, routing, observation, and cleanup against the pinned server:

ts
import { readFile } from "node:fs/promises";
import { QueryCancelledError } from "@typed-sql/core";
import { postgres, sql, typePolicy } from "@typed-sql/postgres";
import { createPgDatabase } from "@typed-sql/postgres/pg";
import { describe, it, strict } from "poku";
import { loadAccountWorkspace } from "../src/batches.js";
import { exportAccountsAsCsv, importAccounts } from "../src/bulk.js";
import { waitUntilAborted, waitUntilDeadline } from "../src/cancellation.js";
import { findAccount, listActiveAccounts, requireAccount } from "../src/cardinality.js";
import { deleteAccount, deleteProjectsByOwner, updateAccountStatus } from "../src/mutations.js";
import { createOperationLog } from "../src/observation.js";
import { loadIndependentReports } from "../src/pipelines.js";
import { prepareAccountQueries } from "../src/prepared.js";
import { accountById, accountProjectSummary } from "../src/queries.js";
import { createAccountRouter } from "../src/routing.js";
import { collectActiveAccounts, firstActiveAccount } from "../src/streams.js";
import { createAccountWithProject } from "../src/transactions.js";
import { validatedAccountById } from "../src/validation.js";
import { connectionString } from "../typed-sql.config.js";

const pgCursorPackage: string = "pg-cursor";
const pgCopyStreamsPackage: string = "pg-copy-streams";
const operationLog = createOperationLog();
const database = await createPgDatabase({
  connectionString,
  typePolicy,
  observer: operationLog.observer,
  poolConfig: { pipeline: true },
  cursorImporter: () => import(pgCursorPackage),
  copyStreamsImporter: () => import(pgCopyStreamsPackage),
});

async function removeAccounts(ids: readonly bigint[]): Promise<void> {
  for (const id of ids) {
    await database.execute(deleteProjectsByOwner(id));
    await database.execute(deleteAccount(id));
  }
}

try {
  await describe("PostgreSQL example against pg", async () => {
    await it("executes queries, cardinality, CTEs, prepared statements, batches, pipelines, and streams", async () => {
      strict.strictEqual((await listActiveAccounts(database)).length, 1);
      strict.deepStrictEqual(await requireAccount(database, 1n), {
        id: 1n,
        email: "alice@example.com",
        status: "active",
      });
      strict.strictEqual(await findAccount(database, -1n), undefined);
      strict.strictEqual((await database.all(accountProjectSummary)).length, 2);

      const prepared = prepareAccountQueries(database);
      strict.strictEqual(prepared.accountById.statementName, "example-account-by-id");
      strict.deepStrictEqual(await database.one(prepared.accountById(2n)), {
        id: 2n,
        email: "bob@example.com",
        status: "suspended",
      });

      const workspace = await loadAccountWorkspace(database, 1n);
      strict.strictEqual(workspace.projects.length, 1);
      const reports = await loadIndependentReports(database, 1n);
      strict.strictEqual(reports.summary.length, 2);
      strict.strictEqual((await collectActiveAccounts(database)).length, 1);
      strict.deepStrictEqual(await firstActiveAccount(database), {
        id: 1n,
        email: "alice@example.com",
        status: "active",
        budget: "12500.50",
      });
      strict.deepStrictEqual(await database.one(validatedAccountById(1n)), {
        id: 1n,
        email: "alice@example.com",
        status: "active",
      });
    });

    await it("commits typed mutations atomically", async () => {
      await removeAccounts([9_001n]);
      const created = await createAccountWithProject(
        database,
        { id: 9_001n, email: "transaction.pg@example.com", status: "active" },
        { id: 9_001n, name: "PostgreSQL transaction", budget: "42.50" },
      );
      strict.deepStrictEqual(created.account, {
        id: 9_001n,
        email: "transaction.pg@example.com",
        status: "active",
      });
      strict.deepStrictEqual(await database.one(updateAccountStatus(9_001n, "suspended")), {
        id: 9_001n,
        email: "transaction.pg@example.com",
        status: "suspended",
      });
      await removeAccounts([9_001n]);
      strict.strictEqual(await findAccount(database, 9_001n), undefined);
    });

    await it("imports and exports typed rows through COPY", async () => {
      const ids = [9_101n, 9_102n] as const;
      await removeAccounts(ids);
      const result = await importAccounts(database, [
        { id: ids[0], email: "copy-one.pg@example.com", status: "active" },
        { id: ids[1], email: "copy-two.pg@example.com", status: "suspended" },
      ]);
      strict.strictEqual(result.rows, 2);
      strict.ok(result.bytes > 0);
      const chunks: Uint8Array[] = [];
      for await (const chunk of exportAccountsAsCsv(database)) chunks.push(chunk);
      const csv = new TextDecoder().decode(Buffer.concat(chunks));
      strict.match(csv, /copy-one\.pg@example\.com/u);
      strict.match(csv, /copy-two\.pg@example\.com/u);
      await removeAccounts(ids);
    });

    await it("cancels in-flight driver work by deadline and AbortSignal", async () => {
      await strict.rejects(waitUntilDeadline(database, 0.1, 10), (error) => {
        strict.ok(error instanceof QueryCancelledError);
        return true;
      });
      const controller = new AbortController();
      const pending = waitUntilAborted(database, 0.1, controller.signal);
      setTimeout(() => controller.abort(), 10);
      await strict.rejects(pending, (error) => {
        strict.ok(error instanceof QueryCancelledError);
        return true;
      });
      strict.deepStrictEqual(await database.one(sql<{ value: number }>`SELECT 1 AS value`), { value: 1 });
    });

    await it("routes proven reads with the grammar snapshot and emits redacted observations", async () => {
      const snapshot = postgres().validateSnapshot(
        JSON.parse(await readFile(new URL("../generated/db/schema.json", import.meta.url), "utf8")),
      );
      const replica = await createPgDatabase({ connectionString, typePolicy, observer: operationLog.observer });
      try {
        const routed = createAccountRouter(database, [replica], snapshot).context();
        strict.deepStrictEqual(await routed.one(accountById(1n)), {
          id: 1n,
          email: "alice@example.com",
          status: "active",
        });
      } finally {
        await replica.close();
      }
      strict.ok(operationLog.starts.length > 0);
      strict.strictEqual(operationLog.starts.length, operationLog.ends.length);
      strict.ok(operationLog.starts.every((event) => !("text" in event) && !("values" in event)));
    });
  });
} finally {
  await removeAccounts([9_001n, 9_101n, 9_102n]).catch(() => undefined);
  await database.close();
}

Run it

From the repository root:

sh
pnpm --filter @typed-sql/example-postgres db:up
pnpm --filter @typed-sql/example-postgres generate
pnpm --filter @typed-sql/example-postgres check
pnpm --filter @typed-sql/example-postgres test
pnpm --filter @typed-sql/example-postgres start
pnpm --filter @typed-sql/example-postgres test:database
pnpm --filter @typed-sql/example-postgres db:down

Set TYPED_SQL_CONTAINER_ENGINE=podman before the container commands to use Podman. Set DATABASE_URL to target another PostgreSQL database. The PostgreSQL dialect guide covers the full grammar and adapter surface.

Released under the MIT License.