packages feed

kiroku-store-0.7.0.0: src/Kiroku/Store/HistoryRetention/SQL.hs

{-# LANGUAGE MultilineStrings #-}

module Kiroku.Store.HistoryRetention.SQL (
    acquireLeaseStmt,
    lockCoordinatorStmt,
    readLeaseForUpdateStmt,
    renewLeaseStmt,
    releaseLeaseStmt,
    leaseInventoryStmt,
    pruneLeasesStmt,
    activeConflictStmt,
    lockStreamHistoryStmt,
    lockAffectedStreamsForHardDeleteStmt,
) where

import Contravariant.Extras (contrazip2, contrazip3)
import Data.Functor.Contravariant ((>$<))
import Data.Int (Int32, Int64)
import Data.Text (Text)
import Data.Time.Clock (DiffTime, UTCTime)
import Data.UUID (UUID)
import Data.Vector (Vector)
import Hasql.Decoders qualified as D
import Hasql.Encoders qualified as E
import Hasql.Statement (Statement, preparable)
import Kiroku.Store.HistoryRetention.Types
import Kiroku.Store.Types (GlobalPosition (..), StreamId (..), StreamInfo (..), StreamName (..), StreamVersion (..))

acquireLeaseStmt :: Statement (Text, Text, DiffTime) HistoryRetentionLease
acquireLeaseStmt =
    preparable
        """
        WITH coordinator AS MATERIALIZED (
          SELECT singleton
          FROM history_retention_coordinator
          WHERE singleton
          FOR UPDATE
        ), snapshot AS MATERIALIZED (
          SELECT streams.stream_version AS protected_through,
                 clock_timestamp() AS database_now
          FROM streams
          CROSS JOIN coordinator
          WHERE streams.stream_id = 0
        ), inserted AS (
          INSERT INTO history_retention_leases
            (owner, reason, protected_through, created_at, renewed_at, expires_at)
          SELECT $1, $2, protected_through, database_now, database_now, database_now + $3
          FROM snapshot
          RETURNING lease_id, owner, reason, protected_through,
                    created_at, renewed_at, expires_at, released_at
        )
        SELECT lease_id, owner, reason, protected_through,
               created_at, renewed_at, expires_at, released_at,
               'active'::text
        FROM inserted
        """
        (contrazip3 textParam textParam intervalParam)
        (D.singleRow leaseRow)

lockCoordinatorStmt :: Statement () Bool
lockCoordinatorStmt =
    preparable
        """
        SELECT singleton
        FROM history_retention_coordinator
        WHERE singleton
        FOR UPDATE
        """
        E.noParams
        (D.singleRow (column D.bool))

readLeaseForUpdateStmt :: Statement UUID (Maybe HistoryRetentionLease)
readLeaseForUpdateStmt =
    preparable
        """
        SELECT lease_id, owner, reason, protected_through,
               created_at, renewed_at, expires_at, released_at,
               CASE
                 WHEN released_at IS NOT NULL THEN 'released'
                 WHEN expires_at <= clock_timestamp() THEN 'expired'
                 ELSE 'active'
               END::text
        FROM history_retention_leases
        WHERE lease_id = $1
        FOR UPDATE
        """
        uuidParam
        (D.rowMaybe leaseRow)

renewLeaseStmt :: Statement (UUID, DiffTime) HistoryRetentionLease
renewLeaseStmt =
    preparable
        """
        WITH renewal_time AS MATERIALIZED (
          SELECT clock_timestamp() AS database_now
        ), renewed AS (
          UPDATE history_retention_leases
          SET renewed_at = renewal_time.database_now,
              expires_at = GREATEST(
                history_retention_leases.expires_at,
                renewal_time.database_now + $2
              )
          FROM renewal_time
          WHERE lease_id = $1
          RETURNING lease_id, owner, reason, protected_through,
                    created_at, renewed_at, expires_at, released_at
        )
        SELECT lease_id, owner, reason, protected_through,
               created_at, renewed_at, expires_at, released_at,
               'active'::text
        FROM renewed
        """
        (contrazip2 uuidParam intervalParam)
        (D.singleRow leaseRow)

releaseLeaseStmt :: Statement UUID HistoryRetentionLease
releaseLeaseStmt =
    preparable
        """
        WITH released AS (
          UPDATE history_retention_leases
          SET released_at = clock_timestamp()
          WHERE lease_id = $1
          RETURNING lease_id, owner, reason, protected_through,
                    created_at, renewed_at, expires_at, released_at
        )
        SELECT lease_id, owner, reason, protected_through,
               created_at, renewed_at, expires_at, released_at,
               'released'::text
        FROM released
        """
        uuidParam
        (D.singleRow leaseRow)

leaseInventoryStmt :: Statement Int32 (Vector HistoryRetentionLease)
leaseInventoryStmt =
    preparable
        """
        WITH inventory_time AS MATERIALIZED (
          SELECT clock_timestamp() AS database_now
        )
        SELECT lease_id, owner, reason, protected_through,
               created_at, renewed_at, expires_at, released_at,
               CASE
                 WHEN released_at IS NOT NULL THEN 'released'
                 WHEN expires_at <= inventory_time.database_now THEN 'expired'
                 ELSE 'active'
               END::text
        FROM history_retention_leases
        CROSS JOIN inventory_time
        ORDER BY created_at, lease_id
        LIMIT $1
        """
        int4Param
        (D.rowVector leaseRow)

pruneLeasesStmt :: Statement UTCTime HistoryRetentionPruneResult
pruneLeasesStmt =
    preparable
        """
        WITH released AS (
          DELETE FROM history_retention_leases
          WHERE released_at IS NOT NULL
            AND released_at < $1
          RETURNING 1
        ), expired AS (
          DELETE FROM history_retention_leases
          WHERE released_at IS NULL
            AND expires_at < $1
          RETURNING 1
        )
        SELECT (SELECT count(*) FROM expired),
               (SELECT count(*) FROM released)
        """
        timestamptzParam
        ( D.singleRow
            ( HistoryRetentionPruneResult
                <$> column D.int8
                <*> column D.int8
            )
        )

activeConflictStmt :: Statement () (Maybe HistoryRetentionConflict)
activeConflictStmt =
    preparable
        """
        SELECT count(*), min(expires_at)
        FROM history_retention_leases
        WHERE released_at IS NULL
          AND expires_at > clock_timestamp()
        HAVING count(*) > 0
        """
        E.noParams
        ( D.rowMaybe
            ( HistoryRetentionConflict
                <$> column D.int8
                <*> column D.timestamptz
            )
        )

lockStreamHistoryStmt :: Statement Text (Maybe StreamInfo)
lockStreamHistoryStmt =
    preparable
        """
        SELECT stream_id, stream_name, stream_version,
               created_at, deleted_at, truncate_before
        FROM streams
        WHERE stream_name = $1
        FOR SHARE
        """
        textParam
        (D.rowMaybe streamInfoRow)

lockAffectedStreamsForHardDeleteStmt :: Statement Int64 (Vector Int64)
lockAffectedStreamsForHardDeleteStmt =
    preparable
        """
        WITH affected_streams AS (
          SELECT $1::bigint AS stream_id
          UNION
          SELECT stream_events.stream_id
          FROM stream_events
          WHERE stream_events.original_stream_id = $1
            AND stream_events.stream_id <> 0
        )
        SELECT streams.stream_id
        FROM streams
        JOIN affected_streams USING (stream_id)
        ORDER BY streams.stream_id
        FOR UPDATE OF streams
        """
        int8Param
        (D.rowVector (column D.int8))

leaseRow :: D.Row HistoryRetentionLease
leaseRow =
    makeLease
        <$> column D.uuid
        <*> column D.text
        <*> column D.text
        <*> column D.int8
        <*> column D.timestamptz
        <*> column D.timestamptz
        <*> column D.timestamptz
        <*> D.column (D.nullable D.timestamptz)
        <*> column D.text

makeLease :: UUID -> Text -> Text -> Int64 -> UTCTime -> UTCTime -> UTCTime -> Maybe UTCTime -> Text -> HistoryRetentionLease
makeLease leaseUuid ownerText reasonText frontier created renewed expires released stateText =
    HistoryRetentionLease
        { leaseId = HistoryRetentionLeaseId leaseUuid
        , owner = requireValidated (mkHistoryRetentionLeaseOwner ownerText)
        , reason = requireValidated (mkHistoryRetentionLeaseReason reasonText)
        , protectedThrough = GlobalPosition frontier
        , createdAt = created
        , renewedAt = renewed
        , expiresAt = expires
        , releasedAt = released
        , state = case stateText of
            "active" -> HistoryRetentionLeaseActive
            "expired" -> HistoryRetentionLeaseExpired
            "released" -> HistoryRetentionLeaseReleased
            unexpected -> error ("unknown history retention lease state from database: " <> show unexpected)
        }

streamInfoRow :: D.Row StreamInfo
streamInfoRow =
    StreamInfo
        <$> (StreamId <$> column D.int8)
        <*> (StreamName <$> column D.text)
        <*> (StreamVersion <$> column D.int8)
        <*> column D.timestamptz
        <*> D.column (D.nullable D.timestamptz)
        <*> (StreamVersion <$> column D.int8)

requireValidated :: Either error value -> value
requireValidated = either (const (error "database returned an invalid history retention lease")) (\value -> value)

column :: D.Value value -> D.Row value
column = D.column . D.nonNullable

textParam :: E.Params Text
textParam = E.param (E.nonNullable E.text)

uuidParam :: E.Params UUID
uuidParam = E.param (E.nonNullable E.uuid)

int4Param :: E.Params Int32
int4Param = E.param (E.nonNullable E.int4)

int8Param :: E.Params Int64
int8Param = E.param (E.nonNullable E.int8)

intervalParam :: E.Params DiffTime
intervalParam = E.param (E.nonNullable E.interval)

timestamptzParam :: E.Params UTCTime
timestamptzParam = E.param (E.nonNullable E.timestamptz)