kiroku-store-migrations 0.3.2.1 → 0.4.0.0
raw patch · 9 files changed
+449/−15 lines, 9 files
Files
- CHANGELOG.md +56/−0
- README.md +35/−7
- kiroku-store-migrations.cabal +2/−1
- ledger-fixups/2026-07-05-realign-kiroku-migration-timestamps.sql +62/−0
- ledger-fixups/2026-08-16-rebaseline-0010-checksum.sql +83/−0
- migrations/0010.sql +35/−1
- migrations/0011.sql +51/−0
- migrations/manifest +1/−0
- test/Main.hs +124/−6
CHANGELOG.md view
@@ -1,5 +1,61 @@ # Changelog +## 0.4.0.0 — 2026-08-16++### Breaking Changes++* **The payload of migration `0010` is corrected, which changes its checksum.**+ `pg-migrate` keys an applied migration by `(component, migration)` and+ verifies the exact SHA-256 of its payload bytes, so a database that already+ applied `0010` from 0.3.2.0 or 0.3.2.1 will fail every `up` and `verify` with+ a `MigrationChecksumMismatch` until its ledger row is re-baselined. Run+ `ledger-fixups/2026-08-16-rebaseline-0010-checksum.sql` against such a+ database once, before migrating; it rewrites that one checksum and touches+ nothing else. A database that never reached `0010` — every PostgreSQL 17+ upgrade, which is what this release fixes — needs nothing: `0010` is still+ pending there and applies from the corrected payload.++ Editing a released payload is normally forbidden by this package, and a+ forward migration is the documented remedy. There is none available here: the+ withdrawn payload fails at DDL parse time, so no migration ordered after it+ can ever run. 0.3.2.0 and 0.3.2.1 are deprecated on Hackage.++### Fixes++* Migration `0010` no longer defaults `history_retention_leases.lease_id` to an+ unqualified `uuidv7()` (BUG-1). `uuidv7()` is a PostgreSQL 18 builtin; on+ PostgreSQL 17 the name comes from the fallback `0001` installs into the+ Kiroku schema, reachable only through the `search_path` that `0001` itself+ sets. `0010` therefore parsed on a fresh install, where `0001` had just run in+ the same session, and failed with SQLSTATE 42883 on every ordinary upgrade of+ a database already bootstrapped through `0009`. Reported by Kioku; confirmed+ and fixed against PostgreSQL 17.10.++### New Features++* `kiroku.uuidv7()` is now the component's version-independent, always+ schema-qualified UUIDv7 generator. `0010` publishes it on PostgreSQL 18 as an+ alias for the builtin, and PostgreSQL 17 already had it from `0001`'s+ fallback. Migrations after `0001` can name one generator that resolves without+ any session state on every supported PostgreSQL version.+* Added forward migration `0011`, which converges databases that applied the+ withdrawn `0010`: it publishes `kiroku.uuidv7()` where missing and binds+ `lease_id`'s stored default to it. It is a no-op on a database that applied+ the corrected `0010`. Verified on PostgreSQL 17.10 and 18.4: a converged+ database and a fresh install dump identically.++### Other Changes++* The test suite covers the upgrade path that this defect broke — applying the+ pending tail of the plan in a session that never ran `0001` and cannot reach+ the Kiroku schema through `search_path`. The suite connects as role `kiroku`,+ whose default `"$user"` `search_path` entry resolves to the Kiroku schema, so+ every previous case was masked from exactly this class of failure. The new+ case fails on the withdrawn payload against PostgreSQL 17 and passes on the+ corrected one; on PostgreSQL 18 the builtin makes both payloads parse, so+ PostgreSQL 17 coverage is what guards this.+* The `ledger-fixups/` scripts ship in the source distribution.+ ## 0.3.2.1 — 2026-08-15 ### Other Changes
README.md view
@@ -2,9 +2,9 @@ `kiroku-store-migrations` owns Kiroku's PostgreSQL schema as one native `pg-migrate` component named `kiroku`. The component embeds an ordered manifest-and ten SQL payloads, so applications can compose it with other libraries+and eleven SQL payloads, so applications can compose it with other libraries without copying Kiroku SQL. The first seven payloads are immutable historical-Codd bytes; `0008`, `0009`, and `0010` are native-only forward migrations.+Codd bytes; `0008` through `0011` are native-only forward migrations. ## Public API @@ -111,8 +111,29 @@ Migration `0010` adds replay-history retention leases, the per-schema coordinator, an indexed active-lease predicate, and statement-level-`DELETE`/`TRUNCATE` guards on the three event-store data tables.+`DELETE`/`TRUNCATE` guards on the three event-store data tables. Migration+`0011` converges databases that applied the withdrawn 0.3.2.x payload of `0010`+(see below). +## The `kiroku.uuidv7()` generator++`uuidv7()` is a PostgreSQL 18 builtin. On PostgreSQL 17 `0001` installs a+fallback into the Kiroku schema, and it does so under its own+`SET search_path`, so the bare name resolves only in a session that ran `0001`.+Every migration after `0001` runs in whatever session the operator's upgrade+happens to use, so none of them may name it unqualified — that is BUG-1, fixed+in 0.4.0.0.++`0010` therefore publishes `kiroku.uuidv7()` on every supported major version:+PostgreSQL 17 already has it from `0001`, and PostgreSQL 18 gets a thin alias+for the builtin. **New migrations that need a UUIDv7 value must call+`kiroku.uuidv7()`, never bare `uuidv7()`.** The qualified name resolves without+any session state on every version the component supports.++The same rule holds for every other object: name it `kiroku.<name>`. Only+`0001` may rely on `search_path`, because it is the only migration guaranteed+to have set it.+ ## Recovery Migrations are forward-only. Before a persistent upgrade, take a backup. If an@@ -120,7 +141,14 @@ migration. Do not delete or rewrite an applied `pgmigrate.migrations` row except through the reviewed `pg-migrate` repair workflow. -The historical script under `ledger-fixups/` remains checked in only as source-evidence for databases that previously needed Codd timestamp repair. New native-migrations use component-local numeric identities and do not use timestamped-filenames.+`ledger-fixups/` holds operator scripts that adjust the migration ledger's+bookkeeping without touching your schema. Read the header of a script before+running it; each states exactly which databases need it.++* `2026-07-05-realign-kiroku-migration-timestamps.sql` is historical, kept as+ source evidence for databases that once needed Codd timestamp repair. New+ native migrations use component-local numeric identities.+* `2026-08-16-rebaseline-0010-checksum.sql` re-baselines `0010`'s stored+ checksum for databases that applied the withdrawn 0.3.2.0/0.3.2.1 payload.+ Required before migrating such a database onto 0.4.0.0 or later; see the+ changelog.
kiroku-store-migrations.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: kiroku-store-migrations-version: 0.3.2.1+version: 0.4.0.0 synopsis: Schema migrations for kiroku-store description: Native pg-migrate component, Codd history mapping, and migration executable@@ -15,6 +15,7 @@ category: Database, Eventing extra-doc-files: CHANGELOG.md extra-source-files:+ ledger-fixups/*.sql migrations/*.sql migrations/manifest migrations.lock
+ ledger-fixups/2026-07-05-realign-kiroku-migration-timestamps.sql view
@@ -0,0 +1,62 @@+-- Ledger realignment for the kiroku migration-timestamp rename.+--+-- The kiroku-store migrations were renamed from hand-assigned sentinel+-- timestamps (…-00-00-00, …-00-00-01, …) to their real UTC authoring times+-- (commits dac1a0b and e1f6c02). codd decides whether a migration is already+-- applied by FILENAME (`SELECT … FROM codd.sql_migrations WHERE name = ?`),+-- so a database that already applied the old names would otherwise treat every+-- renamed file as pending and re-run it.+--+-- This script rewrites the `name` and `migration_timestamp` columns of the+-- codd ledger from the old identity to the new one, so codd sees the renamed+-- migrations as already applied and skips them. It changes ONLY codd's+-- bookkeeping — never your schema.+--+-- WHEN TO RUN: once per long-lived database (staging/prod/persistent local),+-- BEFORE the next `codd up` / migrate that carries the renamed files. This+-- includes downstream databases that ran kiroku migrations bundled with a+-- consumer's own (e.g. keiro's combined kiroku<>keiro ledger). Ephemeral /+-- template-per-suite test databases do not need it — they apply from scratch.+--+-- SAFETY: the remap is 1:1 onto brand-new values, so neither UNIQUE(name) nor+-- UNIQUE(migration_timestamp) can be violated (the new timestamps also do not+-- collide with any keiro rows in a combined ledger); and it is idempotent — a+-- second run matches no rows. Wrapped in a transaction so it is all-or-nothing.+--+-- LEDGER LOCATION: codd v0.1.8 stores fresh ledgers at+-- `codd.sql_migrations` and auto-renames older `codd_schema` ledgers on first+-- contact. This script detects `codd.sql_migrations` first and falls back to+-- `codd_schema.sql_migrations` for databases that have not yet been touched by+-- a v0.1.8 migrate.++BEGIN;++DO $$+DECLARE+ ledger_table regclass;+BEGIN+ ledger_table := to_regclass('codd.sql_migrations');+ IF ledger_table IS NULL THEN+ ledger_table := to_regclass('codd_schema.sql_migrations');+ END IF;++ IF ledger_table IS NULL THEN+ RAISE EXCEPTION 'Could not find codd.sql_migrations or codd_schema.sql_migrations';+ END IF;++ EXECUTE format('UPDATE %s SET name = %L, migration_timestamp = %L::timestamptz WHERE name = %L', ledger_table, '2026-05-16-12-17-14-kiroku-bootstrap.sql', '2026-05-16 12:17:14+00', '2026-05-16-00-00-00-kiroku-bootstrap.sql');+ EXECUTE format('UPDATE %s SET name = %L, migration_timestamp = %L::timestamptz WHERE name = %L', ledger_table, '2026-05-29-15-26-04-add-subscription-dead-letters.sql', '2026-05-29 15:26:04+00', '2026-05-26-00-00-00-add-subscription-dead-letters.sql');+ EXECUTE format('UPDATE %s SET name = %L, migration_timestamp = %L::timestamptz WHERE name = %L', ledger_table, '2026-06-14-13-17-09-notify-trigger-append-guard.sql', '2026-06-14 13:17:09+00', '2026-06-11-00-00-00-notify-trigger-append-guard.sql');+ EXECUTE format('UPDATE %s SET name = %L, migration_timestamp = %L::timestamptz WHERE name = %L', ledger_table, '2026-06-14-13-25-40-dead-letters-event-id-index.sql', '2026-06-14 13:25:40+00', '2026-06-11-00-00-01-dead-letters-event-id-index.sql');+ EXECUTE format('UPDATE %s SET name = %L, migration_timestamp = %L::timestamptz WHERE name = %L', ledger_table, '2026-06-14-13-54-48-index-hygiene-and-streams-fillfactor.sql', '2026-06-14 13:54:48+00', '2026-06-11-00-00-02-index-hygiene-and-streams-fillfactor.sql');+ EXECUTE format('UPDATE %s SET name = %L, migration_timestamp = %L::timestamptz WHERE name = %L', ledger_table, '2026-06-14-14-01-17-stream-name-length-check.sql', '2026-06-14 14:01:17+00', '2026-06-11-00-00-03-stream-name-length-check.sql');+ EXECUTE format('UPDATE %s SET name = %L, migration_timestamp = %L::timestamptz WHERE name = %L', ledger_table, '2026-06-24-09-42-22-stream-truncate-before.sql', '2026-06-24 09:42:22+00', '2026-06-24-00-00-00-stream-truncate-before.sql');+END $$;++-- Sanity check: no stale sentinel-named kiroku rows should remain.+-- Expect zero rows.+-- SELECT name FROM codd.sql_migrations+-- WHERE name LIKE '2026-%' AND substr(name, 12, 8) IN+-- ('00-00-00','00-00-01','00-00-02','00-00-03');++COMMIT;
+ ledger-fixups/2026-08-16-rebaseline-0010-checksum.sql view
@@ -0,0 +1,83 @@+-- Ledger re-baseline for the corrected payload of migration 0010 (BUG-1).+--+-- kiroku-store-migrations 0.3.2.0 and 0.3.2.1 shipped a payload of 0010 whose+-- history_retention_leases.lease_id default named uuidv7() unqualified. On+-- PostgreSQL 17 that name resolves only through search_path, which is set by+-- 0001 and by nothing else, so 0010 could not parse in any session that did not+-- also apply 0001 -- that is, in every ordinary upgrade. 0.4.0.0 corrects the+-- payload; there is no forward-migration alternative, because the withdrawn+-- payload fails at DDL parse time and nothing after it can run.+--+-- pg-migrate keys an applied migration by (component, migration) and verifies+-- the exact SHA-256 of its payload bytes. Correcting the payload therefore+-- changes 0010's checksum, and a database that already applied the withdrawn+-- payload will fail every subsequent `up` and `verify` with a+-- MigrationChecksumMismatch until its stored checksum is re-baselined. This+-- script performs that re-baseline and nothing else.+--+-- WHEN TO RUN: once per long-lived database that ALREADY APPLIED the withdrawn+-- 0010 -- PostgreSQL 18, or a fresh PostgreSQL 17 install performed by+-- 0.3.2.0/0.3.2.1 -- BEFORE the next migrate carrying 0.4.0.0. A database that+-- never reached 0010 (the PostgreSQL 17 upgrade path this bug broke) does not+-- need it: 0010 is still pending there and applies from the corrected payload.+-- Ephemeral test databases do not need it either; they apply from scratch.+--+-- WHAT IT DOES NOT DO: it does not touch your schema. The withdrawn and+-- corrected payloads differ in two schema-visible ways -- the corrected one+-- publishes kiroku.uuidv7() on PostgreSQL 18 and binds lease_id's default to+-- it. Forward migration 0011 applies both to an already-0010 database through+-- the ordinary runner, so run this script first and then migrate normally.+-- Verified on PostgreSQL 17.10 and 18.4: a database converged this way dumps+-- byte-for-byte identically to a fresh 0.4.0.0 install.+--+-- SAFETY: the UPDATE matches only a row still carrying the withdrawn checksum,+-- so it is idempotent -- a second run, or a run against a database that applied+-- the corrected payload, matches no rows. Wrapped in a transaction.+--+-- LEDGER LOCATION: pg-migrate's default ledger schema is `pgmigrate`. If your+-- application configured a different schema through `ledgerConfig`, change the+-- schema name in the to_regclass call below to match.++BEGIN;++DO $$+DECLARE+ ledger_table regclass;+ withdrawn_checksum bytea :=+ decode('257d94b8ea24156af0ee477196c5a6682cf5616cf8dfd0cfc527a49b5e7a97ac', 'hex');+ corrected_checksum bytea :=+ decode('debc19187d79cd263be66c6c85cd789c1176e508d562a27390095aaa70f650c4', 'hex');+ rebaselined integer;+BEGIN+ ledger_table := to_regclass('pgmigrate.migrations');++ IF ledger_table IS NULL THEN+ RAISE EXCEPTION+ 'Could not find pgmigrate.migrations; edit this script if the ledger uses a different schema';+ END IF;++ EXECUTE format(+ 'UPDATE %s SET checksum = $1+ WHERE component = ''kiroku'' AND migration = ''0010'' AND checksum = $2',+ ledger_table+ )+ USING corrected_checksum, withdrawn_checksum;++ GET DIAGNOSTICS rebaselined = ROW_COUNT;++ IF rebaselined = 0 THEN+ RAISE NOTICE+ 'no kiroku/0010 row carried the withdrawn checksum; nothing to re-baseline';+ ELSE+ RAISE NOTICE 're-baselined the kiroku/0010 checksum';+ END IF;+END $$;++-- Sanity check: kiroku/0010 must now carry the corrected checksum, or be+-- absent because this database has not reached 0010 yet. Expect one row with+-- ok = true, or zero rows.+-- SELECT encode(checksum, 'hex') =+-- 'debc19187d79cd263be66c6c85cd789c1176e508d562a27390095aaa70f650c4' AS ok+-- FROM pgmigrate.migrations WHERE component = 'kiroku' AND migration = '0010';++COMMIT;
migrations/0010.sql view
@@ -1,5 +1,39 @@ -- add replay history retention +-- Publish kiroku.uuidv7() as the component's version-independent, always+-- schema-qualified UUIDv7 generator.+--+-- PostgreSQL 18 provides pg_catalog.uuidv7(), so 0001 installed no fallback+-- and the name exists only in pg_catalog. PostgreSQL 17 has no builtin, so+-- 0001 installed the fallback into the Kiroku schema. Naming uuidv7()+-- unqualified therefore resolves only through search_path, which no migration+-- after 0001 may depend on. Establishing kiroku.uuidv7() on both versions lets+-- this migration -- and every migration after it -- name one generator that+-- resolves without session state on every PostgreSQL version the component+-- supports.+DO $$+BEGIN+ IF to_regprocedure('kiroku.uuidv7()') IS NOT NULL THEN+ -- PostgreSQL 17: 0001's fallback already occupies the name.+ RETURN;+ END IF;++ IF to_regprocedure('pg_catalog.uuidv7()') IS NULL THEN+ RAISE EXCEPTION+ 'no uuidv7() generator: neither pg_catalog.uuidv7() nor kiroku.uuidv7() exists';+ END IF;++ -- PostgreSQL 18+: alias the builtin so the qualified name is available.+ EXECUTE $fn$+ CREATE FUNCTION kiroku.uuidv7()+ RETURNS uuid+ LANGUAGE sql+ VOLATILE+ AS 'SELECT pg_catalog.uuidv7()'+ $fn$;+END+$$;+ -- A schema-local singleton row serializes lease lifecycle changes with every -- destructive statement without adding work to append or ordinary read paths. CREATE TABLE kiroku.history_retention_coordinator (@@ -15,7 +49,7 @@ -- set to remain stable. Active state is derived from released_at and database -- time; expiry needs no worker or cleanup mutation. CREATE TABLE kiroku.history_retention_leases (- lease_id UUID PRIMARY KEY DEFAULT uuidv7(),+ lease_id UUID PRIMARY KEY DEFAULT kiroku.uuidv7(), owner TEXT NOT NULL, reason TEXT NOT NULL, protected_through BIGINT NOT NULL,
+ migrations/0011.sql view
@@ -0,0 +1,51 @@+-- converge databases that applied the withdrawn 0.3.2.x payload of 0010++-- kiroku-store-migrations 0.3.2.0 and 0.3.2.1 shipped a payload of 0010 whose+-- lease_id default named uuidv7() unqualified. That payload could not parse on+-- PostgreSQL 17 outside the bootstrap session (BUG-1), and where it did apply+-- -- PostgreSQL 18, or a fresh PostgreSQL 17 install -- it left the schema in a+-- state the corrected 0010 does not produce: no kiroku.uuidv7(), and a lease_id+-- default bound to whichever uuidv7() the applying session happened to resolve.+--+-- The corrected 0010 cannot reach those databases: its ledger row is already+-- present, and re-baselining that row (see ledger-fixups/) restores the+-- checksum without replaying the SQL. This migration is what makes both+-- populations converge, and it is a no-op on any database that applied the+-- corrected 0010.++-- 1. kiroku.uuidv7() exists on every supported major version. Identical to the+-- header of the corrected 0010, and skipped there because 0010 ran first.+DO $$+BEGIN+ IF to_regprocedure('kiroku.uuidv7()') IS NOT NULL THEN+ -- PostgreSQL 17, or a database that applied the corrected 0010.+ RETURN;+ END IF;++ IF to_regprocedure('pg_catalog.uuidv7()') IS NULL THEN+ RAISE EXCEPTION+ 'no uuidv7() generator: neither pg_catalog.uuidv7() nor kiroku.uuidv7() exists';+ END IF;++ EXECUTE $fn$+ CREATE FUNCTION kiroku.uuidv7()+ RETURNS uuid+ LANGUAGE sql+ VOLATILE+ AS 'SELECT pg_catalog.uuidv7()'+ $fn$;+END+$$;++-- 2. Bind lease_id's default to the qualified generator. A column default is+-- stored as a resolved function OID, so a database that applied the+-- withdrawn payload on PostgreSQL 18 holds pg_catalog.uuidv7() here. Both+-- generators produce the same values; this only makes the stored default+-- match what the corrected 0010 installs, so a dumped schema is identical+-- whichever payload the database applied. Existing lease rows are untouched+-- and no table is rewritten.+ALTER TABLE kiroku.history_retention_leases+ ALTER COLUMN lease_id SET DEFAULT kiroku.uuidv7();++COMMENT ON SCHEMA kiroku IS+ 'Managed by pg-migrate component kiroku through 0011';
migrations/manifest view
@@ -8,3 +8,4 @@ 0008-schema-management-comment.sql 0009.sql 0010.sql+0011.sql
test/Main.hs view
@@ -11,6 +11,8 @@ import Data.Foldable (toList) import Data.Int (Int32, Int64) import Data.List (sort)+import Data.List.NonEmpty (NonEmpty ((:|)))+import Data.List.NonEmpty qualified as NonEmpty import Data.Text (Text) import Data.Text qualified as Text import Data.Text.IO qualified as Text.IO@@ -42,7 +44,7 @@ main :: IO () main = hspec $ do describe "native Kiroku migration definition" $ do- it "tracks the ten native files in manifest order" $ do+ it "tracks the eleven native files in manifest order" $ do directory <- findMigrationsDirectory manifest <- Text.lines <$> Text.IO.readFile (directory </> "manifest") manifest `shouldBe` Text.pack <$> nativeMigrationFiles@@ -55,7 +57,7 @@ bytes <- ByteString.readFile (directory </> nativeName) lookup legacyName lockEntries `shouldBe` Just (checksumText bytes) - it "builds component kiroku and a ten-migration plan" $ do+ it "builds component kiroku and an eleven-migration plan" $ do component <- requireRight kirokuMigrations component `seq` pure () plan <- requirePlan@@ -91,7 +93,7 @@ `shouldReturn` "0007-existing.sql\n" describe "fresh native databases" $ do- it "applies all ten, verifies strictly, and reports AlreadyApplied on rerun" $ do+ it "applies all eleven, verifies strictly, and reports AlreadyApplied on rerun" $ do plan <- requirePlan result <- withMigratedDatabase plan $ \connection -> do assertSchema connection@@ -273,6 +275,66 @@ facts `shouldBe` (1, 9, True, True, 6) either (expectationFailure . show) pure result + -- BUG-1. Migrations 0002 onward are parsed in sessions that never ran 0001,+ -- so a name that only 0001's `SET search_path` makes resolvable parses on a+ -- fresh install and fails on every ordinary upgrade. 0.3.2.x's 0010 named+ -- uuidv7() that way and could not be applied to a database bootstrapped+ -- through 0009 on PostgreSQL 17. These cases apply the tail of the plan in a+ -- session that carries none of 0001's state.+ describe "upgrades of already-bootstrapped databases" $ do+ it "applies the pending tail in a session that never ran the bootstrap" $ do+ plan <- requirePlan+ throughBootstrap <- planThrough (length nativeMigrationFiles - 2)+ withKirokuPg $ \database -> do+ let settings = Pg.connectionSettings database+ bootstrapped <-+ runMigrationPlan defaultRunOptions settings throughBootstrap+ >>= requireMigration+ reportOutcomes bootstrapped+ `shouldBe` replicate (length nativeMigrationFiles - 2) AppliedNow++ -- A separate session, and one that cannot reach the Kiroku+ -- schema through search_path. The suite connects as role+ -- "kiroku", so the default "$user" entry resolves to the Kiroku+ -- schema and would hide exactly the name resolution this case+ -- exists to prove -- the same accident that let 0001's+ -- SET search_path carry a fresh install through 0.3.2.x's 0010.+ withConnection settings $ \upgradeSession -> do+ useSession upgradeSession (Session.script "SET search_path TO pg_catalog")+ upgraded <-+ runMigrationPlanWith defaultRunOptions (providerFor upgradeSession) plan+ >>= requireMigration+ reportOutcomes upgraded+ `shouldBe` replicate (length nativeMigrationFiles - 2) AlreadyApplied+ <> replicate 2 AppliedNow++ verified <- verifyMigrationPlan defaultRunOptions settings plan >>= requireMigration+ case verified of+ VerificationReport verificationIssues applied _ _ -> do+ verificationIssues `shouldBe` []+ length applied `shouldBe` length nativeMigrationFiles+ withConnection settings assertSchema++ it "resolves the lease default through the qualified component generator" $ do+ plan <- requirePlan+ result <- withMigratedDatabase plan $ \connection -> do+ -- kiroku.uuidv7() is the component's version-independent+ -- generator: PostgreSQL 17 gets 0001's fallback, PostgreSQL 18+ -- gets 0010's alias for the builtin. Either way the stored+ -- default names it, and no later migration needs search_path to+ -- reach a UUIDv7 generator.+ generator <- useSession connection (Session.statement () leaseDefaultStatement)+ generator `shouldBe` (True, Just "kiroku.uuidv7()")+ either (expectationFailure . show) pure result++ it "acquires a lease from a session whose search_path excludes kiroku" $ do+ plan <- requirePlan+ result <- withMigratedDatabase plan $ \connection -> do+ useSession connection (Session.script "SET search_path TO pg_catalog")+ version <- useSession connection (Session.statement () insertLeaseStatement)+ version `shouldBe` 7+ either (expectationFailure . show) pure result+ describe "Codd history import" $ do it "imports a current codd V5 ledger, verifies, and never replays SQL" $ importFixture "codd"@@ -318,14 +380,14 @@ pendingIds <- traverse (requireRight . migrationId "kiroku")- ["0008-schema-management-comment", "0009", "0010"]+ ["0008-schema-management-comment", "0009", "0010", "0011"] verifiedBeforeCanary <- verifyMigrationPlan defaultRunOptions settings plan >>= requireMigration case verifiedBeforeCanary of VerificationReport verificationIssues _ _ _ -> verificationIssues `shouldBe` (PendingMigration <$> pendingIds) up <- runMigrationPlan defaultRunOptions settings plan >>= requireMigration- reportOutcomes up `shouldBe` replicate 7 AlreadyApplied <> replicate 3 AppliedNow+ reportOutcomes up `shouldBe` replicate 7 AlreadyApplied <> replicate 4 AppliedNow verifiedAfterCanary <- verifyMigrationPlan defaultRunOptions settings plan >>= requireMigration case verifiedAfterCanary of VerificationReport verificationIssues _ _ _ ->@@ -355,8 +417,27 @@ , "0008-schema-management-comment.sql" , "0009.sql" , "0010.sql"+ , "0011.sql" ] +{- | The plan truncated to its first @count@ migrations, read from the checked-in+directory. It stands in for a database that a previous release bootstrapped: the+ledger it produces is byte-identical to the one the full plan would have written+for those entries, so the full plan later sees exactly the remainder as pending.+-}+planThrough :: Int -> IO MigrationPlan+planThrough count = do+ directory <- findMigrationsDirectory+ entries <- traverse (readEntry directory) (take count nativeMigrationFiles)+ component <-+ requireRight+ (migrationComponentFromEmbeddedSql "kiroku" mempty (NonEmpty.fromList entries))+ requireRight (migrationPlan (component :| []))+ where+ readEntry directory file = do+ bytes <- ByteString.readFile (directory </> file)+ pure (file, bytes)+ findMigrationsDirectory :: IO FilePath findMigrationsDirectory = findDirectory ["kiroku-store-migrations/migrations", "migrations"]@@ -472,11 +553,48 @@ (EXISTS (SELECT 1 FROM pg_catalog.pg_indexes WHERE schemaname = 'kiroku' AND indexname = 'ix_dead_letters_event_id')), (EXISTS (SELECT 1 FROM pg_catalog.pg_constraint WHERE conname = 'chk_streams_stream_name_length')), (EXISTS (SELECT 1 FROM pg_catalog.pg_attribute a JOIN pg_catalog.pg_class c ON c.oid = a.attrelid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'kiroku' AND c.relname = 'streams' AND a.attname = 'truncate_before' AND NOT a.attisdropped)),- (obj_description(to_regnamespace('kiroku'), 'pg_namespace') = 'Managed by pg-migrate component kiroku through 0010')+ (obj_description(to_regnamespace('kiroku'), 'pg_namespace') = 'Managed by pg-migrate component kiroku through 0011') ) AS checks(ok) """ Encoders.noParams (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.bool)))++-- | Whether @kiroku.uuidv7()@ exists, and the stored @lease_id@ default.+leaseDefaultStatement :: Statement () (Bool, Maybe Text)+leaseDefaultStatement =+ Statement.preparable+ """+ SELECT to_regprocedure('kiroku.uuidv7()') IS NOT NULL,+ (SELECT pg_catalog.pg_get_expr(stored.adbin, stored.adrelid)+ FROM pg_catalog.pg_attrdef AS stored+ JOIN pg_catalog.pg_attribute AS attribute+ ON attribute.attrelid = stored.adrelid+ AND attribute.attnum = stored.adnum+ WHERE stored.adrelid = 'kiroku.history_retention_leases'::regclass+ AND attribute.attname = 'lease_id')+ """+ Encoders.noParams+ ( Decoders.singleRow+ ( (,)+ <$> Decoders.column (Decoders.nonNullable Decoders.bool)+ <*> Decoders.column (Decoders.nullable Decoders.text)+ )+ )++-- | Insert one lease and report the UUID version its default generated.+insertLeaseStatement :: Statement () Int32+insertLeaseStatement =+ Statement.preparable+ """+ INSERT INTO kiroku.history_retention_leases+ (owner, reason, protected_through, created_at, renewed_at, expires_at)+ VALUES+ ('kiroku-store-migrations-test', 'search_path independence', 0,+ pg_catalog.now(), pg_catalog.now(), pg_catalog.now() + interval '1 hour')+ RETURNING pg_catalog.get_byte(pg_catalog.uuid_send(lease_id), 6) >> 4+ """+ Encoders.noParams+ (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4))) historyRetentionColumnsStatement :: Statement () [(Text, Text, Text, Bool)] historyRetentionColumnsStatement =