packages feed

clickhouse-migrate-cli 0.1.0.0 → 0.2.0.0

raw patch · 11 files changed

+119/−19 lines, 11 filesdep ~clickhouse-migratePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: clickhouse-migrate

API changes (from Hackage documentation)

- ClickHouseMigrate.CLI.Types: PlanMigration :: !Text -> !Maybe Text -> !Text -> ![PlanStep] -> PlanMigration
- ClickHouseMigrate.CLI.Types: PlanStep :: !Text -> !Text -> PlanStep
+ ClickHouseMigrate.CLI.Types: PlanMigrationChecksums :: !Text -> !Maybe Text -> !Text -> !Maybe Text -> ![PlanStep] -> PlanMigration
+ ClickHouseMigrate.CLI.Types: PlanStepChecksums :: !Text -> !Text -> !Maybe Text -> PlanStep
+ ClickHouseMigrate.CLI.Types: pattern PlanMigration :: Text -> Maybe Text -> Text -> [PlanStep] -> PlanMigration
+ ClickHouseMigrate.CLI.Types: pattern PlanStep :: Text -> Text -> PlanStep

Files

CHANGELOG.md view
@@ -1,5 +1,24 @@ # Changelog for clickhouse-migrate-cli +## 0.2.0.0 — 2026-11-08++### Breaking Changes++- Extend the JSON v1 contract with both checksum formats in offline plans,+  the recorded checksum format in history/status, and replicated initialization+  fields and error codes.++### New Features++- Populate offline plans with v1 and v2 migration and step digests.+- Render replicated history and failures with structured evidence.++### Other Changes++- Retain legacy `PlanStep` and `PlanMigration` construction and matching arities;+  legacy manually constructed reports render unknown v2 digests as null.+- Require `clickhouse-migrate ==0.2.*` and update JSON golden fixtures.+ ## 0.1.0.0 — 2026-10-16  Initial release.
clickhouse-migrate-cli.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.12 name: clickhouse-migrate-cli-version: 0.1.0.0+version: 0.2.0.0 synopsis: Reusable administrative CLI for ClickHouse migrations description:   Reusable administrative CLI for ClickHouse migrations.@@ -40,7 +40,7 @@     aeson >=2.2.4.1 && <2.4,     base >=4.21 && <4.22,     bytestring >=0.12.2 && <0.13,-    clickhouse-migrate ==0.1.*,+    clickhouse-migrate ==0.2.*,     containers >=0.7 && <0.9,     optparse-applicative >=0.18.1 && <0.20,     text >=2.1 && <2.2,
src/ClickHouseMigrate/CLI/Handler.hs view
@@ -25,11 +25,12 @@   [ PlanComponent       (componentNameText (componentViewName component))       (map componentNameText (Set.toAscList (componentDependencies component)))-      [ PlanMigration+      [ PlanMigrationChecksums           (migrationNameText (migrationIdName (migrationId migration)))           (migrationDescription migration)           (migrationChecksumHex (migrationChecksum migration))-          [PlanStep (stepNameText (stepViewName step)) (stepChecksumHex (stepChecksum step)) | step <- NE.toList (migrationSteps migration)]+          (Just (migrationChecksumHex (migrationChecksumFor ChecksumV2 migration)))+          [PlanStepChecksums (stepNameText (stepViewName step)) (stepChecksumHex (stepChecksum step)) (Just (stepChecksumHex (stepChecksumFor ChecksumV2 step))) | step <- NE.toList (migrationSteps migration)]       | migration <- NE.toList (componentMigrations component)       ]   | component <- NE.toList (inspectPlan plan)
src/ClickHouseMigrate/CLI/Render/Json.hs view
@@ -33,7 +33,7 @@  reportValue :: CommandReport -> Value reportValue = \case-  PlanReport components schedule -> object ["kind" .= ("plan" :: Text), "checksumFormat" .= Plan.checksumFormatName Plan.ChecksumV1, "completionPolicy" .= Encoding.decodeUtf8 Plan.completionPolicy, "components" .= componentsValue schedule 0 components, "schedule" .= [object ["ordinal" .= ordinal, "component" .= component, "migration" .= migration] | (ordinal, (component, migration)) <- zip [0 :: Int ..] schedule]]+  PlanReport components schedule -> object ["kind" .= ("plan" :: Text), "checksumFormat" .= Plan.checksumFormatName Plan.ChecksumV1, "checksumFormats" .= (["sha256-v1", "sha256-v2"] :: [Text]), "completionPolicy" .= Encoding.decodeUtf8 Plan.completionPolicy, "components" .= componentsValue schedule 0 components, "schedule" .= [object ["ordinal" .= ordinal, "component" .= component, "migration" .= migration] | (ordinal, (component, migration)) <- zip [0 :: Int ..] schedule]]   StatusReport report -> object ["kind" .= ("status" :: Text), "report" .= inspectionValue report]   HistoryReport report -> object ["kind" .= ("history" :: Text), "report" .= historyValue report]   VerificationReport (R.VerificationReport report current) -> object ["kind" .= ("verification" :: Text), "compatible" .= True, "current" .= current, "report" .= inspectionValue report]@@ -43,7 +43,7 @@ componentsValue :: [(Text, Text)] -> Int -> [PlanComponent] -> [Value] componentsValue _ _ [] = [] componentsValue schedule componentIndex (PlanComponent name dependencies migrations : rest) =-  object ["index" .= componentIndex, "name" .= name, "dependencies" .= dependencies, "migrations" .= [object ["ordinal" .= lookup (name, migration) (zip schedule [0 :: Int ..]), "name" .= migration, "description" .= description, "checksum" .= checksum, "steps" .= [object ["index" .= stepIndex, "name" .= step, "checksum" .= digest] | (stepIndex, PlanStep step digest) <- zip [0 :: Int ..] steps]] | PlanMigration migration description checksum steps <- migrations]] : componentsValue schedule (componentIndex + 1) rest+  object ["index" .= componentIndex, "name" .= name, "dependencies" .= dependencies, "migrations" .= [object ["ordinal" .= lookup (name, migration) (zip schedule [0 :: Int ..]), "name" .= migration, "description" .= description, "checksum" .= checksum, "checksums" .= object ["sha256-v1" .= checksum, "sha256-v2" .= checksumV2], "steps" .= [object ["index" .= stepIndex, "name" .= step, "checksum" .= digest, "checksums" .= object ["sha256-v1" .= digest, "sha256-v2" .= digestV2]] | (stepIndex, PlanStepChecksums step digest digestV2) <- zip [0 :: Int ..] steps]] | PlanMigrationChecksums migration description checksum checksumV2 steps <- migrations]] : componentsValue schedule (componentIndex + 1) rest  definitionValue :: R.DefinitionSummary -> Value definitionValue (R.DefinitionSummary ordinal componentIndex component dependencies migration checksum steps) = object ["ordinal" .= ordinal, "componentIndex" .= componentIndex, "component" .= component, "dependencies" .= dependencies, "migration" .= migration, "checksum" .= checksum, "steps" .= [object ["name" .= name, "checksum" .= digest] | (name, digest) <- steps]]@@ -87,7 +87,7 @@ serverValue identity = object ["serverId" .= T.serverIdText (T.identityServerId identity), "version" .= T.identityVersion identity, "hostName" .= T.identityHostName identity]  historyValue :: R.HistoryReport -> Value-historyValue (R.HistoryReport target server presence position access events migrations unresolved) = object ["target" .= T.targetKeyText target, "server" .= nullable serverValue server, "ledger" .= presenceValue presence, "position" .= fmap L.ledgerPositionText position, "access" .= observationAccess access, "events" .= map eventValue events, "migrations" .= map migrationValue migrations, "unresolved" .= unresolved]+historyValue (R.HistoryReport target server presence position access events migrations unresolved) = object ["target" .= T.targetKeyText target, "checksumFormat" .= recordedFormat events, "server" .= nullable serverValue server, "ledger" .= presenceValue presence, "position" .= fmap L.ledgerPositionText position, "access" .= observationAccess access, "events" .= map eventValue events, "migrations" .= map migrationValue migrations, "unresolved" .= unresolved]  inspectionValue :: R.InspectionReport -> Value inspectionValue (R.InspectionReport history migrations issues) = object ["observation" .= historyValue history, "migrations" .= map migrationValue migrations, "issues" .= map issueValue issues]@@ -118,6 +118,7 @@ bodyValue :: R.HistoryBody -> Value bodyValue = \case   R.LedgerInitialized target -> object ["target" .= T.targetKeyText target]+  R.LedgerInitializedReplicated target -> object ["target" .= T.targetKeyText target, "checksumFormat" .= ("sha256-v2" :: Text)]   R.MigrationDefinition definition -> object ["definition" .= definitionValue definition]   R.StepIntent index query -> object ["step" .= index, "queryId" .= T.queryIdText query]   R.StepCompletion index query -> object ["step" .= index, "queryId" .= T.queryIdText query]@@ -159,13 +160,13 @@ transportValue (L.TransportError code status server) = object ["code" .= transportErrorCode code, "httpStatus" .= status, "serverCode" .= server]  transportErrorCode :: L.TransportErrorCode -> Text-transportErrorCode = \case L.AdministrativeAccessLost -> "administrative-access-lost"; L.InvalidRequest -> "invalid-request"; L.ClientScopeFailed -> "client-scope-failed"; L.ConnectionFailed -> "connection-failed"; L.DeadlineExpired -> "deadline-expired"; L.ResponseInterrupted -> "response-interrupted"; L.ResponseTooLarge -> "response-too-large"; L.UnexpectedResponse -> "unexpected-response"; L.ServerRejected -> "server-rejected"; L.RedirectRejected -> "redirect-rejected"; L.ServerIdentityMismatch -> "server-identity-mismatch"; L.UnsupportedServer -> "unsupported-server"; L.CompletionPolicyMismatch -> "completion-policy-mismatch"; L.CleanupFailed -> "cleanup-failed"; L.CleanupTimedOut -> "cleanup-timed-out"+transportErrorCode = \case L.AdministrativeAccessLost -> "administrative-access-lost"; L.InvalidRequest -> "invalid-request"; L.ClientScopeFailed -> "client-scope-failed"; L.ConnectionFailed -> "connection-failed"; L.DeadlineExpired -> "deadline-expired"; L.ResponseInterrupted -> "response-interrupted"; L.ResponseTooLarge -> "response-too-large"; L.UnexpectedResponse -> "unexpected-response"; L.ServerRejected -> "server-rejected"; L.RedirectRejected -> "redirect-rejected"; L.ServerIdentityMismatch -> "server-identity-mismatch"; L.UnsupportedServer -> "unsupported-server"; L.CompletionPolicyMismatch -> "completion-policy-mismatch"; L.ReplicaSetMismatch -> "replica-set-mismatch"; L.CleanupFailed -> "cleanup-failed"; L.CleanupTimedOut -> "cleanup-timed-out"  accessErrorCode :: Access.AccessError -> Text accessErrorCode = \case Access.InvalidAccessStrategy -> "invalid-access-strategy"; Access.AcquisitionTimedOut -> "acquisition-timed-out"; Access.AcquisitionFailed -> "acquisition-failed"; Access.AccessLost -> "access-lost"; Access.AccessScopeClosed -> "access-scope-closed"; Access.AccessActionFailed -> "access-action-failed"; Access.PredecessorQuiescenceRequired -> "predecessor-quiescence-required"; Access.AccessReleaseFailed -> "access-release-failed"; Access.AccessReleaseTimedOut -> "access-release-timed-out"  ledgerErrorCode :: L.LedgerError -> Text-ledgerErrorCode = \case L.InvalidPosition -> "invalid-position"; L.InvalidAudit -> "invalid-audit"; L.InvalidEvidence -> "invalid-evidence"; L.InvalidEvent -> "invalid-event"; L.UnsupportedLedgerVersion -> "unsupported-ledger-version"; L.SequenceGap -> "sequence-gap"; L.SequenceConflict -> "sequence-conflict"; L.EventIdentityConflict -> "event-identity-conflict"; L.LedgerIdentityMismatch -> "ledger-identity-mismatch"; L.MissingInitialization -> "missing-initialization"; L.ImpossibleTransition -> "impossible-transition"; L.DefinitionMismatch -> "definition-mismatch"; L.InvalidLedgerSchema -> "invalid-ledger-schema"; L.UnsupportedLedgerStorage -> "unsupported-ledger-storage"; L.LedgerAbsent -> "ledger-absent"; L.LedgerUninitialized -> "ledger-uninitialized"; L.InitializationNotAuthorized -> "initialization-not-authorized"; L.StaleLedgerState -> "stale-ledger-state"; L.UnresolvedHistory -> "unresolved-history"; L.OutstandingRequests -> "outstanding-requests"; L.LedgerLimitExceeded -> "ledger-limit-exceeded"+ledgerErrorCode = \case L.InvalidPosition -> "invalid-position"; L.InvalidAudit -> "invalid-audit"; L.InvalidEvidence -> "invalid-evidence"; L.InvalidEvent -> "invalid-event"; L.UnsupportedLedgerVersion -> "unsupported-ledger-version"; L.SequenceGap -> "sequence-gap"; L.SequenceConflict -> "sequence-conflict"; L.EventIdentityConflict -> "event-identity-conflict"; L.LedgerIdentityMismatch -> "ledger-identity-mismatch"; L.MissingInitialization -> "missing-initialization"; L.ImpossibleTransition -> "impossible-transition"; L.DefinitionMismatch -> "definition-mismatch"; L.InvalidLedgerSchema -> "invalid-ledger-schema"; L.LedgerDatabaseMissing -> "ledger-database-missing"; L.UnsupportedLedgerStorage -> "unsupported-ledger-storage"; L.LedgerAbsent -> "ledger-absent"; L.LedgerUninitialized -> "ledger-uninitialized"; L.InitializationNotAuthorized -> "initialization-not-authorized"; L.StaleLedgerState -> "stale-ledger-state"; L.UnresolvedHistory -> "unresolved-history"; L.OutstandingRequests -> "outstanding-requests"; L.LedgerLimitExceeded -> "ledger-limit-exceeded"  adoptionErrorCode :: A.AdoptionError -> Text adoptionErrorCode = \case A.InvalidSourceEvidence -> "invalid-source-evidence"; A.UnsupportedSourceChecksum -> "unsupported-source-checksum"; A.InvalidSourceChecksum -> "invalid-source-checksum"; A.DuplicateSourceRow -> "duplicate-source-row"; A.DuplicateMapping -> "duplicate-mapping"; A.IncompleteMapping -> "incomplete-mapping"; A.UnknownSourceRow -> "unknown-source-row"; A.MappingOrderMismatch -> "mapping-order-mismatch"; A.MappingDefinitionMismatch -> "mapping-definition-mismatch"; A.HistoricalChecksumMismatch -> "historical-checksum-mismatch"; A.HistoricalChecksumRequired -> "historical-checksum-required"; A.AdoptionTargetMismatch -> "adoption-target-mismatch"; A.AdoptionInitializationRequired -> "adoption-initialization-required"; A.AdoptionExpectedStateMismatch -> "adoption-expected-state-mismatch"; A.AdoptionNativeConflict -> "adoption-native-conflict"; A.AdoptionBatchConflict -> "adoption-batch-conflict"; A.AdoptionResumeRequired -> "adoption-resume-required"; A.AdoptionBatchTooLarge -> "adoption-batch-too-large"@@ -180,6 +181,7 @@ runErrorCode = \case   R.InvalidRuntimePlan _ -> "invalid-runtime-plan"   R.LedgerNamespaceInSql _ -> "ledger-namespace-in-sql"+  R.DatabaseCreationOnReplicatedTarget _ -> "database-creation-on-replicated-target"   R.DefinitionTooLarge _ -> "definition-too-large"   R.AccessFailed problem -> accessErrorCode problem   R.TargetFailed _ -> "target-failed"@@ -209,6 +211,7 @@     fields = \case       R.InvalidRuntimePlan step -> ["step" .= stepReferenceValue step]       R.LedgerNamespaceInSql step -> ["step" .= stepReferenceValue step]+      R.DatabaseCreationOnReplicatedTarget step -> ["step" .= stepReferenceValue step]       R.DefinitionTooLarge definition -> ["definition" .= definitionValue definition]       R.AccessFailed _ -> []       R.TargetFailed problem -> ["transport" .= transportValue problem]@@ -240,3 +243,9 @@   R.RecoveryStepStartEvent -> "step-start"   R.RecoveryStepSuccessEvent -> "step-success"   R.RecoveryStepFailureEvent -> "step-failure"++recordedFormat :: [R.HistoryEvent] -> Maybe Text+recordedFormat events = case [kind | R.HistoryEvent _ _ _ _ _ _ _ _ _ kind <- events] of+  R.LedgerInitialized _ : _ -> Just "sha256-v1"+  R.LedgerInitializedReplicated _ : _ -> Just "sha256-v2"+  _ -> Nothing
src/ClickHouseMigrate/CLI/Types.hs view
@@ -1,5 +1,29 @@+{-# LANGUAGE PatternSynonyms #-}+ -- | Command values contain input, never an acquired administrative capability.-module ClickHouseMigrate.CLI.Types where+module ClickHouseMigrate.CLI.Types+  ( OutputFormat (..),+    Invocation (..),+    AuditInput (..),+    UpInput (..),+    RepairOperation (..),+    RepairInput (..),+    AdoptionOperation (..),+    ExpectedInput (..),+    AdoptionInput (..),+    Command (..),+    CommandPolicy (..),+    CommandEnvironment (..),+    CommandError (..),+    CommandFailure (..),+    PlanStep (PlanStep, PlanStepChecksums),+    PlanMigration (PlanMigration, PlanMigrationChecksums),+    PlanComponent (..),+    CommandReport (..),+    CommandResult (..),+    ParseIssue (..),+  )+where  import ClickHouseMigrate.Adoption (AdoptionError) import ClickHouseMigrate.Event (DiagnosticPolicy, EventSink)@@ -69,9 +93,25 @@   | AdoptionRejected !AdoptionError !R.HistoryReport   deriving stock (Show) -data PlanStep = PlanStep !Text !Text deriving stock (Eq, Show)+data PlanStep = PlanStepChecksums !Text !Text !(Maybe Text) deriving stock (Eq, Show) -data PlanMigration = PlanMigration !Text !(Maybe Text) !Text ![PlanStep] deriving stock (Eq, Show)+data PlanMigration = PlanMigrationChecksums !Text !(Maybe Text) !Text !(Maybe Text) ![PlanStep] deriving stock (Eq, Show)++-- Preserve existing construction and matching arities while retaining additive+-- v2 data. Manually constructed legacy reports have no known v2 digest.+pattern PlanStep :: Text -> Text -> PlanStep+pattern PlanStep name checksum <- PlanStepChecksums name checksum _+  where+    PlanStep name checksum = PlanStepChecksums name checksum Nothing++pattern PlanMigration :: Text -> Maybe Text -> Text -> [PlanStep] -> PlanMigration+pattern PlanMigration name description checksum steps <- PlanMigrationChecksums name description checksum _ steps+  where+    PlanMigration name description checksum steps = PlanMigrationChecksums name description checksum Nothing steps++{-# COMPLETE PlanStep #-}++{-# COMPLETE PlanMigration #-}  data PlanComponent = PlanComponent !Text ![Text] ![PlanMigration] deriving stock (Eq, Show) 
test/Main.hs view
@@ -11,6 +11,7 @@ import Data.Aeson qualified as JSON import Data.ByteString qualified as BS import Data.List.NonEmpty (NonEmpty (..))+import Data.List.NonEmpty qualified as NE import Data.Set qualified as Set import Data.Text qualified as Text import Data.Text.Encoding qualified as Encoding@@ -45,6 +46,16 @@       decode = decodeAdoptionInput key (L.ExpectAbsent key) authorization quiet audit   offline <- handleCommand (error "Offline plan forced configuration") plan PlanCommand   assert "offline plan missing" (case offline of CommandResult "plan" (Right (PlanReport [_] [("app", "one")])) -> True; _ -> False)+  let boundMigration = NE.head (componentMigrations (NE.head (inspectPlan plan)))+      boundStep = NE.head (migrationSteps boundMigration)+      v1 = migrationChecksumHex (migrationChecksum boundMigration)+      v2 = migrationChecksumHex (migrationChecksumFor ChecksumV2 boundMigration)+      rendered = Encoding.decodeUtf8 (renderJson offline)+      contains key value = ("\"" <> key <> "\":\"" <> value <> "\"") `Text.isInfixOf` rendered+  assert "v1/v2 migration hashes must differ" (v1 /= v2)+  assert "offline existing checksum moved from v1" (contains "checksum" v1)+  assert "offline migration checksum formats missing" (contains "sha256-v1" v1 && contains "sha256-v2" v2)+  assert "offline step checksum formats missing" (contains "sha256-v1" (stepChecksumHex (stepChecksum boundStep)) && contains "sha256-v2" (stepChecksumHex (stepChecksumFor ChecksumV2 boundStep)))   let two = right (sqlMigration "two" (step :| []))       a = right (migrationComponent "a" Set.empty (migration :| [two]))       b = right (migrationComponent "b" (Set.singleton "a") (migration :| []))
test/golden/accepted.json view
@@ -69,7 +69,8 @@             ]           }         ],-        "unresolved": true+        "unresolved": true,+        "checksumFormat": null       },       "migrations": [         {
test/golden/delayed-ledger.json view
@@ -95,7 +95,8 @@             ]           }         ],-        "unresolved": true+        "unresolved": true,+        "checksumFormat": null       },       "migrations": [         {
test/golden/partial.json view
@@ -100,7 +100,8 @@             ]           }         ],-        "unresolved": true+        "unresolved": true,+        "checksumFormat": null       },       "migrations": [         {
test/golden/recovery-too-large.json view
@@ -82,7 +82,8 @@             ]           }         ],-        "unresolved": true+        "unresolved": true,+        "checksumFormat": null       },       "migrations": [         {
test/golden/schedule.json view
@@ -17,14 +17,22 @@             "name": "one",             "description": null,             "checksum": "a1-hash",-            "steps": []+            "steps": [],+            "checksums": {+              "sha256-v1": "a1-hash",+              "sha256-v2": null+            }           },           {             "ordinal": 2,             "name": "two",             "description": "upgrade",             "checksum": "a2-hash",-            "steps": []+            "steps": [],+            "checksums": {+              "sha256-v1": "a2-hash",+              "sha256-v2": null+            }           }         ]       },@@ -40,7 +48,11 @@             "name": "one",             "description": null,             "checksum": "b1-hash",-            "steps": []+            "steps": [],+            "checksums": {+              "sha256-v1": "b1-hash",+              "sha256-v2": null+            }           }         ]       }@@ -61,6 +73,10 @@         "component": "a",         "migration": "two"       }+    ],+    "checksumFormats": [+      "sha256-v1",+      "sha256-v2"     ]   },   "error": null