packages feed

kioku-migrations 0.11.0.0 → 0.12.0.0

raw patch · 20 files changed

+852/−24 lines, 20 filesdep ~keiro-migrationsdep ~keiro-test-supportdep ~kioku-migrationsPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: keiro-migrations, keiro-test-support, kioku-migrations, kiroku-store-migrations

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -2,6 +2,22 @@  ## [Unreleased] +## 0.12.0.0 — 2026-10-27++### Added++- Test the complete previous 62-entry cohort using frozen Kiroku migrations+  0.7.1.0 fixtures, proving that only `kiroku/0016` applies and repeat runs+  are no-ops.++### Changed++- Require `mori://shinzui/keiro` migrations and test support 0.22.0.0 and+  `mori://shinzui/kiroku/packages/kiroku-store-migrations` 0.7.2.0.+- Compose 63 migrations (16 Kiroku, 32 Keiro, 15 Kioku). The additive upstream+  migration changes subscription constraints and the Kiroku schema comment;+  all historical Kioku SQL and checksums are preserved. No ledger fixup is needed.+ ## 0.11.0.0 — 2026-10-19  ### Added
kioku-migrations.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: kioku-migrations-version: 0.11.0.0+version: 0.12.0.0 synopsis: Schema migrations for kioku description:   Owns kioku schema evolution as a native pg-migrate component and composes@@ -31,6 +31,9 @@   test/fixtures/kiroku-0.6.0.0/README.md   test/fixtures/kiroku-0.6.0.0/SHA256SUMS   test/fixtures/kiroku-0.6.0.0/manifest+  test/fixtures/kiroku-0.7.1.0/*.sql+  test/fixtures/kiroku-0.7.1.0/README.md+  test/fixtures/kiroku-0.7.1.0/manifest  source-repository head   type: git@@ -75,8 +78,8 @@     containers >=0.6 && <0.8,     hasql >=1.10 && <1.11,     hasql-transaction >=1.2 && <1.3,-    keiro-migrations ^>=0.21.0.0,-    kiroku-store-migrations ^>=0.7.1.0,+    keiro-migrations ^>=0.22.0.0,+    kiroku-store-migrations ^>=0.7.2.0,     pg-migrate ^>=1.2.0.0,     pg-migrate-embed ^>=1.2.0.0,     pg-migrate-import-codd ^>=1.2.0.0,@@ -92,8 +95,8 @@     base >=4.18 && <5,     directory >=1.3 && <1.4,     ephemeral-pg >=0.3.1 && <0.4,-    keiro-test-support ^>=0.21.0.0,-    kioku-migrations ^>=0.11.0.0,+    keiro-test-support ^>=0.22.0.0,+    kioku-migrations ^>=0.12.0.0,     text >=2.0 && <2.2,     unix >=2.8 && <2.9, @@ -115,10 +118,10 @@     bytestring >=0.11 && <0.13,     containers >=0.6 && <0.8,     hasql >=1.6,-    keiro-migrations ^>=0.21.0.0,-    kioku-migrations ^>=0.11.0.0,+    keiro-migrations ^>=0.22.0.0,+    kioku-migrations ^>=0.12.0.0,     kioku-migrations:test-support,-    kiroku-store-migrations ^>=0.7.1.0,+    kiroku-store-migrations ^>=0.7.2.0,     pg-migrate ^>=1.2.0.0,     pg-migrate-embed ^>=1.2.0.0,     pg-migrate-import-codd ^>=1.2.0.0,
test/Main.hs view
@@ -89,7 +89,9 @@         ],       testCase "the full migration chain applies to a fresh database" testFreshDatabase,       testCase "0.10.0.0 upgrades with one recency index and unchanged rows" testRecencyUpgrade,-      testCase "the released previous cohort preserves data and checkpoints through 0013–0015" testPreviousCohortUpgrade,+      testCase "the released previous cohort preserves data and checkpoints through 0013–0016" testPreviousCohortUpgrade,+      testCase "the 62-entry Keiro 0.21 cohort adds only Kiroku 0016" testKeiro021Upgrade,+      testCase "a changed previous checksum refuses before migration writes" testKeiro021ChecksumRefusal,       testGroup         "migration-session isolation"         [testCase "restores the host search path before later components" testHostSearchPathRestored],@@ -473,7 +475,7 @@   checkpointsBefore <- query connStr previousCheckpointSnapshot   query connStr previousCohortIsLegacy >>= (@?= True)   migrated <- runMigrationPlan defaultRunOptions settings current >>= either (assertFailure . show) pure-  appliedNow migrated @?= [expectRight (migrationId "kiroku" n) | n <- ["0013", "0014", "0015"]]+  appliedNow migrated @?= [expectRight (migrationId "kiroku" n) | n <- ["0013", "0014", "0015", "0016"]]   query connStr previousCohortSnapshot >>= (@?= before)   query connStr previousCohortLedger >>= (@?= ledgerBefore)   query connStr previousCheckpointSnapshot >>= (@?= checkpointsBefore)@@ -481,12 +483,12 @@   verification <- verifyMigrationPlan defaultRunOptions settings current >>= either (assertFailure . show) pure   let VerificationReport {issues, appliedMigrations, pendingMigrations, unknownMigrations} = verification   issues @?= []-  length appliedMigrations @?= 62+  length appliedMigrations @?= 63   pendingMigrations @?= []   unknownMigrations @?= []   repeated <- runMigrationPlan defaultRunOptions settings current >>= either (assertFailure . show) pure   let MigrationReport {results} = repeated-  length [() | MigrationResult {outcome = AlreadyApplied} <- toList results] @?= 62+  length [() | MigrationResult {outcome = AlreadyApplied} <- toList results] @?= 63   appliedNow repeated @?= []   query connStr previousCohortSnapshot >>= (@?= before)   query connStr previousCheckpointSnapshot >>= (@?= checkpointsBefore)@@ -545,7 +547,7 @@     """     SELECT jsonb_agg(to_jsonb(t) ORDER BY component, migration)::text     FROM pgmigrate.migrations t-    WHERE NOT (component = 'kiroku' AND migration IN ('0013', '0014', '0015'))+    WHERE NOT (component = 'kiroku' AND migration IN ('0013', '0014', '0015', '0016'))     """     E.noParams     (D.singleRow (D.column (D.nonNullable D.text)))@@ -578,6 +580,118 @@     E.noParams     (D.singleRow (D.column (D.nonNullable D.bool))) +-- | Frozen released bytes, rather than private pg-migrate representation access.+previous021Plan :: IO MigrationPlan+previous021Plan = do+  entries <- checkMigrationManifest "test/fixtures/kiroku-0.7.1.0/manifest" >>= either (assertFailure . show) pure+  [(Text.dropEnd 4 (Text.pack name), checksumHex bytes) | (name, bytes) <- toList entries] @?= previous021Checksums+  oldKiroku <- either (assertFailure . show) pure (migrationComponentFromEmbeddedSql "kiroku" Set.empty entries)+  keiro <- either (assertFailure . show) pure KeiroMigrations.keiroMigrations+  kioku <- either (assertFailure . show) pure kiokuMigrations+  either (assertFailure . show) pure (migrationPlan (oldKiroku :| [keiro, kioku]))++previous021Checksums :: [(Text, Text)]+previous021Checksums =+  [ ("0001-kiroku-bootstrap", "af7c777f8702d9b5bce6b3ea16540ab7542bc78725cb45e945c6d5b6025a4d66"),+    ("0002-add-subscription-dead-letters", "2771cdd2605a8808ea469c0bcba175c8ba4c5d2465f43eb05c56edb188fa8141"),+    ("0003-notify-trigger-append-guard", "432796c59389d45478ba773b62e9f38d0a562807061eb49280d295ec83248351"),+    ("0004-dead-letters-event-id-index", "f698db092ebf34beeddecc949bbd506afc0b9c63d3e9983ad5d14f203f2ee1cd"),+    ("0005-index-hygiene-and-streams-fillfactor", "80c28c7743f1d9967eadf921c0509fc8477db16500466bf7a49c5f59aadb0fa2"),+    ("0006-stream-name-length-check", "a0e4338a2feadf8f01499a3809d3de78966966689b095ea46195c4d07743e4aa"),+    ("0007-stream-truncate-before", "a4fe42ad8702b43c3d6f72edf263d6b301e783493d49f40a35dd14d2a7928fa7"),+    ("0008-schema-management-comment", "3fb3dd5a7172da3cd14edecb0877cc9963fc9a6dceaff9ed41705a2365070f70"),+    ("0009", "62c80d627dc8f17393d26ff7dbc00cabde11ed38ee7bf3cb1ae42ea89f484c74"),+    ("0010", "debc19187d79cd263be66c6c85cd789c1176e508d562a27390095aaa70f650c4"),+    ("0011", "bf8b76a712a31cb602aee793a11691283ccd1eafdc16224e45e14484512b5af7"),+    ("0012", "32a0a0c877ff1aa6c1f74de8db62c74884e96a9c7500b2b65725b9e654101a7a"),+    ("0013", "49e519280a578e5b0f02603173b492075b072d0ae52d0e5848fea8ae1d6f7d53"),+    ("0014", "b93580ef072e85a6a598724fa8746aaa079346d7dd4a9c859412cc07113ff4d1"),+    ("0015", "821b12e410c1cece52aee018f1916a8cb01b2e900784817ba9b33d37b78d3be5")+  ]++-- The event and application fixture is shared, while modern bindings cover all+-- three pre-namespace target kinds and nonzero checkpoint positions.+prepare021 :: Text -> IO ()+prepare021 connStr = do+  old <- previous021Plan+  installed <- runMigrationPlan defaultRunOptions (Settings.connectionString connStr) old >>= either (assertFailure . show) pure+  length (appliedNow installed) @?= 62+  withConnection connStr $ \conn -> do+    run conn (Session.script (fst (Text.breakOn "INSERT INTO kiroku.subscriptions" previousCohortSeed)))+    run conn (Session.script (Text.replace "kiroku.kioku_" "kioku." relocationSeedRows))+    run+      conn+      ( Session.script+          "INSERT INTO kiroku.subscriptions (subscription_name,last_seen,consumer_group_member,consumer_group_size,target_kind,target_category) VALUES ('upgrade-exact',1,0,1,'category','kioku_memory'),('upgrade-all',1,0,1,'all',NULL),('upgrade-unbound',0,0,1,'unbound',NULL)"+      )++ledgerWithout0016 :: Statement () Text+ledgerWithout0016 =+  preparable+    "SELECT jsonb_agg(to_jsonb(t) ORDER BY component,migration)::text FROM pgmigrate.migrations t WHERE NOT (component='kiroku' AND migration='0016')"+    E.noParams+    (D.singleRow (D.column (D.nonNullable D.text)))++bindingsSnapshot :: Statement () Text+bindingsSnapshot =+  preparable+    "SELECT jsonb_agg(to_jsonb(t) ORDER BY subscription_name,consumer_group_member)::text FROM kiroku.subscriptions t"+    E.noParams+    (D.singleRow (D.column (D.nonNullable D.text)))++testKeiro021Upgrade :: Assertion+testKeiro021Upgrade = withBareDatabase $ \connStr -> do+  prepare021 connStr+  rows <- query connStr previousCohortSnapshot+  ledger <- query connStr ledgerWithout0016+  bindings <- query connStr bindingsSnapshot+  checkpoints <- query connStr previousCheckpointSnapshot+  current <- either (assertFailure . show) pure kiokuMigrationPlan+  migrated <- runMigrationPlan defaultRunOptions (Settings.connectionString connStr) current >>= either (assertFailure . show) pure+  appliedNow migrated @?= [expectRight (migrationId "kiroku" "0016")]+  query connStr previousCohortSnapshot >>= (@?= rows)+  query connStr ledgerWithout0016 >>= (@?= ledger)+  query connStr bindingsSnapshot >>= (@?= bindings)+  query connStr previousCheckpointSnapshot >>= (@?= checkpoints)+  verification <- verifyMigrationPlan defaultRunOptions (Settings.connectionString connStr) current >>= either (assertFailure . show) pure+  let VerificationReport {issues, appliedMigrations, pendingMigrations, unknownMigrations} = verification+  issues @?= []+  length appliedMigrations @?= 63+  pendingMigrations @?= []+  unknownMigrations @?= []+  beforeRepeat <- query connStr fullLedgerSnapshot+  repeated <- runMigrationPlan defaultRunOptions (Settings.connectionString connStr) current >>= either (assertFailure . show) pure+  appliedNow repeated @?= []+  let MigrationReport {results} = repeated+  length [() | MigrationResult {outcome = AlreadyApplied} <- toList results] @?= 63+  query connStr fullLedgerSnapshot >>= (@?= beforeRepeat)+  query connStr previousCohortSnapshot >>= (@?= rows)+  query connStr bindingsSnapshot >>= (@?= bindings)+  query connStr previousCheckpointSnapshot >>= (@?= checkpoints)++testKeiro021ChecksumRefusal :: Assertion+testKeiro021ChecksumRefusal = withBareDatabase $ \connStr -> do+  prepare021 connStr+  withConnection connStr $ \conn ->+    run+      conn+      ( Session.script+          "UPDATE pgmigrate.migrations SET checksum=decode(repeat('00',32),'hex') WHERE component='kiroku' AND migration='0015'"+      )+  ledger <- query connStr fullLedgerSnapshot+  schema <- query connStr cohortSchemaSnapshotStatement+  rows <- query connStr previousCohortSnapshot+  bindings <- query connStr bindingsSnapshot+  current <- either (assertFailure . show) pure kiokuMigrationPlan+  result <- runMigrationPlan defaultRunOptions (Settings.connectionString connStr) current+  case result of+    Left _ -> pure ()+    Right report -> assertFailure ("changed checksum unexpectedly accepted: " <> show report)+  query connStr fullLedgerSnapshot >>= (@?= ledger)+  query connStr cohortSchemaSnapshotStatement >>= (@?= schema)+  query connStr previousCohortSnapshot >>= (@?= rows)+  query connStr bindingsSnapshot >>= (@?= bindings)+ -- * The fresh-database test  -- | The rewritten body still applies inside the real pg-migrate chain.@@ -592,7 +706,7 @@         runMigrationPlan defaultRunOptions (Settings.connectionString connStr) plan           >>= either (assertFailure . show) pure       let MigrationReport {results = repeatedResults} = repeated-      length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 62+      length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 63       length [() | MigrationResult {outcome = AppliedNow} <- toList repeatedResults] @?= 0       verification <-         verifyMigrationPlan defaultRunOptions (Settings.connectionString connStr) plan@@ -711,10 +825,10 @@     plan <- either (fail . show) pure kiokuMigrationPlan     let settings = Settings.connectionString connStr     initial <- runMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure-    length (appliedNow initial) @?= 62+    length (appliedNow initial) @?= 63      baselineLedger <- query connStr fullLedgerSnapshot-    fst baselineLedger @?= 62+    fst baselineLedger @?= 63     baselineSchema <- query connStr cohortSchemaSnapshotStatement     fixup <- Text.IO.readFile ledgerFixupPath @@ -740,7 +854,7 @@      repeated <- runMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure     let MigrationReport {results = repeatedResults} = repeated-    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 62+    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 63     length [() | MigrationResult {outcome = AppliedNow} <- toList repeatedResults] @?= 0      withConnection connStr \conn -> run conn (Session.script fixup)@@ -1205,12 +1319,12 @@     full <- either (fail . show) pure kiokuMigrationPlan      kirokuReport <- runMigrationPlan defaultRunOptions settings kirokuOnly >>= either (assertFailure . show) pure-    length (appliedNow kirokuReport) @?= 15+    length (appliedNow kirokuReport) @?= 16     ledgerBefore <- query connStr kirokuLedgerSnapshot      adoption <- runMigrationPlan defaultRunOptions settings full >>= either (assertFailure . show) pure     let MigrationReport {results = adoptionResults} = adoption-    length [() | MigrationResult {outcome = AlreadyApplied} <- toList adoptionResults] @?= 15+    length [() | MigrationResult {outcome = AlreadyApplied} <- toList adoptionResults] @?= 16     length [() | MigrationResult {outcome = AppliedNow} <- toList adoptionResults] @?= 47      -- Verified and skipped, never re-executed: the stored rows keep their checksums and their@@ -1224,7 +1338,7 @@     verification <- verifyMigrationPlan defaultRunOptions settings full >>= either (assertFailure . show) pure     let VerificationReport {issues = adoptionIssues, appliedMigrations, pendingMigrations, unknownMigrations} = verification     adoptionIssues @?= []-    length appliedMigrations @?= 62+    length appliedMigrations @?= 63     pendingMigrations @?= []     unknownMigrations @?= [] @@ -1575,13 +1689,13 @@     verification <- verifyMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure     let VerificationReport {issues = verificationIssues, appliedMigrations, pendingMigrations, unknownMigrations} = verification     verificationIssues @?= []-    length appliedMigrations @?= 62+    length appliedMigrations @?= 63     pendingMigrations @?= []     unknownMigrations @?= []      repeated <- runMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure     let MigrationReport {results = repeatedResults} = repeated-    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 62+    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 63     length [() | MigrationResult {outcome = AppliedNow} <- toList repeatedResults] @?= 0  testSchemaConvergence :: Assertion@@ -1750,6 +1864,7 @@           migrationId "kiroku" "0013",           migrationId "kiroku" "0014",           migrationId "kiroku" "0015",+          migrationId "kiroku" "0016",           migrationId "keiro" "0015-keiro-outbox-claim-order-index",           migrationId "keiro" "0016-keiro-inbox-drop-received-idx",           migrationId "keiro" "0017-schema-management-comment",@@ -1817,7 +1932,7 @@       (EXISTS (SELECT 1 FROM information_schema.columns         WHERE table_schema = 'kiroku' AND table_name = 'streams' AND column_name = 'truncate_before'))::int       + (coalesce(obj_description(to_regnamespace('kiroku'), 'pg_namespace'), '') =-          'Managed by pg-migrate component kiroku through 0015')::int+          'Managed by pg-migrate component kiroku through 0016')::int       + (to_regclass('keiro.keiro_outbox_claim_order_idx') IS NOT NULL)::int       + (to_regclass('keiro.keiro_inbox_received_idx') IS NULL)::int       + (coalesce(obj_description(to_regnamespace('keiro'), 'pg_namespace'), '') =@@ -1864,7 +1979,7 @@   old <- either (assertFailure . show) pure (migrationPlan (kiroku :| [keiro, oldKioku]))   current <- either (assertFailure . show) pure kiokuMigrationPlan   initial <- runMigrationPlan defaultRunOptions settings old >>= either (assertFailure . show) pure-  length (appliedNow initial) @?= 61+  length (appliedNow initial) @?= 62   withConnection connStr $ \conn ->     run       conn
+ test/fixtures/kiroku-0.7.1.0/0001-kiroku-bootstrap.sql view
@@ -0,0 +1,229 @@+-- Kiroku Store bootstrap migration (codd)+-- Supports PostgreSQL 17+.+--+-- kiroku-store-migrations is the schema owner. codd applies this file+-- verbatim, records that the timestamped migration ran, and skips it on later+-- runs. Future schema changes add new timestamped SQL files in this directory+-- instead of changing kiroku-store.+--+-- All Kiroku-owned objects live in the dedicated `kiroku` schema, leaving+-- `public` free for application objects. Creating the schema and setting+-- search_path first means every unqualified object name in the rest of this+-- file resolves into the Kiroku schema.+CREATE SCHEMA IF NOT EXISTS kiroku;+SET search_path TO kiroku, pg_catalog;++-- PostgreSQL 18 provides pg_catalog.uuidv7(); PostgreSQL 17 needs this+-- Kiroku-schema fallback before events.event_id DEFAULT uuidv7() is parsed.+-- With search_path set above, the unqualified CREATE FUNCTION lands in the+-- Kiroku schema, and to_regprocedure('uuidv7()') resolves through search_path+-- (pg_catalog first for the built-in, then the Kiroku schema for the fallback).+DO $$+BEGIN+    IF to_regprocedure('pg_catalog.uuidv7()') IS NULL+       AND to_regprocedure('uuidv7()') IS NULL THEN+        EXECUTE $fn$+            CREATE FUNCTION uuidv7()+            RETURNS uuid+            AS $body$+            DECLARE+                unix_ts_ms bytea;+                uuid_bytes bytea;+            BEGIN+                unix_ts_ms = substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3);+                uuid_bytes = uuid_send(gen_random_uuid());+                uuid_bytes = overlay(uuid_bytes placing unix_ts_ms from 1 for 6);+                uuid_bytes = set_byte(uuid_bytes, 6, (b'0111' || get_byte(uuid_bytes, 6)::bit(4))::bit(8)::int);+                RETURN encode(uuid_bytes, 'hex')::uuid;+            END+            $body$+            LANGUAGE plpgsql+            VOLATILE+        $fn$;+    END IF;+END+$$;++-- Streams (including $all as stream_id = 0)+CREATE TABLE IF NOT EXISTS streams (+    stream_id    BIGSERIAL    PRIMARY KEY,+    stream_name  TEXT         NOT NULL,+    category     TEXT         GENERATED ALWAYS AS (split_part(stream_name, '-', 1)) STORED,+    stream_version BIGINT     NOT NULL DEFAULT 0,+    created_at   TIMESTAMPTZ  NOT NULL DEFAULT now(),+    deleted_at   TIMESTAMPTZ,+    CONSTRAINT ix_streams_stream_name UNIQUE (stream_name)+);++-- Seed the $all stream+INSERT INTO streams (stream_id, stream_name, stream_version)+VALUES (0, '$all', 0)+ON CONFLICT DO NOTHING;++-- Reset sequence past the reserved stream_id=0+SELECT setval('streams_stream_id_seq', GREATEST((SELECT MAX(stream_id) FROM streams), 1));++-- Events (flat table — stream membership tracked in stream_events)+CREATE TABLE IF NOT EXISTS events (+    event_id       UUID         PRIMARY KEY DEFAULT uuidv7(),+    event_type     TEXT         NOT NULL,+    causation_id   UUID,+    correlation_id UUID,+    data           JSONB        NOT NULL,+    metadata       JSONB,+    created_at     TIMESTAMPTZ  NOT NULL DEFAULT now()+);++-- Stream-event junction (each event gets 2+ rows: source stream + $all + any links)+CREATE TABLE IF NOT EXISTS stream_events (+    event_id                UUID   NOT NULL REFERENCES events(event_id),+    stream_id               BIGINT NOT NULL REFERENCES streams(stream_id),+    stream_version          BIGINT NOT NULL,+    original_stream_id      BIGINT NOT NULL,+    original_stream_version BIGINT NOT NULL,+    PRIMARY KEY (event_id, stream_id)+);++-- Indexes++-- Primary read path: fetch events from a stream in order+CREATE INDEX IF NOT EXISTS ix_stream_events_stream_version+    ON stream_events (stream_id, stream_version);++-- Event type filtering (for server-side subscription filtering)+CREATE INDEX IF NOT EXISTS ix_events_event_type+    ON events (event_type);++-- Correlation tracing+CREATE INDEX IF NOT EXISTS ix_events_correlation_id+    ON events (correlation_id) WHERE correlation_id IS NOT NULL;++-- Causation tracing+CREATE INDEX IF NOT EXISTS ix_events_causation_id+    ON events (causation_id) WHERE causation_id IS NOT NULL;++-- Category filtering (for readCategory — uses generated column, not LIKE)+CREATE INDEX IF NOT EXISTS ix_streams_category+    ON streams (category);++-- Category read path: find $all entries by originating stream, ordered by global position+-- Enables efficient category reads by allowing the planner to: look up category stream_ids →+-- index scan $all for each → merge ordered by stream_version+CREATE INDEX IF NOT EXISTS ix_stream_events_all_by_origin+    ON stream_events (original_stream_id, stream_version)+    WHERE stream_id = 0;++-- Subscriptions (checkpoint persistence for subscription positions).+-- consumer_group_member / consumer_group_size carry static consumer-group+-- topology (ExecPlan 28 / EP-1). Non-group subscriptions are member 0, size 1.+-- The unique key is composite (subscription_name, consumer_group_member) so each+-- group member persists its own checkpoint under one shared subscription name.+CREATE TABLE IF NOT EXISTS subscriptions (+    subscription_id       BIGSERIAL    PRIMARY KEY,+    subscription_name     TEXT         NOT NULL,+    stream_name           TEXT         NOT NULL DEFAULT '$all',+    last_seen             BIGINT       NOT NULL DEFAULT 0,+    consumer_group_member INT          NOT NULL DEFAULT 0,+    consumer_group_size   INT          NOT NULL DEFAULT 1,+    created_at            TIMESTAMPTZ  NOT NULL DEFAULT now(),+    updated_at            TIMESTAMPTZ  NOT NULL DEFAULT now()+);++-- Idempotent convergence for databases created before EP-1: add the columns if+-- missing, drop the old auto-named single-column unique constraint if present,+-- and install the composite unique index. All guarded so the bootstrap body is+-- still safe in disposable local databases; codd records the timestamped+-- migration and does not reapply it after a successful run.+ALTER TABLE subscriptions ADD COLUMN IF NOT EXISTS consumer_group_member INT NOT NULL DEFAULT 0;+ALTER TABLE subscriptions ADD COLUMN IF NOT EXISTS consumer_group_size   INT NOT NULL DEFAULT 1;+ALTER TABLE subscriptions DROP CONSTRAINT IF EXISTS subscriptions_subscription_name_key;+CREATE UNIQUE INDEX IF NOT EXISTS ix_subscriptions_name_member+    ON subscriptions (subscription_name, consumer_group_member);++-- Triggers++-- NOTIFY on stream changes (fires once per append, not per event)+CREATE OR REPLACE FUNCTION notify_events() RETURNS TRIGGER AS $$+BEGIN+    PERFORM pg_notify(+        TG_TABLE_SCHEMA || '.events',+        NEW.stream_name || ',' || NEW.stream_id || ',' || NEW.stream_version+    );+    RETURN NEW;+END;+$$ LANGUAGE plpgsql;++DROP TRIGGER IF EXISTS stream_events_notify ON streams;+CREATE TRIGGER stream_events_notify+    AFTER INSERT OR UPDATE ON streams+    FOR EACH ROW EXECUTE FUNCTION notify_events();++-- Immutability: prevent event mutation+CREATE OR REPLACE FUNCTION prevent_mutation() RETURNS TRIGGER AS $$+BEGIN+    RAISE EXCEPTION 'Immutable table: % cannot be updated', TG_TABLE_NAME;+END;+$$ LANGUAGE plpgsql;++DROP TRIGGER IF EXISTS no_update_events ON events;+CREATE TRIGGER no_update_events+    BEFORE UPDATE ON events+    FOR EACH ROW EXECUTE FUNCTION prevent_mutation();++DROP TRIGGER IF EXISTS no_update_stream_events ON stream_events;+CREATE TRIGGER no_update_stream_events+    BEFORE UPDATE ON stream_events+    FOR EACH ROW EXECUTE FUNCTION prevent_mutation();++-- Gated hard deletes (for maintenance/GDPR only)+CREATE OR REPLACE FUNCTION protect_deletion() RETURNS TRIGGER AS $$+BEGIN+    IF current_setting('kiroku.enable_hard_deletes', true) = 'on' THEN+        RETURN OLD;+    END IF;+    RAISE EXCEPTION 'Hard deletes require: SET LOCAL kiroku.enable_hard_deletes = ''on''';+END;+$$ LANGUAGE plpgsql;++DROP TRIGGER IF EXISTS no_delete_events ON events;+CREATE TRIGGER no_delete_events+    BEFORE DELETE ON events+    FOR EACH ROW EXECUTE FUNCTION protect_deletion();++DROP TRIGGER IF EXISTS no_delete_stream_events ON stream_events;+CREATE TRIGGER no_delete_stream_events+    BEFORE DELETE ON stream_events+    FOR EACH ROW EXECUTE FUNCTION protect_deletion();++DROP TRIGGER IF EXISTS no_delete_streams ON streams;+CREATE TRIGGER no_delete_streams+    BEFORE DELETE ON streams+    FOR EACH ROW EXECUTE FUNCTION protect_deletion();++-- TRUNCATE bypasses row-level triggers, so the BEFORE DELETE triggers above+-- do not protect against an operator running TRUNCATE on these tables. Add+-- statement-level BEFORE TRUNCATE triggers gated by the same GUC so the+-- protection is symmetric. See EP-1 F6.+CREATE OR REPLACE FUNCTION protect_truncation() RETURNS TRIGGER AS $$+BEGIN+    IF current_setting('kiroku.enable_hard_deletes', true) = 'on' THEN+        RETURN NULL;+    END IF;+    RAISE EXCEPTION 'TRUNCATE requires: SET LOCAL kiroku.enable_hard_deletes = ''on''';+END;+$$ LANGUAGE plpgsql;++DROP TRIGGER IF EXISTS no_truncate_events ON events;+CREATE TRIGGER no_truncate_events+    BEFORE TRUNCATE ON events+    FOR EACH STATEMENT EXECUTE FUNCTION protect_truncation();++DROP TRIGGER IF EXISTS no_truncate_stream_events ON stream_events;+CREATE TRIGGER no_truncate_stream_events+    BEFORE TRUNCATE ON stream_events+    FOR EACH STATEMENT EXECUTE FUNCTION protect_truncation();++DROP TRIGGER IF EXISTS no_truncate_streams ON streams;+CREATE TRIGGER no_truncate_streams+    BEFORE TRUNCATE ON streams+    FOR EACH STATEMENT EXECUTE FUNCTION protect_truncation();
+ test/fixtures/kiroku-0.7.1.0/0002-add-subscription-dead-letters.sql view
@@ -0,0 +1,31 @@+-- Add the kiroku.dead_letters table (MasterPlan 6 / EP-2, docs/plans/40-...).+--+-- Forward, additive migration: codd applies this file once, records it, and+-- skips it on later runs. It does not edit the bootstrap migration and does not+-- mutate existing event data.+--+-- A dead-letter row records one event that a subscription handler asked to+-- "dead-letter" (return DeadLetter, or exhaust its bounded retry budget). The+-- event itself stays immutable in kiroku.events; this table references it by+-- event_id and global_position rather than copying the payload. The+-- consumer_group_member column (default 0, matching kiroku.subscriptions)+-- attributes the row to the member that produced it, so a consumer group's dead+-- letters are per-member. The worker writes a row and advances the member's+-- checkpoint in one atomic statement (see SQL.insertDeadLetterAndCheckpointStmt).++CREATE TABLE IF NOT EXISTS kiroku.dead_letters (+    dead_letter_id        BIGSERIAL    PRIMARY KEY,+    subscription_name     TEXT         NOT NULL,+    consumer_group_member INT          NOT NULL DEFAULT 0,+    global_position       BIGINT       NOT NULL,+    event_id              UUID         NOT NULL REFERENCES kiroku.events(event_id),+    reason                JSONB        NOT NULL,+    reason_summary        TEXT         NOT NULL,+    attempt_count         INT          NOT NULL,+    created_at            TIMESTAMPTZ  NOT NULL DEFAULT now(),+    UNIQUE (subscription_name, consumer_group_member, global_position, event_id)+);++-- Operator read path: list a subscription member's dead letters by recency.+CREATE INDEX IF NOT EXISTS ix_dead_letters_subscription_created_at+    ON kiroku.dead_letters (subscription_name, consumer_group_member, created_at);
+ test/fixtures/kiroku-0.7.1.0/0003-notify-trigger-append-guard.sql view
@@ -0,0 +1,26 @@+-- Guard append notifications at the trigger level.+--+-- The trigger function and payload format stay unchanged:+--   stream_name,stream_id,stream_version+--+-- INSERT covers newly-created streams. UPDATE covers later appends to an+-- existing stream. Both exclude the internal $all row (stream_id = 0), and the+-- UPDATE trigger only fires when an append advances stream_version, not for+-- lifecycle updates such as soft-delete or undelete.++DROP TRIGGER IF EXISTS stream_events_notify ON kiroku.streams;++DROP TRIGGER IF EXISTS stream_events_notify_insert ON kiroku.streams;+CREATE TRIGGER stream_events_notify_insert+    AFTER INSERT ON kiroku.streams+    FOR EACH ROW+    WHEN (NEW.stream_id <> 0)+    EXECUTE FUNCTION kiroku.notify_events();++DROP TRIGGER IF EXISTS stream_events_notify_update ON kiroku.streams;+CREATE TRIGGER stream_events_notify_update+    AFTER UPDATE ON kiroku.streams+    FOR EACH ROW+    WHEN (NEW.stream_id <> 0+          AND NEW.stream_version IS DISTINCT FROM OLD.stream_version)+    EXECUTE FUNCTION kiroku.notify_events();
+ test/fixtures/kiroku-0.7.1.0/0004-dead-letters-event-id-index.sql view
@@ -0,0 +1,10 @@+-- Index dead_letters by event_id (MasterPlan 9 / EP-5, docs/plans/60-...).+--+-- dead_letters.event_id has a FK to kiroku.events. The UNIQUE key leads with+-- subscription_name, so every referential-integrity check triggered by a+-- DELETE on kiroku.events (the hard-delete path) was a sequential scan of+-- dead_letters, and the hard-delete transaction's own dead-letter pre-delete+-- (Kiroku.Store.SQL.deleteDeadLettersForOrphanedEventsStmt) needs the same+-- access path.+CREATE INDEX IF NOT EXISTS ix_dead_letters_event_id+    ON kiroku.dead_letters (event_id);
+ test/fixtures/kiroku-0.7.1.0/0005-index-hygiene-and-streams-fillfactor.sql view
@@ -0,0 +1,33 @@+-- Index hygiene and $all hot-row tuning (MasterPlan 9 / EP-5, docs/plans/60-...).++-- 1. ix_events_event_type is referenced by no statement in kiroku-store; it is+--    pure write amplification on every append. Server-side event-type pushdown+--    (see the EventTypeFilter haddock in kiroku-store) should re-add a+--    fit-for-purpose index when it ships.+DROP INDEX IF EXISTS kiroku.ix_events_event_type;++-- 2. Stream versions are unique per stream by construction (assigned under the+--    stream row lock; $all versions are the global position). Enforce it so a+--    version-assignment bug surfaces as a loud 23505 instead of silent+--    duplicates. Built as a new unique index, then the old non-unique index is+--    dropped (an index cannot be altered to unique in place).+CREATE UNIQUE INDEX IF NOT EXISTS ux_stream_events_stream_version+    ON kiroku.stream_events (stream_id, stream_version);+DROP INDEX IF EXISTS kiroku.ix_stream_events_stream_version;++-- 3. readDeadLetters orders by (global_position DESC, dead_letter_id DESC) --+--    the store's canonical, deterministic "newest first". Re-key the read+--    index to match so the read is index-ordered instead of sorting each time.+CREATE INDEX IF NOT EXISTS ix_dead_letters_subscription_position+    ON kiroku.dead_letters+       (subscription_name, consumer_group_member,+        global_position DESC, dead_letter_id DESC);+DROP INDEX IF EXISTS kiroku.ix_dead_letters_subscription_created_at;++-- 4. The $all row (stream_id 0) is updated by every append in the database.+--    Its updated column (stream_version) is not indexed, so updates are+--    HOT-eligible when the page has free space; fillfactor 50 reserves that+--    space on newly written pages. Existing pages converge through normal+--    update/prune activity (VACUUM cannot run inside this migration's+--    transaction). Autovacuum tuning for this table is left to operators.+ALTER TABLE kiroku.streams SET (fillfactor = 50);
+ test/fixtures/kiroku-0.7.1.0/0006-stream-name-length-check.sql view
@@ -0,0 +1,9 @@+-- Defense-in-depth bound on stream-name length (MasterPlan 9 / EP-5,+-- docs/plans/60-...). The Haskell store validates this before any SQL+-- (StoreError StreamNameTooLong, maxStreamNameBytes = 512); the constraint+-- catches writers that bypass the library (raw SQL via runTransaction,+-- psql sessions). 512 bytes is far below pg_notify's 8,000-byte payload+-- limit, so the append-notification trigger can never abort on payload size.+ALTER TABLE kiroku.streams+    ADD CONSTRAINT chk_streams_stream_name_length+    CHECK (octet_length(stream_name) <= 512);
+ test/fixtures/kiroku-0.7.1.0/0007-stream-truncate-before.sql view
@@ -0,0 +1,8 @@+-- Logical truncate-before marker for close-the-book compaction+-- (ExecPlan docs/plans/65). Per-stream cursor: ordered stream reads return+-- only events whose stream_version >= truncate_before. Default 0 keeps all+-- events (per-stream versions are 1-based). Reversible; the global $all log+-- is never affected. UPDATE on streams is already permitted (soft-delete+-- uses it), so no trigger/GUC changes are needed.+ALTER TABLE kiroku.streams+    ADD COLUMN IF NOT EXISTS truncate_before BIGINT NOT NULL DEFAULT 0;
+ test/fixtures/kiroku-0.7.1.0/0008-schema-management-comment.sql view
@@ -0,0 +1,2 @@+COMMENT ON SCHEMA kiroku IS+  'Managed by pg-migrate component kiroku through 0008-schema-management-comment';
+ test/fixtures/kiroku-0.7.1.0/0009.sql view
@@ -0,0 +1,35 @@+-- Publish the frozen, owner-rights, structurally read-only v1 checkpoint relation.+CREATE VIEW kiroku.subscription_checkpoints_v1+    (subscription_name,+     consumer_group_member,+     checkpoint_position,+     checkpoint_updated_at)+WITH (security_invoker = false)+AS+WITH checkpoint_rows AS NOT MATERIALIZED (+    SELECT subscription_name,+           consumer_group_member,+           last_seen AS checkpoint_position,+           updated_at AS checkpoint_updated_at+    FROM kiroku.subscriptions+)+SELECT subscription_name,+       consumer_group_member,+       checkpoint_position,+       checkpoint_updated_at+FROM checkpoint_rows;++COMMENT ON VIEW kiroku.subscription_checkpoints_v1 IS+  'Stable read-only v1 relation of exact persisted subscription-member checkpoints; its columns and order are frozen, and its rows are unordered unless the caller supplies ORDER BY.';++COMMENT ON COLUMN kiroku.subscription_checkpoints_v1.subscription_name IS+  'Persisted subscription name; member zero does not distinguish a non-group subscription from member zero of a consumer group.';++COMMENT ON COLUMN kiroku.subscription_checkpoints_v1.consumer_group_member IS+  'Persisted consumer-group member key; member zero carries no topology classification.';++COMMENT ON COLUMN kiroku.subscription_checkpoints_v1.checkpoint_position IS+  'Exact persisted global position for this subscription member; an explicit reset may move it backward or forward.';++COMMENT ON COLUMN kiroku.subscription_checkpoints_v1.checkpoint_updated_at IS+  'Time of the latest checkpoint-row upsert; it does not imply position advancement or worker liveness.';
+ test/fixtures/kiroku-0.7.1.0/0010.sql view
@@ -0,0 +1,146 @@+-- 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 (+    singleton BOOLEAN PRIMARY KEY DEFAULT TRUE,+    CONSTRAINT chk_history_retention_coordinator_singleton CHECK (singleton)+);++INSERT INTO kiroku.history_retention_coordinator (singleton)+VALUES (TRUE)+ON CONFLICT DO NOTHING;++-- Durable operational evidence for rebuilds that require the retained event+-- 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 kiroku.uuidv7(),+    owner             TEXT        NOT NULL,+    reason            TEXT        NOT NULL,+    protected_through BIGINT      NOT NULL,+    created_at        TIMESTAMPTZ NOT NULL,+    renewed_at        TIMESTAMPTZ NOT NULL,+    expires_at        TIMESTAMPTZ NOT NULL,+    released_at       TIMESTAMPTZ,+    CONSTRAINT chk_history_retention_lease_owner_bytes+        CHECK (octet_length(owner) BETWEEN 1 AND 512),+    CONSTRAINT chk_history_retention_lease_reason_bytes+        CHECK (octet_length(reason) BETWEEN 1 AND 2048),+    CONSTRAINT chk_history_retention_lease_frontier+        CHECK (protected_through >= 0),+    CONSTRAINT chk_history_retention_lease_renewal_time+        CHECK (renewed_at >= created_at),+    CONSTRAINT chk_history_retention_lease_expiry+        CHECK (expires_at > created_at),+    CONSTRAINT chk_history_retention_lease_release_time+        CHECK (released_at IS NULL OR released_at >= created_at)+);++CREATE INDEX ix_history_retention_leases_unreleased_expiry+    ON kiroku.history_retention_leases (expires_at)+    WHERE released_at IS NULL;++-- This function is attached only to DELETE and TRUNCATE. TG_TABLE_SCHEMA is+-- quoted into each dynamic statement so a schema-qualified maintenance command+-- cannot be redirected to a coordinator selected through the caller's+-- search_path.+CREATE FUNCTION kiroku.protect_replay_history_from_destruction()+RETURNS TRIGGER+LANGUAGE plpgsql+AS $$+DECLARE+    active_count BIGINT;+BEGIN+    EXECUTE format(+        'SELECT singleton FROM %I.history_retention_coordinator WHERE singleton FOR UPDATE',+        TG_TABLE_SCHEMA+    );++    EXECUTE format(+        'SELECT count(*) FROM %I.history_retention_leases WHERE released_at IS NULL AND expires_at > clock_timestamp()',+        TG_TABLE_SCHEMA+    ) INTO active_count;++    IF active_count > 0 THEN+        RAISE EXCEPTION USING+            ERRCODE = 'KR001',+            MESSAGE = format(+                'replay history is protected by %s active retention lease(s)',+                active_count+            );+    END IF;++    RETURN NULL;+END;+$$;++CREATE TRIGGER protect_replay_history_delete+    BEFORE DELETE ON kiroku.events+    FOR EACH STATEMENT+    EXECUTE FUNCTION kiroku.protect_replay_history_from_destruction();++CREATE TRIGGER protect_replay_history_truncate+    BEFORE TRUNCATE ON kiroku.events+    FOR EACH STATEMENT+    EXECUTE FUNCTION kiroku.protect_replay_history_from_destruction();++CREATE TRIGGER protect_replay_history_delete+    BEFORE DELETE ON kiroku.stream_events+    FOR EACH STATEMENT+    EXECUTE FUNCTION kiroku.protect_replay_history_from_destruction();++CREATE TRIGGER protect_replay_history_truncate+    BEFORE TRUNCATE ON kiroku.stream_events+    FOR EACH STATEMENT+    EXECUTE FUNCTION kiroku.protect_replay_history_from_destruction();++CREATE TRIGGER protect_replay_history_delete+    BEFORE DELETE ON kiroku.streams+    FOR EACH STATEMENT+    EXECUTE FUNCTION kiroku.protect_replay_history_from_destruction();++CREATE TRIGGER protect_replay_history_truncate+    BEFORE TRUNCATE ON kiroku.streams+    FOR EACH STATEMENT+    EXECUTE FUNCTION kiroku.protect_replay_history_from_destruction();++COMMENT ON TABLE kiroku.history_retention_leases IS+  'Durable, expiring replay-history retention leases; rows are operational evidence and remain until explicitly pruned.';++COMMENT ON SCHEMA kiroku IS+  'Managed by pg-migrate component kiroku through 0010';
+ test/fixtures/kiroku-0.7.1.0/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';
+ test/fixtures/kiroku-0.7.1.0/0012.sql view
@@ -0,0 +1,51 @@+-- denormalize category onto $all junction rows for category reads++-- Category reads (readCategoryForwardSQL and its consumer-group variant in+-- kiroku-store) used to start from every stream of the category and probe+-- stream_events once per stream, so a caught-up poll cost work proportional to+-- the number of streams ever written in the category (BUG-2). Carrying the+-- originating stream's category on each $all junction row lets both reads run+-- as one index range scan from (category, checkpoint) that stops at the limit.+--+-- The column is populated only on $all rows (stream_id = 0). Home rows and link+-- rows keep it NULL; nothing reads them by category. The CHECK below makes any+-- inserter that forgets the column fail loudly instead of writing rows that a+-- category read cannot see.+--+-- The whole file runs in one transaction. The backfill rewrites every $all row+-- and the index build blocks writes, so appends wait for the duration; apply it+-- in a maintenance window on a large store and VACUUM (ANALYZE)+-- kiroku.stream_events afterwards.++ALTER TABLE kiroku.stream_events+    ADD COLUMN category TEXT;++COMMENT ON COLUMN kiroku.stream_events.category IS+  'Originating stream''s category, present on $all rows (stream_id = 0) only; equals streams.category of original_stream_id.';++-- Backfill every existing $all row from its originating stream. The immutability+-- trigger rejects every UPDATE on this table, so it is suspended for this one+-- statement and re-enabled before the transaction ends. Runs as the table owner.+ALTER TABLE kiroku.stream_events DISABLE TRIGGER no_update_stream_events;++UPDATE kiroku.stream_events AS se+SET category = s.category+FROM kiroku.streams AS s+WHERE se.stream_id = 0+  AND s.stream_id = se.original_stream_id;++ALTER TABLE kiroku.stream_events ENABLE TRIGGER no_update_stream_events;++ALTER TABLE kiroku.stream_events+    ADD CONSTRAINT ck_stream_events_all_category+    CHECK (stream_id <> 0 OR category IS NOT NULL);++-- Category read path: rows of one category in global-position order. The+-- INCLUDE column lets the consumer-group hash predicate run on index tuples.+CREATE INDEX ix_stream_events_all_by_category+    ON kiroku.stream_events (category, stream_version)+    INCLUDE (original_stream_id)+    WHERE stream_id = 0;++COMMENT ON SCHEMA kiroku IS+  'Managed by pg-migrate component kiroku through 0012';
+ test/fixtures/kiroku-0.7.1.0/0013.sql view
@@ -0,0 +1,13 @@+-- Derive consumer-group topology from existing member checkpoints.+-- Stop all subscription workers before applying this migration.+-- An incomplete legacy row set derives an underestimate: startup refuses the+-- configured topology until the operator explicitly equalizes it with resize.+UPDATE kiroku.subscriptions AS s+SET consumer_group_size = derived.size+FROM (+    SELECT subscription_name, max(consumer_group_member) + 1 AS size+    FROM kiroku.subscriptions+    GROUP BY subscription_name+) AS derived+WHERE s.subscription_name = derived.subscription_name+  AND s.consumer_group_size <> derived.size;
+ test/fixtures/kiroku-0.7.1.0/0014.sql view
@@ -0,0 +1,11 @@+-- bind subscription checkpoints to their targets+++-- Stop subscription workers before applying this catalog change and CHECK scan.+-- Constant defaults preserve legacy rows without a heap rewrite.+ALTER TABLE kiroku.subscriptions+    ADD COLUMN target_kind TEXT NOT NULL DEFAULT 'unbound'+        CHECK (target_kind IN ('unbound', 'all', 'category')),+    ADD COLUMN target_category TEXT+        CHECK ((target_kind = 'category') = (target_category IS NOT NULL)),+    DROP COLUMN stream_name;
+ test/fixtures/kiroku-0.7.1.0/0015.sql view
@@ -0,0 +1,16 @@+-- Stable byte-ordered stream catalog browsing (MasterPlan 13 / plan 88).+-- The original unique-name constraint and category index remain unchanged.+-- This one browse index also serves literal prefixes and exact-category ranges;+-- plan 54's bounded global-event windows require no separate prefix index.+-- PostgreSQL applies this transactional index build once. It blocks writes for+-- the build duration; schedule the upgrade in a maintenance window on large+-- stores. New-stream inserts and non-HOT application-stream updates pay its+-- maintenance cost. The reserved global stream is outside this partial index.+CREATE INDEX ix_streams_browse_name+    ON kiroku.streams (stream_name COLLATE "C")+    WHERE stream_id <> 0;++COMMENT ON INDEX kiroku.ix_streams_browse_name IS+    'Byte-ordered stream browsing and literal-prefix seeks; does not change unique-name or subscription event ordering.';+COMMENT ON SCHEMA kiroku IS+    'Managed by pg-migrate component kiroku through 0015';
+ test/fixtures/kiroku-0.7.1.0/README.md view
@@ -0,0 +1,8 @@+# Released Kiroku 0.7.1.0 migration payloads++Frozen from the Hackage `kiroku-store-migrations-0.7.1.0.tar.gz` archive,+owned by mori://shinzui/kiroku/packages/kiroku-store-migrations. All fifteen+SQL files match the retained prefix at release commit+`22d103e4f424caab06c6a2b76ad92b408c262bc0`. Tests freeze their SHA-256s and+require the current composed plan to verify the installed checksums before+applying only 0016. Never regenerate these from current SQL.
+ test/fixtures/kiroku-0.7.1.0/manifest view
@@ -0,0 +1,15 @@+0001-kiroku-bootstrap.sql+0002-add-subscription-dead-letters.sql+0003-notify-trigger-append-guard.sql+0004-dead-letters-event-id-index.sql+0005-index-hygiene-and-streams-fillfactor.sql+0006-stream-name-length-check.sql+0007-stream-truncate-before.sql+0008-schema-management-comment.sql+0009.sql+0010.sql+0011.sql+0012.sql+0013.sql+0014.sql+0015.sql