duckdb-ffi-1.4.1.2: test/ArrowInterfaceDeprecatedTests.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ForeignFunctionInterface #-}
module ArrowInterfaceDeprecatedTests (tests) where
import Control.Exception (bracket, finally)
import Control.Monad (unless, when)
import Data.Int (Int32, Int64)
import Data.List (isInfixOf)
import Data.Maybe (isNothing)
import Database.DuckDB.FFI
import Database.DuckDB.FFI.Deprecated
import Foreign.C.String (peekCString, peekCStringLen, withCString)
import Foreign.C.Types (CChar)
import Foreign.Marshal.Alloc (alloca)
import Foreign.Ptr (FunPtr, Ptr, castPtr, nullFunPtr, nullPtr, plusPtr)
import Foreign.Storable (peek, peekElemOff, poke)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
import Utils (withConnection, withDatabase, withResult)
tests :: TestTree
tests =
testGroup
"Deprecated Arrow Interface"
[ queryArrowExposesSchemaAndArrays
, queryArrowReportsErrors
, preparedArrowSchemaMatchesStatement
, executePreparedArrowProducesRows
, resultArrowArrayMirrorsChunk
, arrowRowsChangedReflectsMutations
, arrowArrayScanRegistersView
, arrowStreamScanRegistersView
, arrowPointerHelpersClearInternalState
]
-- basic query ---------------------------------------------------------------
queryArrowExposesSchemaAndArrays :: TestTree
queryArrowExposesSchemaAndArrays =
testCase "query_arrow exposes schema metadata and arrays" $
withDatabase \db ->
withConnection db \conn ->
withSuccessfulArrow conn "SELECT 1::INTEGER AS id, 'duck'::VARCHAR AS label" \arrow -> do
columnCount <- c_duckdb_arrow_column_count arrow
columnCount @?= 2
rowCount <- c_duckdb_arrow_row_count arrow
rowCount @?= 1
rowsChanged <- c_duckdb_arrow_rows_changed arrow
rowsChanged @?= 0
errPtr <- c_duckdb_query_arrow_error arrow
when (errPtr /= nullPtr) $ do
errMsg <- peekCString errPtr
errMsg @?= ""
-- schema/array access validated in dedicated scan test
-- error handling ------------------------------------------------------------
queryArrowReportsErrors :: TestTree
queryArrowReportsErrors =
testCase "query_arrow surfaces execution errors" $
withDatabase \db ->
withConnection db \conn ->
withCString "SELECT * FROM missing_table" \querySql ->
alloca \arrowPtr -> do
poke arrowPtr nullPtr
state <- c_duckdb_query_arrow conn querySql arrowPtr
state @?= DuckDBError
arrow <- peek arrowPtr
assertBool "arrow result should still be allocated on error" (arrow /= nullPtr)
errPtr <- c_duckdb_query_arrow_error arrow
assertBool "error message should be present" (errPtr /= nullPtr)
errMsg <- peekCString errPtr
assertBool "error message should mention missing_table" ("missing_table" `isInfixOf` errMsg)
c_duckdb_destroy_arrow arrowPtr
-- prepared statements -------------------------------------------------------
preparedArrowSchemaMatchesStatement :: TestTree
preparedArrowSchemaMatchesStatement =
testCase "prepared_arrow_schema reflects projected columns" $
withDatabase \db ->
withConnection db \conn ->
withPrepared conn "SELECT id, label FROM (VALUES (1, 'a')) AS t(id, label)" \stmt -> do
alloca \schemaOut -> do
poke schemaOut nullPtr
stSchema <- c_duckdb_prepared_arrow_schema stmt schemaOut
case stSchema of
DuckDBSuccess -> do
schemaWrapper <- peek schemaOut
assertBool "prepared arrow schema pointer should not be null" (schemaWrapper /= nullPtr)
DuckDBError -> do
schemaWrapper <- peek schemaOut
schemaWrapper @?= nullPtr
errPtr <- c_duckdb_prepare_error stmt
when (errPtr /= nullPtr) $ do
errMsg <- peekCString errPtr
errMsg @?= ""
executePreparedArrowProducesRows :: TestTree
executePreparedArrowProducesRows =
testCase "execute_prepared_arrow materialises a result set" $
withDatabase \db ->
withConnection db \conn ->
withPrepared conn "SELECT ?::INTEGER + 5 AS computed" \stmt -> do
c_duckdb_bind_int32 stmt 1 (5 :: Int32) >>= (@?= DuckDBSuccess)
withPreparedArrow stmt \arrow -> do
rowCount <- c_duckdb_arrow_row_count arrow
rowCount @?= 1
colCount <- c_duckdb_arrow_column_count arrow
colCount @?= 1
-- result conversion ---------------------------------------------------------
resultArrowArrayMirrorsChunk :: TestTree
resultArrowArrayMirrorsChunk =
-- NOTE: duckdb_result_arrow_array writes release callbacks into the
-- provided ArrowArray storage. Always zero-initialise the struct
-- (see zeroArrowArray) and honour the release function before the
-- stack memory goes out of scope.
testCase "result_arrow_array converts materialised chunks to Arrow arrays" $
withDatabase \db ->
withConnection db \conn -> do
execStatement conn "CREATE TABLE arrow_chunks(id BIGINT, label VARCHAR);"
execStatement conn "INSERT INTO arrow_chunks VALUES (10, 'ten'), (11, 'eleven');"
withResult conn "SELECT id, label FROM arrow_chunks ORDER BY id" \resPtr -> do
chunk <- c_duckdb_result_get_chunk resPtr 0
assertBool "fetch_chunk returned a null chunk" (chunk /= nullPtr)
chunkSize <- c_duckdb_data_chunk_get_size chunk
chunkCols <- c_duckdb_data_chunk_get_column_count chunk
alloca \arrowArrayPtr -> do
poke arrowArrayPtr zeroArrowArray
let duckArray :: DuckDBArrowArray
duckArray = castPtr arrowArrayPtr
alloca \arrayOut -> do
poke arrayOut duckArray
c_duckdb_result_arrow_array resPtr chunk arrayOut
array <- peek arrowArrayPtr
arrowArrayLength array @?= fromIntegral chunkSize
arrowArrayChildCount array @?= fromIntegral chunkCols
validateChunkChildren array
releaseArrowArray arrowArrayPtr
destroyChunk chunk
-- rows changed --------------------------------------------------------------
arrowRowsChangedReflectsMutations :: TestTree
arrowRowsChangedReflectsMutations =
testCase "arrow_rows_changed reports mutation counts" $
withDatabase \db ->
withConnection db \conn -> do
execStatement conn "CREATE TABLE arrow_changes(val INTEGER);"
withSuccessfulArrow conn "INSERT INTO arrow_changes VALUES (1), (2), (3)" \arrow -> do
rowCount <- c_duckdb_arrow_row_count arrow
assertBool "modification result should not report negative rows" (rowCount >= 0)
changed <- c_duckdb_arrow_rows_changed arrow
assertBool "rows_changed should report positive count" (changed > 0)
-- arrow scans ----------------------------------------------------------------
arrowArrayScanRegistersView :: TestTree
arrowArrayScanRegistersView =
-- NOTE: Both the schema and array buffers must be initialised to
-- zeroed Arrow structures before calling the query helpers. DuckDB
-- fills in release callbacks and expects us to invoke
-- duckdb_destroy_arrow_stream on the out stream.
testCase "arrow_array_scan registers a view and yields a release stream" $
withDatabase \db ->
withConnection db \conn -> do
execStatement conn "CREATE TABLE arrow_scan_source(i BIGINT, label VARCHAR);"
execStatement conn "INSERT INTO arrow_scan_source VALUES (5, 'five'), (6, 'six');"
withSuccessfulArrow conn "SELECT i, label FROM arrow_scan_source ORDER BY i" \arrow -> do
alloca \schemaStorage -> do
poke schemaStorage zeroArrowSchema
let schemaHandle = castPtr schemaStorage :: DuckDBArrowSchema
alloca \schemaOut -> do
poke schemaOut schemaHandle
schemaState <- c_duckdb_query_arrow_schema arrow schemaOut
schemaState @?= DuckDBSuccess
alloca \arrayStorage -> do
poke arrayStorage zeroArrowArray
let arrayHandle = castPtr arrayStorage :: DuckDBArrowArray
alloca \arrayOut -> do
poke arrayOut arrayHandle
arrayState <- c_duckdb_query_arrow_array arrow arrayOut
arrayState @?= DuckDBSuccess
withCString "arrow_array_view" \viewName ->
alloca \streamOut -> do
poke streamOut nullPtr
scanState <- c_duckdb_arrow_array_scan conn viewName schemaHandle arrayHandle streamOut
scanState @?= DuckDBSuccess
streamWrapper <- peek streamOut
assertBool "arrow_array_scan returned a null stream handle" (streamWrapper /= nullPtr)
withResult conn "SELECT COUNT(*) FROM arrow_array_view" \resPtr -> do
count <- c_duckdb_value_int64 resPtr 0 0
count @?= 2
c_duckdb_destroy_arrow_stream streamOut
releaseArrowArray arrayStorage
releaseArrowSchema schemaStorage
arrowStreamScanRegistersView :: TestTree
arrowStreamScanRegistersView =
-- NOTE: duckdb_arrow_array_scan returns DuckDBError if the target
-- view name already exists (even if it is a table). Use a fresh
-- view name that will be dropped implicitly when the connection
-- closes.
testCase "arrow_scan registers a view from an Arrow stream" $
withDatabase \db ->
withConnection db \conn -> do
execStatement conn "CREATE TABLE arrow_stream_source(i BIGINT, label VARCHAR);"
execStatement conn "INSERT INTO arrow_stream_source VALUES (7, 'seven'), (8, 'eight');"
withSuccessfulArrow conn "SELECT i, label FROM arrow_stream_source ORDER BY i" \arrow -> do
alloca \schemaStorage ->
finally
( do
poke schemaStorage zeroArrowSchema
let schemaHandle = castPtr schemaStorage :: DuckDBArrowSchema
alloca \schemaOut -> do
poke schemaOut schemaHandle
schemaState <- c_duckdb_query_arrow_schema arrow schemaOut
schemaState @?= DuckDBSuccess
alloca \arrayStorage ->
finally
( do
poke arrayStorage zeroArrowArray
let arrayHandle = castPtr arrayStorage :: DuckDBArrowArray
alloca \arrayOut -> do
poke arrayOut arrayHandle
arrayState <- c_duckdb_query_arrow_array arrow arrayOut
arrayState @?= DuckDBSuccess
withCString "arrow_stream_array_view" \sourceView ->
alloca \streamOut -> do
poke streamOut nullPtr
arrayScanState <- c_duckdb_arrow_array_scan conn sourceView schemaHandle arrayHandle streamOut
arrayScanState @?= DuckDBSuccess
streamHandle <- peek streamOut
assertBool "arrow_array_scan returned a null stream" (streamHandle /= nullPtr)
withCString "arrow_stream_view" \streamView -> do
streamScanState <- c_duckdb_arrow_scan conn streamView streamHandle
streamScanState @?= DuckDBSuccess
withResult conn "SELECT COUNT(*) FROM arrow_stream_view" \resPtr -> do
count <- c_duckdb_value_int64 resPtr 0 0
count @?= 2
c_duckdb_destroy_arrow_stream streamOut
)
(releaseArrowArray arrayStorage)
)
(releaseArrowSchema schemaStorage)
arrowPointerHelpersClearInternalState :: TestTree
arrowPointerHelpersClearInternalState =
-- NOTE: The deprecated Arrow handles are thin wrappers whose
-- internal_ptr fields can be inspected/reset via the exported
-- duckdbArrow{Schema,Array,Stream}{Internal,Clear} helpers. We avoid
-- direct foreign imports here so future changes to the shims only
-- require updates in the Arrow module.
testCase "arrow pointer helper functions read and clear internal_ptr fields" $
alloca \schemaFieldPtr ->
alloca \arrayFieldPtr ->
alloca \streamFieldPtr -> do
let schemaField = schemaFieldPtr :: Ptr (Ptr ())
schemaHandle = castPtr schemaField :: DuckDBArrowSchema
schemaSentinel = ArrowSchemaPtr (castPtr schemaField)
poke schemaField (castPtr (unArrowSchemaPtr schemaSentinel))
schemaPtr <- duckdbArrowSchemaInternal schemaHandle
fmap unArrowSchemaPtr schemaPtr @?= Just (unArrowSchemaPtr schemaSentinel)
duckdbArrowSchemaClear schemaHandle
schemaCleared <- duckdbArrowSchemaInternal schemaHandle
assertBool "schema internal_ptr should be cleared" (isNothing schemaCleared)
let arrayField = arrayFieldPtr :: Ptr (Ptr ())
arrayHandle = castPtr arrayField :: DuckDBArrowArray
arraySentinel = ArrowArrayPtr (castPtr arrayField)
poke arrayField (castPtr (unArrowArrayPtr arraySentinel))
arrayPtr <- duckdbArrowArrayInternal arrayHandle
fmap unArrowArrayPtr arrayPtr @?= Just (unArrowArrayPtr arraySentinel)
duckdbArrowArrayClear arrayHandle
arrayCleared <- duckdbArrowArrayInternal arrayHandle
assertBool "array internal_ptr should be cleared" (isNothing arrayCleared)
let streamField = streamFieldPtr :: Ptr (Ptr ())
streamHandle = castPtr streamField :: DuckDBArrowStream
streamSentinel = ArrowStreamPtr (castPtr streamField)
poke streamField (castPtr (unArrowStreamPtr streamSentinel))
streamPtr <- duckdbArrowStreamInternal streamHandle
fmap unArrowStreamPtr streamPtr @?= Just (unArrowStreamPtr streamSentinel)
duckdbArrowStreamClear streamHandle
streamCleared <- duckdbArrowStreamInternal streamHandle
assertBool "stream internal_ptr should be cleared" (isNothing streamCleared)
zeroArrowArray :: ArrowArray
zeroArrowArray =
ArrowArray
{ arrowArrayLength = 0
, arrowArrayNullCount = 0
, arrowArrayOffset = 0
, arrowArrayBufferCount = 0
, arrowArrayChildCount = 0
, arrowArrayBuffers = nullPtr
, arrowArrayChildren = nullPtr
, arrowArrayDictionary = nullPtr
, arrowArrayRelease = nullFunPtr
, arrowArrayPrivateData = nullPtr
}
zeroArrowSchema :: ArrowSchema
zeroArrowSchema =
ArrowSchema
{ arrowSchemaFormat = nullPtr
, arrowSchemaName = nullPtr
, arrowSchemaMetadata = nullPtr
, arrowSchemaFlags = 0
, arrowSchemaChildCount = 0
, arrowSchemaChildren = nullPtr
, arrowSchemaDictionary = nullPtr
, arrowSchemaRelease = nullFunPtr
, arrowSchemaPrivateData = nullPtr
}
validateChunkChildren :: ArrowArray -> IO ()
validateChunkChildren array = do
let expectedIds = [10, 11] :: [Int64]
expectedLabels = ["ten", "eleven"]
childCount = fromIntegral (arrowArrayChildCount array) :: Int
rowCount = fromIntegral (arrowArrayLength array) :: Int
childCount @?= 2
rowCount @?= length expectedIds
let childrenPtr = arrowArrayChildren array
assertBool "Arrow array did not expose child arrays" (childrenPtr /= nullPtr)
let ensurePtr name ptrPred =
unless ptrPred $
assertFailure ("Arrow array " ++ name ++ " pointer is null")
intChildPtr <- peekElemOff childrenPtr 0
ensurePtr "integer child" (intChildPtr /= nullPtr)
intChild <- peek intChildPtr
arrowArrayNullCount intChild @?= 0
let intBufferCount = fromIntegral (arrowArrayBufferCount intChild) :: Int
assertBool "Integer child did not expose the expected buffers" (intBufferCount >= 2)
let intBuffers = arrowArrayBuffers intChild
ensurePtr "integer buffers" (intBuffers /= nullPtr)
valueBufferRaw <- peekElemOff intBuffers 1
ensurePtr "integer value buffer" (valueBufferRaw /= nullPtr)
let valueBuffer = castPtr valueBufferRaw :: Ptr Int64
values <- mapM (peekElemOff valueBuffer) [0 .. rowCount - 1]
values @?= expectedIds
strChildPtr <- peekElemOff childrenPtr 1
ensurePtr "varchar child" (strChildPtr /= nullPtr)
strChild <- peek strChildPtr
arrowArrayNullCount strChild @?= 0
let strBufferCount = fromIntegral (arrowArrayBufferCount strChild) :: Int
assertBool "Varchar child did not expose the expected buffers" (strBufferCount >= 3)
let strBuffers = arrowArrayBuffers strChild
ensurePtr "varchar buffers" (strBuffers /= nullPtr)
offsetsRaw <- peekElemOff strBuffers 1
dataRaw <- peekElemOff strBuffers 2
ensurePtr "varchar offsets buffer" (offsetsRaw /= nullPtr)
ensurePtr "varchar data buffer" (dataRaw /= nullPtr)
let offsetsPtr = castPtr offsetsRaw :: Ptr Int32
dataPtr = castPtr dataRaw :: Ptr CChar
labels <-
mapM
( \idx -> do
start <- fromIntegral <$> peekElemOff offsetsPtr idx
end <- fromIntegral <$> peekElemOff offsetsPtr (idx + 1)
peekCStringLen (dataPtr `plusPtr` start, end - start)
)
[0 .. rowCount - 1]
labels @?= expectedLabels
releaseArrowArray :: Ptr ArrowArray -> IO ()
releaseArrowArray arrayPtr = do
array <- peek arrayPtr
let releaseFun = arrowArrayRelease array
when (releaseFun /= nullFunPtr) $ do
let release = mkArrowArrayRelease releaseFun
release arrayPtr
releaseArrowSchema :: Ptr ArrowSchema -> IO ()
releaseArrowSchema schemaPtr = do
schema <- peek schemaPtr
let releaseFun = arrowSchemaRelease schema
when (releaseFun /= nullFunPtr) $ do
let release = mkArrowSchemaRelease releaseFun
release schemaPtr
foreign import ccall "dynamic"
mkArrowArrayRelease :: FunPtr (Ptr ArrowArray -> IO ()) -> Ptr ArrowArray -> IO ()
foreign import ccall "dynamic"
mkArrowSchemaRelease :: FunPtr (Ptr ArrowSchema -> IO ()) -> Ptr ArrowSchema -> IO ()
withSuccessfulArrow :: DuckDBConnection -> String -> (DuckDBArrow -> IO a) -> IO a
withSuccessfulArrow conn sql action =
withCString sql \sqlPtr ->
alloca \arrowPtr ->
bracket
( do
poke arrowPtr nullPtr
state <- c_duckdb_query_arrow conn sqlPtr arrowPtr
state @?= DuckDBSuccess
arrow <- peek arrowPtr
assertBool "duckdb_query_arrow returned null result" (arrow /= nullPtr)
pure arrow
)
(\_ -> c_duckdb_destroy_arrow arrowPtr)
action
withPrepared :: DuckDBConnection -> String -> (DuckDBPreparedStatement -> IO a) -> IO a
withPrepared conn sql action =
withCString sql \sqlPtr ->
alloca \stmtPtr ->
bracket
( do
state <- c_duckdb_prepare conn sqlPtr stmtPtr
state @?= DuckDBSuccess
stmt <- peek stmtPtr
assertBool "prepare should produce a statement" (stmt /= nullPtr)
pure stmt
)
(\_ -> c_duckdb_destroy_prepare stmtPtr)
action
withPreparedArrow :: DuckDBPreparedStatement -> (DuckDBArrow -> IO a) -> IO a
withPreparedArrow stmt action =
alloca \arrowPtr ->
bracket
( do
poke arrowPtr nullPtr
state <- c_duckdb_execute_prepared_arrow stmt arrowPtr
state @?= DuckDBSuccess
arrow <- peek arrowPtr
assertBool "execute_prepared_arrow returned null result" (arrow /= nullPtr)
pure arrow
)
(\_ -> c_duckdb_destroy_arrow arrowPtr)
action
destroyChunk :: DuckDBDataChunk -> IO ()
destroyChunk chunk =
alloca \ptr -> poke ptr chunk >> c_duckdb_destroy_data_chunk ptr
execStatement :: DuckDBConnection -> String -> IO ()
execStatement conn sql =
withCString sql \sqlPtr ->
alloca \resPtr -> do
st <- c_duckdb_query conn sqlPtr resPtr
st @?= DuckDBSuccess
c_duckdb_destroy_result resPtr