The five maps and the signatures read schema.Migrations instead of map[schema.MigrationKey]schema.Migration, and the two derivations are methods on it (targetVersion, minVersion). No behavior change.
Steps were keyed by a bare int that meant "from": map[int]schema.Migration{16: ...}. They are
now keyed by schema.MigrationKey{From, To}, so each step says what it does:
{From: 16, To: 17}: schema.Query(...). Every key was copied, not recomputed (16 became
{16, 17}), so no step moved. A key that does not go exactly one version fails at startup,
next to the gap check. Stored versions are unchanged.
Migrate took the target version as a separate argument, kept in step with the steps by hand:
adding a step without bumping the version constant silently skipped it on existing installs,
while fresh installs got the new tables. The target is now the version after the last step (1
without steps), and the steps must be contiguous, so a forgotten one fails at startup. The
per-store version constants are gone.
Instead of on every start: the schema is defined in one place again. Postgres gets
idx_message_topic_published_id (topic, published, id), SQLite idx_topic_published_time (topic,
published, time), both with IF NOT EXISTS (ntfy.sh got the Postgres one by hand). A downgrade to
v2.31.0 or earlier now needs the schema version set back by hand, as described in the release notes;
from v2.31.1 on, a newer schema only logs a warning (#2059).
Each migration step is now a schema.Migration: schema.Query, schema.Func or schema.Nop for additive
steps, wrapped in schema.Breaking for a breaking one. Migrate derives the store's minimum version
(the version after the newest breaking step, or 1) instead of taking it from a per-store constant,
so a breaking migration cannot be added without raising the minimum. The existing steps are all
marked additive; older code never checks the minimum, so classifying them changes nothing.
#2059 lets older code start on a newer schema, which is only safe while migrations are additive.
For the rare breaking migration, each store now records the oldest schema version whose code still
runs on its schema (min_version in the version table). A breaking migration raises it; older code
then fails at startup with a clear error instead of misreading the data.
- schema.Migrate takes minVersion; every store passes its MinSchemaVersion constant (1: no
breaking migration recorded)
- The version table gets a min_version column (default 1), added in place to existing tables;
older code writes only the version and keeps working on the new table
The replay query of #2040 matched "id > marker OR (time >= marker time AND id <> marker)". The index
can check neither side of the OR, so Postgres read every row of the topic from the table and filtered
it afterwards; after the v2.31.0 upgrade, the reconnects of ntfy.sh's subscribers saturated a read
replica within a minute.
The two halves are now separate queries joined with UNION ALL (they cannot overlap: one has
id > marker, the other id < marker), and the index checks every condition of each: rows after
the marker as in v2.30.0, and late-written rows through a time range. SQLite gets an index on
(topic, published, time) for the time half, created on every start instead of in a migration, so
older versions still open the cache file. On ntfy.sh's busiest topic the replay now takes ~60 ms
(rows after the marker) and 0.2 ms (late-written rows), both from the index alone.
SQLite gets a nop 15 -> 16 so the two backends keep the same version numbers;
the claim column is Postgres-only because SQLite is single-node by definition.
Also fixes a comment that no longer matched the code: claimMessagesDue stopped
marking rows published two commits ago.