packages feed

kioku-migrations 0.8.0.2 → 0.9.0.0

raw patch · 19 files changed

+855/−20 lines, 19 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
@@ -1,12 +1,29 @@ # Changelog +## [Unreleased]++## 0.9.0.0 — 2026-10-12++### Breaking Changes++- Compose Keiro migrations 0.20 and Kiroku migrations 0.7.1: **60 entries**+  (15 Kiroku, 32 Keiro, 13 Kioku). Upstream 0013/0014 require subscription+  workers stopped and historical targets confirmed; 0015 needs an index-build+  write pause. No Kioku SQL bytes or existing checksums change, and no new+  Kioku ledger fixup is required.++### Changed++- Align the public test-support sublibrary with Keiro test support 0.20.+- Exercise the genuine released 0.6 Kiroku manifest with events, checkpoints,+  and Kioku projections before upgrading; preserve the historical 30-entry Codd import.+- Add explicit types for existing Codd evidence helpers under warnings-as-errors.+ ## 0.8.0.2 — 2026-10-06  ### Fixed  - Pin bare-database fixtures to the same stable effective-uid temporary root owned by `mori://shinzui/keiro/packages/keiro-test-support` as the delegated migrated fixtures; keep stale-instance sweeping enabled and require `mori://shinzui/ephemeral-pg/packages/ephemeral-pg` >=0.3.1 && <0.4.--## [Unreleased]  ## 0.8.0.1 — 2026-10-05 
kioku-migrations.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: kioku-migrations-version: 0.8.0.2+version: 0.9.0.0 synopsis: Schema migrations for kioku description:   Owns kioku schema evolution as a native pg-migrate component and composes@@ -27,6 +27,10 @@   migrations/*.sql   migrations/manifest   test/fixtures/*.sql+  test/fixtures/kiroku-0.6.0.0/*.sql+  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  source-repository head   type: git@@ -71,8 +75,8 @@     containers >=0.6 && <0.8,     hasql >=1.10 && <1.11,     hasql-transaction >=1.2 && <1.3,-    keiro-migrations ^>=0.19.0.0,-    kiroku-store-migrations ^>=0.6.0.0,+    keiro-migrations ^>=0.20.0.0,+    kiroku-store-migrations ^>=0.7.1.0,     pg-migrate ^>=1.2.0.0,     pg-migrate-embed ^>=1.2.0.0,     pg-migrate-import-codd ^>=1.2.0.0,@@ -88,8 +92,8 @@     base >=4.18 && <5,     directory >=1.3 && <1.4,     ephemeral-pg >=0.3.1 && <0.4,-    keiro-test-support ^>=0.19.0.0,-    kioku-migrations ^>=0.8.0.2,+    keiro-test-support ^>=0.20.0.0,+    kioku-migrations ^>=0.9.0.0,     text >=2.0 && <2.2,     unix >=2.8 && <2.9, @@ -111,10 +115,10 @@     bytestring >=0.11 && <0.13,     containers >=0.6 && <0.8,     hasql >=1.6,-    keiro-migrations ^>=0.19.0.0,-    kioku-migrations ^>=0.8.0.2,+    keiro-migrations ^>=0.20.0.0,+    kioku-migrations ^>=0.9.0.0,     kioku-migrations:test-support,-    kiroku-store-migrations ^>=0.6.0.0,+    kiroku-store-migrations ^>=0.7.1.0,     pg-migrate ^>=1.2.0.0,     pg-migrate-embed ^>=1.2.0.0,     pg-migrate-import-codd ^>=1.2.0.0,
src/Kioku/Migrations/History/Codd.hs view
@@ -35,9 +35,11 @@ import Database.PostgreSQL.Migrate   ( Confirmation,     ConnectionProvider,+    EvidenceKey,     EvidenceRequirement (AllOf, Evidence),     HistoryMapping,     PayloadRelation (EquivalentState, SamePayload),+    StateValidationError,     StateValidator,     evidenceKey,     historyMapping,@@ -223,18 +225,22 @@           else Left sessionRegistryValidationError   ] +keiroSchemaEvidenceKey :: EvidenceKey keiroSchemaEvidenceKey =   definitionInvariant (evidenceKey "keiro:pre-pg-migrate-schema-relocated") +keiroSchemaValidationError :: StateValidationError keiroSchemaValidationError =   definitionInvariant     ( stateValidationError         "expected the relocated Keiro schema, tables, indexes, recovery columns, workflow backfill, and generation-aware workflow-step primary key"     ) +sessionRegistryEvidenceKey :: EvidenceKey sessionRegistryEvidenceKey =   definitionInvariant (evidenceKey "kioku:session-readmodels-v3") +sessionRegistryValidationError :: StateValidationError sessionRegistryValidationError =   definitionInvariant     ( stateValidationError
test/Main.hs view
@@ -88,6 +88,7 @@           testCase "public schema (long-lived dev databases)" (assertRegistryBump "public")         ],       testCase "the full migration chain applies to a fresh database" testFreshDatabase,+      testCase "the released previous cohort preserves data and checkpoints through 0013–0015" testPreviousCohortUpgrade,       testGroup         "migration-session isolation"         [testCase "restores the host search path before later components" testHostSearchPathRestored],@@ -445,6 +446,137 @@     Nothing -> fail ("no manifest migration named " <> name)     Just bytes -> pure (Text.Encoding.decodeUtf8 bytes) +-- | A genuine released 12-entry Kiroku manifest, composed with the unchanged+-- Keiro and Kioku payloads. No current SQL is sliced to simulate old history.+testPreviousCohortUpgrade :: Assertion+testPreviousCohortUpgrade = withBareDatabase $ \connStr -> do+  let fixtureRoot = "test/fixtures/kiroku-0.6.0.0/"+      settings = Settings.connectionString connStr+  entries <- checkMigrationManifest (fixtureRoot <> "manifest") >>= either (assertFailure . show) pure+  frozen <- Text.IO.readFile (fixtureRoot <> "SHA256SUMS") >>= either assertFailure pure . parseMigrationLock+  [(name, checksumHex bytes) | (name, bytes) <- toList entries] @?= frozen+  length entries @?= 12+  oldKiroku <- either (assertFailure . show) pure (migrationComponentFromEmbeddedSql "kiroku" Set.empty entries)+  keiro <- either (assertFailure . show) pure KeiroMigrations.keiroMigrations+  kioku <- either (assertFailure . show) pure kiokuMigrations+  old <- either (assertFailure . show) pure (migrationPlan (oldKiroku :| [keiro, kioku]))+  current <- either (assertFailure . show) pure kiokuMigrationPlan+  installed <- runMigrationPlan defaultRunOptions settings old >>= either (assertFailure . show) pure+  length (appliedNow installed) @?= 57+  withConnection connStr $ \conn -> do+    run conn (Session.script previousCohortSeed)+    -- Seed all seven application projections using the existing preservation fixture.+    run conn (Session.script (Text.replace "kiroku.kioku_" "kioku." relocationSeedRows))+  before <- query connStr previousCohortSnapshot+  ledgerBefore <- query connStr previousCohortLedger+  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"]]+  query connStr previousCohortSnapshot >>= (@?= before)+  query connStr previousCohortLedger >>= (@?= ledgerBefore)+  query connStr previousCheckpointSnapshot >>= (@?= checkpointsBefore)+  query connStr upgradedCheckpointShape >>= (@?= True)+  verification <- verifyMigrationPlan defaultRunOptions settings current >>= either (assertFailure . show) pure+  let VerificationReport {issues, appliedMigrations, pendingMigrations, unknownMigrations} = verification+  issues @?= []+  length appliedMigrations @?= 60+  pendingMigrations @?= []+  unknownMigrations @?= []+  repeated <- runMigrationPlan defaultRunOptions settings current >>= either (assertFailure . show) pure+  let MigrationReport {results} = repeated+  length [() | MigrationResult {outcome = AlreadyApplied} <- toList results] @?= 60+  appliedNow repeated @?= []+  query connStr previousCohortSnapshot >>= (@?= before)+  query connStr previousCheckpointSnapshot >>= (@?= checkpointsBefore)++previousCohortSeed :: Text+previousCohortSeed =+  """+  INSERT INTO kiroku.streams (stream_id, stream_name, stream_version)+  VALUES (42, 'kioku_memory-upgrade-fixture', 1);+  INSERT INTO kiroku.events (event_id, event_type, data, metadata)+  VALUES ('00000000-0000-4000-8000-000000000048', 'UpgradeFixture', '{"content":"preserved"}', '{"origin":"previous-cohort"}');+  INSERT INTO kiroku.stream_events+    (event_id, stream_id, stream_version, original_stream_id, original_stream_version, category)+  VALUES ('00000000-0000-4000-8000-000000000048', 42, 1, 42, 1, NULL),+         ('00000000-0000-4000-8000-000000000048', 0, 1, 42, 1, 'kioku_memory');+  UPDATE kiroku.streams SET stream_version = 1 WHERE stream_id = 0;+  INSERT INTO kiroku.subscriptions+    (subscription_name, stream_name, last_seen, consumer_group_member, consumer_group_size)+  VALUES ('kioku-memory-embedding', 'kioku_memory', 1, 0, 1),+         ('upgrade-group', 'kioku_memory', 1, 0, 1),+         ('upgrade-group', 'kioku_memory', 0, 1, 1);+  """++-- Exact data, not just row counts: the envelope, junctions, stream metadata and+-- every Kioku projection survive. Stable ordering makes the comparison deterministic.+previousCohortSnapshot :: Statement () Text+previousCohortSnapshot =+  preparable+    """+    SELECT jsonb_build_object(+      'events', (SELECT jsonb_agg(to_jsonb(t) ORDER BY event_id) FROM kiroku.events t),+      'streams', (SELECT jsonb_agg(to_jsonb(t) ORDER BY stream_id) FROM kiroku.streams t),+      'junctions', (SELECT jsonb_agg(to_jsonb(t) ORDER BY stream_id, stream_version) FROM kiroku.stream_events t),+      'memories', (SELECT jsonb_agg(to_jsonb(t) ORDER BY memory_id) FROM kioku.memories t),+      'sessions', (SELECT jsonb_agg(to_jsonb(t) ORDER BY session_id) FROM kioku.sessions t),+      'turns', (SELECT jsonb_agg(to_jsonb(t) ORDER BY turn_id) FROM kioku.turns t),+      'watermarks', (SELECT jsonb_agg(to_jsonb(t) ORDER BY session_id) FROM kioku.l1_watermarks t),+      'decisions', (SELECT jsonb_agg(to_jsonb(t) ORDER BY decision_id) FROM kioku.consolidation_decisions t),+      'scenes', (SELECT jsonb_agg(to_jsonb(t) ORDER BY scene_id) FROM kioku.scenes t),+      'personas', (SELECT jsonb_agg(to_jsonb(t) ORDER BY persona_id) FROM kioku.personas t)+    )::text+    """+    E.noParams+    (D.singleRow (D.column (D.nonNullable D.text)))++previousCheckpointSnapshot :: Statement () Text+previousCheckpointSnapshot =+  preparable+    "SELECT jsonb_agg(to_jsonb(t) ORDER BY subscription_name, consumer_group_member)::text FROM kiroku.subscription_checkpoints_v1 t"+    E.noParams+    (D.singleRow (D.column (D.nonNullable D.text)))++previousCohortLedger :: Statement () Text+previousCohortLedger =+  preparable+    """+    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'))+    """+    E.noParams+    (D.singleRow (D.column (D.nonNullable D.text)))++previousCohortIsLegacy :: Statement () Bool+previousCohortIsLegacy =+  preparable+    """+    SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'kiroku'+      AND table_name = 'subscriptions' AND column_name = 'stream_name')+      AND NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'kiroku'+      AND table_name = 'subscriptions' AND column_name = 'target_kind')+      AND (SELECT bool_and(consumer_group_size = 1) FROM kiroku.subscriptions)+    """+    E.noParams+    (D.singleRow (D.column (D.nonNullable D.bool)))++upgradedCheckpointShape :: Statement () Bool+upgradedCheckpointShape =+  preparable+    """+    SELECT (SELECT count(*) = 3 AND bool_and(target_kind = 'unbound' AND target_category IS NULL)+      AND bool_and(consumer_group_size = CASE WHEN subscription_name = 'upgrade-group' THEN 2 ELSE 1 END)+      FROM kiroku.subscriptions)+      AND NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'kiroku'+        AND table_name = 'subscriptions' AND column_name = 'stream_name')+      AND EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'kiroku'+        AND indexname = 'ix_streams_browse_name')+    """+    E.noParams+    (D.singleRow (D.column (D.nonNullable D.bool)))+ -- * The fresh-database test  -- | The rewritten body still applies inside the real pg-migrate chain.@@ -459,7 +591,7 @@         runMigrationPlan defaultRunOptions (Settings.connectionString connStr) plan           >>= either (assertFailure . show) pure       let MigrationReport {results = repeatedResults} = repeated-      length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 57+      length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 60       length [() | MigrationResult {outcome = AppliedNow} <- toList repeatedResults] @?= 0       verification <-         verifyMigrationPlan defaultRunOptions (Settings.connectionString connStr) plan@@ -578,10 +710,10 @@     plan <- either (fail . show) pure kiokuMigrationPlan     let settings = Settings.connectionString connStr     initial <- runMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure-    length (appliedNow initial) @?= 57+    length (appliedNow initial) @?= 60      baselineLedger <- query connStr fullLedgerSnapshot-    fst baselineLedger @?= 57+    fst baselineLedger @?= 60     baselineSchema <- query connStr cohortSchemaSnapshotStatement     fixup <- Text.IO.readFile ledgerFixupPath @@ -607,7 +739,7 @@      repeated <- runMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure     let MigrationReport {results = repeatedResults} = repeated-    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 57+    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 60     length [() | MigrationResult {outcome = AppliedNow} <- toList repeatedResults] @?= 0      withConnection connStr \conn -> run conn (Session.script fixup)@@ -1072,12 +1204,12 @@     full <- either (fail . show) pure kiokuMigrationPlan      kirokuReport <- runMigrationPlan defaultRunOptions settings kirokuOnly >>= either (assertFailure . show) pure-    length (appliedNow kirokuReport) @?= 12+    length (appliedNow kirokuReport) @?= 15     ledgerBefore <- query connStr kirokuLedgerSnapshot      adoption <- runMigrationPlan defaultRunOptions settings full >>= either (assertFailure . show) pure     let MigrationReport {results = adoptionResults} = adoption-    length [() | MigrationResult {outcome = AlreadyApplied} <- toList adoptionResults] @?= 12+    length [() | MigrationResult {outcome = AlreadyApplied} <- toList adoptionResults] @?= 15     length [() | MigrationResult {outcome = AppliedNow} <- toList adoptionResults] @?= 45      -- Verified and skipped, never re-executed: the stored rows keep their checksums and their@@ -1091,7 +1223,7 @@     verification <- verifyMigrationPlan defaultRunOptions settings full >>= either (assertFailure . show) pure     let VerificationReport {issues = adoptionIssues, appliedMigrations, pendingMigrations, unknownMigrations} = verification     adoptionIssues @?= []-    length appliedMigrations @?= 57+    length appliedMigrations @?= 60     pendingMigrations @?= []     unknownMigrations @?= [] @@ -1442,13 +1574,13 @@     verification <- verifyMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure     let VerificationReport {issues = verificationIssues, appliedMigrations, pendingMigrations, unknownMigrations} = verification     verificationIssues @?= []-    length appliedMigrations @?= 57+    length appliedMigrations @?= 60     pendingMigrations @?= []     unknownMigrations @?= []      repeated <- runMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure     let MigrationReport {results = repeatedResults} = repeated-    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 57+    length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 60     length [() | MigrationResult {outcome = AppliedNow} <- toList repeatedResults] @?= 0  testSchemaConvergence :: Assertion@@ -1614,6 +1746,9 @@           migrationId "kiroku" "0010",           migrationId "kiroku" "0011",           migrationId "kiroku" "0012",+          migrationId "kiroku" "0013",+          migrationId "kiroku" "0014",+          migrationId "kiroku" "0015",           migrationId "keiro" "0015-keiro-outbox-claim-order-index",           migrationId "keiro" "0016-keiro-inbox-drop-received-idx",           migrationId "keiro" "0017-schema-management-comment",@@ -1679,7 +1814,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 0012')::int+          'Managed by pg-migrate component kiroku through 0015')::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'), '') =
+ test/fixtures/kiroku-0.6.0.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.6.0.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.6.0.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.6.0.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.6.0.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.6.0.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.6.0.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.6.0.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.6.0.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.6.0.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.6.0.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.6.0.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.6.0.0/README.md view
@@ -0,0 +1,18 @@+# Released previous-cohort migration fixture++These are verbatim migration bytes from+mori://shinzui/kiroku/packages/kiroku-store-migrations version 0.6.0.0,+project-relative `kiroku-store-migrations/migrations/` (artifact-level SQL URIs pending).+The authoritative Hackage archive is+https://hackage.haskell.org/package/kiroku-store-migrations-0.6.0.0/kiroku-store-migrations-0.6.0.0.tar.gz+with SHA-256 `8c0d8e9b98e39c332457a0d34d37c8e909e3c2618dc6d334b6a16cc98fc353de`.+Every payload was compared byte-for-byte with upstream tag+`kiroku-store-migrations-v0.6.0.0`, peeled commit+`691c6efed78ca6bb344c9cac5d122eb5ca4980b2`, on 2026-10-13.+`SHA256SUMS` freezes each payload; the test checks these before composing the old plan.++This fixture is solely for disposable database evolution. Its comments and SQL+are preserved verbatim. Do not edit it to repair a new migration failure.+Keiro's 32 and Kioku's 13 payloads are unchanged across this upgrade; the fixture+composes those current components after this genuine 12-entry Kiroku component.+It is separate from the older 30-entry Codd cutover fixture.
+ test/fixtures/kiroku-0.6.0.0/SHA256SUMS view
@@ -0,0 +1,12 @@+af7c777f8702d9b5bce6b3ea16540ab7542bc78725cb45e945c6d5b6025a4d66  0001-kiroku-bootstrap.sql+2771cdd2605a8808ea469c0bcba175c8ba4c5d2465f43eb05c56edb188fa8141  0002-add-subscription-dead-letters.sql+432796c59389d45478ba773b62e9f38d0a562807061eb49280d295ec83248351  0003-notify-trigger-append-guard.sql+f698db092ebf34beeddecc949bbd506afc0b9c63d3e9983ad5d14f203f2ee1cd  0004-dead-letters-event-id-index.sql+80c28c7743f1d9967eadf921c0509fc8477db16500466bf7a49c5f59aadb0fa2  0005-index-hygiene-and-streams-fillfactor.sql+a0e4338a2feadf8f01499a3809d3de78966966689b095ea46195c4d07743e4aa  0006-stream-name-length-check.sql+a4fe42ad8702b43c3d6f72edf263d6b301e783493d49f40a35dd14d2a7928fa7  0007-stream-truncate-before.sql+3fb3dd5a7172da3cd14edecb0877cc9963fc9a6dceaff9ed41705a2365070f70  0008-schema-management-comment.sql+62c80d627dc8f17393d26ff7dbc00cabde11ed38ee7bf3cb1ae42ea89f484c74  0009.sql+debc19187d79cd263be66c6c85cd789c1176e508d562a27390095aaa70f650c4  0010.sql+bf8b76a712a31cb602aee793a11691283ccd1eafdc16224e45e14484512b5af7  0011.sql+32a0a0c877ff1aa6c1f74de8db62c74884e96a9c7500b2b65725b9e654101a7a  0012.sql
+ test/fixtures/kiroku-0.6.0.0/manifest view
@@ -0,0 +1,12 @@+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