SQL Lab

SQL Lab explains how to run the book's examples. Each chapter's full scripts live next to the book together with the PostgreSQL version they were checked on, so you can create the schema, run the operations and see the constraint errors for yourself. The appendix covers starting a local PostgreSQL instance and how the scripts are organized.

What you need

The 52 pattern and anti-pattern chapters each have one script at book/assets/<slug>/<slug>.sql. Their headers record PostgreSQL 18.6 as the checked version. Use a PostgreSQL 18 server and its psql client: examples include uuidv7(), temporal keys, PostgreSQL ranges, PL/pgSQL and psql backslash commands. Other engines' implementation notes in the chapters are porting guidance, not claims that these files run unchanged there.

Use a disposable cluster with a superuser connection. Some examples create roles, change privileges and install the supplied ltree or btree_gist extensions. The tenant examples also create passwordless login roles, and Database per Tenant creates dt_a and dt_b and reconnects as its runtime roles. Those reconnects require the lab's local trust authentication. A fresh database alone does not isolate cluster-wide roles or those extra databases.

The commands below start from the repository root. Choose either server setup, then run one example or the complete suite. Keep server messages in English: several scripts compare the exact diagnostic as well as its SQLSTATE. Leave plpgsql.check_asserts enabled so the scripts' ASSERT checks execute.

A PostgreSQL 18 Docker container

The official PostgreSQL image provides the server and clients. This lab uses no published port, disables container networking and stores the cluster in temporary memory; local trust lets the tenant scripts reconnect without passwords. PostgreSQL 18 images use /var/lib/postgresql as the volume location.

docker run --detach --name ddp-sql-lab \
  --network none \
  --tmpfs /var/lib/postgresql \
  --env POSTGRES_HOST_AUTH_METHOD=trust \
  --mount "type=bind,source=$PWD,target=/book,readonly" \
  postgres:18

until docker exec ddp-sql-lab pg_isready -q -h 127.0.0.1 -U postgres; do
  sleep 1
done
docker exec ddp-sql-lab psql -X -U postgres -d postgres \
  -c 'SELECT version();'

If readiness never arrives, inspect docker logs ddp-sql-lab. The repository is mounted read-only at /book; its SQL scripts remain ordinary files, not initialization hooks. For example, run Surrogate Key in its own fresh database:

docker exec ddp-sql-lab createdb -U postgres --template=template0 lab_one
docker exec ddp-sql-lab psql -X -v ON_ERROR_STOP=1 -U postgres \
  -d lab_one -f /book/book/assets/surrogate-key/surrogate-key.sql
docker exec ddp-sql-lab dropdb -U postgres lab_one

When you finish all experiments, remove the container and its temporary cluster:

docker rm --force --volumes ddp-sql-lab

A local throwaway cluster

Install the full PostgreSQL 18 server distribution, including initdb, pg_ctl, postgres, clients and supplied extensions. A client-only installation cannot create this cluster. Put that distribution's bin directory first on PATH; for Homebrew on Apple Silicon the path is shown below. On another installation, replace it with your PostgreSQL 18 binary directory.

export PATH=/opt/homebrew/opt/postgresql@18/bin:$PATH
postgres --version
psql --version

lab_dir=$(mktemp -d /tmp/ddp.XXXXXX)
initdb -D "$lab_dir/data" -U postgres --locale=C --encoding=UTF8 \
  --auth-local=trust --auth-host=reject
pg_ctl -D "$lab_dir/data" -l "$lab_dir/server.log" \
  -o "-k $lab_dir -c listen_addresses=''" -w start

export PGHOST="$lab_dir" PGPORT=5432 PGUSER=postgres PGDATABASE=postgres
psql -X -c 'SELECT version();'

Run this as your ordinary operating-system user, not root. initdb creates the cluster and the named database superuser; pg_ctl starts it and waits for readiness. The short /tmp path keeps the Unix socket pathname short. An empty listen_addresses disables TCP; PGHOST selects the socket directory, so this does not use an existing server listening on TCP port 5432.

Keep the shell and lab_dir value until cleanup. After running the examples, stop the server before deleting its directory:

pg_ctl -D "$lab_dir/data" -m fast -w stop
rm -rf -- "$lab_dir"
unset PGHOST PGPORT PGUSER PGDATABASE

Run one script

With the local cluster running and the connection variables exported, create a database from template0, run one file, and drop the database:

createdb --maintenance-db=postgres --template=template0 lab_one
psql -X -v ON_ERROR_STOP=1 --dbname=lab_one \
  --file=book/assets/surrogate-key/surrogate-key.sql
dropdb --maintenance-db=postgres lab_one

psql -X ignores startup files, and ON_ERROR_STOP=1 makes unexpected errors stop the file with a nonzero exit status. Keep that option even though most scripts also set it themselves. Do not add --single-transaction: scripts own their transaction boundaries, some deliberately commit, and Database per Tenant issues CREATE DATABASE and reconnects.

Many examples roll their lab transaction back at the end. Others leave schemas and rows for inspection, so use a new database for each attempt rather than assuming a file is rerunnable. The tenant labs leave cluster-wide objects too; use the complete runner below for their cleanup or discard the entire cluster after running them manually. Sequential example statements demonstrate protocols and assertions; a passing file does not itself exercise every two-session race shown in a chapter.

Recognize intended failures

An ERROR line is sometimes the demonstration succeeding. Judge it by the nearby expectation check and the final process status, not by searching the output for the word ERROR.

Convention in the scripts What happens What makes a run fail
Savepoint plus temporarily disabled ON_ERROR_STOP An invalid statement prints an error; the script saves :SQLSTATE, enables stopping again and rolls back to the savepoint Saved state differs from the expected one, including when the invalid statement unexpectedly succeeds
DO block with a named exception handler An inner block attempts the invalid operation, catches only the expected condition and prints a notice An unexpected exception escapes, a checked diagnostic differs, or the explicit failure after unexpected success escapes
pg_temp.expect_error helper in the anti-pattern labs Dynamic SQL runs inside an exception block; stacked diagnostics capture state and message Either state or exact message differs; no exception also fails the comparison
Assertions and result gates ASSERT, explicit IF ... RAISE EXCEPTION, or a query followed by \gset and \if checks resulting data A failed gate raises an exception, deliberately evaluates 1 / 0, or issues \quit 1

For example, Surrogate Key's script uses this sequence inside its lab transaction:

SAVEPOINT expected_error;
\set ON_ERROR_STOP off
INSERT INTO customer (customer_number, display_name)
VALUES ('C-204', 'Duplicate');
\set rejected_state :SQLSTATE
\set ON_ERROR_STOP on
ROLLBACK TO SAVEPOINT expected_error;
SELECT :'rejected_state' = '23505' AS rejected \gset
\if :rejected
\else
    \quit 1
\endif

The saved state must be 23505 (unique violation). It is captured before ROLLBACK TO, because subsequent SQL changes :SQLSTATE. expected_error is a savepoint name used by several scripts, not a special PostgreSQL feature; other files use descriptive names such as rejected_operation. The rollback recovers the transaction so later assertions can run.

Exclusive Arc instead catches check_violation or foreign_key_violation in DO blocks and prints the expected SQLSTATE. Its deliberate RAISE EXCEPTION after an operation ensures that unexpected success is not silently accepted. Some handlers also compare SQLERRM. Delimited Lists defines the reusable temporary helper, while Association Table includes ASSERT checks of retained rows and cascade effects.

Successful checks can therefore produce raw errors, notices and ordinary query output in the same run. A nonzero psql status is a broken lab run even if earlier expected-error notices appeared. An unchanged script failing on a different diagnostic language or server version still needs investigation; do not suppress its assertions to obtain a pass.

Run all scripts

The POSIX shell runner uses set -eu and PostgreSQL clients from PATH. It takes host/socket, port, user and other connection defaults from the standard PG environment variables. PGDATABASE names the maintenance database, defaulting to postgres; each script gets an explicitly selected fresh database. The Database per Tenant example also reconnects to postgres, so keep that database available.

For the local cluster:

sh book/assets/sql-lab/run-all.sh

For the Docker setup, run the same file inside the container using its installed clients and local socket:

docker exec \
  --env PGHOST=/var/run/postgresql --env PGUSER=postgres \
  --env PGDATABASE=postgres \
  ddp-sql-lab sh /book/book/assets/sql-lab/run-all.sh

The runner discovers book/assets/*/*.sql in pathname order, independently of your working directory. For each file it creates a uniquely named database from template0, runs psql -X -v ON_ERROR_STOP=1, drops the database and prints PASS <slug>. It stops at the first nonzero script exit and prints FAIL <slug>; its exit trap attempts cleanup on failure or interruption. Cleanup failures also fail the run. With all current files passing, the final line is PASS: 52 scripts.

The runner also removes the tenant labs' auxiliary databases (dt_a, dt_b) and roles (ss_app, st_owner, st_app_a, st_app_b, dt_owner, dt_app_a, dt_app_b). Audit Log creates audit_example_app inside a transaction that rolls back. A preflight check rejects any of these existing names before running anything; it does not reuse or delete pre-existing lab objects. Run only one suite at a time in its disposable cluster. After an uncatchable termination or a cleanup failure, discard that cluster and start another.

The complete suite has been run successfully on a throwaway PostgreSQL 18.6 cluster: all 52 files passed with these checks enabled. The outbox lab assumes a broker acknowledgement in SQL rather than contacting a broker; queue and reservation examples advance test state instead of waiting for real worker deadlines.

Script catalog

The groups and chapter order below follow the book's Summary. Each file is self-contained for its documented lab setup; the preceding chapter is not a prerequisite.

Identity

Chapter Script Demonstration
Surrogate Key surrogate-key.sql Stable internal references through a business-number change; business uniqueness and UUID alternatives.
Natural Key natural-key.sql Composite line identity and cascading correction through dependent references.
Time-Ordered Identifier time-ordered-identifier.sql UUIDv7 generation, extraction, explicit insertion and duplicate rejection.
Public Identifier public-identifier.sql Resolve and rotate an external handle while retaining internal references.

Relationships

Chapter Script Demonstration
Association Table association-table.sql Membership attributes, pair uniqueness, endpoint FKs and cascade effects.
Table Split table-split.sql Optional shared-key details and the deferred mandatory-extension variant.
Ordered Collection ordered-collection.sql Gapped positions, renumbering, swaps and owner-scoped uniqueness.
Localized Values localized-values.sql Translation rows, required default, atomic default switch and language-specific search.

Reference and State

Chapter Script Demonstration
Lookup Table lookup-table.sql Maintained status labels and retirement guards, with separate enum/domain alternatives.
Status History status-history.sql Guarded transitions, revision rejection and atomic current/history writes.

Hierarchies

Chapter Script Demonstration
Adjacency List adjacency-list.sql Recursive descendants and breadcrumbs, guarded move and subtree deletion.
Materialized Path materialized-path.sql ltree containment and transactional branch-path rewrites with parent consistency.
Nested Sets nested-sets.sql Insert, move and delete dense intervals, then audit density and crossings.
Closure Table closure-table.sql Maintain ancestor pairs through a move/delete and compare closure with direct edges.

Types and Attributes

Chapter Script Demonstration
Single Table Inheritance single-table-inheritance.sql Discriminator-based row-shape checks and family-wide references.
Class Table Inheritance class-table-inheritance.sql Deferred subtype completeness, wrong-type rejection and atomic conversion.
Concrete Table Inheritance concrete-table-inheritance.sql Separate complete types, shared allocation, union reads and subtype-specific FKs.
Exclusive Arc exclusive-arc.sql Exactly-one real target, ownership transfer and target deletion behavior.
JSON Column json-column.sql Validate/index document paths and use a version for whole-document editing.
Typed Properties typed-properties.sql Publish property profiles, validate typed values/completeness and migrate a product's profile.

Time and Deletion

Chapter Script Demonstration
History Table history-table.sql Capture row versions, query past values, prevent identity reuse and apply retention.
Effective Dating effective-dating.sql Nonoverlapping business periods, as-of selection and temporal coverage references.
Bitemporal Table bitemporal-table.sql Correct business history while retaining earlier recorded beliefs and overlap protection.
Audit Log audit-log.sql Attributed atomic actions and runtime permissions that reject evidence rewriting.
Soft Delete soft-delete.sql Live-only uniqueness, retained references, restore conflict and physical-delete protection.

Domain Structures

Chapter Script Demonstration
Party party.sql Common counterparty identity, typed extensions and registration uniqueness.
Role Object role-object.sql Dated participation episodes, typed role references and conflicting-period rejection.
Quantity quantity.sql Compatible multiplicative conversions, dimension checks and refused currency conversion.
Observation observation.sql Immutable typed evidence, replacements, rejection and derived observation status.
Bill of Materials bill-of-materials.sql Release/pin recipe versions, explode path quantities and select releases by business date.
Double-Entry Ledger double-entry-ledger.sql Balanced atomic posting, immutable entries, currency checks and exact reversal.

Reliable Writes

Chapter Script Demonstration
Version Column version-column.sql Conditional updates reject stale snapshots and advance an aggregate token.
Reservation reservation.sql Slot overlap protection and pooled-stock hold, confirmation, cancellation and expiry.
Idempotency Record idempotency-record.sql Saved-result replay, changed-payload rejection and consumer effect deduplication.
Transactional Outbox transactional-outbox.sql Atomic producer staging, ordered relay acknowledgement and rollback of failed production.
Queue Table queue-table.sql Claims, lease generations, stale acknowledgements, renewal, recovery and terminal attempts.

Tenant Isolation

Chapter Script Demonstration
Shared Schema shared-schema.sql Tenant-scoped FKs, restricted-role RLS and transaction-local context reset.
Schema per Tenant schema-per-tenant.sql Separate grants, transaction-local routing and per-schema migration markers.
Database per Tenant database-per-tenant.sql Provision two databases and credentials, validate destination identity and reject cross-database access.

Growth and Change

Chapter Script Demonstration
Summary Table summary-table.sql Maintain deltas, reconcile drift, rebuild totals and refresh a materialized view.
Table Partitioning table-partitioning.sql Route ranges, enforce composite keys, handle missing partitions and detach/reattach data.
Expand and Contract expand-contract.sql Bridge old/new contracts through rename, table split and type-conversion rollouts.

Anti-patterns

Chapter Script Demonstration
Delimited Lists delimited-lists.sql Expose substring mistakes, parse imported members and quarantine unknown IDs.
Mandatory Surrogates mandatory-surrogates.sql Repair pair identity while retaining an old-ID map and showing the unique-pair alternative.
Missing Foreign Keys missing-foreign-keys.sql Quarantine orphans, enforce new references and validate existing rows.
Repeated Columns repeated-columns.sql Unpivot numbered slots into constrained membership rows.
Polymorphic Associations polymorphic-associations.sql Replace unchecked kind/ID values with a validated exclusive arc.
Untyped Properties untyped-properties.sql Audit conversions, quarantine invalid properties and populate typed constrained columns.
Volatile Enums volatile-enums.sql Migrate a schema vocabulary to maintained lookup rows with retained references.
Fixed-Depth Queries fixed-depth-queries.sql Compare truncated joins with recursive traversal and retain parent integrity.
Cloned Tables cloned-tables.sql Consolidate dated tables with a reviewed identity map and one constrained relation.
Inexact Arithmetic inexact-arithmetic.sql Review approximate amounts, migrate to exact decimals and reject invalid scale/nonfinite values.

results matching ""

    No results matching ""