packages feed

kiroku-store-migrations 0.3.2.1 → 0.4.0.0

raw patch · 9 files changed

+449/−15 lines, 9 files

Files

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 =