pg-migrate-import-codd-1.1.0.0: src/Database/PostgreSQL/Migrate/History/Codd/Import.hs
module Database.PostgreSQL.Migrate.History.Codd.Import
( importCoddHistory,
importCoddHistoryWithValidators,
buildCoddEvidence,
)
where
import Data.Aeson qualified as Aeson
import Data.Bifunctor (first)
import Data.ByteString qualified as ByteString
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text qualified as Text
import Database.PostgreSQL.Migrate
import Database.PostgreSQL.Migrate.History.Codd.Ledger
( coddUnlockFailure,
readCoddHistoryOnConnection,
sourceUnlockCleanupIssue,
withLockedCoddHistory,
)
import Database.PostgreSQL.Migrate.History.Codd.Types
import Database.PostgreSQL.Migrate.Internal (migrationChecksumBytes)
import Numeric qualified
import PgMigrate.History.Codd.Prelude
-- | Read Codd under its source lock, then atomically import target metadata.
importCoddHistory ::
ImportOptions ->
CoddSourceConfig ->
ConnectionProvider ->
MigrationPlan ->
NonEmpty HistoryMapping ->
IO (Either CoddImportError HistoryImportReport)
importCoddHistory options config targetProvider plan mappings =
importCoddHistoryWithValidators options [] config targetProvider plan mappings
-- | As 'importCoddHistory', with read-only state validators available to
-- 'EquivalentState' mappings. The source advisory lock remains held while the
-- target import validates state and writes its ledger rows.
importCoddHistoryWithValidators ::
ImportOptions ->
[StateValidator] ->
CoddSourceConfig ->
ConnectionProvider ->
MigrationPlan ->
NonEmpty HistoryMapping ->
IO (Either CoddImportError HistoryImportReport)
importCoddHistoryWithValidators options validators config targetProvider plan mappings =
case validateImportDefinition config validators plan mappings of
Left err -> pure (Left err)
Right () -> do
(sourceResult, sourceUnlockIssues) <-
withLockedCoddHistory config $ \sourceConnection -> do
loaded <- readCoddHistoryOnConnection config sourceConnection
case loaded of
Left err -> pure (Left err)
Right history ->
case buildHistoryImport config history validators mappings of
Left err -> pure (Left err)
Right historyImportDefinition -> do
imported <- importMigrationHistory options targetProvider plan historyImportDefinition
pure (first CoddTargetImportFailed imported)
pure $ case sourceResult of
Left primary ->
case sourceUnlockIssues of
issue : _ -> Left (coddUnlockFailure (sourceLockKey config) (Just primary) issue)
[] -> Left primary
Right HistoryImportReport {importResults, cleanupIssues} ->
Right
HistoryImportReport
{ importResults,
cleanupIssues = cleanupIssues <> (sourceUnlockCleanupIssue <$> sourceUnlockIssues)
}
validateImportDefinition ::
CoddSourceConfig ->
[StateValidator] ->
MigrationPlan ->
NonEmpty HistoryMapping ->
Either CoddImportError ()
validateImportDefinition config@CoddSourceConfig {selectedFilenames, confirmation, sourceManifest} validators plan mappings = do
if any isSamePayload mappings && confirmation /= Confirmed
then Left CoddConfirmationRequired
else pure ()
if any isSamePayload mappings && sourceManifest == Nothing
then Left CoddSamePayloadRequiresManifest
else pure ()
placeholderEvidence <- buildPlaceholderEvidence selectedFilenames
case historyImport "codd" placeholderEvidence validators mappings (importReason config) of
Left err -> Left (CoddHistoryDefinitionFailed err)
Right _ -> Right ()
first
(CoddTargetImportFailed . HistoryImportValidationFailed)
(validateHistoryMappingTargets plan mappings)
where
isSamePayload mapping =
case historyMappingPayloadRelation mapping of
SamePayload _ -> True
EquivalentState -> False
buildPlaceholderEvidence ::
NonEmpty FilePath ->
Either CoddImportError (Map EvidenceKey ImportEvidence)
buildPlaceholderEvidence filenames =
Map.fromList . toList <$> traverse placeholder filenames
where
placeholder filename = do
key <- first CoddDefinitionFailed (coddEvidenceKey filename)
evidence <-
first
CoddHistoryDefinitionFailed
(ledgerOnlyEvidence (Text.pack filename) Nothing Nothing Aeson.Null)
Right (key, evidence)
buildHistoryImport ::
CoddSourceConfig ->
CoddHistory ->
[StateValidator] ->
NonEmpty HistoryMapping ->
Either CoddImportError HistoryImport
buildHistoryImport config history validators mappings = do
evidence <- buildCoddEvidence config history
first
CoddHistoryDefinitionFailed
(historyImport "codd" evidence validators mappings (importReason config))
buildCoddEvidence ::
CoddSourceConfig ->
CoddHistory ->
Either CoddImportError (Map EvidenceKey ImportEvidence)
buildCoddEvidence config@CoddSourceConfig {strictSource, sourceManifest} CoddHistory {schemaVersion, selectedRows} = do
case sourceManifest of
Just (CoddManifest manifest)
| strictSource ->
let selected = Set.fromList (filename <$> toList selectedRows)
missing = Set.toAscList (selected `Set.difference` Map.keysSet manifest)
extras = Set.toAscList (Map.keysSet manifest `Set.difference` selected)
in case missing of
missingFilename : _ -> Left (CoddManifestEntryMissing missingFilename)
[] -> if null extras then Right () else Left (CoddManifestHasUnexpected extras)
_ -> Right ()
Map.fromList . toList <$> traverse (rowEvidence config schemaVersion) selectedRows
rowEvidence ::
CoddSourceConfig ->
CoddSchemaVersion ->
CoddHistoryRow ->
Either CoddImportError (EvidenceKey, ImportEvidence)
rowEvidence CoddSourceConfig {sourcePayloads, sourceManifest} version row@CoddHistoryRow {filename, appliedAt} = do
key <- first CoddDefinitionFailed (coddEvidenceKey filename)
let maybeBytes = Map.lookup filename sourcePayloads
details = rowDetails version row
timestamp = AbsoluteTime <$> appliedAt
evidence <-
case sourceManifest of
Nothing ->
first
CoddHistoryDefinitionFailed
(ledgerOnlyEvidence (Text.pack filename) timestamp Nothing details)
Just (CoddManifest manifest) ->
case Map.lookup filename manifest of
Nothing ->
first
CoddHistoryDefinitionFailed
(ledgerOnlyEvidence (Text.pack filename) timestamp Nothing details)
Just expected -> do
bytes <- maybe (Left (CoddSourcePayloadMissing filename)) Right maybeBytes
let actualChecksum = migrationFingerprint bytes
actual = checksumText actualChecksum
if actual /= expected
then Left (CoddManifestChecksumMismatch filename expected actual)
else
first
CoddHistoryDefinitionFailed
( sourceManifestVerifiedEvidence
(Text.pack filename)
timestamp
(Just actualChecksum)
details
)
Right (key, evidence)
rowDetails :: CoddSchemaVersion -> CoddHistoryRow -> Aeson.Value
rowDetails version CoddHistoryRow {filename, migrationTimestamp, appliedAt, numAppliedStatements, noTransactionFailedAt} =
Aeson.object
[ "adapter" Aeson..= ("codd" :: Text),
"schemaVersion" Aeson..= schemaVersionText version,
"filename" Aeson..= filename,
"migrationTimestamp" Aeson..= migrationTimestamp,
"appliedAt" Aeson..= appliedAt,
"numAppliedStatements" Aeson..= numAppliedStatements,
"noTransactionFailedAt" Aeson..= noTransactionFailedAt
]
schemaVersionText :: CoddSchemaVersion -> Text
schemaVersionText version =
case version of
CoddV1 -> "v1"
CoddV2 -> "v2"
CoddV3 -> "v3"
CoddV4 -> "v4"
CoddV5 -> "v5"
checksumText :: MigrationChecksum -> Text
checksumText =
Text.pack . concatMap renderByte . ByteString.unpack . migrationChecksumBytes
where
renderByte byte =
case Numeric.showHex byte "" of
[digit] -> ['0', digit]
digits -> digits