kiroku-store-migrations 0.7.2.0 → 0.7.3.0
raw patch · 6 files changed
+237/−11 lines, 6 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +6/−0
- README.md +7/−2
- kiroku-store-migrations.cabal +1/−1
- migrations/0017.sql +108/−0
- migrations/manifest +1/−0
- test/Main.hs +114/−8
CHANGELOG.md view
@@ -1,5 +1,11 @@ # Changelog +## 0.7.3.0 — 2026-10-28++### New Features++* Migration `0017` installs the immutable store identity and empty append-only event-compaction ledger, with named constraints and mutation guards. The manifest now has seventeen entries; ordinary hot tables are unchanged. Existing sixteen-step databases gain one pending migration and schema comment `Managed by pg-migrate component kiroku through 0017`; migration-count fixtures must advance to seventeen.+ ## 0.7.2.0 — 2026-10-26 ### New Features
README.md view
@@ -2,9 +2,9 @@ `kiroku-store-migrations` owns Kiroku's PostgreSQL schema as one native `pg-migrate` component named `kiroku`. The component embeds an ordered manifest-and sixteen SQL payloads, so applications can compose it with other libraries+and seventeen SQL payloads, so applications can compose it with other libraries without copying Kiroku SQL. The first seven payloads are immutable historical-Codd bytes; `0008` through `0016` are native-only forward migrations.+Codd bytes; `0008` through `0017` are native-only forward migrations. ## Public API @@ -183,3 +183,8 @@ checksum for databases that applied the withdrawn 0.3.2.0/0.3.2.1 payload. Required before migrating such a database onto 0.4.0.0 or later; see the changelog.++Migration `0017` adds the immutable, clone-shared store identity and append-only+compaction ledger. Apply it before using Store 0.13 compaction or identity APIs.+Existing sixteen-entry native stores gain one pending migration. See+[selective compaction](../docs/user/compaction.md) for the operator workflow.
kiroku-store-migrations.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: kiroku-store-migrations-version: 0.7.2.0+version: 0.7.3.0 synopsis: Schema migrations for kiroku-store description: Native pg-migrate component, Codd history mapping, and migration executable
+ migrations/0017.sql view
@@ -0,0 +1,108 @@+-- add store identity and event-compaction ledger++-- A schema-installed, immutable identity for this store. Generated once, on the+-- PostgreSQL side, the first time this migration runs. A restored clone shares+-- its source's identity by construction; that is intended, so a maintenance+-- manifest rehearsed on a clone still names the production store.+CREATE TABLE kiroku.store_identity (+ singleton BOOLEAN PRIMARY KEY DEFAULT TRUE,+ store_id UUID NOT NULL DEFAULT kiroku.uuidv7(),+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),+ CONSTRAINT chk_store_identity_singleton CHECK (singleton)+);++INSERT INTO kiroku.store_identity (singleton)+VALUES (TRUE)+ON CONFLICT DO NOTHING;++-- Durable, append-only record of every applied selective compaction. Rows are+-- written only by kiroku-store's compaction apply transaction; the table ships+-- empty and its full contract is fixed here so the feature needs one schema+-- release. manifest_digest and report_digest are raw SHA-256 bytes.+CREATE TABLE kiroku.event_compactions (+ compaction_id UUID PRIMARY KEY DEFAULT kiroku.uuidv7(),+ manifest_digest BYTEA NOT NULL UNIQUE,+ report_digest BYTEA NOT NULL,+ store_id UUID NOT NULL,+ operation TEXT NOT NULL,+ dead_letter_policy TEXT NOT NULL,+ causation_policy TEXT NOT NULL,+ selected_events BIGINT NOT NULL,+ home_memberships BIGINT NOT NULL,+ global_memberships BIGINT NOT NULL,+ link_memberships BIGINT NOT NULL,+ dead_letters_removed BIGINT NOT NULL,+ causation_dependents BIGINT NOT NULL,+ lowest_global_position BIGINT NOT NULL,+ highest_global_position BIGINT NOT NULL,+ affected_streams JSONB NOT NULL,+ applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),+ applied_by TEXT NOT NULL DEFAULT session_user,+ CONSTRAINT chk_event_compactions_manifest_digest_bytes+ CHECK (octet_length(manifest_digest) = 32),+ CONSTRAINT chk_event_compactions_report_digest_bytes+ CHECK (octet_length(report_digest) = 32),+ CONSTRAINT chk_event_compactions_operation_bytes+ CHECK (octet_length(operation) BETWEEN 1 AND 512),+ CONSTRAINT chk_event_compactions_policies+ CHECK (dead_letter_policy IN ('refuse', 'remove')+ AND causation_policy IN ('refuse', 'allow')),+ CONSTRAINT chk_event_compactions_counts+ CHECK (selected_events > 0+ AND home_memberships = selected_events+ AND global_memberships = selected_events+ AND link_memberships >= 0+ AND dead_letters_removed >= 0+ AND causation_dependents >= 0),+ CONSTRAINT chk_event_compactions_positions+ CHECK (0 < lowest_global_position+ AND lowest_global_position <= highest_global_position)+);++CREATE INDEX ix_event_compactions_applied_at+ ON kiroku.event_compactions (applied_at DESC, compaction_id);++-- Immutability and gated destruction, reusing the functions 0001 installed.+-- Nothing here touches events, stream_events, or streams.+CREATE TRIGGER no_update_store_identity+ BEFORE UPDATE ON kiroku.store_identity+ FOR EACH ROW EXECUTE FUNCTION kiroku.prevent_mutation();++CREATE TRIGGER no_delete_store_identity+ BEFORE DELETE ON kiroku.store_identity+ FOR EACH ROW EXECUTE FUNCTION kiroku.protect_deletion();++CREATE TRIGGER no_truncate_store_identity+ BEFORE TRUNCATE ON kiroku.store_identity+ FOR EACH STATEMENT EXECUTE FUNCTION kiroku.protect_truncation();++CREATE TRIGGER no_update_event_compactions+ BEFORE UPDATE ON kiroku.event_compactions+ FOR EACH ROW EXECUTE FUNCTION kiroku.prevent_mutation();++CREATE TRIGGER no_delete_event_compactions+ BEFORE DELETE ON kiroku.event_compactions+ FOR EACH ROW EXECUTE FUNCTION kiroku.protect_deletion();++CREATE TRIGGER no_truncate_event_compactions+ BEFORE TRUNCATE ON kiroku.event_compactions+ FOR EACH STATEMENT EXECUTE FUNCTION kiroku.protect_truncation();++COMMENT ON TABLE kiroku.store_identity IS+ 'Single immutable row identifying this store; a restored clone shares its source identity.';+COMMENT ON COLUMN kiroku.store_identity.store_id IS+ 'UUIDv7 generated once by migration 0017; never updated.';++COMMENT ON TABLE kiroku.event_compactions IS+ 'Append-only ledger of applied selective compactions keyed by manifest digest; rows are audit evidence and are never updated.';+COMMENT ON COLUMN kiroku.event_compactions.manifest_digest IS+ 'Raw SHA-256 of the canonical manifest encoding; unique so an exact manifest can be applied once.';+COMMENT ON COLUMN kiroku.event_compactions.report_digest IS+ 'Raw SHA-256 of the canonical report encoding recorded at apply time.';+COMMENT ON COLUMN kiroku.event_compactions.affected_streams IS+ 'JSON array of {"stream", "head_version"} objects sorted by stream name, recording the verified head of every affected stream.';+COMMENT ON COLUMN kiroku.event_compactions.applied_by IS+ 'PostgreSQL session_user that applied the manifest; recorded by default, not supplied by the caller.';++COMMENT ON SCHEMA kiroku IS+ 'Managed by pg-migrate component kiroku through 0017';
migrations/manifest view
@@ -14,3 +14,4 @@ 0014.sql 0015.sql 0016.sql+0017.sql
test/Main.hs view
@@ -44,7 +44,7 @@ main :: IO () main = hspec $ do describe "native Kiroku migration definition" $ do- it "tracks the sixteen native files in manifest order" $ do+ it "tracks the seventeen native files in manifest order" $ do directory <- findMigrationsDirectory manifest <- Text.lines <$> Text.IO.readFile (directory </> "manifest") manifest `shouldBe` Text.pack <$> nativeMigrationFiles@@ -57,7 +57,7 @@ bytes <- ByteString.readFile (directory </> nativeName) lookup legacyName lockEntries `shouldBe` Just (checksumText bytes) - it "builds component kiroku and a sixteen-migration plan" $ do+ it "builds component kiroku and a seventeen-migration plan" $ do component <- requireRight kirokuMigrations component `seq` pure () plan <- requirePlan@@ -93,7 +93,7 @@ `shouldReturn` "0007-existing.sql\n" describe "fresh native databases" $ do- it "applies all sixteen, verifies strictly, and reports AlreadyApplied on rerun" $ do+ it "applies all seventeen, verifies strictly, and reports AlreadyApplied on rerun" $ do plan <- requirePlan result <- withMigratedDatabase plan $ \connection -> do assertSchema connection@@ -246,6 +246,43 @@ Text.isInfixOf "CTE Scan" planText `shouldBe` False either (expectationFailure . show) pure result + describe "compaction schema" $ do+ it "publishes the exact store identity and ledger columns" $ do+ plan <- requirePlan+ result <- withMigratedDatabase plan $ \connection -> do+ columns <- useSession connection (Session.statement () compactionColumnsStatement)+ columns+ `shouldBe` [ ("event_compactions", "compaction_id", "uuid", True),+ ("event_compactions", "manifest_digest", "bytea", True),+ ("event_compactions", "report_digest", "bytea", True),+ ("event_compactions", "store_id", "uuid", True),+ ("event_compactions", "operation", "text", True),+ ("event_compactions", "dead_letter_policy", "text", True),+ ("event_compactions", "causation_policy", "text", True),+ ("event_compactions", "selected_events", "bigint", True),+ ("event_compactions", "home_memberships", "bigint", True),+ ("event_compactions", "global_memberships", "bigint", True),+ ("event_compactions", "link_memberships", "bigint", True),+ ("event_compactions", "dead_letters_removed", "bigint", True),+ ("event_compactions", "causation_dependents", "bigint", True),+ ("event_compactions", "lowest_global_position", "bigint", True),+ ("event_compactions", "highest_global_position", "bigint", True),+ ("event_compactions", "affected_streams", "jsonb", True),+ ("event_compactions", "applied_at", "timestamp with time zone", True),+ ("event_compactions", "applied_by", "text", True),+ ("store_identity", "singleton", "boolean", True),+ ("store_identity", "store_id", "uuid", True),+ ("store_identity", "created_at", "timestamp with time zone", True)+ ]+ either (expectationFailure . show) pure result++ it "installs one identity row, named constraints, the ledger index, six protection triggers, and leaves hot tables unchanged" $ do+ plan <- requirePlan+ result <- withMigratedDatabase plan $ \connection -> do+ facts <- useSession connection (Session.statement () compactionFactsStatement)+ facts `shouldBe` (1, 0, 10, True, 6, 6)+ either (expectationFailure . show) pure result+ describe "history retention schema" $ do it "publishes the exact coordinator and lease table columns" $ do plan <- requirePlan@@ -287,11 +324,11 @@ withConnection settings $ \connection -> useSession connection (Session.script "INSERT INTO kiroku.subscriptions(subscription_name, last_seen, target_kind, target_category) VALUES ('legacy-category', 42, 'category', 'pm')") upgraded <- runMigrationPlan defaultRunOptions settings plan >>= requireMigration- reportOutcomes upgraded `shouldBe` replicate 15 AlreadyApplied <> [AppliedNow]+ reportOutcomes upgraded `shouldBe` replicate 15 AlreadyApplied <> replicate (length nativeMigrationFiles - 15) AppliedNow withConnection settings $ \connection -> useSession connection (Session.script "INSERT INTO kiroku.subscriptions(subscription_name, last_seen, target_kind, target_category) VALUES ('namespace', 0, 'category_namespace', 'pm'); DO $$ BEGIN IF NOT EXISTS(SELECT FROM kiroku.subscriptions WHERE subscription_name='legacy-category' AND last_seen=42 AND target_kind='category' AND target_category='pm') THEN RAISE EXCEPTION 'binding changed'; END IF; END $$") rerun <- runMigrationPlan defaultRunOptions settings plan >>= requireMigration- reportOutcomes rerun `shouldBe` replicate 16 AlreadyApplied+ reportOutcomes rerun `shouldBe` replicate (length nativeMigrationFiles) AlreadyApplied it "applies the pending tail in a session that never ran the bootstrap" $ do plan <- requirePlan@@ -473,7 +510,7 @@ pendingIds <- traverse (requireRight . migrationId "kiroku")- ["0008-schema-management-comment", "0009", "0010", "0011", "0012", "0013", "0014", "0015", "0016"]+ ["0008-schema-management-comment", "0009", "0010", "0011", "0012", "0013", "0014", "0015", "0016", "0017"] verifiedBeforeCanary <- verifyMigrationPlan defaultRunOptions settings plan >>= requireMigration case verifiedBeforeCanary of VerificationReport verificationIssues _ _ _ ->@@ -515,7 +552,8 @@ "0013.sql", "0014.sql", "0015.sql",- "0016.sql"+ "0016.sql",+ "0017.sql" ] -- | The plan truncated to its first @count@ migrations, read from the checked-in@@ -658,7 +696,7 @@ (EXISTS (SELECT 1 FROM pg_catalog.pg_constraint WHERE conname = 'chk_streams_stream_name_length')), (EXISTS (SELECT 1 FROM pg_catalog.pg_attribute a JOIN pg_catalog.pg_class c ON c.oid = a.attrelid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'kiroku' AND c.relname = 'streams' AND a.attname = 'truncate_before' AND NOT a.attisdropped)), (EXISTS (SELECT 1 FROM pg_catalog.pg_indexes WHERE schemaname = 'kiroku' AND indexname = 'ix_streams_browse_name' AND indexdef LIKE '%COLLATE "C"%' AND indexdef LIKE '%WHERE (stream_id <> 0)%')),- (obj_description(to_regnamespace('kiroku'), 'pg_namespace') = 'Managed by pg-migrate component kiroku through 0016')+ (obj_description(to_regnamespace('kiroku'), 'pg_namespace') = 'Managed by pg-migrate component kiroku through 0017') ) AS checks(ok) """ Encoders.noParams@@ -1268,3 +1306,71 @@ """ Encoders.noParams (Decoders.singleRow ((,,,) <$> Decoders.column (Decoders.nonNullable Decoders.text) <*> Decoders.column (Decoders.nullable Decoders.text) <*> Decoders.column (Decoders.nonNullable Decoders.int8) <*> Decoders.column (Decoders.nonNullable Decoders.bool)))++compactionColumnsStatement :: Statement () [(Text, Text, Text, Bool)]+compactionColumnsStatement =+ Statement.preparable+ """+ SELECT relation.relname::text,+ attribute.attname::text,+ pg_catalog.format_type(attribute.atttypid, attribute.atttypmod),+ attribute.attnotnull+ FROM pg_catalog.pg_class AS relation+ JOIN pg_catalog.pg_namespace AS namespace+ ON namespace.oid = relation.relnamespace+ JOIN pg_catalog.pg_attribute AS attribute+ ON attribute.attrelid = relation.oid+ WHERE namespace.nspname = 'kiroku'+ AND relation.relname IN ('event_compactions', 'store_identity')+ AND attribute.attnum > 0+ AND NOT attribute.attisdropped+ ORDER BY relation.relname, attribute.attnum+ """+ Encoders.noParams+ ( Decoders.rowList+ ( (,,,)+ <$> column Decoders.text+ <*> column Decoders.text+ <*> column Decoders.text+ <*> column Decoders.bool+ )+ )+ where+ column = Decoders.column . Decoders.nonNullable++compactionFactsStatement :: Statement () (Int64, Int64, Int64, Bool, Int64, Int64)+compactionFactsStatement =+ Statement.preparable+ """+ SELECT+ (SELECT count(*) FROM kiroku.store_identity WHERE singleton),+ (SELECT count(*) FROM kiroku.event_compactions),+ (SELECT count(*) FROM pg_catalog.pg_constraint+ WHERE connamespace = 'kiroku'::regnamespace AND conname IN (+ 'store_identity_pkey', 'chk_store_identity_singleton',+ 'event_compactions_pkey', 'event_compactions_manifest_digest_key',+ 'chk_event_compactions_manifest_digest_bytes', 'chk_event_compactions_report_digest_bytes',+ 'chk_event_compactions_operation_bytes', 'chk_event_compactions_policies',+ 'chk_event_compactions_counts', 'chk_event_compactions_positions')),+ EXISTS (SELECT 1 FROM pg_catalog.pg_indexes WHERE schemaname = 'kiroku'+ AND indexname = 'ix_event_compactions_applied_at' AND indexdef LIKE '%applied_at DESC%'),+ (SELECT count(*) FROM pg_catalog.pg_trigger+ WHERE tgrelid IN ('kiroku.store_identity'::regclass, 'kiroku.event_compactions'::regclass)+ AND NOT tgisinternal AND tgname LIKE 'no_%'),+ (SELECT count(*) FROM pg_catalog.pg_trigger+ WHERE tgrelid IN ('kiroku.events'::regclass, 'kiroku.stream_events'::regclass, 'kiroku.streams'::regclass)+ AND NOT tgisinternal AND tgname IN ('protect_replay_history_delete', 'protect_replay_history_truncate'))+ """+ Encoders.noParams+ ( Decoders.singleRow+ ( (,,,,,)+ <$> col Decoders.int8+ <*> col Decoders.int8+ <*> col Decoders.int8+ <*> col Decoders.bool+ <*> col Decoders.int8+ <*> col Decoders.int8+ )+ )+ where+ col = Decoders.column . Decoders.nonNullable