packages feed

clickhouse-migrate-0.1.0.0: src/ClickHouseMigrate/Internal/Preflight.hs

-- | Force the complete definition/target envelope before contacting a server.
module ClickHouseMigrate.Internal.Preflight (preflight, validateRecoveryEvent, validateRecoveryCapacity) where

import ClickHouseMigrate.Internal.LedgerCodec (contextFor, definitions, 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.TargetTypes qualified as T
import ClickHouseMigrate.Plan qualified as Plan
import ClickHouseMigrate.Report qualified as R
import ClickHouseMigrate.Target (ledgerNamespaceText)
import Data.ByteString qualified as BS
import Data.List.NonEmpty qualified as NE
import Data.Word (Word64)

preflight :: T.TargetConfig -> L.AuditContext -> Plan.MigrationPlan -> Either R.RunError ()
preflight (T.TargetConfig target _ _ 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
  if BS.length (encodeEvent (envelope (L.Initialize target) Nothing)) > 61440 then Left R.AuditTooLarge else pure ()
  mapM_
    ( \(definition, (_, steps)) -> do
        context <- either (const (Left (R.DefinitionTooLarge (summaryDefinition definition)))) Right (contextFor definition Nothing)
        if BS.length (encodeEvent (envelope (L.StartMigration definition) (Just context))) > 61440 then Left (R.DefinitionTooLarge (summaryDefinition definition)) else pure ()
        mapM_
          ( \(index, step) -> case stepReference definition index of
              Nothing -> Left R.UnexpectedRuntimeFailure
              Just reference -> do
                either (const (Left (R.InvalidRuntimePlan reference))) Right (validateSql (Plan.stepSql step))
                mentioned <- either (const (Left (R.InvalidRuntimePlan reference))) Right (mentionsIdentifier (ledgerNamespaceText namespace) (Plan.stepSql step))
                if mentioned then Left (R.LedgerNamespaceInSql reference) else pure ()
          )
          (zip [0 ..] steps)
    )
    sources

-- Count the one canonical UTF-8/JSON encoding, including repeated evidence.
validateRecoveryEvent :: R.RecoveryEventKind -> L.LedgerEvent -> Either R.RunError ()
validateRecoveryEvent kind event
  | BS.length (encodeEvent event) > maxEventBytes = Left (R.RecoveryEventTooLarge kind)
  | otherwise = either (Left . R.LedgerFailed) Right (validateEvent event)

validateRecoveryCapacity :: Word64 -> Integer -> Either R.RunError ()
validateRecoveryCapacity current needed
  | needed < 0 || toInteger current + needed > toInteger (maxBound :: Word64) = Left (R.LedgerFailed L.LedgerLimitExceeded)
  | otherwise = Right ()