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 +27/−0
- keiro-migrations.cabal +6/−6
- src/Keiro/Migrations/SchemaCheck.hs +141/−82
- test/Main.hs +39/−13
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)