packages feed

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

-- | Typed plan and profile trees from EXPLAIN\/PROFILE queries.
module Database.Bolty.Plan
  ( PlanNode(..)
  , ProfileNode(..)
  , parsePlan
  , parseProfile
  , renderPlan
  ) 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 qualified Data.Vector                   as V

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


-- | A node in the query execution plan tree (from EXPLAIN).
type PlanNode :: Type
data PlanNode = PlanNode
  { pnOperatorType :: !T.Text
  , pnArguments    :: !(H.HashMap T.Text Ps)
  , pnIdentifiers  :: !(V.Vector T.Text)
  , pnEstimatedRows :: !Double
  , pnChildren     :: !(V.Vector PlanNode)
  } deriving stock (Show, Eq)


-- | A node in the query profile tree (from PROFILE).
-- Extends 'PlanNode' with actual execution statistics.
type ProfileNode :: Type
data ProfileNode = ProfileNode
  { prOperatorType    :: !T.Text
  , prArguments       :: !(H.HashMap T.Text Ps)
  , prIdentifiers     :: !(V.Vector T.Text)
  , prEstimatedRows   :: !Double
  , prDbHits          :: !Int64
  , prRows            :: !Int64
  , prPageCacheHits   :: !Int64
  , prPageCacheMisses :: !Int64
  , prTime            :: !Int64              -- ^ microseconds
  , prChildren        :: !(V.Vector ProfileNode)
  } deriving stock (Show, Eq)


-- | Parse the raw @plan@ field from PULL SUCCESS metadata into a typed 'PlanNode' tree.
parsePlan :: Maybe Ps -> Maybe PlanNode
parsePlan Nothing = Nothing
parsePlan (Just (PsDictionary m)) = parsePlanDict m
parsePlan (Just _) = Nothing


-- | Parse the raw @profile@ field from PULL SUCCESS metadata into a typed 'ProfileNode' tree.
parseProfile :: Maybe Ps -> Maybe ProfileNode
parseProfile Nothing = Nothing
parseProfile (Just (PsDictionary m)) = parseProfileDict m
parseProfile (Just _) = Nothing


parsePlanDict :: H.HashMap T.Text Ps -> Maybe PlanNode
parsePlanDict m = do
  opType <- lookupText "operatorType" m
  let args = case H.lookup "args" m of
               Just (PsDictionary a) -> a
               _                     -> H.empty
  let idents = case H.lookup "identifiers" m of
                 Just (PsList v) -> V.mapMaybe extractText v
                 _               -> V.empty
  let estRows = lookupDouble "estimatedRows" m
  let children = case H.lookup "children" m of
                   Just (PsList v) -> V.mapMaybe parsePlanPs v
                   _               -> V.empty
  Just PlanNode
    { pnOperatorType  = opType
    , pnArguments     = args
    , pnIdentifiers   = idents
    , pnEstimatedRows = estRows
    , pnChildren      = children
    }


parsePlanPs :: Ps -> Maybe PlanNode
parsePlanPs (PsDictionary m) = parsePlanDict m
parsePlanPs _ = Nothing


parseProfileDict :: H.HashMap T.Text Ps -> Maybe ProfileNode
parseProfileDict m = do
  opType <- lookupText "operatorType" m
  let args = case H.lookup "args" m of
               Just (PsDictionary a) -> a
               _                     -> H.empty
  let idents = case H.lookup "identifiers" m of
                 Just (PsList v) -> V.mapMaybe extractText v
                 _               -> V.empty
  let estRows = lookupDouble "estimatedRows" m
  let dbHits = lookupInt64OrZero "dbHits" m
  let rows = lookupInt64OrZero "rows" m
  let pcHits = lookupInt64OrZero "pageCacheHits" m
  let pcMisses = lookupInt64OrZero "pageCacheMisses" m
  let time = lookupInt64OrZero "time" m
  let children = case H.lookup "children" m of
                   Just (PsList v) -> V.mapMaybe parseProfilePs v
                   _               -> V.empty
  Just ProfileNode
    { prOperatorType    = opType
    , prArguments       = args
    , prIdentifiers     = idents
    , prEstimatedRows   = estRows
    , prDbHits          = dbHits
    , prRows            = rows
    , prPageCacheHits   = pcHits
    , prPageCacheMisses = pcMisses
    , prTime            = time
    , prChildren        = children
    }


parseProfilePs :: Ps -> Maybe ProfileNode
parseProfilePs (PsDictionary m) = parseProfileDict m
parseProfilePs _ = Nothing


-- Helpers

lookupText :: T.Text -> H.HashMap T.Text Ps -> Maybe T.Text
lookupText key m = case H.lookup key m of
  Just (PsString t) -> Just t
  _                 -> Nothing


extractText :: Ps -> Maybe T.Text
extractText (PsString t) = Just t
extractText _            = Nothing


lookupDouble :: T.Text -> H.HashMap T.Text Ps -> Double
lookupDouble key m = case H.lookup key m of
  Just (PsFloat d) -> d
  Just (PsInteger n) -> case fromPSInteger n of
    Just (i :: Int64) -> fromIntegral i
    Nothing           -> 0.0
  _ -> 0.0


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


-- | Render a 'PlanNode' tree as an indented text block. Each child is
-- indented two spaces deeper than its parent. Identifier vectors (the
-- @vars:@ line) are shown when non-empty so a reader can match
-- variables to operators.
--
-- Operator arguments ('pnArguments') are intentionally dropped for
-- terseness -- the operator name + estimated rows + identifiers are
-- enough to read most plans, and the args dictionary is verbose.
--
-- Pass @0@ for the initial @depth@. Suitable for printing the result
-- of 'queryExplain'.
--
-- Example output:
--
-- > + ProduceResults  (~1.0 rows)
-- >     vars: u, name
-- >   + Projection  (~1.0 rows)
-- >       vars: u, name
-- >     + NodeUniqueIndexSeek  (~1.0 rows)
-- >         vars: u
renderPlan :: Int -> PlanNode -> T.Text
renderPlan depth (PlanNode op _args idents est children) =
  let indent = T.replicate depth "  "
      header = indent <> "+ " <> op
             <> "  (~" <> T.pack (show est) <> " rows)"
      idLine = if V.null idents
                 then ""
                 else indent <> "    vars: " <> T.intercalate ", " (V.toList idents) <> "\n"
      kids   = T.concat $ V.toList $ V.map (renderPlan (depth + 1)) children
  in header <> "\n" <> idLine <> kids