packages feed

duckdb-ffi-1.4.1.2: test/ScalarFunctionsTest.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ForeignFunctionInterface #-}

module ScalarFunctionsTest (tests) where

import Control.Exception (bracket)
import Control.Monad (forM_, unless, when)
import Data.Char (toLower)
import Data.Int (Int32)
import Data.List (isInfixOf)
import Database.DuckDB.FFI
import Database.DuckDB.FFI.Deprecated
import Foreign.C.String (peekCString, withCString)
import Foreign.Marshal.Alloc (alloca, free, mallocBytes)
import Foreign.Ptr (Ptr, castPtr, nullPtr)
import Foreign.Storable (peek, peekElemOff, poke, pokeElemOff, sizeOf)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, testCase, (@?=))
import Utils (withConnection, withDatabase, withResultCString)

-- | Coverage of scalar function registration and bind/exec helpers.
tests :: TestTree
tests =
    testGroup
        "Scalar Functions"
        [ scalarFunctionRoundtrip
        , scalarFunctionSetFeatures
        ]

scalarFunctionRoundtrip :: TestTree
scalarFunctionRoundtrip =
    testCase "register custom scalar function and execute" $ do
        withDatabase \db ->
            withConnection db \conn -> do
                withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType -> do
                    funPtr <- mkScalarFun negateCallback
                    withScalarFunction \fun -> do
                        withCString "negate_int" $ \name -> c_duckdb_scalar_function_set_name fun name
                        c_duckdb_scalar_function_add_parameter fun intType
                        c_duckdb_scalar_function_set_return_type fun intType
                        c_duckdb_scalar_function_set_function fun funPtr

                        c_duckdb_register_scalar_function conn fun >>= (@?= DuckDBSuccess)

                        withCString "SELECT negate_int(5)" $ \sql ->
                            withResultCString conn sql $ \resPtr ->
                                c_duckdb_value_int32 resPtr 0 0 >>= (@?= (-5))

-- Callback ------------------------------------------------------------------

negateCallback :: DuckDBFunctionInfo -> DuckDBDataChunk -> DuckDBVector -> IO ()
negateCallback _ chunk outVec = do
    rows <- c_duckdb_data_chunk_get_size chunk
    inVec <- c_duckdb_data_chunk_get_vector chunk 0
    inPtr <- castToInt32Ptr <$> c_duckdb_vector_get_data inVec
    outPtr <- castToInt32Ptr <$> c_duckdb_vector_get_data outVec

    forM_ [0 .. fromIntegral rows - 1] \idx -> do
        val <- peekElemOff inPtr idx
        pokeElemOff outPtr idx (negate val)

    c_duckdb_data_chunk_set_size chunk rows

varargBind :: Ptr () -> DuckDBDeleteCallback -> DuckDBCopyCallback -> Int -> DuckDBBindInfo -> IO ()
varargBind extraPtr deleteBindCb copyCb bindDataSize info = do
    extra <- c_duckdb_scalar_function_bind_get_extra_info info
    assertBool "extra info propagated to bind phase" (extra == extraPtr)

    argCount <- c_duckdb_scalar_function_bind_get_argument_count info

    alloca \ctxPtr -> do
        c_duckdb_scalar_function_get_client_context info ctxPtr
        ctx <- peek ctxPtr
        unless (ctx == nullPtr) $
            c_duckdb_destroy_client_context ctxPtr

    if argCount == 0
        then withCString "at least one argument required" $ \msg ->
            c_duckdb_scalar_function_bind_set_error info msg
        else do
            expr <- c_duckdb_scalar_function_bind_get_argument info 0
            alloca \exprPtr -> poke exprPtr expr >> c_duckdb_destroy_expression exprPtr

            bindStorage <- mallocBytes bindDataSize
            poke (castPtr bindStorage :: Ptr Int32) (fromIntegral argCount)

            c_duckdb_scalar_function_set_bind_data info bindStorage deleteBindCb
            c_duckdb_scalar_function_set_bind_data_copy info copyCb

varargExec :: Ptr () -> Int -> DuckDBFunctionInfo -> DuckDBDataChunk -> DuckDBVector -> IO ()
varargExec extraPtr _ info chunk outVec = do
    bindData <- c_duckdb_scalar_function_get_bind_data info
    argCount <-
        if bindData == nullPtr
            then pure (0 :: Integer)
            else fromIntegral <$> peek (castPtr bindData :: Ptr Int32)

    extra <- c_duckdb_scalar_function_get_extra_info info
    assertBool "extra info visible during execution" (extra == extraPtr)

    rowCount <- fromIntegral <$> c_duckdb_data_chunk_get_size chunk
    vectors <- mapM (c_duckdb_data_chunk_get_vector chunk . fromIntegral) [0 .. argCount - 1]
    dataPtrs <- mapM (fmap castToInt32Ptr . c_duckdb_vector_get_data) vectors
    outPtr <- castToInt32Ptr <$> c_duckdb_vector_get_data outVec

    let loop idx
            | idx >= rowCount = pure False
            | otherwise = do
                values <- mapM (`peekElemOff` idx) dataPtrs
                let total = sum values
                if total < 0
                    then do
                        withCString "negative sum not allowed" $ \msg ->
                            c_duckdb_scalar_function_set_error info msg
                        c_duckdb_data_chunk_set_size chunk 0
                        pure True
                    else do
                        pokeElemOff outPtr idx total
                        loop (idx + 1)

    errored <- loop 0
    unless errored $
        c_duckdb_data_chunk_set_size chunk (fromIntegral rowCount)

scalarFunctionSetFeatures :: TestTree
scalarFunctionSetFeatures =
    testCase "scalar function bind helpers and function sets" $
        withDatabase \db ->
            withConnection db \conn ->
                withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType -> do
                    let bindDataSize = sizeOf (undefined :: Int32)

                    extraStorage <- mallocBytes bindDataSize
                    poke (castPtr extraStorage) (123 :: Int32)
                    deleteExtraCb <- mkDeleteCallback \ptr ->
                        when (ptr /= nullPtr) (free ptr)

                    deleteBindCb <- mkDeleteCallback \ptr ->
                        when (ptr /= nullPtr) (free ptr)

                    let copyBindData src
                            | src == nullPtr = pure nullPtr
                            | otherwise = do
                                newPtr <- mallocBytes bindDataSize
                                value <- peek (castPtr src :: Ptr Int32)
                                poke (castPtr newPtr) value
                                pure newPtr
                    copyCb <- mkCopyCallback copyBindData

                    bindFun <- mkScalarBindFun (varargBind (castPtr extraStorage) deleteBindCb copyCb bindDataSize)
                    execFun <- mkScalarFun (varargExec (castPtr extraStorage) bindDataSize)

                    let functionName = "haskell_vararg"

                    withScalarFunction \fun -> do
                        withCString functionName $ \cName ->
                            c_duckdb_scalar_function_set_name fun cName
                        c_duckdb_scalar_function_set_return_type fun intType
                        c_duckdb_scalar_function_set_varargs fun intType
                        c_duckdb_scalar_function_set_special_handling fun
                        c_duckdb_scalar_function_set_volatile fun
                        c_duckdb_scalar_function_set_extra_info fun (castPtr extraStorage) deleteExtraCb
                        c_duckdb_scalar_function_set_bind fun bindFun
                        c_duckdb_scalar_function_set_function fun execFun

                        withScalarFunctionSet functionName \funSet -> do
                            c_duckdb_add_scalar_function_to_set funSet fun >>= (@?= DuckDBSuccess)
                            c_duckdb_register_scalar_function_set conn funSet >>= (@?= DuckDBSuccess)

                            withCString "SELECT haskell_vararg(1, 2, 3)" \sql ->
                                withResultCString conn sql \resPtr ->
                                    c_duckdb_value_int32 resPtr 0 0 >>= (@?= 6)

                            expectQueryError conn "SELECT haskell_vararg()" "at least one argument"
                            expectQueryError conn "SELECT haskell_vararg(-5, 2)" "negative sum"

                    pure ()

-- Wrapper builder -----------------------------------------------------------

foreign import ccall "wrapper"
    mkScalarFun :: (DuckDBFunctionInfo -> DuckDBDataChunk -> DuckDBVector -> IO ()) -> IO DuckDBScalarFunctionFun

foreign import ccall "wrapper"
    mkScalarBindFun :: (DuckDBBindInfo -> IO ()) -> IO DuckDBScalarFunctionBindFun

foreign import ccall "wrapper"
    mkDeleteCallback :: (Ptr () -> IO ()) -> IO DuckDBDeleteCallback

foreign import ccall "wrapper"
    mkCopyCallback :: (Ptr () -> IO (Ptr ())) -> IO DuckDBCopyCallback

-- Resource helpers ----------------------------------------------------------

withLogicalType :: IO DuckDBLogicalType -> (DuckDBLogicalType -> IO a) -> IO a
withLogicalType acquire = bracket acquire destroyLogicalType

destroyLogicalType :: DuckDBLogicalType -> IO ()
destroyLogicalType lt = alloca \ptr -> poke ptr lt >> c_duckdb_destroy_logical_type ptr

withScalarFunction :: (DuckDBScalarFunction -> IO a) -> IO a
withScalarFunction = bracket c_duckdb_create_scalar_function destroy
  where
    destroy fun = alloca \ptr -> poke ptr fun >> c_duckdb_destroy_scalar_function ptr

withScalarFunctionSet :: String -> (DuckDBScalarFunctionSet -> IO a) -> IO a
withScalarFunctionSet name action =
    withCString name \cName ->
        bracket (c_duckdb_create_scalar_function_set cName) destroy action
  where
    destroy set = alloca \ptr -> poke ptr set >> c_duckdb_destroy_scalar_function_set ptr

expectQueryError :: DuckDBConnection -> String -> String -> IO ()
expectQueryError conn sql expectedFragment =
    withCString sql \sqlPtr ->
        alloca \resPtr -> do
            state <- c_duckdb_query conn sqlPtr resPtr
            state @?= DuckDBError
            errPtr <- c_duckdb_result_error resPtr
            errMsg <- peekCString errPtr
            let needle = map toLower expectedFragment
                haystack = map toLower errMsg
            assertBool
                ("expected fragment \"" ++ expectedFragment ++ "\" in error message:\n" ++ errMsg)
                (needle `isInfixOf` haystack)
            c_duckdb_destroy_result resPtr

castToInt32Ptr :: Ptr () -> Ptr Int32
castToInt32Ptr = castPtr