packages feed

kiroku-store-0.13.0.0: src/Kiroku/Store/Compaction/SQL.hs

{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE MultilineStrings #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE NoFieldSelectors #-}

-- | Maintenance statements, exposed for deterministic query-plan checks.
module Kiroku.Store.Compaction.SQL
  ( resolveStreamsStmt,
    survivingSelectedEventsStmt,
    ledgerRecordByDigestStmt,
    compactionRecordRow,
    ledgerColumns,
    lockCompactionStreamsStmt,
    deleteJunctionRowsStmt,
    deleteSelectedDeadLettersStmt,
    deleteSelectedEventsStmt,
    CompactionRecordInsert (..),
    insertCompactionRecordStmt,
    compactionLedgerStmt,
    eventMembershipsStmt,
    deadLetterCountsStmt,
    causationDependentCountsStmt,
  )
where

import Contravariant.Extras (contrazip2)
import Data.Aeson qualified as Aeson
import Data.ByteString (ByteString)
import Data.Functor.Contravariant ((>$<))
import Data.Int (Int32, Int64)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Time (UTCTime)
import Data.UUID (UUID)
import Data.Vector (Vector)
import GHC.Generics (Generic)
import Hasql.Decoders qualified as D
import Hasql.Encoders qualified as E
import Hasql.Statement (Statement, preparable)
import Kiroku.Store.Compaction.Types
import Kiroku.Store.SQL (causationDependentCountsStmt, deadLetterCountsStmt, eventMembershipsStmt)
import Kiroku.Store.Types (GlobalPosition (..), StoreIdentity (..))

resolveStreamsStmt :: Statement (Vector Text) (Vector (Text, Int64, Int64, Bool))
resolveStreamsStmt =
  preparable
    """
    SELECT s.stream_name, s.stream_id, s.stream_version, s.deleted_at IS NOT NULL
    FROM unnest($1::text[]) AS requested(stream_name)
    JOIN streams s USING (stream_name)
    """
    (E.param (E.nonNullable (E.foldableArray (E.nonNullable E.text))))
    (D.rowVector ((,,,) <$> col D.text <*> col D.int8 <*> col D.int8 <*> col D.bool))

survivingSelectedEventsStmt :: Statement (Vector UUID) Int64
survivingSelectedEventsStmt =
  preparable
    "SELECT count(*) FROM events WHERE event_id = ANY($1::uuid[])"
    (E.param (E.nonNullable (E.foldableArray (E.nonNullable E.uuid))))
    (D.singleRow (col D.int8))

ledgerColumns :: Text
ledgerColumns = "compaction_id, manifest_digest, report_digest, store_id, operation, dead_letter_policy, causation_policy, selected_events, home_memberships, global_memberships, link_memberships, dead_letters_removed, causation_dependents, lowest_global_position, highest_global_position, affected_streams, applied_at, applied_by"

ledgerRecordByDigestStmt :: Statement ByteString (Maybe CompactionRecord)
ledgerRecordByDigestStmt =
  preparable
    ("SELECT " <> ledgerColumns <> " FROM event_compactions WHERE manifest_digest = $1")
    (E.param (E.nonNullable E.bytea))
    (D.rowMaybe compactionRecordRow)

-- | Decode the stored report digest verbatim. Audits compare it with the
-- recomputed digest; reading an audit record must not silently reseal it.
compactionRecordRow :: D.Row CompactionRecord
compactionRecordRow = do
  cid <- CompactionId <$> col D.uuid
  md <- col digestValue
  rd <- col digestValue
  identity <- StoreIdentity <$> col D.uuid
  operation <- col (D.refine (either (Left . T.pack . show) Right . mkCompactionOperation) D.text)
  dead <- col (D.refine (\case "refuse" -> Right RefuseDeadLetters; "remove" -> Right RemoveDeadLetters; _ -> Left "invalid dead-letter policy") D.text)
  cause <- col (D.refine (\case "refuse" -> Right RefuseCausationDependents; "allow" -> Right AllowDanglingCausation; _ -> Left "invalid causation policy") D.text)
  selected <- col D.int8
  home <- col D.int8
  global <- col D.int8
  links <- col D.int8
  deadLetters <- col D.int8
  dependents <- col D.int8
  lo <- GlobalPosition <$> col D.int8
  hi <- GlobalPosition <$> col D.int8
  streams <- col (D.refine parseHeads D.jsonb)
  applied <- col D.timestamptz
  actor <- col D.text
  pure (CompactionRecord cid (CompactionReport md identity operation dead cause selected home global links deadLetters dependents lo hi streams rd) applied actor)
  where
    digestValue = D.refine (maybe (Left "invalid SHA-256 byte length") Right . mkCompactionDigest) D.bytea
    parseHeads value = case Aeson.fromJSON value of
      Aeson.Error err -> Left (T.pack err)
      Aeson.Success heads -> Right heads

col :: D.Value a -> D.Row a
col = D.column . D.nonNullable

lockCompactionStreamsStmt :: Statement (Vector Text) (Vector (Text, Int64, Int64, Maybe UTCTime))
lockCompactionStreamsStmt =
  preparable
    "SELECT stream_name,stream_id,stream_version,deleted_at FROM streams WHERE stream_name = ANY($1::text[]) ORDER BY stream_id FOR UPDATE"
    (E.param (E.nonNullable (E.foldableArray (E.nonNullable E.text))))
    (D.rowVector ((,,,) <$> col D.text <*> col D.int8 <*> col D.int8 <*> D.column (D.nullable D.timestamptz)))

deleteJunctionRowsStmt :: Statement (Vector UUID, Vector Int64) (Vector (Int64, Int64))
deleteJunctionRowsStmt =
  preparable
    "DELETE FROM stream_events se USING unnest($1::uuid[], $2::bigint[]) AS k(event_id,stream_id) WHERE se.event_id=k.event_id AND se.stream_id=k.stream_id RETURNING se.stream_id,se.original_stream_id"
    (contrazip2 (E.param (E.nonNullable (E.foldableArray (E.nonNullable E.uuid)))) (E.param (E.nonNullable (E.foldableArray (E.nonNullable E.int8)))))
    (D.rowVector ((,) <$> col D.int8 <*> col D.int8))

deleteSelectedDeadLettersStmt :: Statement (Vector UUID) Int64
deleteSelectedDeadLettersStmt =
  preparable
    "WITH removed AS (DELETE FROM dead_letters WHERE event_id=ANY($1::uuid[]) RETURNING 1) SELECT count(*) FROM removed"
    (E.param (E.nonNullable (E.foldableArray (E.nonNullable E.uuid))))
    (D.singleRow (col D.int8))

deleteSelectedEventsStmt :: Statement (Vector UUID) Int64
deleteSelectedEventsStmt =
  preparable
    "WITH removed AS (DELETE FROM events WHERE event_id=ANY($1::uuid[]) RETURNING 1) SELECT count(*) FROM removed"
    (E.param (E.nonNullable (E.foldableArray (E.nonNullable E.uuid))))
    (D.singleRow (col D.int8))

compactionLedgerStmt :: Statement Int32 (Vector CompactionRecord)
compactionLedgerStmt =
  preparable
    ("SELECT " <> ledgerColumns <> " FROM event_compactions ORDER BY applied_at DESC,compaction_id LIMIT $1")
    (E.param (E.nonNullable E.int4))
    (D.rowVector compactionRecordRow)

data CompactionRecordInsert = CompactionRecordInsert
  { manifestDigest :: !CompactionDigest,
    reportDigest :: !CompactionDigest,
    storeId :: !StoreIdentity,
    operation :: !CompactionOperation,
    deadLetterPolicy :: !DeadLetterPolicy,
    causationPolicy :: !CausationPolicy,
    selectedEvents :: !Int64,
    homeMemberships :: !Int64,
    globalMemberships :: !Int64,
    linkMemberships :: !Int64,
    deadLettersRemoved :: !Int64,
    causationDependents :: !Int64,
    lowestGlobalPosition :: !GlobalPosition,
    highestGlobalPosition :: !GlobalPosition,
    affectedStreams :: !(Vector StreamHeadWitness)
  }
  deriving stock (Eq, Show, Generic)

insertCompactionRecordStmt :: Statement CompactionRecordInsert (UUID, UTCTime, Text)
insertCompactionRecordStmt =
  preparable
    """
    INSERT INTO event_compactions (manifest_digest,report_digest,store_id,operation,
      dead_letter_policy,causation_policy,selected_events,home_memberships,
      global_memberships,link_memberships,dead_letters_removed,causation_dependents,
      lowest_global_position,highest_global_position,affected_streams)
    VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
    RETURNING compaction_id,applied_at,applied_by
    """
    ( ((\r -> compactionDigestBytes r.manifestDigest) >$< E.param (E.nonNullable E.bytea))
        <> ((\r -> compactionDigestBytes r.reportDigest) >$< E.param (E.nonNullable E.bytea))
        <> ((\r -> let StoreIdentity uid = r.storeId in uid) >$< E.param (E.nonNullable E.uuid))
        <> ((\r -> compactionOperationText r.operation) >$< E.param (E.nonNullable E.text))
        <> ((\r -> if r.deadLetterPolicy == RefuseDeadLetters then "refuse" else "remove") >$< E.param (E.nonNullable E.text))
        <> ((\r -> if r.causationPolicy == RefuseCausationDependents then "refuse" else "allow") >$< E.param (E.nonNullable E.text))
        <> ((\r -> r.selectedEvents) >$< E.param (E.nonNullable E.int8))
        <> ((\r -> r.homeMemberships) >$< E.param (E.nonNullable E.int8))
        <> ((\r -> r.globalMemberships) >$< E.param (E.nonNullable E.int8))
        <> ((\r -> r.linkMemberships) >$< E.param (E.nonNullable E.int8))
        <> ((\r -> r.deadLettersRemoved) >$< E.param (E.nonNullable E.int8))
        <> ((\r -> r.causationDependents) >$< E.param (E.nonNullable E.int8))
        <> ((\r -> let GlobalPosition n = r.lowestGlobalPosition in n) >$< E.param (E.nonNullable E.int8))
        <> ((\r -> let GlobalPosition n = r.highestGlobalPosition in n) >$< E.param (E.nonNullable E.int8))
        <> ((\r -> Aeson.toJSON r.affectedStreams) >$< E.param (E.nonNullable E.jsonb))
    )
    (D.singleRow ((,,) <$> col D.uuid <*> col D.timestamptz <*> col D.text))