packages feed

glean-hs-0.1.0: haskell/src/Glean/Storage.hs

-- | Storage typeclass for glean-hs.
--
-- A clean, minimal reimplementation of Glean's Storage abstraction.
-- No Meta internal dependencies (no ODS, ServiceData, Util.FFI).
--
-- The Storage typeclass defines the interface between the Glean
-- Haskell layer and the underlying database backend (RocksDB).
--
-- Implementations:
--   Glean.RocksDB   — production storage via Rust substrate
--   Glean.Memory    — in-memory storage for testing (future)

module Glean.Storage
  ( -- * Storage typeclass
    Storage (..)

    -- * Database configuration
  , DbConfig (..)
  , defaultDbConfig

    -- * Fact batch
  , FactBatch (..)
  , emptyBatch
  , batchSize

    -- * Predicate statistics
  , PredicateStats (..)
  , emptyStats

    -- * Database properties
  , DbProperties (..)

    -- * Errors
  , StorageError (..)

    -- * Utilities
  , withStorage
  ) where

import Control.Exception (Exception, bracket)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Int (Int64)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import Data.Word (Word64)

-- ── Errors ────────────────────────────────────────────────────────────────────

-- | Errors that can occur during storage operations.
data StorageError
  = StorageOpenFailed Text     -- ^ Failed to open database
  | StorageWriteFailed Text    -- ^ Failed to write facts
  | StorageReadFailed Text     -- ^ Failed to read facts
  | StorageCloseFailed Text    -- ^ Failed to close database
  | StorageCorrupted Text      -- ^ Database corruption detected
  | StorageVersionMismatch Int64 Int64
      -- ^ Schema version mismatch (expected, actual)
  deriving (Show)

instance Exception StorageError

-- ── Configuration ─────────────────────────────────────────────────────────────

-- | Configuration for opening a database.
data DbConfig = DbConfig
  { dbPath        :: FilePath
    -- ^ Path to the database directory on disk.
  , dbReadOnly    :: Bool
    -- ^ Open in read-only mode (no writes allowed).
  , dbCreate      :: Bool
    -- ^ Create the database if it doesn't exist.
  , dbCacheSize   :: Int
    -- ^ RocksDB block cache size in bytes (default: 128MB).
  , dbStartId     :: Word64
    -- ^ Starting fact ID. Use 1024 (Fid::LOWEST) for new databases.
  , dbVersion     :: Int64
    -- ^ Schema version number.
  } deriving (Show, Eq)

-- | Sensible defaults for a new read-write database.
defaultDbConfig :: FilePath -> DbConfig
defaultDbConfig path = DbConfig
  { dbPath      = path
  , dbReadOnly  = False
  , dbCreate    = True
  , dbCacheSize = 128 * 1024 * 1024  -- 128MB
  , dbStartId   = 1024               -- Fid::LOWEST
  , dbVersion   = 1
  }

-- ── Fact batch ────────────────────────────────────────────────────────────────

-- | A batch of serialized facts ready to store.
-- Facts are encoded using Glean's binary format (nat.rs, binary.rs).
data FactBatch = FactBatch
  { batchData      :: !ByteString
    -- ^ Binary-encoded fact data.
  , batchFirstId   :: !Word64
    -- ^ The fact ID of the first fact in this batch.
  , batchCount     :: !Int
    -- ^ Number of facts in this batch.
  , batchPredicates :: !(Map Word64 Int)
    -- ^ Map from predicate ID (Pid) to count of facts for that predicate.
  } deriving (Show, Eq)

-- | An empty fact batch.
emptyBatch :: Word64 -> FactBatch
emptyBatch firstId = FactBatch
  { batchData       = BS.empty
  , batchFirstId    = firstId
  , batchCount      = 0
  , batchPredicates = Map.empty
  }

-- | Total number of facts in a batch.
batchSize :: FactBatch -> Int
batchSize = batchCount

-- ── Predicate statistics ──────────────────────────────────────────────────────

-- | Statistics for a single predicate.
data PredicateStats = PredicateStats
  { statsCount     :: !Int
    -- ^ Number of facts for this predicate.
  , statsFirstId   :: !Word64
    -- ^ First fact ID for this predicate.
  , statsLastId    :: !Word64
    -- ^ Last fact ID for this predicate.
  } deriving (Show, Eq)

-- | Empty predicate statistics.
emptyStats :: PredicateStats
emptyStats = PredicateStats
  { statsCount   = 0
  , statsFirstId = 0
  , statsLastId  = 0
  }

-- ── Database properties ───────────────────────────────────────────────────────

-- | Properties of an open database.
data DbProperties = DbProperties
  { propVersion    :: !Int64
    -- ^ Schema version number.
  , propFirstId    :: !Word64
    -- ^ First fact ID in the database.
  , propFirstFreeId :: !Word64
    -- ^ Next available fact ID.
  , propFactCount  :: !Int
    -- ^ Total number of facts stored.
  } deriving (Show, Eq)

-- ── Storage typeclass ─────────────────────────────────────────────────────────

-- | Abstract storage backend for a Glean database.
--
-- All operations are in IO and may throw 'StorageError'.
--
-- Minimal complete definition: 'open', 'close', 'store', 'retrieve'.
class Storage s where

  -- | Open a database with the given configuration.
  -- Throws 'StorageOpenFailed' if the database cannot be opened.
  open :: DbConfig -> IO s

  -- | Close a database, flushing any pending writes.
  -- Throws 'StorageCloseFailed' if close fails.
  close :: s -> IO ()

  -- | Store a batch of facts.
  -- Throws 'StorageWriteFailed' if the write fails.
  store :: s -> FactBatch -> IO ()

  -- | Retrieve all stored facts.
  -- Returns Nothing if no facts have been stored yet.
  -- Throws 'StorageReadFailed' if the read fails.
  retrieve :: s -> IO (Maybe FactBatch)

  -- | Commit pending writes to durable storage.
  -- Default implementation: no-op (some backends auto-commit).
  commit :: s -> IO ()
  commit _ = return ()

  -- | Get statistics for all predicates.
  predicateStats :: s -> IO (Map Word64 PredicateStats)
  predicateStats _ = return Map.empty

  -- | Get database properties.
  properties :: s -> IO DbProperties
  properties _ = return $ DbProperties
    { propVersion     = 1
    , propFirstId     = 1024
    , propFirstFreeId = 1024
    , propFactCount   = 0
    }

  -- | Optimize the database (compact, etc.).
  -- Default implementation: no-op.
  optimize :: s -> IO ()
  optimize _ = return ()

  -- | Flush in-memory data to disk.
  -- Default implementation: no-op.
  flush :: s -> IO ()
  flush _ = return ()

  -- | Create a backup of the database at the given path.
  backup :: s -> FilePath -> IO ()
  backup _ _ = return ()

-- ── Utility ───────────────────────────────────────────────────────────────────

-- | Open a database, run an action, and close it safely.
-- Ensures 'close' is called even if the action throws.
--
-- Example:
-- @
-- withStorage (defaultDbConfig "\/tmp\/mydb") $ \db -> do
--   store db myBatch
--   retrieve db
-- @
withStorage :: Storage s => DbConfig -> (s -> IO a) -> IO a
withStorage config = bracket (open config) close