packages feed

duckdb-ffi-1.4.1.2: test/StreamingResultTest.hs

{-# LANGUAGE BlockArguments #-}

module StreamingResultTest (tests) where

import Control.Monad (void)
import Data.Int (Int64)
import Data.List (intercalate)
import Database.DuckDB.FFI
import Database.DuckDB.FFI.Deprecated
import Foreign.C.String (CString, withCString)
import Foreign.C.Types (CBool (..))
import Foreign.Marshal.Alloc (alloca)
import Foreign.Ptr (Ptr, nullPtr)
import Foreign.Storable (peek, poke)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, testCase, (@?=))
import Utils (withConnection, withDatabase, withResult)

tests :: TestTree
tests =
    testGroup
        "Streaming Result Interface"
        [ streamingFetchConsumesAllChunks
        , materializedFetchChunkExhaustsResult
        ]

streamingFetchConsumesAllChunks :: TestTree
streamingFetchConsumesAllChunks =
    testCase "stream_fetch_chunk yields data until exhaustion" $
        withDatabase \db ->
            withConnection db \conn -> do
                setupTable conn "streaming_table" 6

                withCString "SELECT id FROM streaming_table ORDER BY id" \querySql ->
                    alloca \stmtPtr -> do
                        prepareOk <- c_duckdb_prepare conn querySql stmtPtr
                        prepareOk @?= DuckDBSuccess
                        stmt <- peek stmtPtr
                        assertBool "prepared statement should not be null" (stmt /= nullPtr)

                        alloca \pendingPtr -> do
                            stPending <- c_duckdb_pending_prepared_streaming stmt pendingPtr
                            stPending @?= DuckDBSuccess
                            pending <- peek pendingPtr
                            assertBool "pending result should not be null" (pending /= nullPtr)

                            void (c_duckdb_pending_execute_task pending)

                            alloca \resPtr -> do
                                execState <- c_duckdb_execute_pending pending resPtr
                                execState @?= DuckDBSuccess

                                streamingFlag <- c_duckdb_result_is_streaming resPtr
                                streamingFlag @?= CBool 1

                                totalRows <- consumeStreamingChunks resPtr 0
                                totalRows @?= 6

                                c_duckdb_destroy_result resPtr

                            c_duckdb_destroy_pending pendingPtr

                        c_duckdb_destroy_prepare stmtPtr

materializedFetchChunkExhaustsResult :: TestTree
materializedFetchChunkExhaustsResult =
    testCase "fetch_chunk provides chunk and then null for materialized result" $
        withDatabase \db ->
            withConnection db \conn -> do
                setupTable conn "materialized_table" 4

                withResult conn "SELECT id FROM materialized_table ORDER BY id" \resPtr -> do
                    chunk <- c_duckdb_fetch_chunk resPtr
                    assertBool "first fetch_chunk should yield a chunk" (chunk /= nullPtr)

                    chunkSize <- c_duckdb_data_chunk_get_size chunk
                    assertBool "materialized chunk should have rows" (chunkSize > 0)

                    destroyChunk chunk

                    chunkNext <- c_duckdb_fetch_chunk resPtr
                    chunkNext @?= nullPtr

-- helpers ------------------------------------------------------------------

consumeStreamingChunks :: Ptr DuckDBResult -> Int64 -> IO Int64
consumeStreamingChunks resPtr acc = do
    chunk <- c_duckdb_stream_fetch_chunk resPtr
    if chunk == nullPtr
        then pure acc
        else do
            chunkSize <- c_duckdb_data_chunk_get_size chunk
            assertBool "streaming chunk should not be empty" (chunkSize > 0)
            destroyChunk chunk
            consumeStreamingChunks resPtr (acc + fromIntegral chunkSize)

setupTable :: DuckDBConnection -> String -> Int -> IO ()
setupTable conn tableName totalRows = do
    withCString ("CREATE TABLE " <> tableName <> " (id INTEGER);") $ \createSql ->
        execStatement conn createSql
    let values = intercalate ", " ["(" <> show i <> ")" | i <- [1 .. totalRows]]
        insertSql = "INSERT INTO " <> tableName <> " VALUES " <> values <> ";"
    withCString insertSql $ \insertCStr ->
        execStatement conn insertCStr

execStatement :: DuckDBConnection -> CString -> IO ()
execStatement conn sql =
    alloca \resPtr -> do
        st <- c_duckdb_query conn sql resPtr
        st @?= DuckDBSuccess
        c_duckdb_destroy_result resPtr

destroyChunk :: DuckDBDataChunk -> IO ()
destroyChunk chunk =
    alloca \ptr -> do
        poke ptr chunk
        c_duckdb_destroy_data_chunk ptr