bolty-0.2.0.0: src/Database/Bolty/Connection.hs
-- | Internal module. Not part of the public API.
module Database.Bolty.Connection
( queryIO
, queryPIO
, queryIO'
, queryPIO'
, queryWithFieldsIO
, queryPWithFieldsIO
, queryPMetaIO
, requestResponseRunIO
, requestResponsePullIO
, withTxMode
) where
import Prelude
import Database.Bolty.AccessMode (AccessMode)
import qualified Database.Bolty.Connection.Pipe as P
import Database.Bolty.Connection.Type
import Database.Bolty.Logging
import Database.Bolty.Message.Request (Begin(Begin))
import Database.Bolty.Record
import Control.Exception (SomeException, onException, throwIO, try)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Kind (Type)
import Data.Text (Text)
import Data.Word (Word64)
import Debug.Trace (traceEventIO)
import GHC.Clock (getMonotonicTimeNSec)
import GHC.Stack (HasCallStack)
import Database.Bolty.Message.Response
import Database.Bolty.Message.Request
import qualified Data.HashMap.Lazy as H
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.Vector.Mutable as MV
import Data.PackStream.Ps (Ps, fromPs)
import Data.PackStream.Result (Result(..))
-- ---------------------------------------------------------------------------
-- Growable vector (O(n) amortized appends via mutable IOVector + doubling)
-- ---------------------------------------------------------------------------
type GrowVec :: Type -> Type
type role GrowVec representational
data GrowVec a = GrowVec
{ gvBuf :: !(IORef (MV.IOVector a))
, gvLen :: !(IORef Int)
}
newGrowVec :: IO (GrowVec a)
newGrowVec = do
buf <- MV.new 64
GrowVec <$> newIORef buf <*> newIORef 0
pushGrowVec :: GrowVec a -> a -> IO ()
pushGrowVec GrowVec{gvBuf, gvLen} x = do
i <- readIORef gvLen
buf <- readIORef gvBuf
buf' <- if i >= MV.length buf
then MV.unsafeGrow buf (MV.length buf)
else pure buf
MV.write buf' i x
writeIORef gvBuf buf'
writeIORef gvLen (i + 1)
freezeGrowVec :: GrowVec a -> IO (V.Vector a)
freezeGrowVec GrowVec{gvBuf, gvLen} = do
n <- readIORef gvLen
buf <- readIORef gvBuf
V.unsafeFreeze (MV.take n buf)
-- | Run a query, return only records.
queryIO :: HasCallStack => Connection -> Text -> IO (V.Vector Record)
queryIO conn cypher = records <$> queryPIO' conn cypher H.empty
-- | Run a parameterized query, return only records.
queryPIO :: HasCallStack => Connection -> Text -> H.HashMap Text Ps -> IO (V.Vector Record)
queryPIO conn cypher params = records <$> queryPIO' conn cypher params
-- | Run a query, return full response.
queryIO' :: HasCallStack => Connection -> Text -> IO SuccessPull
queryIO' conn cypher = queryPIO' conn cypher H.empty
-- | Run a parameterized query, return full response.
queryPIO' :: HasCallStack => Connection -> Text -> H.HashMap Text Ps -> IO SuccessPull
queryPIO' conn cypher params = do
(runResp, pullResp, clientNs) <- timedQuery conn cypher params
fireLogger conn cypher params runResp pullResp clientNs
fireNotifications conn pullResp
pure pullResp
-- | Send RUN and receive the run response.
requestResponseRunIO :: HasCallStack => Connection -> Text -> H.HashMap Text Ps -> IO SuccessRun
requestResponseRunIO conn cypher params = do
st <- P.getState conn
P.requireStateIO conn [Ready, TXready] "RUN"
let request = case st of
TXready -> RRunExplicitTransaction $ RunExplicitTransaction cypher params defaultRunExtra
_ -> RRunAutoCommitTransaction $ mkRunAutoCommit cypher params
P.flushIO conn request
response <- P.fetchIO conn
case response of
RSuccess meta -> do
P.setState conn $ case st of
TXready -> TXstreaming
_ -> Streaming
case makeResponseRunAutoCommitTransaction meta of
Left err -> throwIO $ WrongMessageFormat err
Right v -> pure v
RIgnored -> do
P.reset conn
throwIO ResponseErrorIgnored
RFailure Failure{code, message} -> do
P.setState conn Failed
P.reset conn
throwIO $ ResponseErrorFailure code message
RRecord _ -> do
P.reset conn
throwIO ResponseErrorRecords
-- | Pull all records from an in-progress query.
requestResponsePullIO :: HasCallStack => Connection -> IO SuccessPull
requestResponsePullIO conn = do
P.requireStateIO conn [Streaming, TXstreaming] "PULL"
let pull = Pull { n = conn.fetchSize, qid = Nothing }
P.flushIO conn $ RPull pull
gv <- newGrowVec
go gv
where
go gv = do
response <- P.fetchIO conn
case response of
RSuccess meta -> do
let has_more = case H.lookup "has_more" meta of
Just hm -> case fromPs hm of
Success True -> True
_ -> False
Nothing -> False
if has_more then do
let pull = Pull { n = conn.fetchSize, qid = Nothing }
P.flushIO conn $ RPull pull
go gv
else do
recs <- freezeGrowVec gv
st <- P.getState conn
P.setState conn $ case st of
TXstreaming -> TXready
_ -> Ready
case makeSuccessPull recs meta of
Left err -> throwIO $ WrongMessageFormat err
Right v -> pure v
RIgnored -> do
P.reset conn
throwIO ResponseErrorIgnored
RFailure Failure{code, message} -> do
P.setState conn Failed
P.reset conn
throwIO $ ResponseErrorFailure code message
RRecord record -> do
pushGrowVec gv record
go gv
-- | Run a parameterized query, returning column names alongside records.
queryPWithFieldsIO :: HasCallStack => Connection -> T.Text -> H.HashMap T.Text Ps -> IO (V.Vector T.Text, V.Vector Record)
queryPWithFieldsIO conn cypher params = do
(runResp, pullResp, clientNs) <- timedQuery conn cypher params
fireLogger conn cypher params runResp pullResp clientNs
fireNotifications conn pullResp
pure (successFields runResp, records pullResp)
-- | Run a query, returning column names alongside records.
queryWithFieldsIO :: HasCallStack => Connection -> T.Text -> IO (V.Vector T.Text, V.Vector Record)
queryWithFieldsIO conn cypher = queryPWithFieldsIO conn cypher H.empty
-- | Run a parameterized query, returning field names, records, and metadata.
queryPMetaIO :: HasCallStack => Connection -> T.Text -> H.HashMap T.Text Ps -> IO (V.Vector T.Text, V.Vector Record, QueryMeta)
queryPMetaIO conn cypher params = do
(runResp, pullResp, clientNs) <- timedQuery conn cypher params
fireLogger conn cypher params runResp pullResp clientNs
fireNotifications conn pullResp
pure (successFields runResp, records pullResp, infos pullResp)
-- ---------------------------------------------------------------------------
-- Internal: timing + logging
-- ---------------------------------------------------------------------------
-- | Run a query with monotonic clock timing, returning the responses and elapsed nanoseconds.
-- Uses Bolt pipelining: sends RUN + PULL before reading either response,
-- saving one network round-trip compared to the sequential approach.
timedQuery :: HasCallStack => Connection -> T.Text -> H.HashMap T.Text Ps -> IO (SuccessRun, SuccessPull, Word64)
timedQuery conn cypher params = do
traceEventIO "START boltWireIO"
t0 <- getMonotonicTimeNSec
-- Build RUN request
st <- P.getState conn
P.requireStateIO conn [Ready, TXready] "RUN"
let runRequest = case st of
TXready -> RRunExplicitTransaction $ RunExplicitTransaction cypher params defaultRunExtra
_ -> RRunAutoCommitTransaction $ mkRunAutoCommit cypher params
-- Pipeline: send RUN + PULL before receiving either response
let pull = Pull { n = conn.fetchSize, qid = Nothing }
P.flushIO conn runRequest
P.flushIO conn (RPull pull)
-- Receive RUN response
runResult <- try @SomeException $ do
runResponse <- P.fetchIO conn
case runResponse of
RSuccess meta -> do
P.setState conn $ case st of
TXready -> TXstreaming
_ -> Streaming
case makeResponseRunAutoCommitTransaction meta of
Left err -> throwIO $ WrongMessageFormat err
Right v -> pure v
RFailure Failure{code, message} -> do
P.setState conn Failed
throwIO $ ResponseErrorFailure code message
RIgnored ->
throwIO ResponseErrorIgnored
RRecord _ ->
throwIO ResponseErrorRecords
case runResult of
Left err -> do
-- RUN failed: consume the IGNORED response for the pipelined PULL
_ <- P.fetchIO conn
P.reset conn
throwIO err
Right runResp -> do
-- Receive PULL responses
gv <- newGrowVec
pullResp <- pullRecords conn gv
t1 <- getMonotonicTimeNSec
traceEventIO "STOP boltWireIO"
pure (runResp, pullResp, t1 - t0)
-- | Pull all records from an in-progress pipelined query.
-- The initial PULL has already been sent; this receives responses and handles has_more pagination.
pullRecords :: HasCallStack => Connection -> GrowVec Record -> IO SuccessPull
pullRecords conn gv = do
response <- P.fetchIO conn
case response of
RSuccess meta -> do
let has_more = case H.lookup "has_more" meta of
Just hm -> case fromPs hm of
Success True -> True
_ -> False
Nothing -> False
if has_more then do
let pull = Pull { n = conn.fetchSize, qid = Nothing }
P.flushIO conn $ RPull pull
pullRecords conn gv
else do
recs <- freezeGrowVec gv
st <- P.getState conn
P.setState conn $ case st of
TXstreaming -> TXready
_ -> Ready
case makeSuccessPull recs meta of
Left err -> throwIO $ WrongMessageFormat err
Right v -> pure v
RIgnored -> do
P.reset conn
throwIO ResponseErrorIgnored
RFailure Failure{code, message} -> do
P.setState conn Failed
P.reset conn
throwIO $ ResponseErrorFailure code message
RRecord record -> do
pushGrowVec gv record
pullRecords conn gv
-- | Fire the query logger callback if configured.
fireLogger :: Connection -> T.Text -> H.HashMap T.Text Ps -> SuccessRun -> SuccessPull -> Word64 -> IO ()
fireLogger Connection{queryLogger} cypher params runResp SuccessPull{records, infos} clientNs =
case queryLogger of
Nothing -> pure ()
Just logger -> logger QueryLog
{ qlCypher = cypher
, qlParameters = params
, qlRowCount = V.length records
, qlServerFirst = successTFirst runResp
, qlServerLast = t_last infos
, qlClientTime = fromIntegral clientNs / 1_000_000
} infos
-- | Fire the notification handler for each parsed notification, if configured.
fireNotifications :: Connection -> SuccessPull -> IO ()
fireNotifications Connection{notificationHandler} SuccessPull{infos = QueryMeta{parsedNotifications}} =
case notificationHandler of
Nothing -> pure ()
Just handler -> V.mapM_ handler parsedNotifications
-- | Run an action inside an explicit transaction opened with the given
-- 'AccessMode'. Automatically commits on success and attempts a rollback
-- on failure.
--
-- The mode controls cluster routing: 'ReadAccess' routes to a follower
-- and engages the server's read-only enforcement; 'WriteAccess' routes
-- to the leader. Higher-level helpers in "Database.Bolty"
-- ('Database.Bolty.withTransaction' / 'Database.Bolty.withReadTransaction')
-- are thin wrappers over this primitive.
withTxMode :: HasCallStack => AccessMode -> Connection -> (Connection -> IO a) -> IO a
withTxMode mode conn action = do
P.beginTx conn (Begin V.empty Nothing H.empty mode Nothing Nothing)
result <- action conn `onException` P.tryRollback conn
_ <- P.commitTx conn
pure result