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 +19/−2
- kioku-migrations.cabal +12/−8
- src/Kioku/Migrations/History/Codd.hs +6/−0
- test/Main.hs +145/−10
- test/fixtures/kiroku-0.6.0.0/0001-kiroku-bootstrap.sql +229/−0
- test/fixtures/kiroku-0.6.0.0/0002-add-subscription-dead-letters.sql +31/−0
- test/fixtures/kiroku-0.6.0.0/0003-notify-trigger-append-guard.sql +26/−0
- test/fixtures/kiroku-0.6.0.0/0004-dead-letters-event-id-index.sql +10/−0
- test/fixtures/kiroku-0.6.0.0/0005-index-hygiene-and-streams-fillfactor.sql +33/−0
- test/fixtures/kiroku-0.6.0.0/0006-stream-name-length-check.sql +9/−0
- test/fixtures/kiroku-0.6.0.0/0007-stream-truncate-before.sql +8/−0
- test/fixtures/kiroku-0.6.0.0/0008-schema-management-comment.sql +2/−0
- test/fixtures/kiroku-0.6.0.0/0009.sql +35/−0
- test/fixtures/kiroku-0.6.0.0/0010.sql +146/−0
- test/fixtures/kiroku-0.6.0.0/0011.sql +51/−0
- test/fixtures/kiroku-0.6.0.0/0012.sql +51/−0
- test/fixtures/kiroku-0.6.0.0/README.md +18/−0
- test/fixtures/kiroku-0.6.0.0/SHA256SUMS +12/−0
- test/fixtures/kiroku-0.6.0.0/manifest +12/−0
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