kioku-migrate (empty) → 0.1.0.0
raw patch · 4 files changed
+328/−0 lines, 4 filesdep +aesondep +basedep +bytestring
Dependencies added: aeson, base, bytestring, hasql, kioku-core, kioku-migrations, kiroku-store, optparse-applicative, pg-migrate, pg-migrate-cli, pg-migrate-import-codd, text
Files
- CHANGELOG.md +16/−0
- LICENSE +29/−0
- app/Main.hs +227/−0
- kioku-migrate.cabal +56/−0
+ CHANGELOG.md view
@@ -0,0 +1,16 @@+# Changelog++## 0.1.0.0 — 2026-07-14++### Added++- Added a pg-migrate administration executable for planning, listing, checking, inspecting,+ verifying, applying, repairing, and creating migrations.+- Added checked Codd-history import for the pinned Kiroku, Keiro, and Kioku migration cohort with+ text or JSON reports.+- Added automatic read-model registry reconciliation after successful migration application.++### Changed++- Updated the executable to pg-migrate 1.1 and the Keiro 0.3/Kiroku Store 0.3 application resource+ model.
+ LICENSE view
@@ -0,0 +1,29 @@+BSD 3-Clause License++Copyright (c) 2026, Nadeem Bitar+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this+ list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+ this list of conditions and the following disclaimer in the documentation+ and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its+ contributors may be used to endorse or promote products derived from+ this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ app/Main.hs view
@@ -0,0 +1,227 @@+module Main where++import Data.Aeson qualified as Aeson+import Data.ByteString.Lazy.Char8 qualified as LazyByteString+import Data.Foldable (for_)+import Data.Int (Int64)+import Data.List.NonEmpty qualified as NonEmpty+import Data.Text qualified as Text+import Data.Text.IO qualified as Text.IO+import Database.PostgreSQL.Migrate+ ( Confirmation (..),+ EquivalentHistoryPolicy (AllowEquivalentHistory),+ HistoryImportOutcome (..),+ HistoryImportReport (..),+ HistoryImportResult (..),+ MigrationPlan,+ connectionProviderFromSettings,+ defaultImportOptions,+ defaultRunOptions,+ withEquivalentHistory,+ )+import Database.PostgreSQL.Migrate.CLI+import Database.PostgreSQL.Migrate.History.Codd+ ( defaultCoddLockKey,+ importCoddHistoryWithValidators,+ withCoddLockKey,+ )+import Hasql.Connection.Settings qualified as Settings+import Kioku.App (runAppIO, withNoopAppEnv)+import Kioku.Migrations (kiokuMigrationPlan)+import Kioku.Migrations.History.Codd+ ( cohortCoddHistoryMappings,+ cohortCoddSourceConfig,+ cohortCoddStateValidators,+ )+import Kioku.ReadModel (ReadModelSchema (..), ReconcileOutcome (..), reconcileReadModelRegistry)+import Kiroku.Store.Connection (defaultConnectionSettings)+import Options.Applicative+import Options.Applicative qualified as Opt+import System.Environment (lookupEnv)+import System.Exit qualified as Exit+import Text.Read qualified as Read++-- | Apply the migration chain, then reconcile keiro's read-model registry to the+-- identity this binary's read models declare.+--+-- The second half is what keeps a read-model version bump from taking production+-- down: keiro's @registerReadModel@ only ever inserts, so an existing registry row+-- stays pinned at its old version and every query for that model fails closed with+-- @ReadModelStaleSchema@. Reconciling here means the repair ships with the schema+-- change instead of needing a hand-written registry migration per bump. Migration+-- time is also the only moment where exactly one process is doing this; doing it at+-- app startup would have every host racing to write the registry on boot.+main :: IO ()+main = do+ plan <- either (fail . show) pure kiokuMigrationPlan+ command <-+ execParser+ ( info+ (kiokuCommandParser plan <**> helper)+ (fullDesc <> progDesc "Manage the Kiroku, Keiro, and Kioku migration components")+ )+ defaultDatabaseUrl <- lookupEnv "DATABASE_URL"+ case command of+ Standard migrationCommand -> runStandardCommand plan defaultDatabaseUrl migrationCommand+ ImportCodd importOptions -> runImportCommand plan defaultDatabaseUrl importOptions++data KiokuCommand+ = Standard !MigrationCommand+ | ImportCodd !CoddImportOptions++data CoddImportOptions = CoddImportOptions+ { targetSettings :: !(Maybe Settings.Settings),+ sourceSettings :: !(Maybe Settings.Settings),+ sourceLockKey :: !Int64,+ strictSource :: !Bool,+ reason :: !Text.Text,+ confirmation :: !Confirmation,+ outputFormat :: !OutputFormat+ }++kiokuCommandParser :: MigrationPlan -> Parser KiokuCommand+kiokuCommandParser plan =+ (Standard <$> migrationCommandParser plan)+ <|> hsubparser+ ( Opt.command+ "import"+ ( info+ (ImportCodd <$> coddImportOptionsParser <**> helper)+ (progDesc "Import the 30-migration pinned Kiroku/Keiro/Kioku Codd cohort without replaying DDL")+ )+ )++coddImportOptionsParser :: Parser CoddImportOptions+coddImportOptionsParser =+ CoddImportOptions+ <$> optional (databaseUrlOption "database-url" "Target PostgreSQL connection string; defaults to DATABASE_URL")+ <*> optional (databaseUrlOption "source-database-url" "Codd source connection string; defaults to the target")+ <*> option+ auto+ ( long "source-lock-key"+ <> metavar "INT64"+ <> value defaultCoddLockKey+ <> showDefault+ <> help "Cooperating Codd advisory-lock key"+ )+ <*> switch (long "strict-source" <> help "Reject unselected rows in the shared Codd ledger")+ <*> strOption (long "reason" <> metavar "TEXT" <> help "Audit reason recorded with every imported migration")+ <*> flag NotConfirmed Confirmed (long "confirm" <> help "Confirm the checked-in Codd payload evidence")+ <*> flag TextOutput JsonOutput (long "json" <> help "Emit JSON schema version 1")+ where+ databaseUrlOption optionName description =+ option+ (Settings.connectionString . Text.pack <$> str)+ (long optionName <> metavar "URL" <> help description)++runStandardCommand :: MigrationPlan -> Maybe String -> MigrationCommand -> IO ()+runStandardCommand plan defaultDatabaseUrl command = do+ let defaultSettings =+ Settings.connectionString (Text.pack (maybe "" id defaultDatabaseUrl))+ environment = cliEnvironment defaultSettings plan defaultRunOptions+ outcome <- runMigrationCommand environment command+ case commandOutputFormat command of+ TextOutput -> Text.IO.putStrLn (renderMigrationCommandText outcome)+ JsonOutput -> LazyByteString.putStrLn (Aeson.encode (renderMigrationCommandJson outcome))+ reconcileAfterUp defaultDatabaseUrl command outcome+ Exit.exitWith+ (case exitClass outcome of ExitSucceeded -> Exit.ExitSuccess; _ -> Exit.ExitFailure 1)++runImportCommand :: MigrationPlan -> Maybe String -> CoddImportOptions -> IO ()+runImportCommand plan defaultDatabaseUrl options = do+ let fallbackSettings = Settings.connectionString (Text.pack (maybe "" id defaultDatabaseUrl))+ targetSettings = maybe fallbackSettings id options.targetSettings+ sourceSettings = maybe targetSettings id options.sourceSettings+ targetProvider = connectionProviderFromSettings targetSettings+ sourceProvider = connectionProviderFromSettings sourceSettings+ config <-+ either+ (Exit.die . ("invalid Codd import configuration: " <>) . show)+ pure+ ( cohortCoddSourceConfig+ sourceProvider+ options.strictSource+ options.reason+ options.confirmation+ )+ imported <-+ importCoddHistoryWithValidators+ (withEquivalentHistory AllowEquivalentHistory defaultImportOptions)+ cohortCoddStateValidators+ (withCoddLockKey options.sourceLockKey config)+ targetProvider+ plan+ cohortCoddHistoryMappings+ report <- either (Exit.die . ("Codd history import failed: " <>) . show) pure imported+ case options.outputFormat of+ JsonOutput -> LazyByteString.putStrLn (Aeson.encode (renderHistoryImportJson "codd" report))+ TextOutput -> renderImportReport report++renderImportReport :: HistoryImportReport -> IO ()+renderImportReport HistoryImportReport {importResults, cleanupIssues} = do+ for_ (NonEmpty.toList importResults) \HistoryImportResult {importedMigration, importOutcome} ->+ putStrLn+ ( show importedMigration+ <> ": "+ <> case importOutcome of+ Imported -> "imported"+ AlreadyImported -> "already imported"+ )+ for_ cleanupIssues \cleanupIssue ->+ putStrLn ("cleanup_issue=" <> show cleanupIssue)++reconcileAfterUp :: Maybe String -> MigrationCommand -> CliOutcome -> IO ()+reconcileAfterUp defaultDatabaseUrl command outcome =+ case (command, exitClass outcome) of+ (Up UpOptions {connection = ConnectionOptions override}, ExitSucceeded) ->+ reconcile (maybe defaultConnectionString settingsConnectionString override)+ _ -> pure ()+ where+ defaultConnectionString = Text.pack (maybe "" id defaultDatabaseUrl)++settingsConnectionString :: Settings.Settings -> Text.Text+settingsConnectionString settings =+ case Read.readMaybe (show settings) of+ Just connectionString -> Text.pack connectionString+ Nothing -> error "Hasql rendered an unreadable connection string"++reconcile :: Text.Text -> IO ()+reconcile connectionString =+ withNoopAppEnv (defaultConnectionSettings connectionString) \env -> do+ result <-+ runAppIO env reconcileReadModelRegistry+ case result of+ Left err -> Exit.die ("read-model registry reconciliation failed: " <> show err)+ Right outcomes -> for_ outcomes report++commandOutputFormat :: MigrationCommand -> OutputFormat+commandOutputFormat command =+ case command of+ Plan PlanOptions {output = OutputOptions format} -> format+ List ListOptions {output = OutputOptions format} -> format+ Check CheckOptions {output = OutputOptions format} -> format+ Status StatusOptions {output = OutputOptions format} -> format+ Verify VerifyOptions {output = OutputOptions format} -> format+ Up UpOptions {output = OutputOptions format} -> format+ Repair RepairOptions {output = OutputOptions format} -> format+ New NewOptions {output = OutputOptions format} -> format++-- | Report only what changed, so a no-op run stays quiet.+report :: (ReadModelSchema, ReconcileOutcome) -> IO ()+report (schema, outcome) =+ case outcome of+ AlreadyCurrent -> pure ()+ Registered -> say "registered read model"+ Reconciled -> say "reconciled read model"+ where+ say verb =+ putStrLn+ ( verb+ <> " "+ <> Text.unpack schema.readModelName+ <> " at v"+ <> show schema.readModelVersion+ <> " ("+ <> Text.unpack schema.readModelShapeHash+ <> ")"+ )
+ kioku-migrate.cabal view
@@ -0,0 +1,56 @@+cabal-version: 3.0+name: kioku-migrate+version: 0.1.0.0+synopsis: The kioku schema migration entry point+description:+ Applies kioku's migration chain and then reconciles keiro's read-model+ registry to the identity the compiled read models declare.+ .+ This lives in its own package rather than as an executable of+ kioku-migrations because it depends on kioku-core (for the read-model+ schemas), and kioku-core's test suite depends on+ kioku-migrations:test-support. Cabal's solver detects dependency cycles at+ package granularity, so an executable stanza inside kioku-migrations would+ close that cycle and make the whole project unsolvable -- even though the+ component graph itself is acyclic.++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+extra-doc-files: CHANGELOG.md++source-repository head+ type: git+ location: https://github.com/shinzui/kioku.git+ subdir: kioku-migrate++executable kioku-migrate+ main-is: Main.hs+ hs-source-dirs: app+ default-language: GHC2024+ default-extensions:+ BlockArguments+ DuplicateRecordFields+ OverloadedRecordDot+ OverloadedStrings++ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ , aeson >=2.2 && <2.3+ , base >=4.21 && <5+ , bytestring >=0.12 && <0.13+ , hasql >=1.10 && <1.11+ , kioku-core ^>=0.1.0.0+ , kioku-migrations ^>=0.1.0.0+ , kiroku-store ^>=0.3.0.1+ , optparse-applicative >=0.17 && <0.20+ , pg-migrate ^>=1.1.0.0+ , pg-migrate-cli ^>=1.1.0.0+ , pg-migrate-import-codd ^>=1.1.0.0+ , text >=2.0 && <2.2