packages feed

kioku-migrations 0.6.0.0 → 0.7.0.0

raw patch · 4 files changed

+437/−95 lines, 4 filesdep +keiro-test-supportdep −pg-migrate-test-supportdep ~keiro-migrationsdep ~kioku-migrationsPVP ok

version bump matches the API change (PVP)

Dependencies added: keiro-test-support

Dependencies removed: pg-migrate-test-support

Dependency ranges changed: keiro-migrations, kioku-migrations

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,5 +1,17 @@ # Changelog +## 0.7.0.0 — 2026-09-18++### Changed++- Requires `keiro-migrations ^>=0.17.0.0` and uses+  `keiro-test-support ^>=0.17.0.0` for the shared Kiroku+Keiro migrated fixture.+- Migration integrity now validates the native manifest, freezes all thirteen native payload+  hashes separately from the ten-entry Codd source evidence, rejects ownership/search-path drift+  in future migrations, and proves fresh/Codd-upgrade schema convergence.+- The composed plan remains **56** migrations: Kiroku 11, Keiro 32, Kioku 13. No SQL payload or+  checksum changes, and no ledger fixup is required.+ ## 0.6.0.0 — 2026-09-08  ### Breaking Changes
kioku-migrations.cabal view
@@ -1,20 +1,21 @@-cabal-version:      3.0-name:               kioku-migrations-version:            0.6.0.0-synopsis:           Schema migrations for kioku+cabal-version: 3.0+name: kioku-migrations+version: 0.7.0.0+synopsis: Schema migrations for kioku description:   Owns kioku schema evolution as a native pg-migrate component and composes   it with the kiroku and keiro migration components. -license:            BSD-3-Clause-license-file:       LICENSE-author:             Nadeem Bitar-maintainer:         nadeem@gmail.com-copyright:          2026 Nadeem Bitar-category:           Database-build-type:         Simple-homepage:           https://github.com/shinzui/kioku-bug-reports:        https://github.com/shinzui/kioku/issues+license: BSD-3-Clause+license-file: LICENSE+author: Nadeem Bitar+maintainer: nadeem@gmail.com+copyright: 2026 Nadeem Bitar+category: Database+build-type: Simple+tested-with: ghc >=9.12 && <9.13+homepage: https://github.com/shinzui/kioku+bug-reports: https://github.com/shinzui/kioku/issues extra-doc-files:   CHANGELOG.md   codd-upgrade/README.md@@ -22,18 +23,29 @@ extra-source-files:   codd-upgrade/*.sql   ledger-fixups/*.sql+  migrations.lock   migrations/*.sql   migrations/manifest-  migrations.lock   test/fixtures/*.sql  source-repository head-  type:     git+  type: git   location: https://github.com/shinzui/kioku.git-  subdir:   kioku-migrations+  subdir: kioku-migrations +common warnings+  ghc-options:+    -Wall+    -Wcompat+    -Widentities+    -Wincomplete-record-updates+    -Wincomplete-uni-patterns+    -Wpartial-fields+    -Wredundant-constraints+    -Werror=incomplete-patterns+ common shared-  default-language:   GHC2024+  default-language: GHC2024   default-extensions:     BlockArguments     DeriveAnyClass@@ -42,8 +54,8 @@     OverloadedStrings  library-  import:          shared-  hs-source-dirs:  src+  import: warnings, shared+  hs-source-dirs: src   exposed-modules:     Kioku.Migrations     Kioku.Migrations.History.Codd@@ -53,53 +65,57 @@     Kioku.Migrations.Internal.EmbedFile    build-depends:-    , aeson                    >=2.1       && <2.3-    , base                     >=4.18      && <5-    , bytestring               >=0.11      && <0.13-    , containers               >=0.6       && <0.8-    , hasql                    >=1.10      && <1.11-    , hasql-transaction        >=1.2       && <1.3-    , keiro-migrations         ^>=0.16.0.0-    , kiroku-store-migrations  ^>=0.4.0.0-    , pg-migrate               ^>=1.1.0.0-    , pg-migrate-embed         ^>=1.1.0.0-    , pg-migrate-import-codd   ^>=1.1.0.0-    , template-haskell         >=2.20      && <2.24-    , text                     >=2.0       && <2.2+    aeson >=2.1 && <2.3,+    base >=4.18 && <5,+    bytestring >=0.11 && <0.13,+    containers >=0.6 && <0.8,+    hasql >=1.10 && <1.11,+    hasql-transaction >=1.2 && <1.3,+    keiro-migrations ^>=0.17.0.0,+    kiroku-store-migrations ^>=0.4.0.0,+    pg-migrate ^>=1.1.0.0,+    pg-migrate-embed ^>=1.1.0.0,+    pg-migrate-import-codd ^>=1.1.0.0,+    template-haskell >=2.20 && <2.24,+    text >=2.0 && <2.2,  library test-support-  import:          shared-  visibility:      public-  hs-source-dirs:  test-support+  import: warnings, shared+  visibility: public+  hs-source-dirs: test-support   exposed-modules: Kioku.Migrations.TestSupport   build-depends:-    , base                     >=4.18     && <5-    , ephemeral-pg             ^>=0.2.2.0-    , hasql                    >=1.10     && <1.11-    , kioku-migrations         ^>=0.6.0.0-    , pg-migrate-test-support  ^>=1.1.0.0-    , text                     >=2.0      && <2.2+    base >=4.18 && <5,+    ephemeral-pg ^>=0.2.2.0,+    keiro-test-support ^>=0.17.0.0,+    kioku-migrations ^>=0.7.0.0,+    text >=2.0 && <2.2,  test-suite kioku-migrations-test-  import:         shared-  type:           exitcode-stdio-1.0-  main-is:        Main.hs+  import: warnings, shared+  type: exitcode-stdio-1.0+  main-is: Main.hs   hs-source-dirs: test-   -- testCoddCohortImport rehearses the deprecated codd import bridge on purpose;   -- the bridge must keep working until the last codd-era database crosses over.-  ghc-options:    -threaded -rtsopts -with-rtsopts=-N -Wno-deprecations+  ghc-options:+    -threaded+    -rtsopts+    -with-rtsopts=-N+    -Wno-deprecations+   build-depends:-    , base                           >=4.18      && <5-    , containers                     >=0.6       && <0.8-    , hasql                          >=1.6-    , keiro-migrations               ^>=0.16.0.0-    , kioku-migrations               ^>=0.6.0.0-    , kioku-migrations:test-support-    , kiroku-store-migrations        ^>=0.4.0.0-    , pg-migrate                     ^>=1.1.0.0-    , pg-migrate-embed               ^>=1.1.0.0-    , pg-migrate-import-codd         ^>=1.1.0.0-    , tasty                          >=1.5-    , tasty-hunit                    >=0.10-    , text                           >=2.0       && <2.2+    base >=4.18 && <5,+    bytestring >=0.11 && <0.13,+    containers >=0.6 && <0.8,+    hasql >=1.6,+    keiro-migrations ^>=0.17.0.0,+    kioku-migrations ^>=0.7.0.0,+    kioku-migrations:test-support,+    kiroku-store-migrations ^>=0.4.0.0,+    pg-migrate ^>=1.1.0.0,+    pg-migrate-embed ^>=1.1.0.0,+    pg-migrate-import-codd ^>=1.1.0.0,+    tasty >=1.5,+    tasty-hunit >=0.10,+    text >=2.0 && <2.2,
test-support/Kioku/Migrations/TestSupport.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE MultilineStrings #-}- module Kioku.Migrations.TestSupport   ( withKiokuMigratedDatabase,     withBareDatabase,@@ -7,24 +5,15 @@ where  import Data.Text (Text)-import Database.PostgreSQL.Migrate.Test qualified as PgMigrate import EphemeralPg qualified-import Hasql.Connection qualified as Connection-import Hasql.Decoders qualified as D-import Hasql.Encoders qualified as E-import Hasql.Session qualified as Session-import Hasql.Statement (Statement, preparable)-import Kioku.Migrations (kiokuMigrationPlan)+import Keiro.Test.Postgres qualified as Keiro+import Kioku.Migrations (kiokuMigrations)  withKiokuMigratedDatabase :: (Text -> IO a) -> IO a withKiokuMigratedDatabase use = do-  plan <- either (fail . show) pure kiokuMigrationPlan-  result <--    PgMigrate.withMigratedDatabase plan \connection -> do-      connectionStringResult <- Connection.use connection connectionStringSession-      connectionString <- either (fail . show) pure connectionStringResult-      use connectionString-  either (fail . show) pure result+  component <- either (fail . show) pure kiokuMigrations+  Keiro.withMigratedSuiteWith [component] \fixture ->+    Keiro.withFreshDatabase fixture use  -- | An ephemeral database with no migrations applied at all — not even keiro's -- bootstrap. Tests that need to build a schema layout by hand (for instance, to@@ -34,22 +23,3 @@ withBareDatabase use = do   result <- EphemeralPg.with (use . EphemeralPg.connectionString)   either (fail . show) pure result---- pg-migrate-test-support deliberately supplies a live Hasql connection so--- assertions cannot inherit runner session state. Kioku's established shim--- supplies a connection string instead, so recover the ephemeral server's--- connection coordinates from that fresh callback session.-connectionStringSession :: Session.Session Text-connectionStringSession = Session.statement () connectionStringStatement--connectionStringStatement :: Statement () Text-connectionStringStatement =-  preparable-    """-    SELECT 'host=' || split_part(current_setting('unix_socket_directories'), ',', 1)-        || ' port=' || current_setting('port')-        || ' dbname=' || current_database()-        || ' user=' || current_user-    """-    E.noParams-    (D.singleRow (D.column (D.nonNullable D.text)))
test/Main.hs view
@@ -11,6 +11,9 @@ module Main where  import Control.Exception (bracket)+import Data.ByteString (ByteString)+import Data.ByteString qualified as ByteString+import Data.Char (isHexDigit, isSpace) import Data.Foldable (toList) import Data.Int (Int64) import Data.List (sort)@@ -38,6 +41,7 @@     defaultImportOptions,     defaultRunOptions,     migrationComponentFromEmbeddedSql,+    migrationFingerprint,     migrationId,     migrationPlan,     runMigrationPlan,@@ -47,6 +51,7 @@   ) import Database.PostgreSQL.Migrate.Embed (checkMigrationManifest) import Database.PostgreSQL.Migrate.History.Codd (importCoddHistoryWithValidators)+import Database.PostgreSQL.Migrate.Internal (migrationChecksumBytes) import Hasql.Connection qualified as Connection import Hasql.Connection.Settings qualified as Settings import Hasql.Decoders qualified as D@@ -61,9 +66,11 @@     cohortCoddSourceConfig,     cohortCoddStateValidators,     kiokuCoddHistoryMappings,+    kiokuLegacyMigrationNames,   ) import Kioku.Migrations.TestSupport (withBareDatabase, withKiokuMigratedDatabase) import Kiroku.Store.Migrations qualified as KirokuMigrations+import Numeric (showHex) import Test.Tasty (TestTree, defaultMain, testGroup) import Test.Tasty.HUnit (Assertion, assertBool, assertFailure, testCase, (@?=)) @@ -89,9 +96,18 @@         [ testCase "restores strict verification and is idempotent" testLedgerChecksumRebaseline,           testCase "rejects a missing default ledger table" testLedgerFixupRequiresDefaultLedger         ],-      testCase "the migration manifest is complete and valid" testManifestIntegrity,+      testGroup+        "migration source integrity"+        [ testCase "the migration manifest is complete and valid" testManifestIntegrity,+          testCase "the Codd lock matches its ten native payloads" testMigrationLockIntegrity,+          testCase "all thirteen released native payload hashes are frozen" testFrozenNativeHashes,+          testCase "the frozen history is exempt and every future entry passes policy" testForwardMigrationPolicy,+          testCase "an unqualified future object reports its filename" testUnqualifiedFutureMigrationRejected,+          testCase "a future search-path mutation reports its filename" testFutureSearchPathMutationRejected+        ],       testCase "the pinned Codd history maps 30 known plan targets" testHistoryMappings,       testCase "the pre-cutover Codd cohort imports 30 rows and applies only the forward migrations" testCoddCohortImport,+      testCase "fresh and supported Codd-upgrade databases converge on the frozen Kioku schema" testSchemaConvergence,       testGroup         "the partition-aware full-text index"         [ testCase "a normal database replaces the content-only GIN" testPartitionAwareFtsIndex,@@ -129,6 +145,213 @@     Left err -> assertFailure ("invalid migration manifest: " <> show err)     Right _ -> pure () +testMigrationLockIntegrity :: Assertion+testMigrationLockIntegrity = do+  entries <- checkedMigrationEntries+  lockText <- Text.IO.readFile "migrations.lock"+  locked <- either assertFailure pure (parseMigrationLock lockText)+  let expected =+        zip+          (toList kiokuLegacyMigrationNames)+          (checksumHex . snd <$> take 10 (toList entries))+  locked @?= expected++testFrozenNativeHashes :: Assertion+testFrozenNativeHashes = do+  entries <- checkedMigrationEntries+  let actual = [(filename, checksumHex bytes) | (filename, bytes) <- toList entries]+  actual @?= frozenNativeMigrationChecksums++testForwardMigrationPolicy :: Assertion+testForwardMigrationPolicy = do+  entries <- checkedMigrationEntries+  futureMigrationPolicyViolations (toList entries) @?= []++testUnqualifiedFutureMigrationRejected :: Assertion+testUnqualifiedFutureMigrationRejected =+  futureMigrationPolicyViolations+    [("0014-unqualified.sql", "CREATE TABLE memories (id bigint);")]+    @?= ["0014-unqualified.sql: application object is not schema-qualified: create table memories"]++testFutureSearchPathMutationRejected :: Assertion+testFutureSearchPathMutationRejected =+  futureMigrationPolicyViolations+    [("0014-search-path.sql", "SET LOCAL search_path TO kioku, pg_catalog;")]+    @?= ["0014-search-path.sql: migration mutates search_path"]++checkedMigrationEntries :: IO (NonEmpty (FilePath, ByteString))+checkedMigrationEntries =+  checkMigrationManifest "migrations/manifest"+    >>= either (assertFailure . ("invalid migration manifest: " <>) . show) pure++parseMigrationLock :: Text -> Either String [(FilePath, Text)]+parseMigrationLock contents = do+  entries <- traverse parseLine (zip [1 :: Int ..] (Text.lines contents))+  let filenames = fst <$> entries+  if length filenames == Set.size (Set.fromList filenames)+    then Right entries+    else Left "migrations.lock contains a duplicate filename"+  where+    parseLine (lineNumber, line) =+      case Text.words line of+        [digest, filename]+          | Text.length digest == 64 && Text.all isLowerHex digest ->+              Right (Text.unpack filename, digest)+        _ -> Left ("invalid migrations.lock entry on line " <> show lineNumber)+    isLowerHex char = isHexDigit char && not (char >= 'A' && char <= 'F')++checksumHex :: ByteString -> Text+checksumHex =+  Text.pack+    . concatMap (leftPad . (`showHex` ""))+    . ByteString.unpack+    . migrationChecksumBytes+    . migrationFingerprint+  where+    leftPad [digit] = ['0', digit]+    leftPad digits = digits++frozenNativeMigrationChecksums :: [(FilePath, Text)]+frozenNativeMigrationChecksums =+  [ ("0001-kioku-base.sql", "bfc4fc77978588405f0f697a4831ca12a4972ab8f0e5fe320561a7e34122fb94"),+    ("0002-kioku-memory-embeddings.sql", "11b42d976d146b61023e102b8633fce0df445ebe2ef4f2b9fedd51e8ad93e349"),+    ("0003-kioku-distillation.sql", "a4dcaf496976a1d206c8c5c903f3e18a768eca62bc32c09a01822e32d5aeebae"),+    ("0004-kioku-session-delegation-lineage.sql", "70ca30dc4737ef5048a9b241ad87ed0d71f0f0c32527f442e9a0ffbf2047f7ba"),+    ("0005-kioku-awaiting-session-state.sql", "d849d7d8a2581ce25475542828462534d56454ab620522251adec5bc95ab1792"),+    ("0006-kioku-session-readmodel-registry-bump.sql", "76c3e43144ee7d158a73088fe38edee771994a38d796cc7dadf6ebd83b122542"),+    ("0007-kioku-l1-watermarks.sql", "2f5bce34b92de0e306dbba7dc978ebb531e5f7f09edb18a0fcc758daa2319a45"),+    ("0008-kioku-schema-hardening.sql", "c6b734e2ba1aacf4736a6714ef218ed26c89cbfb7ce02552af46d220a0be42ae"),+    ("0009-kioku-embedding-schema-heal.sql", "26b42f4fc265fb214ff3ab6b312be893eb447223322ac4bdb09d4ee1a649ece8"),+    ("0010-kioku-scope-identity-recompute.sql", "c88ad236c4d6ad54e8ad6c4b51821f72772d58ff2d028d60d9e4d32a5c3cb125"),+    ("0011-kioku-memory-space-partition.sql", "6c83d3f01f784d0d9395953d5bb1763b8eea6cd9439073df42f79775a85197a9"),+    ("0012-relocate-projections-to-kioku-schema.sql", "5ad602b1379694d30241e09ad77d78858cbf8f8094d2e7bdb0fe7ba12dda88e6"),+    ("0013-partition-aware-fts-index.sql", "b73a9fef16c523ef50d4c9cb0cb550abe276c974b2937b798e8d2ea7497ffd4a")+  ]++futureMigrationPolicyViolations :: [(FilePath, ByteString)] -> [Text]+futureMigrationPolicyViolations entries =+  concatMap+    inspect+    [ (filename, Text.Encoding.decodeUtf8 bytes)+    | (filename, bytes) <- entries,+      filename `Set.notMember` frozenMigrationNames+    ]+  where+    inspect (filename, sql) =+      (prefix filename <$> searchPathViolations sql)+        <> (prefix filename <$> qualificationViolations sql)+    prefix filename rule = Text.pack filename <> ": " <> rule++frozenMigrationNames :: Set.Set FilePath+frozenMigrationNames = Set.fromList (fst <$> frozenNativeMigrationChecksums)++searchPathViolations :: Text -> [Text]+searchPathViolations sql =+  [ "migration mutates search_path"+  | any (`Text.isInfixOf` normalized) ["set search_path", "set local search_path", "set session search_path"]+      || "set schema" `Text.isInfixOf` normalized+      || "set_config('search_path'" `Text.isInfixOf` compact+  ]+  where+    normalized = normalizeSql sql+    compact = Text.filter (not . isSpace) normalized++qualificationViolations :: Text -> [Text]+qualificationViolations sql =+  publicViolation <> concatMap inspectStatement statements+  where+    normalized = normalizeSql sql+    statements = filter (not . Text.null) (Text.strip <$> Text.splitOn ";" normalized)+    publicViolation = ["application object uses the public schema" | "public." `Text.isInfixOf` normalized]++inspectStatement :: Text -> [Text]+inspectStatement statement =+  mapMaybe (unqualifiedObject statement) objectPrefixes+    <> maybeToList (unqualifiedIndexTarget statement)+    <> unqualifiedReferences statement+  where+    objectPrefixes =+      [ "create table ",+        "alter table ",+        "drop table ",+        "truncate table ",+        "create type ",+        "alter type ",+        "drop type ",+        "create function ",+        "create or replace function ",+        "alter function ",+        "drop function ",+        "create view ",+        "create materialized view ",+        "create sequence ",+        "alter sequence ",+        "drop sequence "+      ]++unqualifiedObject :: Text -> Text -> Maybe Text+unqualifiedObject statement prefix = do+  remainder <- Text.stripPrefix prefix statement+  let objectName = firstObjectName remainder+  if isQualifiedObject objectName+    then Nothing+    else Just ("application object is not schema-qualified: " <> Text.stripEnd prefix <> " " <> objectName)++unqualifiedIndexTarget :: Text -> Maybe Text+unqualifiedIndexTarget statement+  | "create index " `Text.isPrefixOf` statement || "create unique index " `Text.isPrefixOf` statement =+      case Text.breakOn " on " statement of+        (_, remainder)+          | Text.null remainder -> Just "CREATE INDEX has no ON target"+          | otherwise ->+              let target = firstObjectName (Text.drop 4 remainder)+               in if isQualifiedObject target+                    then Nothing+                    else Just ("index target is not schema-qualified: " <> target)+  | otherwise = Nothing++unqualifiedReferences :: Text -> [Text]+unqualifiedReferences statement = go (Text.words statement)+  where+    go ("references" : target : remaining) =+      ["foreign-key target is not schema-qualified: " <> cleanObjectName target | not (isQualifiedObject target)]+        <> go remaining+    go (_ : remaining) = go remaining+    go [] = []++firstObjectName :: Text -> Text+firstObjectName =+  cleanObjectName+    . headOrEmpty+    . dropWhile (`elem` ["if", "not", "exists", "only"])+    . Text.words++headOrEmpty :: [Text] -> Text+headOrEmpty (value : _) = value+headOrEmpty [] = ""++cleanObjectName :: Text -> Text+cleanObjectName = Text.takeWhile (\char -> char /= '(' && char /= ',')++isQualifiedObject :: Text -> Bool+isQualifiedObject objectName =+  not (Text.null objectName)+    && "." `Text.isInfixOf` objectName+    && not ("public." `Text.isPrefixOf` objectName)++normalizeSql :: Text -> Text+normalizeSql =+  Text.unwords+    . Text.words+    . Text.toLower+    . Text.unlines+    . fmap (fst . Text.breakOn "--")+    . Text.lines++maybeToList :: Maybe value -> [value]+maybeToList (Just value) = [value]+maybeToList Nothing = []+ testHistoryMappings :: Assertion testHistoryMappings = do   plan <- either (fail . show) pure kiokuMigrationPlan@@ -231,6 +454,20 @@     withConnection connStr \conn -> do       found <- run conn (Session.statement () registryTableExists)       found @?= True+      plan <- either (fail . show) pure kiokuMigrationPlan+      repeated <-+        runMigrationPlan defaultRunOptions (Settings.connectionString connStr) plan+          >>= either (assertFailure . show) pure+      let MigrationReport {results = repeatedResults} = repeated+      length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 56+      length [() | MigrationResult {outcome = AppliedNow} <- toList repeatedResults] @?= 0+      verification <-+        verifyMigrationPlan defaultRunOptions (Settings.connectionString connStr) plan+          >>= either (assertFailure . show) pure+      let VerificationReport {issues = freshIssues, pendingMigrations = freshPending, unknownMigrations = freshUnknown} = verification+      freshIssues @?= []+      freshPending @?= []+      freshUnknown @?= []  registryTableExists :: Statement () Bool registryTableExists =@@ -263,6 +500,12 @@         finalOutcome @?= AppliedNow       [] -> assertFailure "the composed migration plan returned no results"     query connStr hostMigratedColumnExists >>= (@?= True)+    query connStr currentSearchPath >>= (@?= "host_app, pg_catalog")+    verification <- verifyMigrationPlan defaultRunOptions (Settings.connectionString connStr) plan >>= either (assertFailure . show) pure+    let VerificationReport {issues = hostIssues, pendingMigrations = hostPending, unknownMigrations = hostUnknown} = verification+    hostIssues @?= []+    hostPending @?= []+    hostUnknown @?= []  hostComposedPlan :: IO MigrationPlan hostComposedPlan = do@@ -314,6 +557,13 @@     E.noParams     (D.singleRow (D.column (D.nonNullable D.bool))) +currentSearchPath :: Statement () Text+currentSearchPath =+  preparable+    "SELECT current_setting('search_path')"+    E.noParams+    (D.singleRow (D.column (D.nonNullable D.text)))+ -- * Released checksum re-baseline  ledgerFixupPath :: FilePath@@ -1200,6 +1450,100 @@     let MigrationReport {results = repeatedResults} = repeated     length [() | MigrationResult {outcome = AlreadyApplied} <- toList repeatedResults] @?= 56     length [() | MigrationResult {outcome = AppliedNow} <- toList repeatedResults] @?= 0++testSchemaConvergence :: Assertion+testSchemaConvergence = do+  fresh <- withKiokuMigratedDatabase (\connStr -> query connStr kiokuSchemaSnapshotStatement)+  upgraded <- withSupportedCoddUpgrade (\connStr -> query connStr kiokuSchemaSnapshotStatement)+  upgraded @?= fresh+  checksumHex (Text.Encoding.encodeUtf8 fresh) @?= expectedKiokuSchemaChecksum++withSupportedCoddUpgrade :: (Text -> IO a) -> IO a+withSupportedCoddUpgrade use =+  withBareDatabase \connStr -> do+    plan <- either (fail . show) pure kiokuMigrationPlan+    fixture <- Text.IO.readFile "test/fixtures/pre-cutover-schema.sql"+    let legacyNames = fixtureMigrationNames fixture+        settings = Settings.connectionString connStr+        provider = connectionProviderFromSettings settings+    kirokuFixup <- Text.IO.readFile "codd-upgrade/realign-kiroku-migration-timestamps.sql"+    keiroFixup <- Text.IO.readFile "codd-upgrade/realign-keiro-migration-timestamps.sql"+    relocation <- Text.IO.readFile "codd-upgrade/relocate-keiro-tables-to-keiro-schema.sql"+    withConnection connStr \conn -> do+      run conn (Session.script fixture)+      run conn (Session.script (coddV5Ledger legacyNames))+      run conn (Session.script kirokuFixup)+      run conn (Session.script keiroFixup)+      run conn (Session.script relocation)+      run conn (Session.script seedSessionRegistry)+    sourceConfig <-+      either+        (assertFailure . show)+        pure+        (cohortCoddSourceConfig provider False "schema convergence rehearsal" Confirmed)+    _ <-+      importCoddHistoryWithValidators+        (withEquivalentHistory AllowEquivalentHistory defaultImportOptions)+        cohortCoddStateValidators+        sourceConfig+        provider+        plan+        cohortCoddHistoryMappings+        >>= either (assertFailure . show) pure+    _ <- runMigrationPlan defaultRunOptions settings plan >>= either (assertFailure . show) pure+    use connStr++expectedKiokuSchemaChecksum :: Text+expectedKiokuSchemaChecksum = "be5f9e8c2d1b483675e9f93d5b98af53f599c2e43b393b2c7441958a82e0059d"++kiokuSchemaSnapshotStatement :: Statement () Text+kiokuSchemaSnapshotStatement =+  preparable+    """+    WITH facts AS (+      SELECT 'relation|' || c.relname::text || '|' || c.relkind::text AS fact+      FROM pg_catalog.pg_class c+      JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace+      WHERE n.nspname = 'kioku' AND c.relkind IN ('r', 'p')++      UNION ALL++      SELECT 'column|' || c.relname::text || '|' || a.attnum::text || '|'+             || a.attname::text || '|' || pg_catalog.format_type(a.atttypid, a.atttypmod)+             || '|' || a.attnotnull::text || '|'+             || coalesce(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true), '')+      FROM pg_catalog.pg_class c+      JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace+      JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid+      LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = c.oid AND d.adnum = a.attnum+      WHERE n.nspname = 'kioku' AND c.relkind IN ('r', 'p')+        AND a.attnum > 0 AND NOT a.attisdropped++      UNION ALL++      SELECT 'constraint|' || c.relname::text || '|' || con.conname::text || '|'+             || con.contype::text || '|'+             || pg_catalog.regexp_replace(pg_catalog.pg_get_constraintdef(con.oid, true), '\\s+', ' ', 'g')+      FROM pg_catalog.pg_constraint con+      JOIN pg_catalog.pg_class c ON c.oid = con.conrelid+      JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace+      WHERE n.nspname = 'kioku'++      UNION ALL++      SELECT 'index|' || table_class.relname::text || '|' || index_class.relname::text || '|'+             || pg_catalog.regexp_replace(pg_catalog.pg_get_indexdef(index_class.oid), '\\s+', ' ', 'g')+      FROM pg_catalog.pg_index i+      JOIN pg_catalog.pg_class index_class ON index_class.oid = i.indexrelid+      JOIN pg_catalog.pg_class table_class ON table_class.oid = i.indrelid+      JOIN pg_catalog.pg_namespace n ON n.oid = table_class.relnamespace+      WHERE n.nspname = 'kioku'+    )+    SELECT coalesce(string_agg(fact, E'\n' ORDER BY fact COLLATE "C"), '')+    FROM facts+    """+    E.noParams+    (D.singleRow (D.column (D.nonNullable D.text)))  fixtureMigrationNames :: Text -> [FilePath] fixtureMigrationNames =