packages feed

clickhouse-migrate-0.1.0.0: src/ClickHouseMigrate/Adoption.hs

-- | Reviewed source-neutral adoption into fresh history. Adoption never runs SQL.
module ClickHouseMigrate.Adoption
  ( AdoptionError (..),
    SourceChecksum,
    sourceChecksum,
    LegacyRow,
    legacyRow,
    AdoptionMapping,
    attestedMapping,
    verifiedMapping,
    AdoptionRequest,
    adoptionRequest,
    AdoptionPlan,
    planAdoption,
    inspectAdoption,
    adoptionEntries,
    applyAdoption,
    resumeAdoption,
  )
where

import ClickHouseMigrate.Event qualified as Event
import ClickHouseMigrate.Internal.AdoptionTypes
import ClickHouseMigrate.Internal.AdoptionValidation (adoptionRequest, legacyRow, sourceChecksum)
import ClickHouseMigrate.Internal.AdoptionValidation qualified as Validation
import ClickHouseMigrate.Internal.Execution
import ClickHouseMigrate.Internal.LedgerTypes qualified as L
import ClickHouseMigrate.Internal.Reporting qualified as Reporting
import ClickHouseMigrate.Internal.RuntimeTypes (MutationContext (..))
import ClickHouseMigrate.Internal.TargetTypes qualified as T
import ClickHouseMigrate.Plan qualified as Plan
import ClickHouseMigrate.Prelude
import ClickHouseMigrate.Report qualified as R

-- | Filename/time history, even with today's source bytes, remains attested.
attestedMapping :: Text -> Text -> Text -> Text -> [Text] -> Either AdoptionError AdoptionMapping
attestedMapping row component migration checksum refs = Validation.mapping row component migration checksum (Attested refs)

-- | Use only when the predecessor actually stored a supported checksum.
-- Supplied historical source bytes must match that stored value; this does not
-- prove that a partially executed legacy file completed or that native SQL ran.
verifiedMapping :: Text -> Text -> Text -> Text -> ByteString -> [Text] -> Either AdoptionError AdoptionMapping
verifiedMapping row component migration checksum bytes refs = Validation.mapping row component migration checksum (Verified bytes refs)

planAdoption :: Plan.MigrationPlan -> R.InspectionReport -> AdoptionRequest -> Either AdoptionError AdoptionPlan
planAdoption plan (R.InspectionReport snapshot _ _) request = do
  (batch, entries) <- Validation.buildBatch plan request
  _ <- Validation.checkHistory request batch entries snapshot
  pure (AdoptionPlan request batch entries)

inspectAdoption :: AdoptionPlan -> R.AdoptionSummary
inspectAdoption (AdoptionPlan _ (L.AdoptionBatch source snapshot mapping definitions count) _) = R.AdoptionSummary source snapshot mapping definitions count

adoptionEntries :: AdoptionPlan -> [(R.DefinitionSummary, R.EvidenceSummary)]
adoptionEntries (AdoptionPlan _ _ entries) = [(Reporting.summaryDefinition definition, R.EvidenceSummary level refs) | (definition, L.Evidence level refs) <- entries]

applyAdoption :: MutationContext -> Plan.MigrationPlan -> AdoptionPlan -> IO (Either R.RunFailure R.RunReport)
applyAdoption = executeAdoption False

resumeAdoption :: MutationContext -> Plan.MigrationPlan -> AdoptionPlan -> IO (Either R.RunFailure R.RunReport)
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) =
  let validate snapshot = either (Left . R.AdoptionRejected) Right $ do
        (current, definitions) <- Validation.buildBatch plan request
        Validation.require MappingDefinitionMismatch (current == batch && definitions == entries)
        committed <- Validation.checkHistory request batch entries snapshot
        unless committed $ do
          let R.HistoryReport _ _ _ _ _ events _ _ = snapshot
              interrupted = any (\case R.HistoryEvent _ _ _ _ _ _ _ _ _ (R.AdoptionBeginning _) -> True; _ -> False) events
          Validation.require (if resume then AdoptionBatchConflict else AdoptionResumeRequired) (resume == interrupted)
        unless committed $ case snapshot of
          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)
   in withRuntimeObservation prerequisite (Just validate) context audit authorization (Just quiet) plan $ \runtime -> do
        projection <- runtimeProjection runtime
        case L.adoption projection of
          Nothing -> do
            when resume (abort (R.AdoptionRejected AdoptionBatchConflict))
            emit runtime Event.BeforeAdoptionStart Nothing Nothing Nothing
            appendEvent runtime Nothing (L.StartAdoption batch) Nothing
            emit runtime Event.AdoptionStarted Nothing Nothing Nothing
          Just (L.ActiveAdoption current _) -> do
            unless resume (abort (R.AdoptionRejected AdoptionResumeRequired))
            unless (current == batch) (abort (R.AdoptionRejected AdoptionBatchConflict))
        after <- runtimeProjection runtime
        completed <- case L.adoption after of Just (L.ActiveAdoption _ values) -> pure (length values); _ -> abort R.UnexpectedRuntimeFailure
        mapM_
          ( \(definition, evidence) -> do
              emit runtime Event.BeforeAdoptionEntry (Just definition) Nothing Nothing
              bound <- contextAt definition Nothing
              appendEvent runtime Nothing (L.AdoptEntry definition evidence) bound
              emit runtime Event.AdoptionEntryRecorded (Just definition) Nothing Nothing
          )
          (drop completed entries)
        let L.AdoptionBatch _ _ mapping _ _ = batch
        emit runtime Event.BeforeAdoptionComplete Nothing Nothing Nothing
        appendEvent runtime Nothing (L.CompleteAdoption mapping) Nothing
        emit runtime Event.AdoptionCompleted Nothing Nothing Nothing