packages feed

bolty-0.2.0.0: src/Database/Bolty/Stats.hs

-- | Query statistics (nodes created, relationships deleted, etc.).
module Database.Bolty.Stats
  ( QueryStats(..)
  , parseStats
  , formatStatsLine
  ) where

import           Prelude

import           Data.Int                      (Int64)
import           Data.Kind                     (Type)
import qualified Data.HashMap.Lazy             as H
import qualified Data.Text                     as T

import           Data.PackStream.Ps            (Ps(..))
import           Data.PackStream.Integer       (fromPSInteger)


-- | Typed query statistics from the PULL SUCCESS metadata.
-- All integer fields default to 0 and booleans to False when absent.
type QueryStats :: Type
data QueryStats = QueryStats
  { nodesCreated          :: !Int64
  , nodesDeleted          :: !Int64
  , relationshipsCreated  :: !Int64
  , relationshipsDeleted  :: !Int64
  , propertiesSet         :: !Int64
  , labelsAdded           :: !Int64
  , labelsRemoved         :: !Int64
  , indexesAdded           :: !Int64
  , indexesRemoved         :: !Int64
  , constraintsAdded      :: !Int64
  , constraintsRemoved    :: !Int64
  , containsUpdates       :: !Bool
  , containsSystemUpdates :: !Bool
  , systemUpdates         :: !Int64
  }
  deriving stock (Show, Eq)


-- | Parse the raw @stats@ field from PULL SUCCESS metadata.
-- Returns 'Nothing' if the field is absent (read-only queries).
-- The server uses hyphenated keys (e.g. @"nodes-created"@).
parseStats :: Maybe Ps -> Maybe QueryStats
parseStats Nothing = Nothing
parseStats (Just (PsDictionary m)) = Just QueryStats
  { nodesCreated          = intField "nodes-created" m
  , nodesDeleted          = intField "nodes-deleted" m
  , relationshipsCreated  = intField "relationships-created" m
  , relationshipsDeleted  = intField "relationships-deleted" m
  , propertiesSet         = intField "properties-set" m
  , labelsAdded           = intField "labels-added" m
  , labelsRemoved         = intField "labels-removed" m
  , indexesAdded           = intField "indexes-added" m
  , indexesRemoved         = intField "indexes-removed" m
  , constraintsAdded      = intField "constraints-added" m
  , constraintsRemoved    = intField "constraints-removed" m
  , containsUpdates       = boolField "contains-updates" m
  , containsSystemUpdates = boolField "contains-system-updates" m
  , systemUpdates         = intField "system-updates" m
  }
parseStats (Just _) = Nothing


intField :: T.Text -> H.HashMap T.Text Ps -> Int64
intField key m = case H.lookup key m of
  Just (PsInteger n) -> case fromPSInteger n of
    Just i  -> i
    Nothing -> 0
  _ -> 0


boolField :: T.Text -> H.HashMap T.Text Ps -> Bool
boolField key m = case H.lookup key m of
  Just (PsBoolean b) -> b
  _                  -> False


-- | One-line summary of every counter on a 'QueryStats' record. Format:
--
-- > Stats: +nodes=1 -nodes=0 +rels=0 -rels=0 props=1 +labels=1 -labels=0 +idx=0 -idx=0 +constr=0 -constr=0
--
-- Useful for diagnostic logging and CLI rollback messages where you want
-- the whole counter set on one line for terse comparison across runs.
formatStatsLine :: QueryStats -> T.Text
formatStatsLine s = T.concat
  [ "Stats: "
  , "+nodes=", T.pack (show (nodesCreated s))
  , " -nodes=", T.pack (show (nodesDeleted s))
  , " +rels=", T.pack (show (relationshipsCreated s))
  , " -rels=", T.pack (show (relationshipsDeleted s))
  , " props=", T.pack (show (propertiesSet s))
  , " +labels=", T.pack (show (labelsAdded s))
  , " -labels=", T.pack (show (labelsRemoved s))
  , " +idx=", T.pack (show (indexesAdded s))
  , " -idx=", T.pack (show (indexesRemoved s))
  , " +constr=", T.pack (show (constraintsAdded s))
  , " -constr=", T.pack (show (constraintsRemoved s))
  ]