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 +19/−0
- clickhouse-migrate-cli.cabal +2/−2
- src/ClickHouseMigrate/CLI/Handler.hs +3/−2
- src/ClickHouseMigrate/CLI/Render/Json.hs +14/−5
- src/ClickHouseMigrate/CLI/Types.hs +43/−3
- test/Main.hs +11/−0
- test/golden/accepted.json +2/−1
- test/golden/delayed-ledger.json +2/−1
- test/golden/partial.json +2/−1
- test/golden/recovery-too-large.json +2/−1
- test/golden/schedule.json +19/−3
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