packages feed

clickhouse-migrate 0.1.0.0 → 0.2.0.0

raw patch · 30 files changed

+672/−184 lines, 30 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ ClickHouseMigrate.Ledger: LedgerDatabaseMissing :: LedgerError
+ ClickHouseMigrate.Ledger: ReplicaSetMismatch :: TransportErrorCode
+ ClickHouseMigrate.Plan: ChecksumV2 :: ChecksumFormat
+ ClickHouseMigrate.Plan: migrationChecksumFor :: ChecksumFormat -> MigrationView -> MigrationChecksum
+ ClickHouseMigrate.Plan: stepChecksumFor :: ChecksumFormat -> StepView -> StepChecksum
+ ClickHouseMigrate.Report: DatabaseCreationOnReplicatedTarget :: !StepReference -> RunError
+ ClickHouseMigrate.Report: LedgerInitializedReplicated :: !TargetKey -> HistoryBody
+ ClickHouseMigrate.Target: DirectServer :: !ServerId -> TargetTopology
+ ClickHouseMigrate.Target: DuplicateReplica :: TargetError
+ ClickHouseMigrate.Target: InvalidClusterName :: TargetError
+ ClickHouseMigrate.Target: ReplicatedCluster :: !ClusterName -> !NonEmpty ServerId -> TargetTopology
+ ClickHouseMigrate.Target: clusterName :: Text -> Either TargetError ClusterName
+ ClickHouseMigrate.Target: clusterNameText :: ClusterName -> Text
+ ClickHouseMigrate.Target: configuredTopology :: TargetConfig -> TargetTopology
+ ClickHouseMigrate.Target: data ClusterName
+ ClickHouseMigrate.Target: data TargetTopology
+ ClickHouseMigrate.Target: replicatedTargetConfig :: TargetKey -> DirectEndpoint -> ClusterName -> NonEmpty ServerId -> LedgerNamespace -> LedgerBinding -> Credentials -> HttpLimits -> Either TargetError TargetConfig

Files

CHANGELOG.md view
@@ -1,5 +1,28 @@ # Changelog for clickhouse-migrate +## 0.2.0.0 — 2026-11-08++### Breaking Changes++- Add public checksum, target-error, transport-error, ledger-error, history and+  run-error constructors; update exhaustive matches for replicated states.+- Add checksum v2, SQL envelope v2 and replicated ledger initialization and+  completion policies. Replicated targets require v2; direct targets retain v1.++### New Features++- Validate single-shard replica-set targets and replica identity on new connections.+- Support replicated MergeTree SQL, topology-specific preflight, structurally+  validated replicated ledgers and full-replica quorum completion.+- Expose topology and per-format checksum accessors.+- Admit exact Altinity `26.3.60.10001.altinitystable` and+  `26.3.60.10002.altinitystable` Linux arm64 engines.++### Other Changes++- Centralize server policy and add checksum v2 reference vectors, public API+  checks and replicated contract documentation.+ ## 0.1.0.0 — 2026-10-16  Initial release.
clickhouse-migrate.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.12 name: clickhouse-migrate-version: 0.1.0.0+version: 0.2.0.0 synopsis: Core definitions and execution for ClickHouse migrations description:   Core definitions and execution for ClickHouse migrations.@@ -92,6 +92,7 @@     ClickHouseMigrate.Internal.Reporting     ClickHouseMigrate.Internal.RuntimeTypes     ClickHouseMigrate.Internal.ServerPolicy+    ClickHouseMigrate.Internal.ServerProfile     ClickHouseMigrate.Internal.SqlEnvelope     ClickHouseMigrate.Internal.TargetTypes     ClickHouseMigrate.Internal.Transport@@ -138,6 +139,7 @@     ClickHouseMigrate.Internal.Reporting     ClickHouseMigrate.Internal.RuntimeTypes     ClickHouseMigrate.Internal.ServerPolicy+    ClickHouseMigrate.Internal.ServerProfile     ClickHouseMigrate.Internal.SqlEnvelope     ClickHouseMigrate.Internal.TargetTypes     ClickHouseMigrate.Internal.Transport@@ -190,6 +192,7 @@     ClickHouseMigrate.Internal.Reporting     ClickHouseMigrate.Internal.RuntimeTypes     ClickHouseMigrate.Internal.ServerPolicy+    ClickHouseMigrate.Internal.ServerProfile     ClickHouseMigrate.Internal.SqlEnvelope     ClickHouseMigrate.Internal.TargetTypes     ClickHouseMigrate.Internal.Transport
src/ClickHouseMigrate/Adoption.hs view
@@ -61,7 +61,7 @@ resumeAdoption = executeAdoption True  executeAdoption :: Bool -> MutationContext -> Plan.MigrationPlan -> AdoptionPlan -> IO (Either R.RunFailure R.RunReport)-executeAdoption resume context@(MutationContext (T.TargetConfig actual _ _ _ binding _ _) _ _ _) plan (AdoptionPlan request@(AdoptionRequest target _ authorization quiet audit _ _ _ _) batch entries) =+executeAdoption resume context@(MutationContext (T.TargetConfig {T.configKey = actual, T.configBinding = binding, T.configTopology = topology}) _ _ _) plan (AdoptionPlan request@(AdoptionRequest target _ authorization quiet audit _ _ _ _) batch entries) =   let validate snapshot = either (Left . R.AdoptionRejected) Right $ do         (current, definitions) <- Validation.buildBatch plan request         Validation.require MappingDefinitionMismatch (current == batch && definitions == entries)@@ -74,7 +74,9 @@           R.HistoryReport _ _ presence _ _ _ _ _ | presence `elem` [R.LedgerMissing, R.LedgerUninitialized] -> Validation.require AdoptionInitializationRequired (binding == T.FirstUse && isJust authorization)           _ -> pure ()         pure committed-      prerequisite = if target == actual then Right () else Left (R.AdoptionRejected AdoptionTargetMismatch)+      prerequisite = case topology of+        T.ReplicatedCluster _ _ -> Left (R.LedgerFailed L.UnsupportedLedgerVersion)+        T.DirectServer _ -> if target == actual then Right () else Left (R.AdoptionRejected AdoptionTargetMismatch)    in withRuntimeObservation prerequisite (Just validate) context audit authorization (Just quiet) plan $ \runtime -> do         projection <- runtimeProjection runtime         case L.adoption projection of
src/ClickHouseMigrate/Inspection.hs view
@@ -16,6 +16,7 @@ import ClickHouseMigrate.Internal.Reporting qualified as Reporting import ClickHouseMigrate.Internal.RuntimeTypes (InspectionOptions (..), VerifyOptions (..)) import ClickHouseMigrate.Internal.ServerPolicy (TargetCheckError (..), withTransportChecked)+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile import ClickHouseMigrate.Internal.TargetTypes (QueryId (..), TargetConfig (..)) import ClickHouseMigrate.Internal.Transport qualified as Transport import ClickHouseMigrate.Plan (MigrationPlan)@@ -28,7 +29,7 @@ verifyOptions = VerifyOptions  history :: InspectionOptions -> TargetConfig -> IO (Either R.InspectionFailure R.HistoryReport)-history options config@(TargetConfig target _ _ _ _ _ _) = do+history options config@(TargetConfig {configKey = target}) = do   latest <- newIORef Nothing   observed <- try @SomeException $ do     probe <- QueryId . ("chm-" <>) <$> freshUuid@@ -64,7 +65,7 @@     Right (Right report, []) -> pure (Right report)  status :: InspectionOptions -> TargetConfig -> MigrationPlan -> IO (Either R.InspectionFailure R.InspectionReport)-status options config plan = fmap (Reporting.inspectHistory plan) <$> history options config+status options config plan = fmap (Reporting.inspectHistoryFor (Profile.checksumFormat (configTopology config)) plan) <$> history options config  verify :: InspectionOptions -> TargetConfig -> VerifyOptions -> MigrationPlan -> IO (Either R.InspectionFailure R.VerificationReport) verify options config (VerifyOptions requireCurrent) plan = do
src/ClickHouseMigrate/Internal/Checksum.hs view
@@ -2,12 +2,15 @@ module ClickHouseMigrate.Internal.Checksum   ( completionPolicyV1,     encodeStepV1,+    encodeStepV2,     encodeMigrationV1,+    encodeMigrationV2,     digest,     digestHex,   ) where +import ClickHouseMigrate.Internal.ServerProfile qualified as Profile import ClickHouseMigrate.Prelude import Crypto.Hash.SHA256 qualified as SHA256 import Data.ByteString qualified as BS@@ -18,7 +21,7 @@ import Numeric (showHex)  completionPolicyV1 :: ByteString-completionPolicyV1 = "clickhouse-migrate/local-sync-v1;async_insert=0;mutations_sync=1;alter_sync=1"+completionPolicyV1 = Profile.completionPolicyV1  field :: ByteString -> Builder field bs = word64BE (fromIntegral (BS.length bs)) <> byteString bs@@ -38,6 +41,25 @@     toLazyByteString $       byteString "clickhouse-migrate/migration\0v1\0"         <> field completionPolicyV1+        <> foldMap (field . Text.encodeUtf8) [component, migration]+        <> word64BE (fromIntegral (length steps))+        <> foldMap field steps++encodeStepV2 :: Text -> Text -> Text -> ByteString -> ByteString+encodeStepV2 component migration step sql =+  Lazy.toStrict $+    toLazyByteString $+      byteString "clickhouse-migrate/step\0v2\0"+        <> field Profile.completionPolicyV2+        <> foldMap (field . Text.encodeUtf8) [component, migration, step]+        <> field sql++encodeMigrationV2 :: Text -> Text -> [ByteString] -> ByteString+encodeMigrationV2 component migration steps =+  Lazy.toStrict $+    toLazyByteString $+      byteString "clickhouse-migrate/migration\0v2\0"+        <> field Profile.completionPolicyV2         <> foldMap (field . Text.encodeUtf8) [component, migration]         <> word64BE (fromIntegral (length steps))         <> foldMap field steps
src/ClickHouseMigrate/Internal/Execution.hs view
@@ -6,14 +6,15 @@ import ClickHouseMigrate.Internal.Exclusive (checkGuard, guardDescription, withExclusive) import ClickHouseMigrate.Internal.ExclusiveTypes (AccessDescription (..), AccessResult (..), ExclusiveAccess (..), Guard) import ClickHouseMigrate.Internal.Identity (diagnosticTime, freshUuid)-import ClickHouseMigrate.Internal.LedgerCodec (contextFor, definitions)+import ClickHouseMigrate.Internal.LedgerCodec (contextFor, definitionsFor) import ClickHouseMigrate.Internal.LedgerStore qualified as Store import ClickHouseMigrate.Internal.LedgerTypes qualified as L-import ClickHouseMigrate.Internal.Preflight (preflight, validateRecoveryEvent)+import ClickHouseMigrate.Internal.Preflight (preflight, preflightPending, validateRecoveryEvent) import ClickHouseMigrate.Internal.Projection (appendProjection) import ClickHouseMigrate.Internal.Reporting qualified as Reporting import ClickHouseMigrate.Internal.RuntimeTypes import ClickHouseMigrate.Internal.ServerPolicy (TargetCheckError (..), withTransportChecked)+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile import ClickHouseMigrate.Internal.TargetTypes qualified as T import ClickHouseMigrate.Internal.Transport qualified as Transport import ClickHouseMigrate.Ledger qualified as Ledger@@ -59,10 +60,11 @@ -- Adoption validates its fresh read before openStore may initialize anything. -- Ordinary execution retains its existing single-load path. withRuntimeObservation :: Either R.RunError () -> Maybe (R.HistoryReport -> Either R.RunError Bool) -> MutationContext -> L.AuditContext -> Maybe L.InitializationAuthorization -> Maybe L.QuiescenceEvidence -> Plan.MigrationPlan -> (Runtime -> IO ()) -> IO (Either R.RunFailure R.RunReport)-withRuntimeObservation prerequisite validateObservation context@(MutationContext config@(T.TargetConfig target _ _ _ _ _ _) provider@(ExclusiveAccess description _ _ _ _) _ _) audit initialization quiet plan action = do+withRuntimeObservation prerequisite validateObservation context@(MutationContext config@(T.TargetConfig {T.configKey = target}) provider@(ExclusiveAccess description _ _ _ _) _ _) audit initialization quiet plan action = do   seed <- try @SomeException freshUuid-  let identity = either (const "00000000-0000-4000-8000-000000000000") id seed-  latest <- newIORef (Reporting.initialRun identity target description plan)+  let format = Profile.checksumFormat (T.configTopology config)+      identity = either (const "00000000-0000-4000-8000-000000000000") id seed+  latest <- newIORef (Reporting.initialRunFor format identity target description plan)   secondary <- newIORef []   result <- case seed of     Left exception | isAsync exception -> throwIO exception@@ -89,7 +91,7 @@               modifyIORef'                 latest                 ( \report -> case Reporting.runHistory report of-                    R.HistoryReport key _ presence position access events migrations unresolved -> Reporting.updateRun plan (R.HistoryReport key server presence position access events migrations unresolved) report+                    R.HistoryReport key _ presence position access events migrations unresolved -> Reporting.updateRunFor format plan (R.HistoryReport key server presence position access events migrations unresolved) report                 )               let AccessDescription strategy scope = guardDescription guard                   -- The caller's coordinator contract is the authority for a new@@ -101,25 +103,29 @@                 Just validate -> do                   observation <- callStore (Store.load (Just guard) transport config)                   let snapshot = Reporting.observationReport target server (R.GuardedObservation (guardDescription guard)) observation-                  modifyIORef' latest (Reporting.updateRun plan snapshot)+                  modifyIORef' latest (Reporting.updateRunFor format plan snapshot)                   either abort pure (validate snapshot)               if observationOnly                 then do                   emitAt context identity latest Event.RunCompleted Nothing Nothing Nothing                   callStore (Store.checkAccess guard)                 else do+                  when (Profile.requiresLedgerDatabase (T.configTopology config)) $ do+                    before <- callStore (Store.load (Just guard) transport config)+                    let applied = case before of Store.History projection -> length (L.completed projection); _ -> 0+                    either abort pure (preflightPending config plan applied)                   opened <- Store.storeResult (Store.openStore guard transport config initialization established identity)                   store <- case opened of-                    Left (Store.LedgerProblem L.InitializationNotAuthorized) | T.TargetConfig _ _ _ _ (T.ExistingLedger _) _ _ <- config, isNothing initialization -> abort R.LedgerRequired+                    Left (Store.LedgerProblem L.InitializationNotAuthorized) | T.TargetConfig {T.configBinding = T.ExistingLedger _} <- config, isNothing initialization -> abort R.LedgerRequired                     Left failure -> abort (Reporting.storeError failure)                     Right value -> pure value                   let runtime = Runtime context audit plan guard transport store identity latest secondary                   refresh runtime                   case (config, initialization) of-                    (T.TargetConfig _ _ _ _ T.FirstUse _ _, Just (L.InitializationAuthorization _ ledger _)) -> modifyIORef' latest (\report -> report {R.initialized = Just ledger})+                    (T.TargetConfig {T.configBinding = T.FirstUse}, Just (L.InitializationAuthorization _ ledger _)) -> modifyIORef' latest (\report -> report {R.initialized = Just ledger})                     _ -> pure ()                   report <- readIORef latest-                  let R.InspectionReport _ _ issues = Reporting.inspectHistory plan (Reporting.runHistory report)+                  let R.InspectionReport _ _ issues = Reporting.inspectHistoryFor format plan (Reporting.runHistory report)                       changes = [issue | issue@R.DefinitionChanged {} <- issues]                   unless (null changes) (abort (R.IncompatibleHistory changes))                   action runtime@@ -168,7 +174,7 @@ refresh runtime@(Runtime _ _ plan guard transport _ _ latest _) = do   projection <- runtimeProjection runtime   let snapshot = Reporting.fromProjection (Just (Transport.transportIdentity transport)) (R.GuardedObservation (guardDescription guard)) projection-  modifyIORef' latest (Reporting.updateRun plan snapshot)+  modifyIORef' latest (Reporting.updateRunFor (L.projectionChecksumFormat projection) plan snapshot)  emitAt :: MutationContext -> Text -> IORef R.RunReport -> Event.EventPhase -> Maybe L.Definition -> Maybe Int -> Maybe T.QueryId -> IO () emitAt (MutationContext _ _ sink _) identity latest phase definition index query = do@@ -276,5 +282,5 @@   appendEvent runtime (Just attempt) L.CompleteMigration context   emit runtime Event.MigrationCompleted (Just definition) Nothing Nothing -orderedMigrations :: Plan.MigrationPlan -> [(L.Definition, Plan.MigrationView)]-orderedMigrations plan = zip (definitions plan) (NE.toList (Plan.inspectSchedule plan))+orderedMigrations :: Plan.ChecksumFormat -> Plan.MigrationPlan -> [(L.Definition, Plan.MigrationView)]+orderedMigrations format plan = zip (definitionsFor format plan) (NE.toList (Plan.inspectSchedule plan))
src/ClickHouseMigrate/Internal/Http.hs view
@@ -1,6 +1,7 @@ -- | Dedicated direct HTTP resources, bounded full responses, and no replay. module ClickHouseMigrate.Internal.Http (Http, withHttp, withHttpAdmission, withHttpAdmissionChecked, withHttpFinalizer, probeConnection, submit, readJsonRowStream) where +import ClickHouseMigrate.Internal.ServerProfile qualified as Profile import ClickHouseMigrate.Internal.TargetTypes import ClickHouseMigrate.Internal.Transport hiding (request) import ClickHouseMigrate.Prelude@@ -125,14 +126,14 @@     Right value -> pure (ScopeResult (Right value) cleanup)  submit :: Http -> QueryId -> RequestKind -> ByteString -> IO TransportOutcome-submit (Http (TargetConfig _ (DirectEndpoint endpoint) _ _ _ (Credentials user password) (HttpLimits micros maxBytes)) manager submitted gate active inflight) (QueryId identity) kind sql = registered inflight $ withMVar gate $ \() -> do+submit (Http config@(TargetConfig {configEndpoint = DirectEndpoint endpoint, configCredentials = Credentials user password, configLimits = HttpLimits micros maxBytes}) manager submitted gate active inflight) (QueryId identity) kind sql = registered inflight $ withMVar gate $ \() -> do   writeIORef submitted False   live <- readIORef active   if not live || BS.null sql || BS.elem 0 sql     then pure (NotSubmitted (safeError InvalidRequest))     else do       result <- try @SomeException $ timeout micros $ do-        request <- makeRequest endpoint user password micros identity kind sql+        request <- makeRequest config endpoint user password micros identity kind sql         HTTP.withResponse request manager $ \response -> do           let status = statusCode (HTTP.responseStatus response)               exceptionHeader = lookup "X-ClickHouse-Exception-Code" (HTTP.responseHeaders response)@@ -216,9 +217,9 @@ -- dummyManaged supplies no cleanup/replay; the factory owns this socket and closes -- it on failure. The bounded response parser is the same as for ordinary requests. probeConnection :: TargetConfig -> Internal.Connection -> QueryId -> ByteString -> IO TransportOutcome-probeConnection (TargetConfig _ (DirectEndpoint endpoint) _ _ _ (Credentials user password) (HttpLimits micros maxBytes)) connection (QueryId identity) sql = do+probeConnection config@(TargetConfig {configEndpoint = DirectEndpoint endpoint, configCredentials = Credentials user password, configLimits = HttpLimits micros maxBytes}) connection (QueryId identity) sql = do   result <- try @SomeException $ timeout micros $ do-    request <- makeRequest endpoint user password micros identity JsonRows sql+    request <- makeRequest config endpoint user password micros identity JsonRows sql     continuation <- Internal.requestBuilder request connection     bracket       (Internal.getResponse (Just (Internal.MaxHeaderLength 4096)) (Just (Internal.MaxNumberHeaders 64)) (Just micros) request (Internal.dummyManaged connection) continuation)@@ -236,33 +237,13 @@     Right Nothing -> pure (SubmissionUncertain (safeError DeadlineExpired))     Right (Just outcome) -> pure outcome -makeRequest :: Text -> ByteString -> ByteString -> Int -> Text -> RequestKind -> ByteString -> IO HTTP.Request-makeRequest endpoint user password micros identity kind sql = do+makeRequest :: TargetConfig -> Text -> ByteString -> ByteString -> Int -> Text -> RequestKind -> ByteString -> IO HTTP.Request+makeRequest config endpoint user password micros identity kind sql = do   base <- HTTP.parseRequest (Text.unpack endpoint)   let request =         HTTP.applyBasicAuth user password $           HTTP.setQueryString-            ( [ ("query_id", Just (Encoding.encodeUtf8 identity)),-                ("async_insert", Just "0"),-                ("mutations_sync", Just "1"),-                ("alter_sync", Just "1"),-                ("fsync_metadata", Just "1"),-                ("wait_end_of_query", Just "1"),-                ("default_format", Just "JSONEachRow")-              ]-                ++ if kind /= Command-                  then-                    [ ("max_rows_to_read", Just "0"),-                      ("max_bytes_to_read", Just "0"),-                      ("max_result_rows", Just "0"),-                      ("max_result_bytes", Just "0"),-                      ("read_overflow_mode", Just "throw"),-                      ("result_overflow_mode", Just "throw"),-                      ("timeout_overflow_mode", Just "throw"),-                      ("sort_overflow_mode", Just "throw")-                    ]-                  else []-            )+            (Profile.requestSettings (configTopology config) (configLimits config) identity kind)             base               { HTTP.method = "POST",                 HTTP.requestBody = HTTP.RequestBodyBS sql,
src/ClickHouseMigrate/Internal/LedgerCodec.hs view
@@ -6,6 +6,7 @@     eventValue,     definitionValue,     definitions,+    definitionsFor,     validateEvent,     eventKind,     encodeValue,@@ -18,6 +19,7 @@ import ClickHouseMigrate.Internal.Checksum qualified as Checksum import ClickHouseMigrate.Internal.LedgerTypes import ClickHouseMigrate.Internal.LedgerValidation (auditContext, ledgerPositionText, parseLedgerPosition)+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile import ClickHouseMigrate.Plan qualified as Plan import ClickHouseMigrate.Prelude import ClickHouseMigrate.Target qualified as Target@@ -80,7 +82,7 @@  bodyValue :: EventBody -> Value bodyValue = \case-  Initialize target -> object ["target" .= Target.targetKeyText target, "schemaVersion" .= (1 :: Int), "checksumFormat" .= ("sha256-v1" :: Text), "policy" .= Encoding.decodeUtf8 Plan.completionPolicy]+  InitializeFor target format -> object ["target" .= Target.targetKeyText target, "schemaVersion" .= (1 :: Int), "checksumFormat" .= Plan.checksumFormatName format, "policy" .= Encoding.decodeUtf8 (Profile.policyForFormat format)]   StartMigration definition -> definitionValue definition   StartStep step query -> stepValue step query   SucceedStep step query -> stepValue step query@@ -153,7 +155,10 @@ recoveryStep = \case RetryStep step -> Just step; AcceptStep step -> Just step; _ -> Nothing  definitions :: Plan.MigrationPlan -> [Definition]-definitions plan =+definitions = definitionsFor Plan.ChecksumV1++definitionsFor :: Plan.ChecksumFormat -> Plan.MigrationPlan -> [Definition]+definitionsFor format plan =   zipWith     bind     [0 ..]@@ -170,8 +175,8 @@         (Def.componentNameText (Plan.componentViewName component))         (map Def.componentNameText (Set.toAscList (Plan.componentDependencies component)))         (Def.migrationNameText (Def.migrationIdName (Plan.migrationId migration)))-        (Plan.migrationChecksumHex (Plan.migrationChecksum migration))-        [StepDefinition (Def.stepNameText (Plan.stepViewName step)) (Plan.stepChecksumHex (Plan.stepChecksum step)) | step <- NE.toList (Plan.migrationSteps migration)]+        (Plan.migrationChecksumHex (Plan.migrationChecksumFor format migration))+        [StepDefinition (Def.stepNameText (Plan.stepViewName step)) (Plan.stepChecksumHex (Plan.stepChecksumFor format step)) | step <- NE.toList (Plan.migrationSteps migration)]  definitionsDigest :: [Definition] -> Text definitionsDigest values = Checksum.digestHex (Checksum.digest (encodeValue (Aeson.toJSON (map definitionValue values))))@@ -204,8 +209,9 @@     version <- o .: "schemaVersion" :: Parser Int     format <- o .: "checksumFormat" :: Parser Text     policy <- o .: "policy" :: Parser Text-    unless (version == 1 && format == "sha256-v1" && policy == Encoding.decodeUtf8 Plan.completionPolicy) (fail "unsupported format")-    Initialize <$> (o .: "target" >>= validated Target.targetKey)+    parsed <- validated Plan.parseChecksumFormat format+    unless (version == 1 && policy == Encoding.decodeUtf8 (Profile.policyForFormat parsed)) (fail "unsupported format")+    (\target -> InitializeFor target parsed) <$> (o .: "target" >>= validated Target.targetKey)   "migration-started" -> fmap StartMigration . parseDefinition   "step-started" -> parseStep StartStep   "step-succeeded" -> parseStep SucceedStep
src/ClickHouseMigrate/Internal/LedgerSchema.hs view
@@ -14,21 +14,6 @@ qualifiedTable :: LedgerNamespace -> Text qualifiedTable namespace = quoteIdentifier (ledgerNamespaceText namespace) <> ".`events`" -createDatabase :: LedgerNamespace -> ByteString-createDatabase namespace = Encoding.encodeUtf8 ("CREATE DATABASE IF NOT EXISTS " <> quoteIdentifier (ledgerNamespaceText namespace) <> " ENGINE=Atomic")--createTable :: LedgerNamespace -> ByteString-createTable namespace = Encoding.encodeUtf8 ("CREATE TABLE IF NOT EXISTS " <> qualifiedTable namespace <> " " <> columnsSql <> " ENGINE=MergeTree ORDER BY sequence SETTINGS fsync_after_insert=1, fsync_part_directory=1, storage_policy='default'")--columnsSql :: Text-columnsSql = "(sequence UInt64, event_id UUID, ledger_id UUID, event String)"--expectedCreate :: LedgerNamespace -> Text-expectedCreate namespace = "CREATE TABLE " <> ledgerNamespaceText namespace <> ".events (`sequence` UInt64, `event_id` UUID, `ledger_id` UUID, `event` String) ENGINE = " <> expectedEngine--expectedEngine :: Text-expectedEngine = "MergeTree ORDER BY sequence SETTINGS fsync_after_insert = 1, fsync_part_directory = 1, storage_policy = 'default', index_granularity = 8192"- -- Only validated ASCII namespace names enter literals; event/audit data are JSON. namespaceLiteral :: LedgerNamespace -> Text namespaceLiteral = (\value -> "'" <> value <> "'") . ledgerNamespaceText
src/ClickHouseMigrate/Internal/LedgerStore.hs view
@@ -10,6 +10,7 @@ import ClickHouseMigrate.Internal.LedgerSchema qualified as Schema import ClickHouseMigrate.Internal.LedgerTypes import ClickHouseMigrate.Internal.Projection (appendProjection, project)+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile import ClickHouseMigrate.Internal.TargetTypes (TargetConfig (..)) import ClickHouseMigrate.Internal.Transport import ClickHouseMigrate.Prelude@@ -74,8 +75,10 @@  -- Shape state is separate from event projection to avoid inventing an empty -- ledger identity. Return False for absent table after checking every object.-shape :: Maybe Guard -> Transport -> LedgerNamespace -> IO (Bool, Bool)-shape guard transport namespace = do+shape :: Maybe Guard -> Transport -> TargetConfig -> IO (Bool, Bool)+shape guard transport config = do+  let namespace = configNamespace config+      topology = configTopology config   checkLocalStorage guard transport   settings <- readRows guard transport "SELECT toUInt8(getSetting('fsync_metadata')) AS fsync_metadata"   case settings of@@ -86,21 +89,24 @@     _ -> invalid   databases <- readRows guard transport (Schema.databaseQuery namespace)   case databases of-    [] -> pure (False, False)+    [] -> if Profile.requiresLedgerDatabase topology then abort (LedgerProblem LedgerDatabaseMissing) else pure (False, False)     [row] -> do       keys ["name", "engine"] row       name <- member "name" row       engine <- member "engine" row :: IO Text-      unless (name == ledgerNamespaceText namespace && engine == "Atomic") invalid+      unless (name == ledgerNamespaceText namespace && engine == Profile.databaseEngine topology) invalid       tables <- readRows guard transport (Schema.tableQuery namespace)       case tables of         [] -> pure (True, False)         [table] -> do           keys ["name", "engine", "engine_full", "sorting_key", "primary_key", "partition_key", "sampling_key", "create_table_query"] table           actual <- mapM (`member` table) ["name", "engine", "engine_full", "sorting_key", "primary_key", "partition_key", "sampling_key", "create_table_query"] :: IO [Text]-          let base = ["events", "MergeTree", Schema.expectedEngine, "sequence", "sequence", "", ""]-              quotedCreate = Text.replace ("CREATE TABLE " <> ledgerNamespaceText namespace <> ".") ("CREATE TABLE " <> Schema.quoteIdentifier (ledgerNamespaceText namespace) <> ".") (Schema.expectedCreate namespace)-          unless (actual == base ++ [Schema.expectedCreate namespace] || actual == base ++ [quotedCreate]) invalid+          unless (Profile.validTableShape topology namespace actual) invalid+          case Profile.replicaHealthQuery topology namespace of+            Nothing -> pure ()+            Just sql -> do+              health <- readRows guard transport sql+              unless (Profile.validReplicaHealth topology health) invalid           pure (True, True)         _ -> invalid     _ -> invalid@@ -136,8 +142,8 @@     invalid = abort (LedgerProblem UnsupportedLedgerStorage)  load :: Maybe Guard -> Transport -> TargetConfig -> IO Observation-load guard transport (TargetConfig target _ _ namespace binding _ _) = do-  (database, table) <- shape guard transport namespace+load guard transport config@(TargetConfig {configKey = target, configNamespace = namespace, configBinding = binding}) = do+  (database, table) <- shape guard transport config   if not database     then pure Missing     else@@ -150,6 +156,7 @@             else do               events <- mapM decodeRow rows               projection <- checked (project target events)+              unless (projectionChecksumFormat projection == Profile.checksumFormat (configTopology config)) (abort (LedgerProblem UnsupportedLedgerVersion))               case binding of                 ExistingLedger expected -> let LedgerPosition actual _ _ = position projection in unless (expected == actual) (abort (LedgerProblem LedgerIdentityMismatch))                 FirstUse -> pure ()@@ -168,7 +175,7 @@   pure event  openStore :: Guard -> Transport -> TargetConfig -> Maybe InitializationAuthorization -> QuiescenceEvidence -> Text -> IO LedgerStore-openStore guard transport config@(TargetConfig target _ _ namespace binding _ _) authorization (QuiescenceEvidence quietTarget _) run = do+openStore guard transport config@(TargetConfig {configKey = target, configNamespace = namespace, configBinding = binding}) authorization (QuiescenceEvidence quietTarget _) run = do   checkAccess guard   case authorization of     Just (InitializationAuthorization authorized identity _) -> do@@ -195,15 +202,15 @@   pure (LedgerStore guard transport target namespace state gate)  initialize :: Guard -> Transport -> TargetConfig -> LedgerId -> AuditContext -> Text -> IO Projection-initialize guard transport (TargetConfig target _ _ namespace _ _ _) ledger audit run = do+initialize guard transport config@(TargetConfig {configKey = target, configNamespace = namespace}) ledger audit run = do   identity <- freshUuid   time <- diagnosticTime-  let first = LedgerEvent 1 identity ledger run Nothing time audit "0.1.0.0" Nothing (Initialize target)+  let first = LedgerEvent 1 identity ledger run Nothing time audit "0.1.0.0" Nothing (InitializeFor target (Profile.checksumFormat (configTopology config)))   _ <- checked (project target [first])-  ddl "database" (Schema.createDatabase namespace)-  _ <- shape (Just guard) transport namespace-  ddl "table" (Schema.createTable namespace)-  (_, exists) <- shape (Just guard) transport namespace+  mapM_ (ddl "database") (Profile.createDatabase (configTopology config) namespace)+  _ <- shape (Just guard) transport config+  ddl "table" (Profile.createTable (configTopology config) namespace)+  (_, exists) <- shape (Just guard) transport config   unless exists (abort (LedgerProblem InvalidLedgerSchema))   -- Even under exclusivity do not write over preexisting nonempty history.   rows <- readRowsKind JsonRowStream (Just guard) transport (Schema.readQuery namespace)@@ -242,7 +249,7 @@ -- A fresh guarded FULL read, including strict corruption validation. NotObserved -- is evidence of this read only; it grants no authority to reuse a sequence. reconcile :: Guard -> Transport -> TargetConfig -> AppendReceipt -> IO (Reconciliation, Observation)-reconcile guard transport config@(TargetConfig target _ _ _ _ _ _) (AppendReceipt expected ledger number identity digest _) = do+reconcile guard transport config@(TargetConfig {configKey = target}) (AppendReceipt expected ledger number identity digest _) = do   unless (target == expected) (abort (LedgerProblem LedgerIdentityMismatch))   observation <- load (Just guard) transport config   let result = case observation of
src/ClickHouseMigrate/Internal/LedgerTypes.hs view
@@ -1,6 +1,9 @@+{-# LANGUAGE PatternSynonyms #-}+ -- | Private ledger protocol. No unchecked construction is exported publicly. module ClickHouseMigrate.Internal.LedgerTypes where +import ClickHouseMigrate.Internal.Types (ChecksumFormat (..)) import ClickHouseMigrate.Prelude import ClickHouseMigrate.Target (LedgerId, QueryId, TargetKey) import Data.Word (Word64)@@ -39,6 +42,7 @@   | MissingInitialization   | ImpossibleTransition   | DefinitionMismatch+  | LedgerDatabaseMissing   | InvalidLedgerSchema   | UnsupportedLedgerStorage   | LedgerAbsent@@ -72,7 +76,7 @@ data AdoptionBatch = AdoptionBatch !Text !Text !Text !Text !Int deriving stock (Eq, Show)  data EventBody-  = Initialize !TargetKey+  = InitializeFor !TargetKey !ChecksumFormat   | StartMigration !Definition   | StartStep !Int !Text   | SucceedStep !Int !Text@@ -84,6 +88,18 @@   | AdoptEntry !Definition !Evidence   | CompleteAdoption !Text   deriving stock (Eq, Show)++pattern Initialize :: TargetKey -> EventBody+pattern Initialize target <- InitializeFor target _+  where+    Initialize target = InitializeFor target ChecksumV1++{-# COMPLETE Initialize, StartMigration, StartStep, SucceedStep, FailStep, CompleteMigration, AuthorizeRecovery, AcceptCompletion, StartAdoption, AdoptEntry, CompleteAdoption #-}++projectionChecksumFormat :: Projection -> ChecksumFormat+projectionChecksumFormat state = case events state of+  LedgerEvent {body = InitializeFor _ format} : _ -> format+  _ -> ChecksumV1  data EventContext = EventContext !Text !Text !Text !(Maybe (Text, Text)) deriving stock (Eq, Show) 
src/ClickHouseMigrate/Internal/Preflight.hs view
@@ -1,10 +1,11 @@ -- | Force the complete definition/target envelope before contacting a server.-module ClickHouseMigrate.Internal.Preflight (preflight, validateRecoveryEvent, validateRecoveryCapacity) where+module ClickHouseMigrate.Internal.Preflight (preflight, preflightPending, validateRecoveryEvent, validateRecoveryCapacity) where -import ClickHouseMigrate.Internal.LedgerCodec (contextFor, definitions, encodeEvent, maxEventBytes, validateEvent)+import ClickHouseMigrate.Internal.LedgerCodec (contextFor, definitionsFor, encodeEvent, maxEventBytes, validateEvent) import ClickHouseMigrate.Internal.LedgerTypes qualified as L import ClickHouseMigrate.Internal.Reporting (stepReference, summaryDefinition)-import ClickHouseMigrate.Internal.SqlEnvelope (mentionsIdentifier, validateSql)+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile+import ClickHouseMigrate.Internal.SqlEnvelope (createsDatabase, mentionsIdentifier, validateReplicatedPolicy, validateSql) import ClickHouseMigrate.Internal.TargetTypes qualified as T import ClickHouseMigrate.Plan qualified as Plan import ClickHouseMigrate.Report qualified as R@@ -14,11 +15,11 @@ import Data.Word (Word64)  preflight :: T.TargetConfig -> L.AuditContext -> Plan.MigrationPlan -> Either R.RunError ()-preflight (T.TargetConfig target _ _ namespace _ _ _) audit plan = do+preflight config@(T.TargetConfig {T.configKey = target, T.configNamespace = namespace}) audit plan = do   let uuid = "00000000-0000-4000-8000-000000000000"       envelope body context = L.LedgerEvent 1 uuid (T.LedgerId uuid) uuid (Just uuid) "2026-10-16T00:00:00.999999999999Z" audit "0.1.0.0" context body       authored = [(migration, NE.toList (Plan.migrationSteps migration)) | migration <- NE.toList (Plan.inspectSchedule plan)]-      sources = zip (definitions plan) authored+      sources = zip (definitionsFor (Profile.checksumFormat (T.configTopology config)) plan) authored   if BS.length (encodeEvent (envelope (L.Initialize target) Nothing)) > 61440 then Left R.AuditTooLarge else pure ()   mapM_     ( \(definition, (_, steps)) -> do@@ -46,3 +47,21 @@ validateRecoveryCapacity current needed   | needed < 0 || toInteger current + needed > toInteger (maxBound :: Word64) = Left (R.LedgerFailed L.LedgerLimitExceeded)   | otherwise = Right ()++-- Requires an observed prefix. It runs before bootstrap or application writes;+-- applied migrations are excluded without pretending to know history offline.+preflightPending :: T.TargetConfig -> Plan.MigrationPlan -> Int -> Either R.RunError ()+preflightPending config plan applied = case T.configTopology config of+  T.DirectServer _ -> Right ()+  topology@(T.ReplicatedCluster _ _) -> mapM_ check (drop applied (zip (definitionsFor (Profile.checksumFormat topology) plan) (NE.toList (Plan.inspectSchedule plan))))+  where+    check (definition, migration) =+      mapM_+        ( \(index, step) -> do+            creates <- either (const (Left R.UnexpectedRuntimeFailure)) Right (createsDatabase (Plan.stepSql step))+            case stepReference definition index of+              Just reference | creates -> Left (R.DatabaseCreationOnReplicatedTarget reference)+              Just reference -> either (const (Left (R.InvalidRuntimePlan reference))) Right (validateReplicatedPolicy (Plan.stepSql step))+              _ -> Left R.UnexpectedRuntimeFailure+        )+        (zip [0 ..] (NE.toList (Plan.migrationSteps migration)))
src/ClickHouseMigrate/Internal/Projection.hs view
@@ -1,7 +1,7 @@ -- | Pure immutable history fold. No timestamp or merge order chooses a winner. module ClickHouseMigrate.Internal.Projection (project, appendProjection, comparePlan, unresolved, resolvedStep) where -import ClickHouseMigrate.Internal.LedgerCodec (contextFor, definitions, definitionsDigest, validateEvent)+import ClickHouseMigrate.Internal.LedgerCodec (contextFor, definitionsDigest, definitionsFor, validateEvent) import ClickHouseMigrate.Internal.LedgerTypes import ClickHouseMigrate.Plan (MigrationPlan) import ClickHouseMigrate.Prelude@@ -143,7 +143,7 @@  comparePlan :: MigrationPlan -> Projection -> Either LedgerError () comparePlan plan state = do-  let supplied = definitions plan+  let supplied = definitionsFor (projectionChecksumFormat state) plan       recorded =         [definition | RecordedMigration definition _ _ <- completed state]           ++ [definition | Just (ActiveMigration definition _ _ _) <- [active state]]
src/ClickHouseMigrate/Internal/Reporting.hs view
@@ -2,7 +2,7 @@ module ClickHouseMigrate.Internal.Reporting where  import ClickHouseMigrate.Internal.ExclusiveTypes (AccessDescription)-import ClickHouseMigrate.Internal.LedgerCodec (definitions)+import ClickHouseMigrate.Internal.LedgerCodec (definitionsFor) import ClickHouseMigrate.Internal.LedgerStore qualified as Store import ClickHouseMigrate.Internal.LedgerTypes qualified as L import ClickHouseMigrate.Internal.TargetTypes qualified as T@@ -99,9 +99,9 @@     Nothing     [R.StepSummary name checksum R.StepPending Nothing Nothing | L.StepDefinition name checksum <- L.steps definition] -inspectHistory :: Plan.MigrationPlan -> R.HistoryReport -> R.InspectionReport-inspectHistory plan history@(R.HistoryReport _ _ _ _ _ events recorded unresolved) =-  let supplied = definitions plan+inspectHistoryFor :: Plan.ChecksumFormat -> Plan.MigrationPlan -> R.HistoryReport -> R.InspectionReport+inspectHistoryFor format plan history@(R.HistoryReport _ _ _ _ _ events recorded unresolved) =+  let supplied = definitionsFor format plan       actual = [definition | R.MigrationSummary definition _ _ _ <- recorded]       expected = map summaryDefinition supplied       changes = [R.DefinitionChanged index (at index expected) (Just definition) | (index, definition) <- zip [0 ..] actual, at index expected /= Just definition]@@ -133,16 +133,16 @@ runPosition :: R.RunReport -> Maybe L.LedgerPosition runPosition report = case runHistory report of R.HistoryReport _ _ _ position _ _ _ _ -> position -updateRun :: Plan.MigrationPlan -> R.HistoryReport -> R.RunReport -> R.RunReport-updateRun plan history (R.RunReport identity target access acquired _ _ observations initialized) = let R.InspectionReport _ migrations _ = inspectHistory plan history in R.RunReport identity target access acquired history migrations observations initialized+updateRunFor :: Plan.ChecksumFormat -> Plan.MigrationPlan -> R.HistoryReport -> R.RunReport -> R.RunReport+updateRunFor format plan history (R.RunReport identity target access acquired _ _ observations initialized) = let R.InspectionReport _ migrations _ = inspectHistoryFor format plan history in R.RunReport identity target access acquired history migrations observations initialized  recordSql :: R.SqlObservation -> R.RunReport -> R.RunReport recordSql value report@(R.RunReport _ _ _ _ _ _ observations _) = report {R.observations = observations ++ [value]} -initialRun :: Text -> TargetKey -> AccessDescription -> Plan.MigrationPlan -> R.RunReport-initialRun run target access plan =+initialRunFor :: Plan.ChecksumFormat -> Text -> TargetKey -> AccessDescription -> Plan.MigrationPlan -> R.RunReport+initialRunFor format run target access plan =   let history = emptyHistory target (R.GuardedObservation access)-      R.InspectionReport _ migrations _ = inspectHistory plan history+      R.InspectionReport _ migrations _ = inspectHistoryFor format plan history    in R.RunReport run target access False history migrations [] Nothing  historyPosition :: R.HistoryReport -> Maybe L.LedgerPosition@@ -169,6 +169,7 @@  bodyReport :: L.EventBody -> R.HistoryBody bodyReport = \case+  L.InitializeFor target Plan.ChecksumV2 -> R.LedgerInitializedReplicated target   L.Initialize target -> R.LedgerInitialized target   L.StartMigration definition -> R.MigrationDefinition (summaryDefinition definition)   L.StartStep index query -> R.StepIntent index (T.QueryId query)@@ -206,3 +207,18 @@ at index values   | index < 0 = Nothing   | otherwise = case drop index values of value : _ -> Just value; _ -> Nothing++-- Legacy pure helpers keep their v1/recorded-history defaults. Production paths+-- pass the target format explicitly, including before ledger initialization.+inspectHistory :: Plan.MigrationPlan -> R.HistoryReport -> R.InspectionReport+inspectHistory plan history = inspectHistoryFor (historyChecksumFormat history) plan history++updateRun :: Plan.MigrationPlan -> R.HistoryReport -> R.RunReport -> R.RunReport+updateRun plan history = updateRunFor (historyChecksumFormat history) plan history++initialRun :: Text -> TargetKey -> AccessDescription -> Plan.MigrationPlan -> R.RunReport+initialRun = initialRunFor Plan.ChecksumV1++historyChecksumFormat :: R.HistoryReport -> Plan.ChecksumFormat+historyChecksumFormat (R.HistoryReport _ _ _ _ _ events _ _) =+  if any (\(R.HistoryEvent _ _ _ _ _ _ _ _ _ kind) -> case kind of R.LedgerInitializedReplicated _ -> True; _ -> False) events then Plan.ChecksumV2 else Plan.ChecksumV1
src/ClickHouseMigrate/Internal/ServerPolicy.hs view
@@ -12,57 +12,24 @@  import ClickHouseMigrate.Internal.Checksum qualified as Checksum import ClickHouseMigrate.Internal.Http (Http, probeConnection, submit, withHttpAdmissionChecked)+import ClickHouseMigrate.Internal.ServerProfile (TargetCheckError (..))+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile import ClickHouseMigrate.Internal.TargetTypes import ClickHouseMigrate.Internal.Transport import ClickHouseMigrate.Prelude-import ClickHouseMigrate.Target (serverId)-import Data.Aeson (FromJSON, Key, Object, Result (..), fromJSON)-import Data.Aeson.KeyMap qualified as KM import Data.IORef (atomicModifyIORef', newIORef)-import Data.Set qualified as Set import Data.Text qualified as Text import Data.Text.Encoding qualified as Encoding  -- Exact tested-patch policy. Keep coordinated with the checked-in candidate matrix. supportedVersions :: [Text]-supportedVersions = ["26.8.28.14"]--data TargetCheckError = ProbeFailure !TransportOutcome | TargetRejected !TransportError-  deriving stock (Eq, Show)+supportedVersions = ["26.8.28.14", "26.3.60.10001.altinitystable", "26.3.60.10002.altinitystable"]  probeTarget :: Http -> TargetConfig -> QueryId -> IO (Either TargetCheckError ServerIdentity)-probeTarget http config identity = validateOutcome config <$> submit http identity JsonRows identityQuery+probeTarget http config identity = validateOutcome config <$> submit http identity JsonRows (Profile.identityQuery (configTopology config))  validateOutcome :: TargetConfig -> TransportOutcome -> Either TargetCheckError ServerIdentity-validateOutcome (TargetConfig _ _ expected _ _ _ _) outcome = case outcome of-  AcknowledgedSuccess (Rows [row]) -> validate row-  AcknowledgedSuccess _ -> rejected UnexpectedResponse-  failure -> Left (ProbeFailure failure)-  where-    rejected code = Left (TargetRejected (TransportError code Nothing Nothing))-    validate row-      | Set.fromList (KM.keys row) /= Set.fromList ["server_id", "version", "host", "async_insert", "mutations_sync", "alter_sync"] = rejected UnexpectedResponse-      | otherwise = case (get "server_id" row, get "version" row, get "host" row, get "async_insert" row, get "mutations_sync" row, get "alter_sync" row) of-          (Just uuid, Just version, Just host, Just (async :: Int), Just (mutation :: Int), Just (alter :: Int)) -> case serverId uuid of-            Left _ -> rejected UnexpectedResponse-            Right actual-              | actual /= expected -> rejected ServerIdentityMismatch-              | version `notElem` supportedVersions -> rejected UnsupportedServer-              | async /= 0 || mutation /= 1 || alter /= 1 -> rejected CompletionPolicyMismatch-              | otherwise -> Right (ServerIdentity actual version host)-          _ -> rejected UnexpectedResponse--get :: (FromJSON a) => Key -> Object -> Maybe a-get key row = KM.lookup key row >>= \value -> case fromJSON value of Success result -> Just result; Error _ -> Nothing--identityQuery :: ByteString-identityQuery =-  """-  SELECT toString(serverUUID()) AS server_id, version() AS version, hostName() AS host,-         toUInt8(getSetting('async_insert')) AS async_insert,-         toUInt8(getSetting('mutations_sync')) AS mutations_sync,-         toUInt8(getSetting('alter_sync')) AS alter_sync-  """+validateOutcome config = Profile.validateOutcome supportedVersions (configTopology config)  -- The only production constructor for a checked transport. Its Http resources -- close at scope exit; a capability used after exit cannot submit again.@@ -75,7 +42,7 @@   let prefix = "chm-admission-" <> Checksum.digestHex (Checksum.digest (Encoding.encodeUtf8 rootId)) <> "-"       admit connection = do         n <- atomicModifyIORef' counter (\old -> (old + 1, old))-        outcome <- probeConnection config connection (QueryId (prefix <> Text.pack (show n))) identityQuery+        outcome <- probeConnection config connection (QueryId (prefix <> Text.pack (show n))) (Profile.identityQuery (configTopology config))         pure $ case validateOutcome config outcome of           Right _ -> Right ()           Left (TargetRejected failure) -> Left failure@@ -87,4 +54,10 @@     checked <- probeTarget http config probeId     case checked of       Left failure -> pure (Left failure)-      Right identity -> Right <$> action (Transport (submit http) identity)+      Right identity -> do+        membership <- case Profile.membershipQuery (configTopology config) of+          Nothing -> pure (Right ())+          Just sql -> Profile.validateMembership (configTopology config) <$> submit http (QueryId (rootId <> "-members")) JsonRows sql+        case membership of+          Left failure -> pure (Left failure)+          Right () -> Right <$> action (Transport (submit http) identity)
+ src/ClickHouseMigrate/Internal/ServerProfile.hs view
@@ -0,0 +1,202 @@+{-# LANGUAGE MultilineStrings #-}++-- | Server-dependent bytes and expectations. Direct policy is frozen by unit pins.+module ClickHouseMigrate.Internal.ServerProfile where++import ClickHouseMigrate.Internal.SqlEnvelope (replicatedEngineSuffix)+import ClickHouseMigrate.Internal.TargetTypes+import ClickHouseMigrate.Internal.Transport+import ClickHouseMigrate.Internal.Types (ChecksumFormat (..))+import ClickHouseMigrate.Prelude+import ClickHouseMigrate.Target (ledgerNamespaceText, serverId)+import Data.Aeson (FromJSON, Key, Object, Result (..), fromJSON)+import Data.Aeson.Key qualified as DataKey+import Data.Aeson.KeyMap qualified as KM+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++data TargetCheckError = ProbeFailure !TransportOutcome | TargetRejected !TransportError+  deriving stock (Eq, Show)++validateOutcome :: [Text] -> TargetTopology -> TransportOutcome -> Either TargetCheckError ServerIdentity+validateOutcome supportedVersions (DirectServer expected) outcome = case outcome of+  AcknowledgedSuccess (Rows [row]) -> validate row+  AcknowledgedSuccess _ -> rejected UnexpectedResponse+  failure -> Left (ProbeFailure failure)+  where+    rejected code = Left (TargetRejected (TransportError code Nothing Nothing))+    validate row+      | Set.fromList (KM.keys row) /= Set.fromList ["server_id", "version", "host", "async_insert", "mutations_sync", "alter_sync"] = rejected UnexpectedResponse+      | otherwise = case (get "server_id" row, get "version" row, get "host" row, get "async_insert" row, get "mutations_sync" row, get "alter_sync" row) of+          (Just uuid, Just version, Just host, Just (async :: Int), Just (mutation :: Int), Just (alter :: Int)) -> case serverId uuid of+            Left _ -> rejected UnexpectedResponse+            Right actual+              | actual /= expected -> rejected ServerIdentityMismatch+              | version `notElem` supportedVersions -> rejected UnsupportedServer+              | async /= 0 || mutation /= 1 || alter /= 1 -> rejected CompletionPolicyMismatch+              | otherwise -> Right (ServerIdentity actual version host)+          _ -> rejected UnexpectedResponse+validateOutcome versions topology@(ReplicatedCluster (ClusterName cluster) replicas) outcome = case outcome of+  AcknowledgedSuccess (Rows [row]) -> do+    actual <- maybe (rejected UnexpectedResponse) (either (const (rejected UnexpectedResponse)) Right . serverId) (get "server_id" row)+    version <- maybe (rejected UnexpectedResponse) Right (get "version" row)+    host <- maybe (rejected UnexpectedResponse) Right (get "host" row)+    macro <- maybe (rejected UnexpectedResponse) Right (get "cluster" row)+    unless (actual `elem` replicas) (rejected ServerIdentityMismatch)+    unless (version `elem` versions) (rejected UnsupportedServer)+    unless (macro == cluster) (rejected ReplicaSetMismatch)+    unless (get "storage_ok" row == Just (1 :: Int)) (rejected CompletionPolicyMismatch)+    unless (all (\(key, value) -> get key row == Just value) (replicatedEffective topology)) (rejected CompletionPolicyMismatch)+    Right (ServerIdentity actual version host)+  AcknowledgedSuccess _ -> rejected UnexpectedResponse+  failure -> Left (ProbeFailure failure)+  where+    rejected code = Left (TargetRejected (TransportError code Nothing Nothing))++replicatedEffective :: TargetTopology -> [(Key, Text)]+replicatedEffective (DirectServer _) = []+replicatedEffective (ReplicatedCluster _ replicas) =+  [("async_insert", "0"), ("mutations_sync", "2"), ("alter_sync", "2"), ("insert_quorum", Text.pack (show (NE.length replicas))), ("insert_quorum_parallel", "0"), ("distributed_ddl_output_mode", "none"), ("select_sequential_consistency", "1"), ("fsync_metadata", "1")]++get :: (FromJSON a) => Key -> Object -> Maybe a+get key row = KM.lookup key row >>= \value -> case fromJSON value of Success result -> Just result; Error _ -> Nothing++identityQuery :: TargetTopology -> ByteString+identityQuery (DirectServer _) =+  """+  SELECT toString(serverUUID()) AS server_id, version() AS version, hostName() AS host,+         toUInt8(getSetting('async_insert')) AS async_insert,+         toUInt8(getSetting('mutations_sync')) AS mutations_sync,+         toUInt8(getSetting('alter_sync')) AS alter_sync+  """+identityQuery topology@(ReplicatedCluster _ _) =+  "SELECT toString(serverUUID()) AS server_id, version() AS version, hostName() AS host, getMacro('cluster') AS cluster, "+    <> Encoding.encodeUtf8 (Text.intercalate ", " [(if key == "distributed_ddl_output_mode" then "toString(getSetting('" <> key <> "'))" else "toString(toUInt64(getSetting('" <> key <> "')))") <> " AS " <> key | (key, _) <- map (\(key, value) -> (DataKey.toText key, value)) (replicatedEffective topology)])+    <> ", toUInt8((SELECT count() FROM system.storage_policies WHERE policy_name='default' AND length(disks)>0)>0 AND (SELECT count() FROM system.disks WHERE name IN (SELECT arrayJoin(disks) FROM system.storage_policies WHERE policy_name='default') AND (type!='Local' OR is_remote OR is_read_only OR is_broken))=0) AS storage_ok"++requestSettings :: TargetTopology -> HttpLimits -> Text -> RequestKind -> [(ByteString, Maybe ByteString)]+requestSettings (DirectServer _) _ identity kind =+  [ ("query_id", Just (Encoding.encodeUtf8 identity)),+    ("async_insert", Just "0"),+    ("mutations_sync", Just "1"),+    ("alter_sync", Just "1"),+    ("fsync_metadata", Just "1"),+    ("wait_end_of_query", Just "1"),+    ("default_format", Just "JSONEachRow")+  ]+    ++ if kind /= Command+      then+        [ ("max_rows_to_read", Just "0"),+          ("max_bytes_to_read", Just "0"),+          ("max_result_rows", Just "0"),+          ("max_result_bytes", Just "0"),+          ("read_overflow_mode", Just "throw"),+          ("result_overflow_mode", Just "throw"),+          ("timeout_overflow_mode", Just "throw"),+          ("sort_overflow_mode", Just "throw")+        ]+      else []+requestSettings topology@(ReplicatedCluster _ _) limits@(HttpLimits micros _) identity kind =+  [("query_id", Just (Encoding.encodeUtf8 identity))]+    ++ [(Encoding.encodeUtf8 (DataKey.toText key), Just (Encoding.encodeUtf8 value)) | (key, value) <- replicatedEffective topology]+    ++ [("distributed_ddl_task_timeout", Just seconds), ("replication_wait_for_inactive_replica_timeout", Just seconds), ("insert_quorum_timeout", Just millis)]+    ++ [("wait_end_of_query", Just "1"), ("default_format", Just "JSONEachRow")]+    ++ drop 7 (requestSettings (DirectServer (NE.head replicas)) limits identity kind)+  where+    ReplicatedCluster _ replicas = topology+    -- DDL and ALTER waits may occur in sequence. Share the budget rather than+    -- allowing each wait to consume the whole HTTP deadline.+    waitSeconds = max 1 ((micros `div` 1000000 - 2) `div` 3)+    seconds = Encoding.encodeUtf8 (Text.pack (show waitSeconds))+    millis = Encoding.encodeUtf8 (Text.pack (show (waitSeconds * 1000)))++-- A scoped membership read needs no REMOTE privilege. Individual sockets still+-- require expected UUIDs; counts do not attest UUIDs on unvisited replicas.+membershipQuery :: TargetTopology -> Maybe ByteString+membershipQuery (DirectServer _) = Nothing+membershipQuery (ReplicatedCluster (ClusterName cluster) _) = Just (Encoding.encodeUtf8 ("SELECT count() AS replicas, uniqExact(shard_num) AS shards FROM system.clusters WHERE cluster='" <> cluster <> "'"))++validateMembership :: TargetTopology -> TransportOutcome -> Either TargetCheckError ()+validateMembership (DirectServer _) _ = Right ()+validateMembership (ReplicatedCluster _ replicas) outcome = case outcome of+  AcknowledgedSuccess (Rows [row]) | get "replicas" row == Just (NE.length replicas), get "shards" row == Just (1 :: Int) -> Right ()+  AcknowledgedSuccess _ -> Left (TargetRejected (TransportError ReplicaSetMismatch Nothing Nothing))+  failure -> Left (ProbeFailure failure)++completionPolicyV1 :: ByteString+completionPolicyV1 = "clickhouse-migrate/local-sync-v1;async_insert=0;mutations_sync=1;alter_sync=1"++completionPolicy :: TargetTopology -> ByteString+completionPolicy (DirectServer _) = completionPolicyV1+completionPolicy (ReplicatedCluster _ _) = completionPolicyV2++checksumFormat :: TargetTopology -> ChecksumFormat+checksumFormat (DirectServer _) = ChecksumV1+checksumFormat (ReplicatedCluster _ _) = ChecksumV2++databaseEngine :: TargetTopology -> Text+databaseEngine (DirectServer _) = "Atomic"+databaseEngine (ReplicatedCluster _ _) = "Replicated"++quoteIdentifier :: Text -> Text+quoteIdentifier value = "`" <> Text.replace "`" "``" value <> "`"++createDatabase :: TargetTopology -> LedgerNamespace -> Maybe ByteString+createDatabase (DirectServer _) namespace = Just (Encoding.encodeUtf8 ("CREATE DATABASE IF NOT EXISTS " <> quoteIdentifier (ledgerNamespaceText namespace) <> " ENGINE=Atomic"))+createDatabase (ReplicatedCluster _ _) _ = Nothing++createTable :: TargetTopology -> LedgerNamespace -> ByteString+createTable topology namespace = Encoding.encodeUtf8 ("CREATE TABLE IF NOT EXISTS " <> (quoteIdentifier (ledgerNamespaceText namespace) <> ".`events`") <> " " <> columnsSql <> " ENGINE=" <> tableEngine topology <> " ORDER BY sequence SETTINGS fsync_after_insert=1, fsync_part_directory=1, storage_policy='default'")++columnsSql :: Text+columnsSql = "(sequence UInt64, event_id UUID, ledger_id UUID, event String)"++expectedCreate :: TargetTopology -> LedgerNamespace -> Text+expectedCreate topology namespace = "CREATE TABLE " <> ledgerNamespaceText namespace <> ".events (`sequence` UInt64, `event_id` UUID, `ledger_id` UUID, `event` String) ENGINE = " <> expectedEngine topology++expectedEngine :: TargetTopology -> Text+expectedEngine topology = tableEngine topology <> engineTail++engineTail :: Text+engineTail = " ORDER BY sequence SETTINGS fsync_after_insert = 1, fsync_part_directory = 1, storage_policy = 'default', index_granularity = 8192"++tableEngine :: TargetTopology -> Text+tableEngine (DirectServer _) = "MergeTree"+tableEngine (ReplicatedCluster _ _) = "ReplicatedMergeTree"++-- | Entire direct physical shape, including accepted server identifier quoting.+validTableShape :: TargetTopology -> LedgerNamespace -> [Text] -> Bool+validTableShape topology@(DirectServer _) namespace actual =+  let base = ["events", "MergeTree", expectedEngine topology, "sequence", "sequence", "", ""]+      create = expectedCreate topology namespace+      quoted = Text.replace ("CREATE TABLE " <> ledgerNamespaceText namespace <> ".") ("CREATE TABLE " <> quoteIdentifier (ledgerNamespaceText namespace) <> ".") create+   in actual == base ++ [create] || actual == base ++ [quoted]+validTableShape (ReplicatedCluster _ _) namespace actual = case actual of+  ["events", "ReplicatedMergeTree", engine, "sequence", "sequence", "", "", create] ->+    replicatedEngineSuffix engine == Just engineTail+      && let prefix = "CREATE TABLE " <> ledgerNamespaceText namespace <> ".events (`sequence` UInt64, `event_id` UUID, `ledger_id` UUID, `event` String) ENGINE = "+             quoted = Text.replace ("CREATE TABLE " <> ledgerNamespaceText namespace <> ".") ("CREATE TABLE " <> quoteIdentifier (ledgerNamespaceText namespace) <> ".") prefix+          in create == prefix <> engine || create == quoted <> engine+  _ -> False++replicaHealthQuery :: TargetTopology -> LedgerNamespace -> Maybe ByteString+replicaHealthQuery (DirectServer _) _ = Nothing+replicaHealthQuery (ReplicatedCluster _ _) (LedgerNamespace namespace) = Just (Encoding.encodeUtf8 ("SELECT is_readonly, total_replicas FROM system.replicas WHERE database='" <> namespace <> "' AND table='events'"))++validReplicaHealth :: TargetTopology -> [Object] -> Bool+validReplicaHealth (DirectServer _) _ = True+validReplicaHealth (ReplicatedCluster _ replicas) [row] = get "is_readonly" row == Just (0 :: Int) && get "total_replicas" row == Just (NE.length replicas)+validReplicaHealth _ _ = False++requiresLedgerDatabase :: TargetTopology -> Bool+requiresLedgerDatabase (DirectServer _) = False+requiresLedgerDatabase (ReplicatedCluster _ _) = True++completionPolicyV2 :: ByteString+completionPolicyV2 = "clickhouse-migrate/replicated-sync-v1;async_insert=0;mutations_sync=2;alter_sync=2;insert_quorum=all;insert_quorum_parallel=0;distributed_ddl_output_mode=none;select_sequential_consistency=1"++policyForFormat :: ChecksumFormat -> ByteString+policyForFormat ChecksumV1 = completionPolicyV1+policyForFormat ChecksumV2 = completionPolicyV2
src/ClickHouseMigrate/Internal/SqlEnvelope.hs view
@@ -1,5 +1,5 @@ -- | Conservative lexical envelope; never a general SQL safety proof.-module ClickHouseMigrate.Internal.SqlEnvelope (validateSql, mentionsIdentifier) where+module ClickHouseMigrate.Internal.SqlEnvelope (validateSql, mentionsIdentifier, createsDatabase, validateReplicatedPolicy, replicatedEngines, replicatedEngineSuffix) where  import ClickHouseMigrate.Error import ClickHouseMigrate.Prelude@@ -116,7 +116,7 @@   case find protected tokens of     Just (Token n _) -> failure ProtectedSetting (Just n)     Nothing -> pure ()-  case find forbidden (localViewHeader tokens) of+  case find forbidden (withoutReplicatedEngines (localViewHeader tokens)) of     Just (Token n _) -> failure UnsupportedSql (Just n)     Nothing -> pure ()   if any onCluster (pairs tokens) || quotedSettings tokens then failure UnsupportedSql Nothing else pure ()@@ -146,6 +146,22 @@ protectedNames :: [Text] protectedNames = ["ASYNC_INSERT", "MUTATIONS_SYNC", "ALTER_SYNC"] +-- Target-independent binding keeps all previously valid v1 definitions valid.+-- Additional replicated policy identifiers are refused only for pending work on+-- that topology, before any ledger/bootstrap or application write.+validateReplicatedPolicy :: ByteString -> Either DefinitionError ()+validateReplicatedPolicy sql = do+  text <- either (const (failure InvalidUtf8 Nothing)) Right (Encoding.decodeUtf8' sql)+  tokens <- scan 0 (Text.unpack text)+  case find protected tokens of+    Just (Token offset _) -> failure ProtectedSetting (Just offset)+    Nothing -> Right ()+  where+    names = ["INSERT_QUORUM", "INSERT_QUORUM_PARALLEL", "INSERT_QUORUM_TIMEOUT", "DISTRIBUTED_DDL_OUTPUT_MODE", "DISTRIBUTED_DDL_TASK_TIMEOUT", "REPLICATION_WAIT_FOR_INACTIVE_REPLICA_TIMEOUT", "SELECT_SEQUENTIAL_CONSISTENCY"]+    protected (Token _ (Word name)) = name `elem` names+    protected (Token _ (Identifier name)) = name `elem` names+    protected _ = False+ forbiddenWords :: [Text] forbiddenWords =   [ "FORMAT",@@ -210,3 +226,44 @@       matches (Token _ (Identifier value)) = wanted `elem` Text.splitOn "." value       matches _ = False   pure (any matches tokens)++replicatedEngines :: [Text]+replicatedEngines = ["REPLICATEDMERGETREE", "REPLICATEDREPLACINGMERGETREE", "REPLICATEDAGGREGATINGMERGETREE", "REPLICATEDSUMMINGMERGETREE", "REPLICATEDCOLLAPSINGMERGETREE", "REPLICATEDVERSIONEDCOLLAPSINGMERGETREE"]++-- Remove only an engine word in the table/MV engine position. Quoted engine+-- spellings, explicit string arguments and every other occurrence stay forbidden.+withoutReplicatedEngines :: [Token] -> [Token]+withoutReplicatedEngines tokens+  | tableCreate tokens = walk tokens+  | otherwise = tokens+  where+    tableCreate (a : b : _) | keyword "CREATE" a && keyword "TABLE" b = True+    tableCreate (a : b : c : _) = keyword "CREATE" a && keyword "MATERIALIZED" b && keyword "VIEW" c+    tableCreate _ = False+    walk (engine : equal : Token _ (Word word) : rest)+      | keyword "ENGINE" engine && equalIs equal && word `elem` replicatedEngines && allowed rest = engine : equal : walk rest+    walk (engine : Token _ (Word word) : rest)+      | keyword "ENGINE" engine && word `elem` replicatedEngines && allowed rest = engine : walk rest+    walk (token : rest) = token : walk rest+    walk [] = []+    equalIs (Token _ (Symbol '=')) = True+    equalIs _ = False+    allowed (Token _ (Symbol '(') : Token _ Literal : _) = False+    allowed _ = True++createsDatabase :: ByteString -> Either DefinitionError Bool+createsDatabase sql = do+  text <- either (const (failure InvalidUtf8 Nothing)) Right (Encoding.decodeUtf8' sql)+  tokens <- scan 0 (Text.unpack text)+  pure (case tokens of a : b : _ -> keyword "CREATE" a && keyword "DATABASE" b; _ -> False)++-- Validate the server's canonical argument prefix, preserving the suffix bytes.+replicatedEngineSuffix :: Text -> Maybe Text+replicatedEngineSuffix text = case scan 0 (Text.unpack text) of+  Right (Token _ (Word engine) : Token _ (Symbol '(') : Token first Literal : Token _ (Symbol ',') : Token second Literal : Token end (Symbol ')') : _)+    | engine == "REPLICATEDMERGETREE",+      engine `elem` replicatedEngines,+      Text.isPrefixOf "'" (Text.drop first text),+      Text.isPrefixOf "'" (Text.drop second text) ->+        Just (Text.drop (end + 1) text)+  _ -> Nothing
src/ClickHouseMigrate/Internal/TargetTypes.hs view
@@ -21,6 +21,18 @@  data HttpLimits = HttpLimits !Int !Int deriving stock (Eq, Show) -data TargetConfig = TargetConfig !TargetKey !DirectEndpoint !ServerId !LedgerNamespace !LedgerBinding !Credentials !HttpLimits+data ClusterName = ClusterName !Text deriving stock (Eq, Ord, Show)++data TargetTopology = DirectServer !ServerId | ReplicatedCluster !ClusterName !(NonEmpty ServerId) deriving stock (Eq, Show)++data TargetConfig = TargetConfig+  { configKey :: !TargetKey,+    configEndpoint :: !DirectEndpoint,+    configTopology :: !TargetTopology,+    configNamespace :: !LedgerNamespace,+    configBinding :: !LedgerBinding,+    configCredentials :: !Credentials,+    configLimits :: !HttpLimits+  }  data ServerIdentity = ServerIdentity !ServerId !Text !Text deriving stock (Eq, Show)
src/ClickHouseMigrate/Internal/Transport.hs view
@@ -9,7 +9,7 @@  data ResponsePayload = CommandAccepted | Rows ![Object] deriving stock (Eq, Show) -data TransportErrorCode = AdministrativeAccessLost | InvalidRequest | ClientScopeFailed | ConnectionFailed | DeadlineExpired | ResponseInterrupted | ResponseTooLarge | UnexpectedResponse | ServerRejected | RedirectRejected | ServerIdentityMismatch | UnsupportedServer | CompletionPolicyMismatch | CleanupFailed | CleanupTimedOut+data TransportErrorCode = AdministrativeAccessLost | InvalidRequest | ClientScopeFailed | ConnectionFailed | DeadlineExpired | ResponseInterrupted | ResponseTooLarge | UnexpectedResponse | ServerRejected | RedirectRejected | ServerIdentityMismatch | UnsupportedServer | CompletionPolicyMismatch | ReplicaSetMismatch | CleanupFailed | CleanupTimedOut   deriving stock (Eq, Ord, Show)  -- Only safe structured diagnostics; raw server/HTTP exceptions are never rendered.
src/ClickHouseMigrate/Internal/Types.hs view
@@ -20,14 +20,14 @@  data MigrationPlan = MigrationPlan !(NonEmpty ComponentView) !(NonEmpty MigrationView) deriving stock (Eq) -data ChecksumFormat = ChecksumV1 deriving stock (Eq, Ord, Show)+data ChecksumFormat = ChecksumV1 | ChecksumV2 deriving stock (Eq, Ord, Show) -data StepChecksum = StepChecksum !ByteString deriving stock (Eq, Ord)+data StepChecksum = StepChecksum !ByteString | StepChecksumV2 !ByteString deriving stock (Eq, Ord) -data MigrationChecksum = MigrationChecksum !ByteString deriving stock (Eq, Ord)+data MigrationChecksum = MigrationChecksum !ByteString | MigrationChecksumV2 !ByteString deriving stock (Eq, Ord)  data ComponentView = ComponentView !ComponentName !(Set ComponentName) !(NonEmpty MigrationView) deriving stock (Eq) -data MigrationView = MigrationView !MigrationId !(Maybe Text) !(NonEmpty StepView) !MigrationChecksum deriving stock (Eq)+data MigrationView = MigrationView !MigrationId !(Maybe Text) !(NonEmpty StepView) !MigrationChecksum !MigrationChecksum deriving stock (Eq) -data StepView = StepView !StepName !ByteString !StepChecksum deriving stock (Eq)+data StepView = StepView !StepName !ByteString !StepChecksum !StepChecksum deriving stock (Eq)
src/ClickHouseMigrate/Ledger.hs view
@@ -57,12 +57,12 @@  -- | Read-only exact-event reconciliation. NotObserved never authorizes a write. reconcileAppend :: TargetConfig -> ExclusiveAccess -> AppendReceipt -> IO (LedgerCommandResult (Reconciliation, ExpectedLedgerState))-reconcileAppend config@(TargetConfig target _ _ _ _ _ _) provider receipt = command config provider $ \guard transport -> do+reconcileAppend config@(TargetConfig {configKey = target}) provider receipt = command config provider $ \guard transport -> do   (classification, observation) <- Store.reconcile guard transport config receipt   pure (classification, case observation of Store.History state -> ExpectPosition (position state); _ -> ExpectAbsent target)  command :: TargetConfig -> ExclusiveAccess -> (Guard -> Transport -> IO a) -> IO (LedgerCommandResult a)-command config@(TargetConfig target _ _ _ _ _ _) provider@(ExclusiveAccess description _ _ _ _) action = do+command config@(TargetConfig {configKey = target}) provider@(ExclusiveAccess description _ _ _ _) action = do   observed <- try @SomeException $ do     probe <- freshUuid >>= either (const (fail "Invalid diagnostic identity")) pure . queryId . ("chm-" <>)     withExclusive provider target $ \guard ->
src/ClickHouseMigrate/Plan.hs view
@@ -16,13 +16,15 @@     migrationDescription,     migrationSteps,     migrationChecksum,+    migrationChecksumFor,     StepView,     stepViewName,     stepSql,     stepChecksum,+    stepChecksumFor,     StepChecksum,     MigrationChecksum,-    ChecksumFormat (ChecksumV1),+    ChecksumFormat (ChecksumV1, ChecksumV2),     checksumFormatName,     parseChecksumFormat,     stepChecksumFormat,@@ -133,11 +135,12 @@ bindComponent (MigrationComponent component deps migrations) = ComponentView component deps (fmap bindMigration migrations)   where     bindMigration (Migration name description steps) =-      let encodings = fmap (encodeStep name) steps-          boundSteps = NE.zipWith (\(SqlStep step sql) encoded -> StepView step sql (StepChecksum (Checksum.digest encoded))) steps encodings-          checksum = MigrationChecksum (Checksum.digest (Checksum.encodeMigrationV1 (componentNameText component) (migrationNameText name) (NE.toList encodings)))-       in MigrationView (MigrationId component name) description boundSteps checksum-    encodeStep migration (SqlStep step sql) = Checksum.encodeStepV1 (componentNameText component) (migrationNameText migration) (stepNameText step) sql+      let encode encoder (SqlStep step sql) = encoder (componentNameText component) (migrationNameText name) (stepNameText step) sql+          encodingsV1 = fmap (encode Checksum.encodeStepV1) steps+          encodingsV2 = fmap (encode Checksum.encodeStepV2) steps+          boundSteps = NE.zipWith (\(SqlStep step sql) (v1, v2) -> StepView step sql (StepChecksum (Checksum.digest v1)) (StepChecksumV2 (Checksum.digest v2))) steps (NE.zip encodingsV1 encodingsV2)+          checksum encoder encodings = Checksum.digest (encoder (componentNameText component) (migrationNameText name) (NE.toList encodings))+       in MigrationView (MigrationId component name) description boundSteps (MigrationChecksum (checksum Checksum.encodeMigrationV1 encodingsV1)) (MigrationChecksumV2 (checksum Checksum.encodeMigrationV2 encodingsV2))  inspectPlan :: MigrationPlan -> NonEmpty ComponentView inspectPlan (MigrationPlan components _) = components@@ -152,44 +155,58 @@ componentMigrations (ComponentView _ _ migrations) = migrations  migrationId :: MigrationView -> MigrationId-migrationId (MigrationView identity _ _ _) = identity+migrationId (MigrationView identity _ _ _ _) = identity  migrationDescription :: MigrationView -> Maybe Text-migrationDescription (MigrationView _ description _ _) = description+migrationDescription (MigrationView _ description _ _ _) = description  migrationSteps :: MigrationView -> NonEmpty StepView-migrationSteps (MigrationView _ _ steps _) = steps+migrationSteps (MigrationView _ _ steps _ _) = steps  migrationChecksum :: MigrationView -> MigrationChecksum-migrationChecksum (MigrationView _ _ _ checksum) = checksum+migrationChecksum (MigrationView _ _ _ checksum _) = checksum  stepViewName :: StepView -> StepName-stepViewName (StepView name _ _) = name+stepViewName (StepView name _ _ _) = name  stepSql :: StepView -> ByteString-stepSql (StepView _ sql _) = sql+stepSql (StepView _ sql _ _) = sql  stepChecksum :: StepView -> StepChecksum-stepChecksum (StepView _ _ checksum) = checksum+stepChecksum (StepView _ _ checksum _) = checksum  checksumFormatName :: ChecksumFormat -> Text checksumFormatName ChecksumV1 = "sha256-v1"+checksumFormatName ChecksumV2 = "sha256-v2"  parseChecksumFormat :: Text -> Either DefinitionError ChecksumFormat parseChecksumFormat "sha256-v1" = Right ChecksumV1+parseChecksumFormat "sha256-v2" = Right ChecksumV2 parseChecksumFormat _ = Left (DefinitionError UnknownChecksumFormat ["checksum", "format"] Nothing)  stepChecksumFormat :: StepChecksum -> ChecksumFormat-stepChecksumFormat _ = ChecksumV1+stepChecksumFormat (StepChecksum _) = ChecksumV1+stepChecksumFormat (StepChecksumV2 _) = ChecksumV2  migrationChecksumFormat :: MigrationChecksum -> ChecksumFormat-migrationChecksumFormat _ = ChecksumV1+migrationChecksumFormat (MigrationChecksum _) = ChecksumV1+migrationChecksumFormat (MigrationChecksumV2 _) = ChecksumV2  stepChecksumHex :: StepChecksum -> Text stepChecksumHex (StepChecksum bytes) = Checksum.digestHex bytes+stepChecksumHex (StepChecksumV2 bytes) = Checksum.digestHex bytes  migrationChecksumHex :: MigrationChecksum -> Text migrationChecksumHex (MigrationChecksum bytes) = Checksum.digestHex bytes+migrationChecksumHex (MigrationChecksumV2 bytes) = Checksum.digestHex bytes  completionPolicy :: ByteString completionPolicy = Checksum.completionPolicyV1++migrationChecksumFor :: ChecksumFormat -> MigrationView -> MigrationChecksum+migrationChecksumFor ChecksumV1 = migrationChecksum+migrationChecksumFor ChecksumV2 = \(MigrationView _ _ _ _ checksum) -> checksum++stepChecksumFor :: ChecksumFormat -> StepView -> StepChecksum+stepChecksumFor ChecksumV1 = stepChecksum+stepChecksumFor ChecksumV2 = \(StepView _ _ _ checksum) -> checksum
src/ClickHouseMigrate/Repair.hs view
@@ -75,7 +75,7 @@   FinalizeRequest position attempt evidence -> (position, attempt, evidence)  repair :: MutationContext -> Plan.MigrationPlan -> RepairRequest -> IO (Either R.RunFailure R.RunReport)-repair context@(MutationContext (T.TargetConfig target _ _ _ binding _ _) _ _ _) plan request =+repair context@(MutationContext (T.TargetConfig {T.configKey = target, T.configBinding = binding}) _ _ _) plan request =   let (expected@(L.LedgerPosition ledger _ _), requested, RecoveryEvidence audit quiet receipts) = requestParts request       prerequisite = case binding of         T.FirstUse -> Left R.ExistingBindingRequired@@ -94,7 +94,7 @@         let queries = Set.toAscList (Set.fromList ([T.QueryId query | event <- L.events projection, L.StartStep _ query <- [L.body event]] ++ [query | L.AppendReceipt _ _ _ _ _ query <- receipts]))         active <- activeQueries runtime queries         unless (null active) (abort (R.PriorQueriesActive active))-        migration <- case drop (L.ordinal definition) (orderedMigrations plan) of+        migration <- case drop (L.ordinal definition) (orderedMigrations (L.projectionChecksumFormat projection) plan) of           (actual, migration) : _ | actual == definition -> pure migration           _ -> abort (R.LedgerFailed L.DefinitionMismatch)         (action, selection, query) <- case request of
src/ClickHouseMigrate/Report.hs view
@@ -72,6 +72,7 @@  data HistoryBody   = LedgerInitialized !TargetKey+  | LedgerInitializedReplicated !TargetKey   | MigrationDefinition !DefinitionSummary   | StepIntent !Int !QueryId   | StepCompletion !Int !QueryId@@ -155,6 +156,7 @@ data RunError   = InvalidRuntimePlan !StepReference   | LedgerNamespaceInSql !StepReference+  | DatabaseCreationOnReplicatedTarget !StepReference   | DefinitionTooLarge !DefinitionSummary   | AccessFailed !AccessError   | TargetFailed !TransportError@@ -191,6 +193,7 @@  historyKind :: HistoryBody -> Text historyKind = \case+  LedgerInitializedReplicated _ -> "initialized"   LedgerInitialized _ -> "initialized"   MigrationDefinition _ -> "migration-started"   StepIntent _ _ -> "step-started"
src/ClickHouseMigrate/Runner.hs view
@@ -33,4 +33,4 @@ up context (UpOptions audit authorization) plan = withRuntime context audit authorization Nothing plan $ \runtime -> do   projection <- runtimeProjection runtime   when (unresolved projection) (abort (LedgerFailed L.UnresolvedHistory))-  mapM_ (uncurry (executeMigration runtime)) (drop (length (L.completed projection)) (orderedMigrations plan))+  mapM_ (uncurry (executeMigration runtime)) (drop (length (L.completed projection)) (orderedMigrations (L.projectionChecksumFormat projection) plan))
src/ClickHouseMigrate/Target.hs view
@@ -27,6 +27,12 @@     defaultHttpLimits,     TargetConfig,     targetConfig,+    TargetTopology (DirectServer, ReplicatedCluster),+    ClusterName,+    clusterName,+    clusterNameText,+    replicatedTargetConfig,+    configuredTopology,     configuredTargetKey,     configuredLedgerBinding,     TargetError (..),@@ -41,11 +47,13 @@ import ClickHouseMigrate.Internal.TargetTypes import ClickHouseMigrate.Prelude import Data.Char (isAsciiLower, isAsciiUpper)+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 import Text.Read (readMaybe) -data TargetError = InvalidTargetKey | InvalidEndpoint | InvalidServerId | InvalidQueryId | InvalidLedgerId | InvalidLedgerNamespace | InvalidCredentials | InvalidHttpLimits+data TargetError = InvalidTargetKey | InvalidEndpoint | InvalidServerId | InvalidQueryId | InvalidLedgerId | InvalidLedgerNamespace | InvalidCredentials | InvalidHttpLimits | InvalidClusterName | DuplicateReplica   deriving stock (Eq, Ord, Show)  targetKey :: Text -> Either TargetError TargetKey@@ -103,7 +111,7 @@ defaultHttpLimits = HttpLimits 30000000 1048576  targetConfig :: TargetKey -> DirectEndpoint -> ServerId -> LedgerNamespace -> LedgerBinding -> Credentials -> HttpLimits -> TargetConfig-targetConfig = TargetConfig+targetConfig key endpoint identity = TargetConfig key endpoint (DirectServer identity)  targetKeyText :: TargetKey -> Text targetKeyText (TargetKey value) = value@@ -151,8 +159,28 @@  -- | Read-only nonsecret configuration identity for application adapters. configuredTargetKey :: TargetConfig -> TargetKey-configuredTargetKey (TargetConfig key _ _ _ _ _ _) = key+configuredTargetKey = configKey  -- | Binding mode is explicit; this accessor grants no initialization authority. configuredLedgerBinding :: TargetConfig -> LedgerBinding-configuredLedgerBinding (TargetConfig _ _ _ _ binding _ _) = binding+configuredLedgerBinding = configBinding++-- | The admitted server topology, without credentials.+configuredTopology :: TargetConfig -> TargetTopology+configuredTopology = configTopology++clusterName :: Text -> Either TargetError ClusterName+clusterName value+  | valid 128 (\c -> lowerDigit c || c `elem` ("_-" :: String)) value && maybe False (\(c, _) -> isAsciiLower c || c == '_') (Text.uncons value) = Right (ClusterName value)+  | otherwise = Left InvalidClusterName++clusterNameText :: ClusterName -> Text+clusterNameText (ClusterName value) = value++-- | Every expected replica participates in acknowledged writes. The deadline must+-- leave room for bounded server waits and a complete HTTP error response.+replicatedTargetConfig :: TargetKey -> DirectEndpoint -> ClusterName -> NonEmpty ServerId -> LedgerNamespace -> LedgerBinding -> Credentials -> HttpLimits -> Either TargetError TargetConfig+replicatedTargetConfig key endpoint cluster replicas namespace binding credentials limits@(HttpLimits micros _)+  | Set.size (Set.fromList (NE.toList replicas)) /= NE.length replicas = Left DuplicateReplica+  | micros < 5000000 = Left InvalidHttpLimits+  | otherwise = Right (TargetConfig key endpoint (ReplicatedCluster cluster replicas) namespace binding credentials limits)
test/Main.hs view
@@ -5,8 +5,11 @@ import ClickHouseMigrate.Internal.Checksum qualified as Checksum import ClickHouseMigrate.Internal.Http (readJsonRowStream) import ClickHouseMigrate.Internal.LedgerCodec qualified as Codec+import ClickHouseMigrate.Internal.Preflight qualified as Preflight+import ClickHouseMigrate.Internal.ServerProfile qualified as Profile import ClickHouseMigrate.Internal.Transport qualified as Transport-import Control.Monad (forM_, unless)+import ClickHouseMigrate.Report qualified as Report+import Control.Monad (forM_, unless, void) import Data.Aeson qualified as Aeson import Data.ByteString (ByteString) import Data.ByteString qualified as BS@@ -45,6 +48,8 @@   streamValidation   scheduleValidation   targetValidation+  directProfilePins+  replicatedTargetValidation   let sql = "CREATE TABLE app.events (id UInt64) ENGINE=MergeTree ORDER BY id"   create <- right (sqlStep "create" sql)   migration <- right (sqlMigration "0001" (create :| []))@@ -80,7 +85,7 @@     assert "multiple SQL statements" (isDefinitionError MultipleStatements (sqlStep "x" multiple))   forM_ ["INSERT INTO app.t VALUES ('open", "INSERT INTO app.t VALUES ($x$open)", "CREATE TABLE app.t (x UInt8) /*", "CREATE TABLE app.\"t (x UInt8)"] $ \unterminated ->     assert "unterminated SQL construct" (isDefinitionError UnterminatedSql (sqlStep "x" unterminated))-  forM_ ["BEGIN", "COMMIT", "ROLLBACK", "USE app", "SET x=1", "SELECT 1", "SYSTEM FLUSH LOGS", "CREATE TABLE app.t ON /* gap */ CLUSTER c (x UInt8)", "INSERT INTO app.t FORMAT CSV\n1", "CREATE MATERIALIZED VIEW app.v REFRESH EVERY 1 SECOND AS SELECT 1", "ALTER TABLE app.t MOVE PARTITION 1 TO TABLE app.other", "CREATE TABLE app.t ENGINE=ReplicatedMergeTree ORDER BY tuple()", "CREATE TABLE unqualified (x UInt8)", "\\i file.sql"] $ \unsupported ->+  forM_ ["BEGIN", "COMMIT", "ROLLBACK", "USE app", "SET x=1", "SELECT 1", "SYSTEM FLUSH LOGS", "CREATE TABLE app.t ON /* gap */ CLUSTER c (x UInt8)", "INSERT INTO app.t FORMAT CSV\n1", "CREATE MATERIALIZED VIEW app.v REFRESH EVERY 1 SECOND AS SELECT 1", "ALTER TABLE app.t MOVE PARTITION 1 TO TABLE app.other", "CREATE TABLE unqualified (x UInt8)", "\\i file.sql"] $ \unsupported ->     assert "unsupported SQL" (isDefinitionError UnsupportedSql (sqlStep "x" unsupported))   forM_ ["async_insert", "mutations_sync", "alter_sync", "AsYnC_InSeRt", "`alter_sync`", "\"mutations_sync\""] $ \setting ->     assert "protected setting" (isDefinitionError ProtectedSetting (sqlStep "x" (Encoding.encodeUtf8 ("INSERT INTO app.t VALUES (1) SETTINGS " <> setting <> " /*comment*/ = 0"))))@@ -90,6 +95,8 @@     c <- right (migrationComponent "c" Set.empty (m :| []))     p <- right (migrationPlan (c :| []))     assert "SQL exact bytes preserved" (stepSql (NE.head (migrationSteps (NE.head (componentMigrations (NE.head (inspectPlan p)))))) == valid)+  forM_ ["CREATE TABLE app.t (x UInt64) ENGINE=ReplicatedMergeTree ORDER BY x", "CREATE TABLE app.t (x UInt64, updated_at UInt64) ENGINE=ReplicatedReplacingMergeTree(updated_at) ORDER BY x", "CREATE MATERIALIZED VIEW app.v ENGINE ReplicatedAggregatingMergeTree ORDER BY x AS SELECT x FROM app.t"] $ \sql -> void (right (sqlStep "replicated" sql))+  forM_ ["CREATE TABLE app.t (x UInt8) ENGINE=ReplicatedMergeTree('/keeper','replica') ORDER BY x", "CREATE DATABASE app ENGINE=Replicated", "CREATE TABLE app.\"ReplicatedMergeTree\" (x UInt8) ENGINE=Memory", "CREATE TABLE app.t (x UInt8) ENGINE=Distributed(c,app,t)"] $ \sql -> assert "replicated envelope refuses unsafe form" (either (const True) (const False) (sqlStep "replicated" sql))   -- Boundary combinations cover escaped delimiters, comment forms and heredoc tags.   forM_ [";", "'; DROP TABLE app.t; --", "/* ; */", "雪;", "\\;", "# ;"] $ \payload ->     forM_ ["tag", "other", "_x9"] $ \tag -> do@@ -100,18 +107,20 @@   let firstMigration = NE.head . componentMigrations . NE.head . inspectPlan   assert "description excluded from checksum" (migrationChecksum (firstMigration plan) == migrationChecksum (firstMigration describedPlan))   assert "description available" (migrationDescription (firstMigration describedPlan) == Just "metadata")-  assert "unknown checksum format" (isDefinitionError UnknownChecksumFormat (parseChecksumFormat "sha256-v2"))+  assert "unknown checksum format" (isDefinitionError UnknownChecksumFormat (parseChecksumFormat "sha256-v3"))   assert "known checksum format" (parseChecksumFormat "sha256-v1" == Right ChecksumV1)   vectorLines <- filter (not . Text.isPrefixOf "#") . Text.lines <$> Text.readFile "test/vectors/checksum-v1.tsv"-  forM_ vectorLines checkVector-  putStrLn "Definition, ordering, SQL boundary and 24 independent checksum-vector checks passed."+  forM_ vectorLines (checkVectorFor ChecksumV1)+  vectorV2 <- filter (not . Text.isPrefixOf "#") . Text.lines <$> Text.readFile "test/vectors/checksum-v2.tsv"+  forM_ vectorV2 (checkVectorFor ChecksumV2)+  putStrLn "Definition, ordering, SQL boundary and 48 independent checksum-vector checks passed." -checkVector :: Text -> IO ()-checkVector line = case Text.splitOn "\t" line of+checkVectorFor :: ChecksumFormat -> Text -> IO ()+checkVectorFor format line = case Text.splitOn "\t" line of   [label, kind, component, name, sources, expectedInput, expectedDigest] -> do     steps <- mapM parseStep (Text.splitOn "," sources)-    let encodedSteps = fmap (\(key, sql) -> Checksum.encodeStepV1 component name key sql) (NE.fromList steps)-        input = if kind == "step" then NE.head encodedSteps else Checksum.encodeMigrationV1 component name (NE.toList encodedSteps)+    let encodedSteps = fmap (\(key, sql) -> (if format == ChecksumV1 then Checksum.encodeStepV1 else Checksum.encodeStepV2) component name key sql) (NE.fromList steps)+        input = if kind == "step" then NE.head encodedSteps else (if format == ChecksumV1 then Checksum.encodeMigrationV1 else Checksum.encodeMigrationV2) component name (NE.toList encodedSteps)     expected <- unhex expectedInput     assert (Text.unpack (label <> " encoded bytes")) (input == expected)     assert (Text.unpack (label <> " SHA-256")) (Checksum.digestHex (Checksum.digest input) == expectedDigest)@@ -120,7 +129,7 @@     c <- right (migrationComponent component Set.empty (m :| []))     p <- right (migrationPlan (c :| []))     let bound = NE.head (componentMigrations (NE.head (inspectPlan p)))-        actual = if kind == "step" then stepChecksumHex (stepChecksum (NE.head (migrationSteps bound))) else migrationChecksumHex (migrationChecksum bound)+        actual = if kind == "step" then stepChecksumHex (stepChecksumFor format (NE.head (migrationSteps bound))) else migrationChecksumHex (migrationChecksumFor format bound)     assert (Text.unpack (label <> " public bound checksum")) (actual == expectedDigest)   _ -> fail "Invalid checked-in checksum vector"   where@@ -203,3 +212,66 @@   late <- run 1024 [bytes <> "\nCode: 999. synthetic failure\n"]   assert "late error discards rows" (case late of Transport.ObservedServerFailure (Transport.TransportError Transport.ServerRejected _ (Just 999)) -> True; _ -> False)   assert "empty stream" . success 0 =<< run 1024 []++-- Exact direct policy bytes are compatibility obligations, not inferred behavior.+directProfilePins :: IO ()+directProfilePins = do+  identity <- right (serverId "00000000-0000-0000-0000-000000000000")+  namespace <- right (ledgerNamespace "ledger")+  let topology = DirectServer identity+      base = [("query_id", Just "pin"), ("async_insert", Just "0"), ("mutations_sync", Just "1"), ("alter_sync", Just "1"), ("fsync_metadata", Just "1"), ("wait_end_of_query", Just "1"), ("default_format", Just "JSONEachRow")]+      readSettings = [("max_rows_to_read", Just "0"), ("max_bytes_to_read", Just "0"), ("max_result_rows", Just "0"), ("max_result_bytes", Just "0"), ("read_overflow_mode", Just "throw"), ("result_overflow_mode", Just "throw"), ("timeout_overflow_mode", Just "throw"), ("sort_overflow_mode", Just "throw")]+  assert "direct command settings byte pin" (Profile.requestSettings topology defaultHttpLimits "pin" Transport.Command == base)+  forM_ [Transport.JsonRows, Transport.JsonRowStream] $ \kind ->+    assert "direct read settings byte pin" (Profile.requestSettings topology defaultHttpLimits "pin" kind == base ++ readSettings)+  assert "direct policy byte pin" (Profile.completionPolicy topology == "clickhouse-migrate/local-sync-v1;async_insert=0;mutations_sync=1;alter_sync=1")+  assert "direct checksum format pin" (Profile.checksumFormat topology == ChecksumV1)+  assert "direct database DDL pin" (Profile.createDatabase topology namespace == Just "CREATE DATABASE IF NOT EXISTS `ledger` ENGINE=Atomic")+  assert "direct table DDL pin" (Profile.createTable topology namespace == "CREATE TABLE IF NOT EXISTS `ledger`.`events` (sequence UInt64, event_id UUID, ledger_id UUID, event String) ENGINE=MergeTree ORDER BY sequence SETTINGS fsync_after_insert=1, fsync_part_directory=1, storage_policy='default'")+  assert "direct engine metadata pin" (Profile.expectedEngine topology == "MergeTree ORDER BY sequence SETTINGS fsync_after_insert = 1, fsync_part_directory = 1, storage_policy = 'default', index_granularity = 8192")+  assert "direct create metadata pin" (Profile.expectedCreate topology namespace == "CREATE TABLE ledger.events (`sequence` UInt64, `event_id` UUID, `ledger_id` UUID, `event` String) ENGINE = MergeTree ORDER BY sequence SETTINGS fsync_after_insert = 1, fsync_part_directory = 1, storage_policy = 'default', index_granularity = 8192")++replicatedTargetValidation :: IO ()+replicatedTargetValidation = do+  identity <- right (serverId "00000000-0000-0000-0000-000000000000")+  key <- right (targetKey "replicated/unit")+  endpoint <- right (directEndpoint "https://example.com")+  cluster <- right (clusterName "tan-production")+  namespace <- right (ledgerNamespace "ledger")+  credentials <- right (basicCredentials "app" "password")+  let build replicas limits = replicatedTargetConfig key endpoint cluster replicas namespace FirstUse credentials limits+  assert "duplicate replicas rejected" (either (== DuplicateReplica) (const False) (build (identity :| [identity]) defaultHttpLimits))+  short <- right (httpLimits 4999999 1024)+  assert "replicated deadline leaves a response margin" (either (== InvalidHttpLimits) (const False) (build (identity :| []) short))+  config <- right (build (identity :| []) defaultHttpLimits)+  assert "replicated topology retained" (configuredTopology config == ReplicatedCluster cluster (identity :| []))+  forM_ ["", "x' OR 1=1", "x.y", "-start", "雪"] $ \value -> assert "cluster name cannot inject SQL" (clusterName value == Left InvalidClusterName)+  let topology = configuredTopology config+      engine = "ReplicatedMergeTree('/operator/path', 'replica')" <> Profile.engineTail+      create = "CREATE TABLE ledger.events (`sequence` UInt64, `event_id` UUID, `ledger_id` UUID, `event` String) ENGINE = " <> engine+      shape = ["events", "ReplicatedMergeTree", engine, "sequence", "sequence", "", "", create]+  assert "replicated engine metadata accepts operator paths" (Profile.validTableShape topology namespace shape)+  assert "replicated metadata rejects trailing engine changes" (not (Profile.validTableShape topology namespace ["events", "ReplicatedMergeTree", engine <> " SETTINGS a=0", "sequence", "sequence", "", "", create]))++  let probe actual version macro =+        Aeson.object ([("server_id", Aeson.String actual), ("version", Aeson.String version), ("host", Aeson.String "owned-host"), ("cluster", Aeson.String macro), ("storage_ok", Aeson.Number 1)] ++ [(key, Aeson.String value) | (key, value) <- Profile.replicatedEffective topology])+      outcome value = case Aeson.fromJSON value of Aeson.Success row -> Transport.AcknowledgedSuccess (Transport.Rows [row]); Aeson.Error _ -> error "Invalid probe fixture"+      validate actual version macro = Profile.validateOutcome ["26.3.60.10002.altinitystable"] topology (outcome (probe actual version macro))+      rejects code = \case Left (Profile.TargetRejected (Transport.TransportError actual _ _)) -> actual == code; _ -> False+  assert "replicated probe admits expected socket" (fmap identityServerId (validate (serverIdText identity) "26.3.60.10002.altinitystable" "tan-production") == Right identity)+  assert "replicated probe refuses a foreign socket" (rejects Transport.ServerIdentityMismatch (validate "11111111-1111-1111-1111-111111111111" "26.3.60.10002.altinitystable" "tan-production"))+  assert "replicated probe refuses unqualified versions" (rejects Transport.UnsupportedServer (validate (serverIdText identity) "unlisted" "tan-production"))+  assert "replicated probe refuses wrong cluster macros" (rejects Transport.ReplicaSetMismatch (validate (serverIdText identity) "26.3.60.10002.altinitystable" "other-cluster"))++  let heredocEngine = "ReplicatedMergeTree($$path$$, $$replica$$)" <> Profile.engineTail+      heredocCreate = "CREATE TABLE ledger.events (`sequence` UInt64, `event_id` UUID, `ledger_id` UUID, `event` String) ENGINE = " <> heredocEngine+  assert "replicated metadata requires single-quoted engine arguments" (not (Profile.validTableShape topology namespace ["events", "ReplicatedMergeTree", heredocEngine, "sequence", "sequence", "", "", heredocCreate]))++  guardedStep <- right (sqlStep "guarded" "INSERT INTO app.t VALUES (1) SETTINGS insert_quorum=0")+  guardedMigration <- right (sqlMigration "guarded" (guardedStep :| []))+  guardedComponent <- right (migrationComponent "guarded" Set.empty (guardedMigration :| []))+  guardedPlan <- right (migrationPlan (guardedComponent :| []))+  let direct = targetConfig key endpoint identity namespace FirstUse credentials defaultHttpLimits+  assert "direct v1 definitions keep accepting unrelated settings" (Preflight.preflightPending direct guardedPlan 0 == Right ())+  assert "replicated pending policy overrides rejected" (case Preflight.preflightPending config guardedPlan 0 of Left (Report.InvalidRuntimePlan _) -> True; _ -> False)+  assert "applied replicated definitions are not rechecked" (Preflight.preflightPending config guardedPlan 1 == Right ())
+ test/public/TargetCoercion.hs view
@@ -0,0 +1,7 @@+module TargetCoercion where++import ClickHouseMigrate.Target+import Data.Coerce (coerce)++invalid :: () -> TargetConfig+invalid = coerce
+ test/public/TargetConstructor.hs view
@@ -0,0 +1,5 @@+module TargetConstructor where++import ClickHouseMigrate.Target++invalid = TargetConfig
+ test/vectors/checksum-v2.tsv view
@@ -0,0 +1,25 @@+# label	kind	component	migration	steps(name:sql-hex,...)	input-hex	sha256-hex+baseline	step	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	8f88bdc18d2842dc722223c041a59aac87b84cfedecd4b04da172bc08bc59640+baseline	migration	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000010000000000000154636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	55be6f229c0fec938ebb19639a460057a3f37de40737cd54ad75c1f61677d38f+whitespace	step	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000041435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a	718eaeba20757b1b5a91a4a90f1628137d2bd9d7ed219723f35d9102c30810e8+whitespace	migration	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000010000000000000155636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000041435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a	a1681a577010e10da8cc19b50ffe9ae5ae78f5a2b8b71448fd6ce43f5ebf53d6+component	step	analytics2	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d31000000000000000a616e616c79746963733200000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	c084dfe095d02b0747389e5a95cdadace41fe4600420c30f6e38b3ec8ee54a19+component	migration	analytics2	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d31000000000000000a616e616c79746963733200000000000000043030303100000000000000010000000000000155636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d31000000000000000a616e616c79746963733200000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	23ac982172c5ddf03ba507d6d0bb475d46ad94426585c3fccd2e9bf8a7c1f9df+migration	step	analytics	0002	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303200000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	ddde1d492def08641805382a030ae1b7cf7e72247cf472b315be5327464d0922+migration	migration	analytics	0002	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303200000000000000010000000000000154636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303200000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	d5d946d2bdf9a65cdd41cfdf1024296a0f7ab854864e224d48e5a557c769dd23+step-name	step	analytics	0001	create2:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000007637265617465320000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	39f0f1bdf7c06c59842f4b83892c165afbe414c10f2ad1b134511454863de793+step-name	migration	analytics	0001	create2:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000010000000000000155636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000007637265617465320000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	ead6a0aebd7bde3b95c3b8e884f02b42694892a3bed0a722e61a1e34521f3f79+two-steps	step	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	8f88bdc18d2842dc722223c041a59aac87b84cfedecd4b04da172bc08bc59640+two-steps	migration	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964,seed:494e5345525420494e544f206170702e6576656e74732056414c55455320283129	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000020000000000000154636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640000000000000133636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000004736565640000000000000021494e5345525420494e544f206170702e6576656e74732056414c55455320283129	887d899e3171ab3919de5451e6db7b2bd73a7542927d1f3391081c022924cf51+reordered	step	analytics	0001	seed:494e5345525420494e544f206170702e6576656e74732056414c55455320283129	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000004736565640000000000000021494e5345525420494e544f206170702e6576656e74732056414c55455320283129	f306bff56533d318684a335ed4b81cd7cc617a3d7f249580d10714a8fe4b9700+reordered	migration	analytics	0001	seed:494e5345525420494e544f206170702e6576656e74732056414c55455320283129,create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000020000000000000133636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000004736565640000000000000021494e5345525420494e544f206170702e6576656e74732056414c554553202831290000000000000154636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	25fadd85b11a98cd38736cc0214ad26b91406399d51d8857c467b06c03895023+boundary-a	step	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000041435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a	718eaeba20757b1b5a91a4a90f1628137d2bd9d7ed219723f35d9102c30810e8+boundary-a	migration	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a,seed:494e5345525420494e544f206170702e6576656e74732056414c55455320283129	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000020000000000000155636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000041435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640a0000000000000133636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000004736565640000000000000021494e5345525420494e544f206170702e6576656e74732056414c55455320283129	d33a51cc7a8aec8cc8829aa39aaa6ddc7f08b0854bc465eaf44dc4915a3b509b+boundary-b	step	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	8f88bdc18d2842dc722223c041a59aac87b84cfedecd4b04da172bc08bc59640+boundary-b	migration	analytics	0001	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964,seed:0a494e5345525420494e544f206170702e6576656e74732056414c55455320283129	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000020000000000000154636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f524445522042592069640000000000000134636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000047365656400000000000000220a494e5345525420494e544f206170702e6576656e74732056414c55455320283129	470b59f8d8565b8b1a228a963521d4ea6a5c79f89f70e3f39e7d77a121a89707+ambiguous-a	step	a	bc	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000001610000000000000002626300000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	78a0e2f41f2778b7f24585477bf374aaf61da102626feb549ff05485279913a8+ambiguous-a	migration	a	bc	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d31000000000000000161000000000000000262630000000000000001000000000000014a636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000001610000000000000002626300000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	009a2dc827323890e1fd165147ed17a1a752c94c2e1d275e5cf7c8941bca92e4+ambiguous-b	step	ab	c	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000002616200000000000000016300000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	bf423f20e0fafa04f023fbe3df3b01173679e6a3d1ebb877ad16f271bf6edf9f+ambiguous-b	migration	ab	c	create:435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d31000000000000000261620000000000000001630000000000000001000000000000014a636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000002616200000000000000016300000000000000066372656174650000000000000040435245415445205441424c45206170702e6576656e7473202869642055496e7436342920454e47494e453d4d6572676554726565204f52444552204259206964	ae6a82d1f50d6b4d729407e694aac50b9a70420615f9be7b75d1b77f50aa2c91+unicode	step	analytics	0001	seed:494e5345525420494e544f206170702e746578742056414c554553202827e99baa3b2729	636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000004736565640000000000000024494e5345525420494e544f206170702e746578742056414c554553202827e99baa3b2729	5022a7e4cd20146e455aa3b0e64a4a1ce35c80f021e22b1ce89a86798b2a346e+unicode	migration	analytics	0001	seed:494e5345525420494e544f206170702e746578742056414c554553202827e99baa3b2729	636c69636b686f7573652d6d6967726174652f6d6967726174696f6e0076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c797469637300000000000000043030303100000000000000010000000000000136636c69636b686f7573652d6d6967726174652f737465700076320000000000000000be636c69636b686f7573652d6d6967726174652f7265706c6963617465642d73796e632d76313b6173796e635f696e736572743d303b6d75746174696f6e735f73796e633d323b616c7465725f73796e633d323b696e736572745f71756f72756d3d616c6c3b696e736572745f71756f72756d5f706172616c6c656c3d303b64697374726962757465645f64646c5f6f75747075745f6d6f64653d6e6f6e653b73656c6563745f73657175656e7469616c5f636f6e73697374656e63793d310000000000000009616e616c79746963730000000000000004303030310000000000000004736565640000000000000024494e5345525420494e544f206170702e746578742056414c554553202827e99baa3b2729	c55b63ceb8fbeb7c7efafe084c8dc9d0c0590307da883ecfaf683a689ecfa7ef