packages feed

duckdb-ffi-0.1.4.1: test/LogicalTypesTest.hs

{-# LANGUAGE BlockArguments #-}

module LogicalTypesTest (tests) where

import Control.Monad (forM_, when, (>=>))
import Data.Word (Word32, Word8)
import Database.DuckDB.FFI
import Foreign.C.String (peekCString, withCString)
import Foreign.Marshal.Array (withArray)
import Foreign.Marshal.Utils (withMany)
import Foreign.Ptr (castPtr, nullPtr)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
import Utils (withConnection, withDatabase, withLogicalType, withResult)

tests :: TestTree
tests =
    testGroup
        "Logical Type Interface"
        [ primitiveLogicalTypes
        , decimalLogicalType
        , enumLogicalType
        , compositeLogicalTypes
        , aliasRoundtrip
        , registerLogicalType
        ]

primitiveLogicalTypes :: TestTree
primitiveLogicalTypes =
    testCase "primitive logical types report their ids" $
        forM_ primitives \(duckType, expected) ->
            withLogicalType (c_duckdb_create_logical_type duckType) \lt -> do
                typeId <- c_duckdb_get_type_id lt
                typeId @?= expected
  where
    primitives =
        [ (DuckDBTypeBoolean, DuckDBTypeBoolean)
        , (DuckDBTypeInteger, DuckDBTypeInteger)
        , (DuckDBTypeVarchar, DuckDBTypeVarchar)
        , (DuckDBTypeBlob, DuckDBTypeBlob)
        ]

decimalLogicalType :: TestTree
decimalLogicalType =
    testCase "decimal logical type exposes width/scale/internal type" $
        withLogicalType (c_duckdb_create_decimal_type width scale) \lt -> do
            c_duckdb_get_type_id lt >>= (@?= DuckDBTypeDecimal)
            c_duckdb_decimal_width lt >>= (@?= width)
            c_duckdb_decimal_scale lt >>= (@?= scale)
            c_duckdb_decimal_internal_type lt >>= (@?= DuckDBTypeBigInt)
  where
    width, scale :: Word8
    width = 18
    scale = 4

enumLogicalType :: TestTree
enumLogicalType =
    testCase "enum logical type exposes dictionary" $ do
        enumType <-
            withMany withCString ["Small", "Medium", "Large"] \namePtrs ->
                withArray namePtrs (`c_duckdb_create_enum_type` 3)
        withLogicalType (pure enumType) \lt -> do
            c_duckdb_get_type_id lt >>= (@?= DuckDBTypeEnum)
            c_duckdb_enum_internal_type lt >>= (@?= DuckDBTypeUTinyInt)
            c_duckdb_enum_dictionary_size lt >>= (@?= (3 :: Word32))
            valuePtr <- c_duckdb_enum_dictionary_value lt 1
            peekCString valuePtr >>= (@?= "Medium")
            c_duckdb_free (castPtr valuePtr)

compositeLogicalTypes :: TestTree
compositeLogicalTypes =
    testCase "composite logical types expose nesting metadata" $ do
        -- List type
        withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \child -> do
            listType <- c_duckdb_create_list_type child
            withLogicalType (pure listType) \lt -> do
                c_duckdb_get_type_id lt >>= (@?= DuckDBTypeList)
                listChild <- c_duckdb_list_type_child_type lt
                withLogicalType (pure listChild) (c_duckdb_get_type_id >=> (@?= DuckDBTypeInteger))

        -- Array type
        withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \arrayChild -> do
            arrayType <- c_duckdb_create_array_type arrayChild 5
            withLogicalType (pure arrayType) \lt -> do
                c_duckdb_get_type_id lt >>= (@?= DuckDBTypeArray)
                arrayChildType <- c_duckdb_array_type_child_type lt
                withLogicalType (pure arrayChildType) (c_duckdb_get_type_id >=> (@?= DuckDBTypeInteger))
                c_duckdb_array_type_array_size lt >>= (@?= 5)

        -- Map type
        withLogicalType (c_duckdb_create_logical_type DuckDBTypeVarchar) \keyType ->
            withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \valType -> do
                mapType <- c_duckdb_create_map_type keyType valType
                withLogicalType (pure mapType) \lt -> do
                    c_duckdb_get_type_id lt >>= (@?= DuckDBTypeMap)
                    mapKey <- c_duckdb_map_type_key_type lt
                    withLogicalType (pure mapKey) (c_duckdb_get_type_id >=> (@?= DuckDBTypeVarchar))
                    mapVal <- c_duckdb_map_type_value_type lt
                    withLogicalType (pure mapVal) (c_duckdb_get_type_id >=> (@?= DuckDBTypeInteger))

        -- Struct type
        withMany withCString ["id", "name"] \fieldNames -> withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \idType ->
            withLogicalType (c_duckdb_create_logical_type DuckDBTypeVarchar) \nameType ->
                withArray [idType, nameType] \childArray ->
                    withArray fieldNames \namesArray -> do
                        structType <- c_duckdb_create_struct_type childArray namesArray 2
                        withLogicalType (pure structType) \lt -> do
                            c_duckdb_get_type_id lt >>= (@?= DuckDBTypeStruct)
                            childCount <- c_duckdb_struct_type_child_count lt
                            childCount @?= 2
                            childName0 <- c_duckdb_struct_type_child_name lt 0
                            peekCString childName0 >>= (@?= "id")
                            c_duckdb_free (castPtr childName0)
                            structChild1 <- c_duckdb_struct_type_child_type lt 1
                            withLogicalType (pure structChild1) (c_duckdb_get_type_id >=> (@?= DuckDBTypeVarchar))

        -- Union type
        withMany withCString ["int_member", "text_member"] \memberNames -> withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intMember ->
            withLogicalType (c_duckdb_create_logical_type DuckDBTypeVarchar) \textMember ->
                withArray [intMember, textMember] \memberArray ->
                    withArray memberNames \nameArray -> do
                        unionType <- c_duckdb_create_union_type memberArray nameArray 2
                        withLogicalType (pure unionType) \lt -> do
                            c_duckdb_get_type_id lt >>= (@?= DuckDBTypeUnion)
                            memberCount <- c_duckdb_union_type_member_count lt
                            memberCount @?= 2
                            memberName0 <- c_duckdb_union_type_member_name lt 0
                            peekCString memberName0 >>= (@?= "int_member")
                            c_duckdb_free (castPtr memberName0)
                            memberChild <- c_duckdb_union_type_member_type lt 1
                            withLogicalType (pure memberChild) (c_duckdb_get_type_id >=> (@?= DuckDBTypeVarchar))

aliasRoundtrip :: TestTree
aliasRoundtrip =
    testCase "logical type aliases can be set and retrieved" $
        withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \lt -> do
            aliasBefore <- c_duckdb_logical_type_get_alias lt
            aliasBefore @?= nullPtr
            withCString "custom_alias" $ \alias -> c_duckdb_logical_type_set_alias lt alias
            aliasAfter <- c_duckdb_logical_type_get_alias lt
            assertBool "alias pointer should not be null" (aliasAfter /= nullPtr)
            when (aliasAfter == nullPtr) $
                assertFailure "expected alias pointer"
            peekCString aliasAfter >>= (@?= "custom_alias")
            c_duckdb_free (castPtr aliasAfter)

registerLogicalType :: TestTree
registerLogicalType =
    testCase "registered logical type alias is accepted in SQL" $
        withDatabase \db ->
            withConnection db \conn ->
                withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \lt -> do
                    let aliasName = "custom_int_alias"
                    withCString aliasName $ \aliasPtr ->
                        c_duckdb_logical_type_set_alias lt aliasPtr
                    c_duckdb_register_logical_type conn lt nullPtr >>= (@?= DuckDBSuccess)

                    let createSql = "CREATE TABLE logical_type_demo (val " ++ aliasName ++ ")"
                    withResult conn createSql \_ -> pure ()

                    withResult conn "PRAGMA table_info('logical_type_demo')" \resPtr -> do
                        c_duckdb_row_count resPtr >>= (@?= 1)
                        typePtr <- c_duckdb_value_varchar resPtr 2 0
                        typeName <- peekCString typePtr
                        c_duckdb_free (castPtr typePtr)
                        typeName @?= aliasName