packages feed

duckdb-simple-0.2.0.0: src/Database/DuckDB/Simple/LogicalRep.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}

{- |
Module      : Database.DuckDB.Simple.LogicalRep
Description : Structured logical-type and value representations for DuckDB.
-}
module Database.DuckDB.Simple.LogicalRep (
    -- * Structured value helpers
    StructField (..),
    StructValue (..),
    UnionMemberType (..),
    UnionValue (..),
    LogicalTypeRep (..),
    structValueTypeRep,
    unionValueTypeRep,
    logicalTypeToRep,
    logicalTypeFromRep,
    destroyLogicalType,
) where

import Control.Exception (bracket, throwIO)
import Control.Monad (forM, when)
import Data.Array (Array, elems, listArray)
import qualified Data.ByteString as BS
import Data.Map.Strict (Map)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as TextEncoding
import Data.Word (Word16, Word64, Word8)
import Database.DuckDB.FFI
import Foreign.C.String (CString)
import Foreign.Marshal.Alloc (alloca)
import Foreign.Marshal.Array (withArray)
import Foreign.Marshal.Utils (withMany)
import Foreign.Ptr (castPtr, nullPtr)
import Foreign.Storable (poke)

-- | A Haskell description of a DuckDB logical type tree.
data LogicalTypeRep
    = LogicalTypeScalar DuckDBType
    | LogicalTypeDecimal !Word8 !Word8
    | LogicalTypeList LogicalTypeRep
    | LogicalTypeArray LogicalTypeRep !Word64
    | LogicalTypeMap LogicalTypeRep LogicalTypeRep
    | LogicalTypeStruct !(Array Int (StructField LogicalTypeRep))
    | LogicalTypeUnion !(Array Int UnionMemberType)
    | LogicalTypeEnum !(Array Int Text)
    deriving (Eq, Show)

-- | A named field within a STRUCT-like value or type.
data StructField a = StructField
    { structFieldName :: !Text
    , structFieldValue :: !a
    }
    deriving (Eq, Show)

-- | A fully materialized STRUCT value together with its type metadata.
data StructValue a = StructValue
    { structValueFields :: !(Array Int (StructField a))
    , structValueTypes :: !(Array Int (StructField LogicalTypeRep))
    , structValueIndex :: !(Map Text Int)
    }
    deriving (Eq, Show)

-- | A named member within a UNION type.
data UnionMemberType = UnionMemberType
    { unionMemberName :: !Text
    , unionMemberType :: !LogicalTypeRep
    }
    deriving (Eq, Show)

-- | A fully materialized UNION value together with its member metadata.
data UnionValue a = UnionValue
    { unionValueIndex :: !Word16
    , unionValueLabel :: !Text
    , unionValuePayload :: !a
    , unionValueMembers :: !(Array Int UnionMemberType)
    }
    deriving (Eq, Show)

-- | Recover the logical STRUCT type corresponding to a @StructValue@.
structValueTypeRep :: StructValue a -> LogicalTypeRep
structValueTypeRep StructValue{structValueTypes} = LogicalTypeStruct structValueTypes

-- | Recover the logical UNION type corresponding to a @UnionValue@.
unionValueTypeRep :: UnionValue a -> LogicalTypeRep
unionValueTypeRep UnionValue{unionValueMembers} = LogicalTypeUnion unionValueMembers

-- | Destroy a logical type handle obtained from DuckDB.
destroyLogicalType :: DuckDBLogicalType -> IO ()
destroyLogicalType logical =
    alloca \ptr -> do
        poke ptr logical
        c_duckdb_destroy_logical_type ptr

-- | Convert a DuckDB logical type handle into the pure @LogicalTypeRep@ tree.
logicalTypeToRep :: DuckDBLogicalType -> IO LogicalTypeRep
logicalTypeToRep logical = do
    dtype <- c_duckdb_get_type_id logical
    case dtype of
        DuckDBTypeStruct -> do
            childCountRaw <- c_duckdb_struct_type_child_count logical
            childCount <- word64ToInt (Text.pack "struct child count") childCountRaw
            fields <-
                forM [0 .. childCount - 1] \idx -> do
                    name <- bracket (c_duckdb_struct_type_child_name logical (fromIntegral idx)) (c_duckdb_free . castPtr) $ \ptr -> do
                        when (ptr == nullPtr) $
                            throwIO (userError "duckdb-simple: struct child name is null")
                        TextEncoding.decodeUtf8 <$> BS.packCString ptr
                    childRep <-
                        bracket (c_duckdb_struct_type_child_type logical (fromIntegral idx)) destroyLogicalType logicalTypeToRep
                    pure StructField{structFieldName = name, structFieldValue = childRep}
            pure $
                LogicalTypeStruct
                    ( if childCount <= 0
                        then listArray (0, -1) []
                        else listArray (0, childCount - 1) fields
                    )
        DuckDBTypeUnion -> do
            memberCountRaw <- c_duckdb_union_type_member_count logical
            memberCount <- word64ToInt (Text.pack "union member count") memberCountRaw
            members <-
                forM [0 .. memberCount - 1] \idx -> do
                    name <- bracket (c_duckdb_union_type_member_name logical (fromIntegral idx)) (c_duckdb_free . castPtr) $ \ptr -> do
                        when (ptr == nullPtr) $
                            throwIO (userError "duckdb-simple: union member name is null")
                        TextEncoding.decodeUtf8 <$> BS.packCString ptr
                    memberRep <-
                        bracket (c_duckdb_union_type_member_type logical (fromIntegral idx)) destroyLogicalType logicalTypeToRep
                    pure UnionMemberType{unionMemberName = name, unionMemberType = memberRep}
            pure $
                LogicalTypeUnion
                    ( if memberCount <= 0
                        then listArray (0, -1) []
                        else listArray (0, memberCount - 1) members
                    )
        DuckDBTypeList -> do
            childRep <- bracket (c_duckdb_list_type_child_type logical) destroyLogicalType logicalTypeToRep
            pure (LogicalTypeList childRep)
        DuckDBTypeArray -> do
            childRep <- bracket (c_duckdb_array_type_child_type logical) destroyLogicalType logicalTypeToRep
            size <- c_duckdb_array_type_array_size logical
            pure (LogicalTypeArray childRep size)
        DuckDBTypeMap -> do
            keyRep <- bracket (c_duckdb_map_type_key_type logical) destroyLogicalType logicalTypeToRep
            valueRep <- bracket (c_duckdb_map_type_value_type logical) destroyLogicalType logicalTypeToRep
            pure (LogicalTypeMap keyRep valueRep)
        DuckDBTypeDecimal -> do
            width <- c_duckdb_decimal_width logical
            scale <- c_duckdb_decimal_scale logical
            pure (LogicalTypeDecimal width scale)
        DuckDBTypeEnum -> do
            dictSize <- c_duckdb_enum_dictionary_size logical
            let count = fromIntegral dictSize :: Int
            entries <-
                forM [0 .. count - 1] \idx -> do
                    entry <- bracket (c_duckdb_enum_dictionary_value logical (fromIntegral idx)) (c_duckdb_free . castPtr) $ \ptr -> do
                        when (ptr == nullPtr) $
                            throwIO (userError "duckdb-simple: enum dictionary value is null")
                        TextEncoding.decodeUtf8 <$> BS.packCString ptr
                    pure entry
            pure $
                LogicalTypeEnum
                    ( if count <= 0
                        then listArray (0, -1) []
                        else listArray (0, count - 1) entries
                    )
        _ ->
            pure (LogicalTypeScalar dtype)

-- | Materialize a DuckDB logical type handle from a @LogicalTypeRep@ tree.
logicalTypeFromRep :: LogicalTypeRep -> IO DuckDBLogicalType
logicalTypeFromRep rep = do
    logical <- create rep
    when (logical == nullPtr) $
        throwIO (userError "duckdb-simple: DuckDB logical type construction failed")
    pure logical
  where
    create = \case
        LogicalTypeScalar dtype -> c_duckdb_create_logical_type dtype
        LogicalTypeDecimal width scale -> do
            when (width < 1 || width > 38 || scale > width) $
                throwIO (userError "duckdb-simple: invalid DECIMAL width or scale")
            c_duckdb_create_decimal_type width scale
        LogicalTypeList elemRep ->
            bracket (logicalTypeFromRep elemRep) destroyLogicalType c_duckdb_create_list_type
        LogicalTypeArray elemRep size ->
            bracket (logicalTypeFromRep elemRep) destroyLogicalType $
                flip c_duckdb_create_array_type size
        LogicalTypeMap keyRep valueRep ->
            bracket (logicalTypeFromRep keyRep) destroyLogicalType \keyType ->
                bracket (logicalTypeFromRep valueRep) destroyLogicalType $
                    c_duckdb_create_map_type keyType
        LogicalTypeStruct fieldArray -> do
            let fields = elems fieldArray
            withMany (\field -> bracket (logicalTypeFromRep (structFieldValue field)) destroyLogicalType) fields \childTypes ->
                withMany withTypeName (map structFieldName fields) \names ->
                    withArray names \nameArray ->
                        withArray childTypes \typeArray ->
                            c_duckdb_create_struct_type typeArray nameArray (fromIntegral (length fields))
        LogicalTypeUnion memberArray -> do
            let members = elems memberArray
            withMany (\member -> bracket (logicalTypeFromRep (unionMemberType member)) destroyLogicalType) members \memberTypes ->
                withMany withTypeName (map unionMemberName members) \names ->
                    withArray names \nameArray ->
                        withArray memberTypes \typeArray ->
                            c_duckdb_create_union_type typeArray nameArray (fromIntegral (length members))
        LogicalTypeEnum dictArray ->
            withMany withTypeName (elems dictArray) \names ->
                withArray names \nameArray ->
                    c_duckdb_create_enum_type nameArray (fromIntegral (length names))

-- | Encode native type names as UTF-8 and reject embedded NUL.
withTypeName :: Text -> (CString -> IO a) -> IO a
withTypeName name action = do
    when (Text.any (== '\0') name) $
        throwIO (userError "duckdb-simple: logical type name contains NUL")
    BS.useAsCString (TextEncoding.encodeUtf8 name) action

word64ToInt :: Text -> Word64 -> IO Int
word64ToInt label value =
    let actual = toInteger value
        limit = toInteger (maxBound :: Int)
     in if actual <= limit
            then pure (fromIntegral value)
            else
                throwIO
                    ( userError
                        ( "duckdb-simple: "
                            <> Text.unpack label
                            <> " exceeds Int range"
                        )
                    )