packages feed

duckdb-ffi-1.4.1.2: test/ReplacementScansTest.hs

{-# LANGUAGE BlockArguments #-}

module ReplacementScansTest (tests) where

import Control.Concurrent (runInBoundThread)
import Control.Exception (bracket)
import Control.Monad (forM_)
import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
import Data.Int (Int64)
import Data.List (isInfixOf)
import Database.DuckDB.FFI
import Database.DuckDB.FFI.Deprecated
import Foreign.C.String (CString, peekCString, withCString)
import Foreign.Marshal.Alloc (alloca)
import Foreign.Ptr (Ptr, freeHaskellFunPtr, nullFunPtr, nullPtr)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, testCase, (@?=))
import Utils (withConnection, withDatabase, withResult, withValue)

tests :: TestTree
tests =
    testGroup
        "Replacement Scans"
        [ replacementScanRewritesAndErrors
        ]

replacementScanRewritesAndErrors :: TestTree
replacementScanRewritesAndErrors =
    testCase "replacement scan rewrites table name and can report errors" $
        runInBoundThread do
            seenTablesRef <- newIORef []

            let startValue = 5
                countValue = 4
                endValue = startValue + countValue

            withReplacementCallback seenTablesRef startValue endValue \callback -> do
                withDatabase \db -> do
                    c_duckdb_add_replacement_scan db callback nullPtr nullFunPtr
                    withConnection db \conn -> do
                        assertReplacementQuery conn startValue countValue
                        assertReplacementError conn

            seenTables <- readIORef seenTablesRef
            assertBool "replacement callback should run for rewrite target" ("haskell_magic" `elem` seenTables)
            assertBool "replacement callback should run for error target" ("failing_magic" `elem` seenTables)

withReplacementCallback ::
    IORef [String] ->
    Int64 ->
    Int64 ->
    (DuckDBReplacementCallback -> IO a) ->
    IO a
withReplacementCallback seenTablesRef startValue endValue =
    bracket acquire freeHaskellFunPtr
  where
    acquire =
        mkReplacementCallback (replacementCallback seenTablesRef startValue endValue)

replacementCallback ::
    IORef [String] ->
    Int64 ->
    Int64 ->
    DuckDBReplacementScanInfo ->
    CString ->
    Ptr () ->
    IO ()
replacementCallback seenTablesRef startValue endValue info tableName _extra = do
    name <- peekCString tableName
    modifyIORef' seenTablesRef (name :)
    case name of
        "haskell_magic" -> do
            withCString "range" $ \fn ->
                c_duckdb_replacement_scan_set_function_name info fn
            withValue (c_duckdb_create_int64 startValue) $ \startVal ->
                c_duckdb_replacement_scan_add_parameter info startVal
            withValue (c_duckdb_create_int64 endValue) $ \endVal ->
                c_duckdb_replacement_scan_add_parameter info endVal
        "failing_magic" ->
            withCString "replacement rejected by test callback" $ \msg ->
                c_duckdb_replacement_scan_set_error info msg
        _ ->
            pure ()

assertReplacementQuery :: DuckDBConnection -> Int64 -> Int64 -> IO ()
assertReplacementQuery conn startValue countValue =
    withResult conn "SELECT range FROM haskell_magic ORDER BY range" \resPtr -> do
        rowCount <- c_duckdb_row_count resPtr
        rowCount @?= fromIntegral countValue
        forM_ [0 .. countValue - 1] \idx -> do
            value <- c_duckdb_value_int64 resPtr 0 (fromIntegral idx)
            value @?= startValue + idx

assertReplacementError :: DuckDBConnection -> IO ()
assertReplacementError conn =
    withCString "SELECT * FROM failing_magic" \sql ->
        alloca \resPtr -> do
            state <- c_duckdb_query conn sql resPtr
            state @?= DuckDBError
            errPtr <- c_duckdb_result_error resPtr
            errMsg <- peekCString errPtr
            assertBool "replacement error message should surface" ("rejected" `isInfixOf` errMsg)
            c_duckdb_destroy_result resPtr

foreign import ccall "wrapper"
    mkReplacementCallback ::
        (DuckDBReplacementScanInfo -> CString -> Ptr () -> IO ()) ->
        IO DuckDBReplacementCallback