duckdb-ffi-0.1.4.1: test/PreparedStatementsTest.hs
{-# LANGUAGE BlockArguments #-}
module PreparedStatementsTest (tests) where
import Control.Monad (when)
import Database.DuckDB.FFI
import Foreign.C.String (peekCString, withCString)
import Foreign.Marshal.Alloc (alloca)
import Foreign.Ptr (nullPtr)
import Foreign.Storable (peek, poke)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
import Utils (withConnection, withDatabase)
tests :: TestTree
tests =
testGroup
"Prepared Statements"
[ preparedStatementMetadata
, preparedStatementError
]
preparedStatementMetadata :: TestTree
preparedStatementMetadata =
testCase "inspect prepared statement metadata" $
withDatabase \db ->
withConnection db \conn -> do
-- Seed table for column metadata checks
withCString "CREATE TABLE numbers(value INTEGER)" \ddl -> do
alloca \resPtr -> do
st <- c_duckdb_query conn ddl resPtr
st @?= DuckDBSuccess
c_duckdb_destroy_result resPtr
let sql = "SELECT value FROM numbers WHERE value = ?"
withCString sql \cSql ->
alloca \stmtPtr -> do
st <- c_duckdb_prepare conn cSql stmtPtr
st @?= DuckDBSuccess
stmt <- peek stmtPtr
assertBool "statement pointer should not be null" (stmt /= nullPtr)
-- Parameter metadata
paramCount <- c_duckdb_nparams stmt
paramCount @?= 1
paramType <- c_duckdb_param_type stmt 0
-- Physical type defaults to invalid until bound; logical type carries information.
paramType @?= DuckDBTypeInvalid
logicalTypePtr <- c_duckdb_param_logical_type stmt 0
when (logicalTypePtr /= nullPtr) $ do
alloca \typePtr -> do
poke typePtr logicalTypePtr
c_duckdb_destroy_logical_type typePtr
namePtr <- c_duckdb_parameter_name stmt 0
when (namePtr /= nullPtr) $ do
name <- peekCString namePtr
name @?= ""
-- Statement type
stmtType <- c_duckdb_prepared_statement_type stmt
stmtType @?= DuckDBStatementTypeSelect
-- Column metadata
colCount <- c_duckdb_prepared_statement_column_count stmt
colCount @?= 1
colNamePtr <- c_duckdb_prepared_statement_column_name stmt 0
colName <- peekCString colNamePtr
colName @?= "value"
colType <- c_duckdb_prepared_statement_column_type stmt 0
colType @?= DuckDBTypeInteger
colLogicalType <- c_duckdb_prepared_statement_column_logical_type stmt 0
assertBool "column logical type should not be null" (colLogicalType /= nullPtr)
alloca \colTypePtr -> do
poke colTypePtr colLogicalType
c_duckdb_destroy_logical_type colTypePtr
-- Clear bindings succeeds even before binding values
c_duckdb_clear_bindings stmt >>= (@?= DuckDBSuccess)
-- No preparation error
errPtr <- c_duckdb_prepare_error stmt
when (errPtr /= nullPtr) $ do
msg <- peekCString errPtr
msg @?= ""
c_duckdb_destroy_prepare stmtPtr
preparedStatementError :: TestTree
preparedStatementError =
testCase "prepare error surfaces message" $
withDatabase \db ->
withConnection db \conn -> do
let badSql = "SELECT * FROM non_existing_table WHERE value = ?"
withCString badSql \cSql ->
alloca \stmtPtr -> do
st <- c_duckdb_prepare conn cSql stmtPtr
st @?= DuckDBError
stmt <- peek stmtPtr
if stmt == nullPtr
then assertFailure "expected prepared statement handle after failure"
else do
errMsgPtr <- c_duckdb_prepare_error stmt
when (errMsgPtr == nullPtr) $ assertFailure "expected error message for failed prepare"
msg <- peekCString errMsgPtr
assertBool "error message should mention table" (not (null msg))
c_duckdb_destroy_prepare stmtPtr