duckdb-ffi-0.1.4.1: test/ExpressionTest.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE NamedFieldPuns #-}
module ExpressionTest (tests) where
import Control.Exception (finally)
import Data.Char (toLower)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.List (isInfixOf)
import Database.DuckDB.FFI
import Foreign.C.String (peekCString, withCString)
import Foreign.Marshal.Alloc (alloca)
import Foreign.Ptr (castPtr, freeHaskellFunPtr, nullFunPtr, nullPtr)
import Foreign.Storable (peek, poke)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, testCase, (@?=))
import Utils (
destroyDuckValue,
destroyErrorData,
destroyLogicalType,
withConnection,
withDatabase,
withLogicalType,
withResult,
withScalarFunction,
)
data ExpressionHarness = ExpressionHarness
{ ehFoldable :: IO (Maybe Bool)
, ehReturnType :: IO (Maybe DuckDBType)
, ehFoldedValue :: IO (Maybe String)
, ehFoldError :: IO (Maybe String)
}
data ExpressionState = ExpressionState
{ esFoldable :: IORef (Maybe Bool)
, esReturnType :: IORef (Maybe DuckDBType)
, esFoldValue :: IORef (Maybe String)
, esFoldError :: IORef (Maybe String)
}
tests :: TestTree
tests =
testGroup
"Expression Interface"
[ expressionFoldLiteral
, expressionNonFoldable
]
expressionFoldLiteral :: TestTree
expressionFoldLiteral =
testCase "folds literal expressions to constant values" $
withDatabase \db ->
withConnection db \conn ->
withExpressionFunction conn "expr_literal" \ExpressionHarness{ehFoldable, ehReturnType, ehFoldedValue, ehFoldError} -> do
withResult conn "SELECT expr_literal(42)" \_ -> pure ()
ehFoldable >>= (@?= Just True)
ehReturnType >>= (@?= Just DuckDBTypeInteger)
ehFoldedValue >>= (@?= Just "42")
ehFoldError >>= (@?= Nothing)
expressionNonFoldable :: TestTree
expressionNonFoldable =
testCase "detects non-foldable column references" $
withDatabase \db ->
withConnection db \conn ->
withExpressionFunction conn "expr_non_foldable" \ExpressionHarness{ehFoldable, ehReturnType, ehFoldedValue, ehFoldError} -> do
withResult conn "SELECT expr_non_foldable(v) FROM (VALUES (7)) t(v)" \_ -> pure ()
ehFoldable >>= (@?= Just False)
ehReturnType >>= (@?= Just DuckDBTypeInteger)
ehFoldedValue >>= (@?= Nothing)
errMsg <- ehFoldError
assertBool "expected fold error message" $
maybe False (isInfixOf "fold" . map toLower) errMsg
withExpressionFunction :: DuckDBConnection -> String -> (ExpressionHarness -> IO a) -> IO a
withExpressionFunction conn funcName action = do
foldRef <- newIORef Nothing
typeRef <- newIORef Nothing
valueRef <- newIORef Nothing
errorRef <- newIORef Nothing
let state = ExpressionState foldRef typeRef valueRef errorRef
bindPtr <- mkScalarBindFun (expressionBind state)
execPtr <- mkScalarExecFun expressionExec
result <-
withScalarFunction $ \scalarFun -> do
withCString funcName $ \name -> c_duckdb_scalar_function_set_name scalarFun name
withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) $ \intType -> do
c_duckdb_scalar_function_add_parameter scalarFun intType
c_duckdb_scalar_function_set_return_type scalarFun intType
c_duckdb_scalar_function_set_bind scalarFun bindPtr
c_duckdb_scalar_function_set_function scalarFun execPtr
c_duckdb_scalar_function_set_extra_info scalarFun nullPtr nullFunPtr
c_duckdb_register_scalar_function conn scalarFun >>= (@?= DuckDBSuccess)
action
ExpressionHarness
{ ehFoldable = readIORef foldRef
, ehReturnType = readIORef typeRef
, ehFoldedValue = readIORef valueRef
, ehFoldError = readIORef errorRef
}
freeHaskellFunPtr bindPtr
freeHaskellFunPtr execPtr
pure result
expressionBind :: ExpressionState -> DuckDBBindInfo -> IO ()
expressionBind ExpressionState{esFoldable, esReturnType, esFoldValue, esFoldError} info = do
argCount <- c_duckdb_scalar_function_bind_get_argument_count info
argCount @?= 1
exprHandle <- c_duckdb_scalar_function_bind_get_argument info 0
finally
( do
foldableFlag <- c_duckdb_expression_is_foldable exprHandle
let isFoldable = foldableFlag /= 0
writeIORef esFoldable (Just isFoldable)
retType <- c_duckdb_expression_return_type exprHandle
typeId <- c_duckdb_get_type_id retType
destroyLogicalType retType
writeIORef esReturnType (Just typeId)
alloca \ctxPtr -> do
c_duckdb_scalar_function_get_client_context info ctxPtr
ctx <- peek ctxPtr
alloca \valuePtr -> do
poke valuePtr nullPtr
errData <- c_duckdb_expression_fold ctx exprHandle valuePtr
if errData == nullPtr
then do
valueHandle <- peek valuePtr
if valueHandle == nullPtr
then do
writeIORef esFoldValue Nothing
writeIORef esFoldError (Just "fold produced null value")
else do
rendered <- duckValueToString valueHandle
writeIORef esFoldValue (Just rendered)
writeIORef esFoldError Nothing
destroyDuckValue valueHandle
else do
msgPtr <- c_duckdb_error_data_message errData
msg <- peekCString msgPtr
writeIORef esFoldValue Nothing
writeIORef esFoldError (Just msg)
destroyErrorData errData
)
(alloca \exprPtr -> poke exprPtr exprHandle >> c_duckdb_destroy_expression exprPtr)
expressionExec :: DuckDBFunctionInfo -> DuckDBDataChunk -> DuckDBVector -> IO ()
expressionExec _ chunk outVec = do
inVec <- c_duckdb_data_chunk_get_vector chunk 0
c_duckdb_vector_reference_vector outVec inVec
-- Helpers ------------------------------------------------------------------
duckValueToString :: DuckDBValue -> IO String
duckValueToString value = do
strPtr <- c_duckdb_value_to_string value
text <- peekCString strPtr
c_duckdb_free (castPtr strPtr)
pure text
-- Wrapper constructors -----------------------------------------------------
foreign import ccall "wrapper"
mkScalarBindFun :: (DuckDBBindInfo -> IO ()) -> IO DuckDBScalarFunctionBindFun
foreign import ccall "wrapper"
mkScalarExecFun :: (DuckDBFunctionInfo -> DuckDBDataChunk -> DuckDBVector -> IO ()) -> IO DuckDBScalarFunctionFun