# Reproduce the Ptah 0.8.1 examples

The post uses the official Ptah release binaries from
<https://github.com/stokaro/ptah/releases/tag/v0.8.1>, and from
<https://github.com/stokaro/ptah/releases/tag/v0.8.0> where it compares the two.
Every example ran on September 25, 2026 (UTC) with the linux/amd64 binaries
inside disposable database containers on the Docker context
`remote-dev-container`:

- `online/` and `lint/` in PostgreSQL 18.6, the image the 0.8.0 post used;
- `atlas/` in `timescale/timescaledb:latest-pg18`, which is PostgreSQL 18.6 with
  TimescaleDB 2.30.1.

In each container, `ptah` and `ptah-compat` are 0.8.1, and the 0.8.0 binaries
sit in `/opt/ptah-0.8.0/`. The `atlas` binary is Atlas CE v1.3.0, built from
the source archive `stokaro/ptah` pins in `scripts/atlas-ce-reference.lock`.
`verified.json` records the archive and binary checksums, the image digests,
and the hash of every file in this directory.

`DATABASE_URL` is the connection URL of an empty database you can discard.
`commands.json` has the exact argument list, exit code, start time and elapsed
time of every command. The URLs in it carry a test-only credential. Elapsed
times include starting `docker exec` over SSH and are not statement timings.

## online: the generated migration under a lock timeout

Run these from `online/`. The fixtures are the 0.8.0 post's online example.
`ptah.yaml` sets `diff.online_alter: true`, and `schema.sql` adds the
`accounts_plan_known` check.

Start with only the `1790100000_create_accounts` pair in `migrations/`. Hash
it, apply it, generate the change, and hash again:

```console
ptah migrations hash --dir migrations
ptah migrations up --db-url "$DATABASE_URL" --migrations-dir migrations --verify-sum
ptah migrations generate --db-url "$DATABASE_URL" --schema-file schema.sql --migrations-dir migrations --name plan_known
ptah migrations hash --dir migrations
```

The version in the file name is the time of the run. The recorded run wrote
`1790317351_plan_known`, which is in `migrations/` as generated, without a
final newline.

Lint the new migration with the default rules, then with the online mode:

```console
ptah migrations lint --dir migrations --dialect postgres --latest 1
ptah migrations lint --dir migrations --dialect postgres --latest 1 --config online-lint.yaml
```

Both exit 0 and print `No lint findings.` The 0.8.0 post recorded `PG305` on
the same file in both runs.

Turn the online mode on for `migrations up` by copying `online-lint.yaml` to
`migrations/.ptah-lint.yaml`. The recorded run did that as a step, so the file
is not in `migrations/` here. Then:

```console
ptah migrations up --db-url "$DATABASE_URL" --migrations-dir migrations --verify-sum --dry-run
ptah migrations up --db-url "$DATABASE_URL" --migrations-dir migrations --verify-sum --dry-run --tx-mode none
ptah migrations up --db-url "$DATABASE_URL" --migrations-dir migrations --verify-sum --dry-run --lock-timeout 3s
```

The first two exit 2 with the same message
(`measured/up-dry-run.stderr.txt`, `measured/up-dry-run-tx-none.stderr.txt`).
The third exits 0. Its standard error
(`measured/up-dry-run-lock-timeout.stderr.txt`) lists `SET lock_timeout =
'3000ms'`, both `ALTER TABLE` statements and `RESET lock_timeout`, in that
order.

### The lock queue

Use three terminals. In the first, hold the table for ten seconds:

```console
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f blocker.sql
```

About two seconds later, apply the migration in the second:

```console
ptah migrations up --db-url "$DATABASE_URL" --migrations-dir migrations --verify-sum --lock-timeout 3s
```

About a second and a half after that, in the third, list the sessions waiting
for a lock, then run a read with a five-second lock timeout. `reader-timed.sql`
turns on psql's `\timing`:

```console
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f waiting.sql
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f reader-timed.sql
```

In the recorded run, the read took 0.691 ms before the migration started
(`measured/reader-before.txt`). While the migration waited, `waiting.sql`
listed the `ALTER TABLE ... ADD CONSTRAINT` waiting on a lock
(`measured/waiting.txt`), and the read took 544.052 ms
(`measured/reader-during.txt`). The migration exited 2 on
`canceling statement due to lock timeout`
(`measured/up-behind-blocker.stderr.txt`; `measured/lock-timeout.txt` is its
last two lines). `measured/log.json` dates the failed attempt from
06:22:43.090280Z to 06:22:46.100550Z. The blocking `psql` was started at
06:22:40.020Z and returned 11.068 s later.

After the timeout, `constraint.sql` returned no row
(`measured/constraint-after-timeout.txt`), and
`migrations status --verify-sum --exit-code` exited 1 with the report in
`measured/status-after-timeout.txt`. A plain retry exited 2
(`measured/up-retry.stderr.txt`). Once the blocking transaction had ended:

```console
ptah migrations up --db-url "$DATABASE_URL" --migrations-dir migrations --verify-sum --lock-timeout 3s --allow-dirty
ptah schema compare --db-url "$DATABASE_URL" --schema-file schema.sql --exit-code
ptah migrations status --db-url "$DATABASE_URL" --migrations-dir migrations --verify-sum --exit-code
```

All three exit 0. `measured/constraint.txt` shows `accounts_plan_known`
validated, and `measured/log.txt` lists the applied, failed and applied
attempts.

## lint: which stream carries the report

Run these from `lint/`. The migration drops a column, so the lint fails:

```console
ptah migrations lint --dir migrations --dialect postgres --format json
```

Both versions exit 1. 0.8.1 wrote the JSON report to standard output and the
`DS110P` note to standard error (`measured/report-0.8.1.json`,
`measured/report-0.8.1.stderr.txt`). 0.8.0 wrote them the other way round
(`measured/report-0.8.0.txt`, `measured/report-0.8.0.stderr.txt`).

## atlas: an Atlas project on TimescaleDB

Run these from `atlas/`. `migrations/` holds `001_events.sql` and
`002_events_kind.sql`, and Atlas CE wrote `migrations/atlas.sum`:

```console
atlas migrate hash --dir file://migrations
```

Each database ran `bootstrap.sql` first, as the project's CI does. The
TimescaleDB image already installs the extension in every new database, so
`bootstrap.sql` printed a notice and changed nothing.

Apply with each `ptah-compat`, read the revision table, and ask Atlas:

```console
/opt/ptah-0.8.0/ptah-compat migrate apply --dir file://migrations --url "$DATABASE_URL"
ptah-compat migrate apply --dir file://migrations --url "$DATABASE_URL"
psql "$DATABASE_URL" -f revisions.sql
atlas migrate status --dir file://migrations --url "$DATABASE_URL"
```

0.8.0 exited 1 (`measured/apply-0.8.0.stderr.txt`). 0.8.1 exited 0
(`measured/apply.txt`), the table holds `001` and `002`
(`measured/revisions.txt`), and Atlas reports OK with nothing pending
(`measured/atlas-status.txt`).

Lint against a dev database, one fresh database for each tool:

```console
/opt/ptah-0.8.0/ptah-compat migrate lint --dir file://migrations --dev-url "$DEV_URL" --latest 2
ptah-compat migrate lint --dir file://migrations --dev-url "$DEV_URL" --latest 2
atlas migrate lint --dir file://migrations --dev-url "$DEV_URL" --latest 2
```

0.8.0 exited 1 (`measured/lint-0.8.0.stderr.txt`). 0.8.1 exited 0
(`measured/lint.txt`); its `PG101` finding on `002_events_kind.sql` advises a
concurrent build, which TimescaleDB refuses on a hypertable
(<https://github.com/stokaro/ptah/issues/3551>). Atlas CE exited 0 with no
diagnostics (`measured/lint-atlas.txt`). Afterwards the dev databases of 0.8.0
and 0.8.1 still had `timescaledb`, and the one Atlas used had only `plpgsql`
(`measured/extensions-*.txt`).

### A history 0.8.0 wrote

On another database, 0.8.0 applied the directory with `--allow-dirty`, to get
past its clean check. It recorded `1` and `2`
(`measured/older-revisions.txt`). On that database, 0.8.1
`ptah-compat migrate status` reported OK with nothing pending
(`measured/older-status.txt`), `ptah-compat migrate apply` had nothing to
execute (`measured/older-apply.txt`), and Atlas CE reported OK at current
version `2` (`measured/older-atlas-status.txt`).

### The native commands

```console
ptah migrations up --db-url "$DATABASE_URL" --migrations-dir migrations --dir-format atlas --revision-format atlas
```

On a fresh database, 0.8.1 recorded `001` and `002` in
`atlas_schema_revisions.atlas_schema_revisions`
(`measured/native-revisions.txt`), and Atlas CE reported OK
(`measured/native-atlas-status.txt`).

On another, 0.8.0 ran the same command. It put the table in `public`
(`measured/native080-table.txt`). 0.8.1 `migrations status` with the same flags
then exited 2 (`measured/native080-status.stderr.txt`). With
`--migrations-schema public --exit-code` it exited 1 and listed both rows as
applied migrations with no file (`measured/native080-status-schema.txt`). That
is <https://github.com/stokaro/ptah/issues/3550>.

## known: the 0.8.0 known issues

Run from `known/` with 0.8.1. `measured/mcp-help.txt` still says six reading
tools, and `measured/data-help.txt` still says the command applies no gating.
`ptah schema export --from sql --to markdown --schema-file schema.sql --out
notes.md` wrote `measured/notes.md`, which shows `id` as nullable.

## What the recorded runs do not establish

The online example held no rows, and no application workload ran. The
lock-queue measurement used one blocking transaction and one read. The
TimescaleDB example has two migrations and an empty hypertable, and nothing
here ran the project the post mentions. A `CREATE INDEX CONCURRENTLY`
canceled by a lock timeout, the refusal of a timeout on SQLite and similar
dialects, and `ptah sql lint` were not run.
