duckdb-simple-0.1.5.0: src/Database/DuckDB/Simple/Copy.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{- |
Module : Database.DuckDB.Simple.Copy
Description : High-level wrappers for DuckDB custom COPY functions.
-}
module Database.DuckDB.Simple.Copy (
CopyBindInfo (..),
CopyInitInfo (..),
CopySinkInfo (..),
CopyFinalizeInfo (..),
registerCopyToFunction,
) where
import Control.Exception (SomeException, bracket, displayException, onException, try)
import Control.Monad (forM, when)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Foreign as TextForeign
import Database.DuckDB.FFI
import Database.DuckDB.Simple.FromField (Field (..))
import Database.DuckDB.Simple.Internal (Connection, destroyLogicalType, mkDeleteCallback, releaseStablePtrData, throwRegistrationError, withConnectionHandle)
import Database.DuckDB.Simple.Materialize (materializeValue)
import Foreign.C.String (CString, peekCString)
import Foreign.Marshal.Alloc (alloca)
import Foreign.Ptr (Ptr, freeHaskellFunPtr, nullPtr)
import Foreign.StablePtr (StablePtr, castPtrToStablePtr, castStablePtrToPtr, deRefStablePtr, freeStablePtr, newStablePtr)
import Foreign.Storable (poke)
-- | Bind-phase metadata for a custom `COPY ... TO` function.
data CopyBindInfo = CopyBindInfo
{ copyBindColumnTypes :: ![DuckDBType]
}
deriving (Eq, Show)
-- | Init-phase inputs for a custom `COPY ... TO` function.
data CopyInitInfo bindState = CopyInitInfo
{ copyInitBindState :: !bindState
, copyInitFilePath :: !FilePath
}
-- | Sink-phase inputs for a custom `COPY ... TO` function.
data CopySinkInfo bindState globalState = CopySinkInfo
{ copySinkBindState :: !bindState
, copySinkGlobalState :: !globalState
}
-- | Finalize-phase inputs for a custom `COPY ... TO` function.
data CopyFinalizeInfo bindState globalState = CopyFinalizeInfo
{ copyFinalizeBindState :: !bindState
, copyFinalizeGlobalState :: !globalState
}
data CopyFunctionResources = CopyFunctionResources
{ copyBindPtr :: !DuckDBCopyFunctionBindFun
, copyInitPtr :: !DuckDBCopyFunctionGlobalInitFun
, copySinkPtr :: !DuckDBCopyFunctionSinkFun
, copyFinalizePtr :: !DuckDBCopyFunctionFinalizeFun
, copyStateDestroyPtr :: !DuckDBDeleteCallback
}
-- | Register a custom `COPY ... TO` implementation backed by Haskell callbacks.
registerCopyToFunction ::
forall bindState globalState.
Connection ->
Text ->
(CopyBindInfo -> IO bindState) ->
(CopyInitInfo bindState -> IO globalState) ->
(CopySinkInfo bindState globalState -> [[Field]] -> IO ()) ->
(CopyFinalizeInfo bindState globalState -> IO ()) ->
IO ()
registerCopyToFunction conn name bindFn initFn sinkFn finalizeFn = do
stateDestroyCb <- mkDeleteCallback releaseStablePtrData
bindPtr <- mkCopyBindFun (copyBindHandler stateDestroyCb bindFn)
initPtr <- mkCopyGlobalInitFun (copyGlobalInitHandler stateDestroyCb initFn)
sinkPtr <- mkCopySinkFun (copySinkHandler sinkFn)
finalizePtr <- mkCopyFinalizeFun (copyFinalizeHandler finalizeFn)
resources <-
newStablePtr
CopyFunctionResources
{ copyBindPtr = bindPtr
, copyInitPtr = initPtr
, copySinkPtr = sinkPtr
, copyFinalizePtr = finalizePtr
, copyStateDestroyPtr = stateDestroyCb
}
destroyCb <- mkDeleteCallback releaseCopyResources
let release =
freeHaskellFunPtr bindPtr
>> freeHaskellFunPtr initPtr
>> freeHaskellFunPtr sinkPtr
>> freeHaskellFunPtr finalizePtr
>> freeHaskellFunPtr stateDestroyCb
>> freeStablePtr resources
>> freeHaskellFunPtr destroyCb
bracket c_duckdb_create_copy_function destroyCopyFunction \copyFun ->
(`onException` release) $ do
TextForeign.withCString name \cName ->
c_duckdb_copy_function_set_name copyFun cName
c_duckdb_copy_function_set_bind copyFun bindPtr
c_duckdb_copy_function_set_global_init copyFun initPtr
c_duckdb_copy_function_set_sink copyFun sinkPtr
c_duckdb_copy_function_set_finalize copyFun finalizePtr
c_duckdb_copy_function_set_extra_info copyFun (castStablePtrToPtr resources) destroyCb
withConnectionHandle conn \connPtr -> do
rc <- c_duckdb_register_copy_function connPtr copyFun
if rc == DuckDBSuccess
then pure ()
else throwRegistrationError "register copy function"
copyBindHandler ::
forall bindState.
DuckDBDeleteCallback ->
(CopyBindInfo -> IO bindState) ->
DuckDBCopyFunctionBindInfo ->
IO ()
copyBindHandler destroyCb bindFn info = do
outcome <- try do
copyBindColumnTypes <- fetchColumnTypes info
bindState <- bindFn CopyBindInfo{copyBindColumnTypes}
stable <- newStablePtr bindState
c_duckdb_copy_function_bind_set_bind_data info (castStablePtrToPtr stable) destroyCb
reportCopyError c_duckdb_copy_function_bind_set_error info outcome
copyGlobalInitHandler ::
forall bindState globalState.
DuckDBDeleteCallback ->
(CopyInitInfo bindState -> IO globalState) ->
DuckDBCopyFunctionGlobalInitInfo ->
IO ()
copyGlobalInitHandler destroyCb initFn info = do
outcome <- try do
rawBindState <- c_duckdb_copy_function_global_init_get_bind_data info
when (rawBindState == nullPtr) $
throwRegistrationError "missing copy bind state"
bindState <- deRefStablePtr (castPtrToStablePtr rawBindState :: StablePtr bindState)
pathPtr <- c_duckdb_copy_function_global_init_get_file_path info
filePath <-
if pathPtr == nullPtr
then pure ""
else peekCString pathPtr
globalState <- initFn CopyInitInfo{copyInitBindState = bindState, copyInitFilePath = filePath}
stable <- newStablePtr globalState
c_duckdb_copy_function_global_init_set_global_state info (castStablePtrToPtr stable) destroyCb
reportCopyError c_duckdb_copy_function_global_init_set_error info outcome
copySinkHandler ::
forall bindState globalState.
(CopySinkInfo bindState globalState -> [[Field]] -> IO ()) ->
DuckDBCopyFunctionSinkInfo ->
DuckDBDataChunk ->
IO ()
copySinkHandler sinkFn info chunk = do
outcome <- try do
bindState <- readStablePtrState c_duckdb_copy_function_sink_get_bind_data info
globalState <- readStablePtrState c_duckdb_copy_function_sink_get_global_state info
rows <- materializeChunkRows chunk
sinkFn CopySinkInfo{copySinkBindState = bindState, copySinkGlobalState = globalState} rows
reportCopyError c_duckdb_copy_function_sink_set_error info outcome
copyFinalizeHandler ::
forall bindState globalState.
(CopyFinalizeInfo bindState globalState -> IO ()) ->
DuckDBCopyFunctionFinalizeInfo ->
IO ()
copyFinalizeHandler finalizeFn info = do
outcome <- try do
bindState <- readStablePtrState c_duckdb_copy_function_finalize_get_bind_data info
globalState <- readStablePtrState c_duckdb_copy_function_finalize_get_global_state info
finalizeFn CopyFinalizeInfo{copyFinalizeBindState = bindState, copyFinalizeGlobalState = globalState}
reportCopyError c_duckdb_copy_function_finalize_set_error info outcome
reportCopyError :: (i -> CString -> IO ()) -> i -> Either SomeException () -> IO ()
reportCopyError _ _ (Right ()) = pure ()
reportCopyError setError info (Left err) =
TextForeign.withCString (Text.pack (displayException err)) \cMsg ->
setError info cMsg
fetchColumnTypes :: DuckDBCopyFunctionBindInfo -> IO [DuckDBType]
fetchColumnTypes info = do
count <- c_duckdb_copy_function_bind_get_column_count info
let indices = [0 .. fromIntegral count - 1] :: [Int]
forM indices \idx -> do
logical <- c_duckdb_copy_function_bind_get_column_type info (fromIntegral idx)
dtype <- c_duckdb_get_type_id logical
destroyLogicalType logical
pure dtype
readStablePtrState :: forall a i. (i -> IO (Ptr ())) -> i -> IO a
readStablePtrState getter info = do
rawPtr <- getter info
if rawPtr == nullPtr
then throwRegistrationError "missing copy callback state"
else deRefStablePtr (castPtrToStablePtr rawPtr :: StablePtr a)
materializeChunkRows :: DuckDBDataChunk -> IO [[Field]]
materializeChunkRows chunk = do
rawColumnCount <- c_duckdb_data_chunk_get_column_count chunk
let columnCount = fromIntegral rawColumnCount :: Int
readers <- mapM (makeColumnReader chunk) [0 .. columnCount - 1]
rawRowCount <- c_duckdb_data_chunk_get_size chunk
let rowCount = fromIntegral rawRowCount :: Int
forM [0 .. rowCount - 1] \row ->
forM readers \reader ->
reader (fromIntegral row)
type ColumnReader = DuckDBIdx -> IO Field
makeColumnReader :: DuckDBDataChunk -> Int -> IO ColumnReader
makeColumnReader chunk columnIndex = do
vector <- c_duckdb_data_chunk_get_vector chunk (fromIntegral columnIndex)
logical <- c_duckdb_vector_get_column_type vector
dtype <- c_duckdb_get_type_id logical
destroyLogicalType logical
dataPtr <- c_duckdb_vector_get_data vector
validity <- c_duckdb_vector_get_validity vector
let name = Text.pack ("column" <> show columnIndex)
pure \rowIdx -> do
fieldValue <- materializeValue dtype vector dataPtr validity (fromIntegral rowIdx)
pure Field{fieldName = name, fieldIndex = columnIndex, fieldValue}
releaseCopyResources :: Ptr () -> IO ()
releaseCopyResources rawPtr =
when (rawPtr /= nullPtr) $ do
let stablePtr = castPtrToStablePtr rawPtr :: StablePtr CopyFunctionResources
CopyFunctionResources{copyBindPtr, copyInitPtr, copySinkPtr, copyFinalizePtr, copyStateDestroyPtr} <- deRefStablePtr stablePtr
freeHaskellFunPtr copyBindPtr
freeHaskellFunPtr copyInitPtr
freeHaskellFunPtr copySinkPtr
freeHaskellFunPtr copyFinalizePtr
freeHaskellFunPtr copyStateDestroyPtr
freeStablePtr stablePtr
destroyCopyFunction :: DuckDBCopyFunction -> IO ()
destroyCopyFunction copyFun =
alloca \ptr -> poke ptr copyFun >> c_duckdb_destroy_copy_function ptr
foreign import ccall "wrapper"
mkCopyBindFun :: (DuckDBCopyFunctionBindInfo -> IO ()) -> IO DuckDBCopyFunctionBindFun
foreign import ccall "wrapper"
mkCopyGlobalInitFun :: (DuckDBCopyFunctionGlobalInitInfo -> IO ()) -> IO DuckDBCopyFunctionGlobalInitFun
foreign import ccall "wrapper"
mkCopySinkFun :: (DuckDBCopyFunctionSinkInfo -> DuckDBDataChunk -> IO ()) -> IO DuckDBCopyFunctionSinkFun
foreign import ccall "wrapper"
mkCopyFinalizeFun :: (DuckDBCopyFunctionFinalizeInfo -> IO ()) -> IO DuckDBCopyFunctionFinalizeFun