packages feed

keiro-migrations 0.21.0.0 → 0.22.0.0

raw patch · 4 files changed

+213/−101 lines, 4 filesdep ~kiroku-storedep ~kiroku-store-migrationsPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: kiroku-store, kiroku-store-migrations

API changes (from Hackage documentation)

+ Keiro.Migrations.SchemaCheck: verifyExpectedSchemaSession :: Session (Either MigrationError [SchemaDrift])

Files

CHANGELOG.md view
@@ -6,6 +6,33 @@  ## [Unreleased] +## 0.22.0.0 — 2026-10-27++### Breaking Changes++- Require [Kiroku Store migrations](mori://shinzui/kiroku/packages/kiroku-store-migrations)+  `^>=0.7.2.0`, adding migration 0016 and raising the composed plan to 48 entries.+  Stop affected subscription workers for cutover and restart with+  [Kiroku Store](mori://shinzui/kiroku/packages/kiroku-store) 0.12 before using+  namespace targets; older binaries must not manage namespace bindings.++### New Features++- Export verifyExpectedSchemaSession for fail-closed verification on an existing pool, preserving managed-object handling.++### Bug Fixes++- Preserve multiline generated/default expressions in schema fingerprints so+  pooled and full-snapshot comparisons agree.++### Other Changes++- Keep workflow classification and browsing on the read side. No inspection-specific generated column, index, or migration changes ordinary writer work.+++- Compare live schema observations in PostgreSQL and return differences only,+  retaining per-call verification, registry permissions, and drift diagnostics.+ ## 0.21.0.0 — 2026-10-14  ### Bug Fixes
keiro-migrations.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: keiro-migrations-version: 0.21.0.0+version: 0.22.0.0 synopsis: Schema migrations for keiro description:   Embedded PostgreSQL schema migrations and a migration runner for the Keiro@@ -333,7 +333,7 @@     bytestring >=0.11 && <0.13,     containers >=0.6 && <0.8,     hasql >=1.10 && <1.11,-    kiroku-store-migrations ^>=0.7.1.0,+    kiroku-store-migrations ^>=0.7.2.0,     pg-migrate ^>=1.2.0.0,     pg-migrate-embed ^>=1.2.0.0,     pg-migrate-import-codd ^>=1.2.0.0,@@ -369,7 +369,7 @@     bytestring >=0.11 && <0.13,     hasql >=1.10 && <1.11,     keiro-migrations,-    kiroku-store-migrations ^>=0.7.1.0,+    kiroku-store-migrations ^>=0.7.2.0,     optparse-applicative >=0.17 && <0.20,     pg-migrate ^>=1.2.0.0,     pg-migrate-cli ^>=1.2.0.0,@@ -415,7 +415,7 @@     hasql >=1.10 && <1.11,     hspec >=2.10 && <2.12,     keiro-migrations,-    kiroku-store-migrations ^>=0.7.1.0,+    kiroku-store-migrations ^>=0.7.2.0,     pg-migrate ^>=1.2.0.0,     pg-migrate-import-codd ^>=1.2.0.0,     pg-migrate-test-support ^>=1.2.0.0,@@ -451,8 +451,8 @@     hasql-pool >=1.2 && <1.5,     hspec >=2.10,     keiro-migrations,-    kiroku-store ^>=0.11.0.0,-    kiroku-store-migrations ^>=0.7.1.0,+    kiroku-store ^>=0.12.0.0,+    kiroku-store-migrations ^>=0.7.2.0,     lens >=5.2 && <5.4,     temporary,     text >=2.0 && <2.2,
src/Keiro/Migrations/SchemaCheck.hs view
@@ -9,6 +9,7 @@     renderSchemaDrift,     snapshotSchema,     verifyExpectedSchema,+    verifyExpectedSchemaSession,   ) where @@ -118,7 +119,7 @@ snapshotSchema schema =   Text.unlines <$> Session.statement schema schemaSnapshotStatement --- | PostgreSQL 18 snapshot regenerated through migration 0031.+-- | PostgreSQL 18 snapshot regenerated through migration 0032. expectedSchemaSnapshot :: Text expectedSchemaSnapshot =   $(embedTextFile "expected-schema/native/keiro-v18.txt")@@ -139,26 +140,27 @@       pure (Left (ConnectionAcquisitionFailed connectionError))     Right connection -> do       result <--        Connection.use connection liveSnapshotSession+        Connection.use connection verifyExpectedSchemaSession           `finally` Connection.release connection       pure $ case result of         Left sessionError -> Left (DatabaseSessionFailed sessionError)-        Right (Left migrationError) -> Left migrationError-        Right (Right (managed, actual)) ->-          Right (compareSchemaSnapshotWithManaged managed expectedSchemaSnapshot actual)-  where-    liveSnapshotSession :: Session (Either MigrationError (Set Text, Text))-    liveSnapshotSession = do-      serverVersionNumber <- Session.statement () serverVersionStatement-      let majorVersion = fromIntegral serverVersionNumber `div` 10000-      if majorVersion == (18 :: Int)-        then do-          privateSchema <- snapshotSchema "keiro"-          publicSchema <- snapshotSchema "keiro_read"-          managed <- managedObjectSnapshotKeys-          pure (Right (managed, privateSchema <> publicSchema))-        else pure (Left (UnsupportedPostgresVersion majorVersion))+        Right result -> result +-- | Verify the live schema on an existing connection or pool. This retains the+-- CLI's PostgreSQL version check and runtime-managed object handling.+verifyExpectedSchemaSession :: Session (Either MigrationError [SchemaDrift])+verifyExpectedSchemaSession = do+  serverVersionNumber <- Session.statement () serverVersionStatement+  let majorVersion = fromIntegral serverVersionNumber `div` 10000+  if majorVersion == (18 :: Int)+    then do+      readable <- Session.statement () registryReadableStatement+      -- Compare every live catalog observation in PostgreSQL. Returning only+      -- differences avoids rebuilding two complete Text/Map snapshots per poll.+      -- No live result is cached, and registry visibility stays role-dependent.+      Right <$> Session.statement () (if readable then managedSchemaDriftStatement else unmanagedSchemaDriftStatement)+    else pure (Left (UnsupportedPostgresVersion majorVersion))+ -- | Snapshot keys of the objects Keiro's runtime created and recorded in -- @keiro.keiro_managed_read_objects@ with @managed_by = 'keiro'@. Every state -- counts: retirement marks a managed object retired without dropping it. Rows@@ -243,73 +245,130 @@ schemaSnapshotStatement :: Statement Text [Text] schemaSnapshotStatement =   Statement.preparable-    """-    WITH configured AS MATERIALIZED (-      SELECT set_config('search_path', 'pg_catalog', true) AS search_path-    )-    SELECT line-    FROM configured-    CROSS JOIN LATERAL (-      SELECT 'table' || E'\t' || c.relname || E'\t' || 'kind=r' AS line-        FROM pg_class c-        JOIN pg_namespace n ON n.oid = c.relnamespace-        WHERE n.nspname = $1-          AND c.relkind = 'r'-          AND configured.search_path = 'pg_catalog'-      UNION ALL-      SELECT 'column' || E'\t' || c.relname || '.' || a.attname || E'\t'-             || format_type(a.atttypid, a.atttypmod)-             || CASE WHEN a.attnotnull THEN ' not null' ELSE '' END-             || coalesce(' default ' || pg_get_expr(d.adbin, d.adrelid), '')-        FROM pg_attribute a-        JOIN pg_class c ON c.oid = a.attrelid-        JOIN pg_namespace n ON n.oid = c.relnamespace-        LEFT JOIN pg_attrdef d-          ON d.adrelid = a.attrelid AND d.adnum = a.attnum-        WHERE n.nspname = $1-          AND c.relkind = 'r'-          AND a.attnum > 0-          AND NOT a.attisdropped-          AND configured.search_path = 'pg_catalog'-      UNION ALL-      SELECT 'constraint' || E'\t' || rel.relname || '.' || con.conname || E'\t'-             || pg_get_constraintdef(con.oid)-        FROM pg_constraint con-        JOIN pg_class rel ON rel.oid = con.conrelid-        JOIN pg_namespace n ON n.oid = rel.relnamespace-        WHERE n.nspname = $1-          AND configured.search_path = 'pg_catalog'-      UNION ALL-      SELECT 'index' || E'\t' || ci.relname || E'\t'-             || pg_get_indexdef(i.indexrelid)-        FROM pg_index i-        JOIN pg_class ci ON ci.oid = i.indexrelid-        JOIN pg_class ct ON ct.oid = i.indrelid-        JOIN pg_namespace n ON n.oid = ct.relnamespace-        WHERE n.nspname = $1-          AND configured.search_path = 'pg_catalog'-      UNION ALL-      SELECT 'view' || E'\t' || n.nspname || '.' || c.relname || E'\t'-             || 'columns='-             || string_agg(-                  a.attnum::text || ':' || a.attname || ' '-                    || format_type(a.atttypid, a.atttypmod),-                  ',' ORDER BY a.attnum-                ) AS line-        FROM pg_class c-        JOIN pg_namespace n ON n.oid = c.relnamespace-        JOIN pg_attribute a ON a.attrelid = c.oid-        WHERE n.nspname = $1-          AND c.relkind = 'v'-          AND a.attnum > 0-          AND NOT a.attisdropped-          AND configured.search_path = 'pg_catalog'-        GROUP BY n.nspname, c.relname-    ) snapshot-    ORDER BY line COLLATE "C"-    """+    schemaSnapshotSql     (Encoders.param (Encoders.nonNullable Encoders.text))     (Decoders.rowList (Decoders.column (Decoders.nonNullable Decoders.text)))++schemaSnapshotSql :: Text+schemaSnapshotSql =+  """+  WITH configured AS MATERIALIZED (+    SELECT set_config('search_path', 'pg_catalog', true) AS search_path+  )+  SELECT line+  FROM configured+  CROSS JOIN LATERAL (+    SELECT 'table' || E'\t' || c.relname || E'\t' || 'kind=r' AS line+      FROM pg_class c+      JOIN pg_namespace n ON n.oid = c.relnamespace+      WHERE n.nspname = $1+        AND c.relkind = 'r'+        AND configured.search_path = 'pg_catalog'+    UNION ALL+    SELECT 'column' || E'\t' || c.relname || '.' || a.attname || E'\t'+           || format_type(a.atttypid, a.atttypmod)+           || CASE WHEN a.attnotnull THEN ' not null' ELSE '' END+           -- JSON quoting keeps a multiline generated expression on one snapshot line.+           || CASE a.attgenerated+                WHEN 's' THEN ' generated always as ' || to_json(pg_get_expr(d.adbin, d.adrelid))::text || ' stored'+                WHEN 'v' THEN ' generated always as ' || to_json(pg_get_expr(d.adbin, d.adrelid))::text || ' virtual'+                ELSE coalesce(' default ' || replace(replace(replace(pg_get_expr(d.adbin, d.adrelid),+                       chr(92), chr(92) || chr(92)), chr(10), chr(92) || 'n'), chr(13), chr(92) || 'r'), '')+              END+      FROM pg_attribute a+      JOIN pg_class c ON c.oid = a.attrelid+      JOIN pg_namespace n ON n.oid = c.relnamespace+      LEFT JOIN pg_attrdef d+        ON d.adrelid = a.attrelid AND d.adnum = a.attnum+      WHERE n.nspname = $1+        AND c.relkind = 'r'+        AND a.attnum > 0+        AND NOT a.attisdropped+        AND configured.search_path = 'pg_catalog'+    UNION ALL+    SELECT 'constraint' || E'\t' || rel.relname || '.' || con.conname || E'\t'+           || pg_get_constraintdef(con.oid)+      FROM pg_constraint con+      JOIN pg_class rel ON rel.oid = con.conrelid+      JOIN pg_namespace n ON n.oid = rel.relnamespace+      WHERE n.nspname = $1+        AND configured.search_path = 'pg_catalog'+    UNION ALL+    SELECT 'index' || E'\t' || ci.relname || E'\t'+           || pg_get_indexdef(i.indexrelid)+      FROM pg_index i+      JOIN pg_class ci ON ci.oid = i.indexrelid+      JOIN pg_class ct ON ct.oid = i.indrelid+      JOIN pg_namespace n ON n.oid = ct.relnamespace+      WHERE n.nspname = $1+        AND configured.search_path = 'pg_catalog'+    UNION ALL+    SELECT 'view' || E'\t' || n.nspname || '.' || c.relname || E'\t'+           || 'columns='+           || string_agg(+                a.attnum::text || ':' || a.attname || ' '+                  || format_type(a.atttypid, a.atttypmod),+                ',' ORDER BY a.attnum+              ) AS line+      FROM pg_class c+      JOIN pg_namespace n ON n.oid = c.relnamespace+      JOIN pg_attribute a ON a.attrelid = c.oid+      WHERE n.nspname = $1+        AND c.relkind = 'v'+        AND a.attnum > 0+        AND NOT a.attisdropped+        AND configured.search_path = 'pg_catalog'+      GROUP BY n.nspname, c.relname+  ) snapshot+  ORDER BY line COLLATE "C"+  """++-- These two prepared statements embed only the immutable expected snapshot.+-- Their SQL is constructed once, not serialized as a large parameter per poll.+managedSchemaDriftStatement :: Statement () [SchemaDrift]+{-# NOINLINE managedSchemaDriftStatement #-}+managedSchemaDriftStatement = schemaDriftStatement managedKeysSql+  where+    managedKeysSql =+      "SELECT ('view' || E'\\t' || object_schema || '.' || object_name) COLLATE \"C\" AS key "+        <> "FROM keiro.keiro_managed_read_objects WHERE managed_by = 'keiro' AND object_kind = 'binding-view'"++unmanagedSchemaDriftStatement :: Statement () [SchemaDrift]+{-# NOINLINE unmanagedSchemaDriftStatement #-}+unmanagedSchemaDriftStatement = schemaDriftStatement "SELECT NULL::text COLLATE \"C\" AS key WHERE false"++schemaDriftStatement :: Text -> Statement () [SchemaDrift]+schemaDriftStatement managedSql = Statement.preparable sql Encoders.noParams decoder+  where+    -- E literals preserve quotes, backslashes and embedded control characters+    -- independently of standard_conforming_strings. No runtime input enters SQL.+    literal value = "E'" <> Text.replace "'" "''" (Text.replace "\\" "\\\\" value) <> "'"+    expectedRows =+      Text.intercalate "," ["(" <> Text.intercalate "," (map literal [key, line, definition]) <> ")" | (key, (line, definition)) <- Map.toAscList (snapshotObjects expectedSchemaSnapshot)]+    sql =+      "WITH live_lines AS MATERIALIZED (SELECT schemas.priority, observed.line FROM "+        <> "(VALUES ('keiro'::text,1),('keiro_read'::text,2)) AS schemas(name,priority) CROSS JOIN LATERAL ("+        <> Text.replace "$1" "schemas.name" schemaSnapshotSql+        <> ") AS observed), actual_keys AS (SELECT priority,line COLLATE \"C\" AS line, "+        <> "(split_part(line,E'\\t',1)||E'\\t'||split_part(line,E'\\t',2)) COLLATE \"C\" AS key FROM live_lines), "+        -- Match Map.fromList over the two sorted snapshots: the public schema+        -- wins duplicate keys, then the final C-sorted line within that schema.+        <> "actual AS (SELECT DISTINCT ON (key) key,line,substring(line FROM length(key)+2) COLLATE \"C\" AS definition "+        <> "FROM actual_keys ORDER BY key,priority DESC,line DESC), "+        <> "expected AS (SELECT key COLLATE \"C\" AS key,line,definition COLLATE \"C\" AS definition FROM (VALUES "+        <> expectedRows+        <> ") AS embedded(key,line,definition)), managed AS ("+        <> managedSql+        <> ") SELECT coalesce(e.key,a.key),e.line,a.line,e.definition,a.definition FROM expected e FULL JOIN actual a USING(key) "+        <> "WHERE a.key IS NULL OR (e.key IS NULL AND NOT EXISTS (SELECT 1 FROM managed m WHERE m.key=a.key)) "+        <> "OR (e.key IS NOT NULL AND a.key IS NOT NULL AND e.definition IS DISTINCT FROM a.definition) ORDER BY coalesce(e.key,a.key) COLLATE \"C\""+    decoder = Decoders.rowList (toDrift <$> required <*> optional <*> optional <*> optional <*> optional)+    required = Decoders.column (Decoders.nonNullable Decoders.text)+    optional = Decoders.column (Decoders.nullable Decoders.text)+    toDrift _ (Just line) Nothing _ _ = MissingObject line+    toDrift _ Nothing (Just line) _ _ = UnexpectedObject line+    toDrift key (Just _) (Just _) (Just expected) (Just actual) = ChangedObject key expected actual+    toDrift _ _ _ _ _ = error "schema comparison returned an impossible null combination"  serverVersionStatement :: Statement () Int32 serverVersionStatement =
test/Main.hs view
@@ -79,7 +79,7 @@           ] -> do             componentNameText kirokuName `shouldBe` "kiroku"             kirokuDependencies `shouldBe` mempty-            length kirokuEntries `shouldBe` 15+            length kirokuEntries `shouldBe` 16             componentNameText keiroName `shouldBe` "keiro"             dependencyName <- requireRight (componentName "kiroku")             keiroDependencies `shouldBe` Set.singleton dependencyName@@ -186,7 +186,7 @@             plan             >>= requireRight         Keiro.pendingMigrations handshake `shouldBe` planMigrationIds plan-        length (Keiro.pendingMigrations handshake) `shouldBe` 47+        length (Keiro.pendingMigrations handshake) `shouldBe` 48         Keiro.ledgerIssues handshake `shouldBe` []         handshakePassed handshake `shouldBe` False @@ -211,7 +211,7 @@         _ <- runMigrationPlan defaultRunOptions settings kirokuOnly >>= requireRight         handshake <-           missingMigrations defaultRunOptions provider plan >>= requireRight-        Keiro.pendingMigrations handshake `shouldBe` drop 15 (planMigrationIds plan)+        Keiro.pendingMigrations handshake `shouldBe` drop 16 (planMigrationIds plan)         length (Keiro.pendingMigrations handshake) `shouldBe` 32         Keiro.ledgerIssues handshake `shouldBe` []         handshakePassed handshake `shouldBe` False@@ -277,6 +277,9 @@                 """             )         drifts <- verifyExpectedSchema settings >>= requireRight+        withConnection settings $ \connection -> do+          actual <- useSession connection ((<>) <$> snapshotSchema "keiro" <*> snapshotSchema "keiro_read")+          drifts `shouldBe` compareSchemaSnapshot expectedSchemaSnapshot actual         let rendered = renderSchemaDrift <$> drifts         rendered           `shouldSatisfy` any@@ -355,18 +358,20 @@             demoDrift = [UnexpectedObject "view\tkeiro.external_read_demo_v1_binding\tcolumns=1:id bigint,2:total bigint"]             checkRole expectedKeys expectedDrifts =               withConnection settings $ \connection -> do-                (keys, drifts) <-+                (keys, drifts, optimized) <-                   useSession                     connection                     ( do                         Session.script "SET ROLE keiro_verify_probe"                         managed <- managedObjectSnapshotKeys                         actual <- (<>) <$> snapshotSchema "keiro" <*> snapshotSchema "keiro_read"-                        pure (managed, compareSchemaSnapshotWithManaged managed expectedSchemaSnapshot actual)+                        optimized <- verifyExpectedSchemaSession+                        pure (managed, compareSchemaSnapshotWithManaged managed expectedSchemaSnapshot actual, optimized)                     )                     `finally` useSession connection (Session.script "RESET ROLE")                 keys `shouldBe` expectedKeys                 drifts `shouldBe` expectedDrifts+                requireRight optimized >>= (`shouldBe` expectedDrifts)         _ <- runMigrationPlan defaultRunOptions settings plan >>= requireRight         withConnection settings $ \connection -> do           useSession connection (Session.script managedBindingFixtureSql)@@ -395,6 +400,26 @@           useSession connection (Session.script "GRANT USAGE ON SCHEMA keiro TO keiro_verify_probe")           checkRole demoKeys [] +    it "preserves snapshot comparison for duplicate keys, quoting, and changed defaults" $ do+      plan <- requirePlan+      result <- withMigratedDatabase plan $ \connection -> do+        useSession connection $+          Session.script+            """+            CREATE TABLE keiro.schema_collision (shared_value text DEFAULT E'quote''slash\\\\tab\\t');+            CREATE TABLE keiro_read.schema_collision (shared_value bigint DEFAULT 42);+            ALTER TABLE keiro.keiro_workflows ALTER COLUMN attempts SET DEFAULT 3;+            ALTER TABLE keiro.keiro_workflows ALTER COLUMN last_error SET DEFAULT E'quote''slash\\\\tab\\t';+            """+        actual <- useSession connection ((<>) <$> snapshotSchema "keiro" <*> snapshotSchema "keiro_read")+        optimized <- useSession connection verifyExpectedSchemaSession >>= requireRight+        optimized `shouldBe` compareSchemaSnapshot expectedSchemaSnapshot actual+        useSession connection (Session.script "ALTER TABLE keiro.keiro_workflows ALTER COLUMN last_error SET DEFAULT E'first\\nsecond\\rtail'")+        multiline <- useSession connection ((<>) <$> snapshotSchema "keiro" <*> snapshotSchema "keiro_read")+        multilineOptimized <- useSession connection verifyExpectedSchemaSession >>= requireRight+        multilineOptimized `shouldBe` compareSchemaSnapshot expectedSchemaSnapshot multiline+      either (expectationFailure . show) pure result+     it "falls back to ordinary drift when the registry is absent" $ do       plan <- requirePlan       withKeiroPg $ \database -> do@@ -423,12 +448,12 @@         assertSchema connection         let provider = providerFor connection         rerun <- runMigrationPlanWith defaultRunOptions provider plan >>= requireRight-        reportOutcomes rerun `shouldBe` replicate 47 AlreadyApplied+        reportOutcomes rerun `shouldBe` replicate 48 AlreadyApplied         verified <- verifyMigrationPlanWith defaultRunOptions provider plan >>= requireRight         case verified of           VerificationReport verificationIssues applied pending unknown -> do             verificationIssues `shouldBe` []-            length applied `shouldBe` 47+            length applied `shouldBe` 48             pending `shouldBe` []             unknown `shouldBe` []       either (expectationFailure . show) pure result@@ -477,7 +502,7 @@             (runMigrationPlan defaultRunOptions settings plan >>= requireRight)             (runMigrationPlan defaultRunOptions settings plan >>= requireRight)         sort [reportOutcomes first, reportOutcomes second]-          `shouldBe` sort [replicate 47 AppliedNow, replicate 47 AlreadyApplied]+          `shouldBe` sort [replicate 48 AppliedNow, replicate 48 AlreadyApplied]      it "upgrades singleton read-model rows into deterministic rebuild groups" $ do       fullPlan <- requirePlan@@ -811,7 +836,7 @@         up <- runMigrationPlan defaultRunOptions settings plan >>= requireRight         reportOutcomes up           `shouldBe` replicate 7 AlreadyApplied-            <> replicate 8 AppliedNow+            <> replicate 9 AppliedNow             <> replicate 16 AlreadyApplied             <> replicate 16 AppliedNow @@ -1248,14 +1273,14 @@     up <- runMigrationPlan defaultRunOptions settings plan >>= requireRight     reportOutcomes up       `shouldBe` replicate 7 AlreadyApplied-        <> replicate 8 AppliedNow+        <> replicate 9 AppliedNow         <> replicate 16 AlreadyApplied         <> replicate 16 AppliedNow     verifiedAfterCanaries <- verifyMigrationPlan defaultRunOptions settings plan >>= requireRight     case verifiedAfterCanaries of       VerificationReport verificationIssues _ _ _ -> verificationIssues `shouldBe` []     rerun <- runMigrationPlan defaultRunOptions settings plan >>= requireRight-    reportOutcomes rerun `shouldBe` replicate 47 AlreadyApplied+    reportOutcomes rerun `shouldBe` replicate 48 AlreadyApplied     second <-       importCoddHistory defaultImportOptions config provider plan frameworkCoddHistoryMappings         >>= requireRight@@ -1265,7 +1290,7 @@       sourceRows <- useSession connection (Session.statement () (sourceRowCountStatement sourceSchema))       sourceRows `shouldBe` 23       facts <- useSession connection (Session.statement () importFactsStatement)-      facts `shouldBe` (47, 23, True)+      facts `shouldBe` (48, 23, True)  postCoddImportPendingIssues :: IO [VerificationIssue] postCoddImportPendingIssues =@@ -1283,6 +1308,7 @@         ("kiroku", "0013"),         ("kiroku", "0014"),         ("kiroku", "0015"),+        ("kiroku", "0016"),         ("keiro", "0017-schema-management-comment"),         ("keiro", "0018"),         ("keiro", "0019-keiro-snapshots-state-shape-hash"),@@ -1550,7 +1576,7 @@       (to_regclass('keiro_read.projection_group_status_v1') IS NOT NULL),       (obj_description(to_regnamespace('keiro_read'), 'pg_namespace') =         'Versioned, owner-rights read contracts for out-of-process Keiro consumers.'),-      (obj_description(to_regnamespace('kiroku'), 'pg_namespace') = 'Managed by pg-migrate component kiroku through 0015'),+      (obj_description(to_regnamespace('kiroku'), 'pg_namespace') = 'Managed by pg-migrate component kiroku through 0016'),       (to_regclass('kiroku.ix_streams_browse_name') IS NOT NULL),       (obj_description(to_regnamespace('keiro'), 'pg_namespace') = 'Managed by pg-migrate component keiro through 0017-schema-management-comment')     ) AS checks(ok)