Playground
Edit a query on the docs schema and watch, as you type, the SQL that pgcast writes and the type TypeScript gives its result; press Run to see what Postgres answers. It starts from the tutorial's first query; nothing you write leaves your browser.
TSYou write
ts
const film = await db.selectOne('films', {
title: 'The Salt Orchard',
}).run(pool);SQLPostgres runs
sql
SELECT to_jsonb("films".*) AS result
FROM "films"
WHERE ("title" = $1)
LIMIT $2
values ["The Salt Orchard", 1]
::TypeScript knows
ts
const film: films.JSONSelectable | undefined
// that is
const film: {
genres: string[];
id: number;
release_year: number;
runtime_minutes: number;
studio_id: number;
tagline: string | null;
title: string;
} | undefined
What runs here
- The types: TypeScript 5.9.3, the compiler of these docs, in Monaco, the editor of VS Code. Hover a name to see its type; an error is underlined as you type.
- The SQL: pgcast's own runtime, in a worker of this page, with a pool that records each query it is given. Each query appears with its values. As you type, a query gets no rows back, so code that needs a row stops after its query, with the error it got.
- Postgres, on Run: PGlite, Postgres 18 compiled to WebAssembly, in that worker: about 5 MB to download, on your first Run only. Each query then shows the rows it returns (the first 50), or its error with its SQLSTATE. Each Run starts on the docs schema as it ships, and stops after 10 seconds.
- One connection: PGlite has one, so the pool lends it to one transaction at a time, as a
pg.Poolwithmax: 1. It takesquery(text, values),connect()andend(), which does nothing. Its rows are parsed aspgparses them, except anintervalor acircle, which stay text, and its values are sent aspgsends them. - Prepared, always: each query is sent with its values, as
pgsends one that has values. So a text of several statements,db.sql`SELECT 1; SELECT 2`, rejects with 42601 here, wherepgsends it as a simple query. - Share: copies a link to this page that holds the code, deflated in its
#code/…part, which browsers do not send to the server: nothing is stored anywhere. Whoever opens it sees the code and its type, and nothing runs until they press Run or edit it. A link holds 100 KB of code at most; a damaged one opens the tutorial's first query. On a phone, the link's code shows read-only. - The keyboard: in the editor, Ctrl+Enter (⌘+Enter on a Mac) runs the code, and Tab indents. To leave it with Tab, press Ctrl+M first (Ctrl+Shift+M on a Mac); press it again to indent with Tab.
The schema
The tables you query, from schema.sql, read-only here. Their types, zapatos/schema, are the ones pgcast generates from it for every page of these docs.
sql
-- Copyright (C) 2026 Alexandre Demartin
-- The docs schema: pgcast's own example world, a film's cast. pgcast's own generator writes zapatos/ from it:
-- `npm run docs:schema` (README.md). Plain Postgres only, loaded as it is on 14, 16 and 18.
-- Every name is invented. Seeded rows have ids from 1000, so a row an example inserts gets a small id; the tables
-- that examples fill themselves (seat_bookings, ratings, tags) start empty.
CREATE TABLE studios
( id SERIAL PRIMARY KEY
, name TEXT NOT NULL );
CREATE TABLE films
( id SERIAL PRIMARY KEY
, studio_id INTEGER NOT NULL REFERENCES studios(id)
, title TEXT NOT NULL
, release_year INTEGER NOT NULL
, runtime_minutes INTEGER NOT NULL
, tagline TEXT
, genres TEXT[] NOT NULL DEFAULT '{}' );
CREATE TABLE actors
( id SERIAL PRIMARY KEY
, name TEXT NOT NULL );
-- The cast: who plays whom in which film (many-to-many)
CREATE TABLE roles
( film_id INTEGER NOT NULL REFERENCES films(id)
, actor_id INTEGER NOT NULL REFERENCES actors(id)
, character TEXT NOT NULL
, billing INTEGER NOT NULL
, PRIMARY KEY (film_id, billing) );
CREATE INDEX roles_actor_id_idx ON roles (actor_id);
COMMENT ON COLUMN roles.billing IS 'Position in the credits: 1 is top billing';
CREATE TYPE crew_job AS ENUM ('director', 'writer', 'editor', 'cinematographer', 'composer');
CREATE TABLE crew
( id SERIAL PRIMARY KEY
, name TEXT NOT NULL
, job crew_job NOT NULL
, mentor_id INTEGER REFERENCES crew(id) );
CREATE TABLE cinemas
( id SERIAL PRIMARY KEY
, name TEXT NOT NULL
, location POINT NOT NULL );
CREATE INDEX cinemas_location_idx ON cinemas USING gist (location);
COMMENT ON COLUMN cinemas.location IS 'Kilometres east and north of the city centre';
CREATE TABLE screenings
( id SERIAL PRIMARY KEY
, film_id INTEGER NOT NULL REFERENCES films(id)
, cinema_id INTEGER NOT NULL REFERENCES cinemas(id)
, starts_at TIMESTAMPTZ NOT NULL
, subtitled BOOLEAN );
COMMENT ON COLUMN screenings.subtitled IS 'NULL until the cinema says';
-- A seat is a row letter and a number: A1 to Z99
CREATE DOMAIN seat_label AS TEXT CHECK (VALUE ~ '^[A-Z][0-9]{1,2}$');
CREATE TABLE seat_bookings
( screening_id INTEGER NOT NULL REFERENCES screenings(id)
, seat seat_label NOT NULL
, customer TEXT NOT NULL
, booked_at TIMESTAMPTZ NOT NULL DEFAULT now()
, PRIMARY KEY (screening_id, seat) );
CREATE TABLE ratings
( film_id INTEGER NOT NULL REFERENCES films(id)
, stars INTEGER NOT NULL CHECK (stars BETWEEN 1 AND 5)
, votes INTEGER NOT NULL DEFAULT 1
, PRIMARY KEY (film_id, stars) );
CREATE TABLE tags
( film_id INTEGER NOT NULL REFERENCES films(id)
, tag TEXT NOT NULL
, PRIMARY KEY (film_id, tag) );
INSERT INTO studios (id, name) VALUES
(1000, 'Larkspur Pictures')
, (1001, 'Greyhaven Studios')
, (1002, 'Ninefold Films');
INSERT INTO films (id, studio_id, title, release_year, runtime_minutes, tagline, genres) VALUES
(1000, 1000, 'The Salt Orchard', 1974, 112, 'Some harvests take a lifetime.', '{drama}')
, (1001, 1001, 'A Lantern for Edith', 1991, 98, NULL, '{drama,romance}')
, (1002, 1000, 'The Cartographer''s Silence', 2008, 131, 'Every map leaves something out.', '{mystery,thriller}')
, (1003, 1002, 'Small Hours at Calder Bay', 2019, 104, NULL, '{}');
INSERT INTO actors (id, name) VALUES
(1000, 'Odile Marchetti')
, (1001, 'Rufus Pendergast')
, (1002, 'Imogen Thackery')
, (1003, 'Teodor Vasko')
, (1004, 'Saoirse Lindqvist')
, (1005, 'Ambrose Okafor')
, (1006, 'Clementine Ashdown')
, (1007, 'Halvard Brecke');
INSERT INTO roles (film_id, actor_id, character, billing) VALUES
(1000, 1000, 'Marguerite Vey', 1)
, (1000, 1001, 'Silas Vey', 2)
, (1000, 1003, 'The surveyor', 3)
, (1001, 1002, 'Edith Carrow', 1)
, (1001, 1005, 'Tom Carrow', 2)
, (1001, 1000, 'Aunt Lise', 3)
, (1002, 1003, 'Anselm Roth', 1)
, (1002, 1004, 'Greta Holm', 2)
, (1002, 1007, 'The ferryman', 3)
, (1003, 1006, 'Nell Ashby', 1)
, (1003, 1005, 'Dr Idris Kane', 2)
, (1003, 1004, 'Mara Ashby', 3);
INSERT INTO crew (id, name, job, mentor_id) VALUES
(1000, 'Wilhelmina Strand', 'director', NULL)
, (1001, 'Corin Halloway', 'director', 1000)
, (1002, 'Petra Ilves', 'editor', 1000)
, (1003, 'Jonah Merriweather', 'cinematographer', 1001);
INSERT INTO cinemas (id, name, location) VALUES
(1000, 'The Lantern', point(0.4, 1.2))
, (1001, 'Kestrel Picture House', point(-2.1, 0.3))
, (1002, 'The Old Mill Screen', point(3.5, -1.8));
INSERT INTO screenings (id, film_id, cinema_id, starts_at, subtitled) VALUES
(1000, 1000, 1000, '2030-03-01 19:30+00', false)
, (1001, 1002, 1000, '2030-03-01 21:45+00', true)
, (1002, 1003, 1001, '2030-03-02 18:00+00', NULL)
, (1003, 1001, 1002, '2030-03-02 20:15+00', NULL);