packages feed

duckdb-ffi-1.5.0.0: test/ProfilingInfoTest.hs

{-# LANGUAGE BlockArguments #-}

module ProfilingInfoTest (tests) where

import Control.Monad (forM)
import Database.DuckDB.FFI
import Foreign.C.String (peekCString, withCString)
import Foreign.Marshal.Alloc (alloca)
import Foreign.Ptr (castPtr, nullPtr)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
import Utils (destroyDuckValue, withConnection, withDatabase)

tests :: TestTree
tests =
    testGroup
        "Profiling Info"
        [ profilingDisabledByDefault
        , profilingMetricsRoundtrip
        ]

profilingDisabledByDefault :: TestTree
profilingDisabledByDefault =
    testCase "profiling info requires enabling profiling" $
        withDatabase \db ->
            withConnection db \conn -> do
                infoPtr <- c_duckdb_get_profiling_info conn
                infoPtr @?= nullPtr

profilingMetricsRoundtrip :: TestTree
profilingMetricsRoundtrip =
    testCase "collect metrics and traverse profiling tree" $
        withDatabase \db ->
            withConnection db \conn -> do
                runStatement conn "PRAGMA enable_profiling='no_output'"
                runStatement conn "CREATE TABLE profiling_numbers(value INTEGER)"
                runStatement conn "INSERT INTO profiling_numbers VALUES (1), (2), (3)"
                runStatement conn "SELECT sum(value) FROM profiling_numbers"

                infoPtr <- c_duckdb_get_profiling_info conn
                assertBool "profiling info pointer should be non-null" (infoPtr /= nullPtr)

                metricsVal <- c_duckdb_profiling_info_get_metrics infoPtr
                entryCountIdx <- c_duckdb_get_map_size metricsVal
                assertBool "expected at least one metric entry" (entryCountIdx > 0)
                entries <- collectMetrics metricsVal entryCountIdx
                destroyDuckValue metricsVal

                assertBool "metrics map should contain entries" (not (null entries))
                let (firstKey, firstValue) = case entries of
                        (e : _) -> e
                        [] -> error "unreachable: entries is non-empty"

                withCString firstKey \keyPtr -> do
                    valueHandle <- c_duckdb_profiling_info_get_value infoPtr keyPtr
                    assertBool ("metric " <> firstKey <> " should be present") (valueHandle /= nullPtr)
                    fetchedValue <- duckValueToString valueHandle
                    destroyDuckValue valueHandle
                    fetchedValue @?= firstValue

                childCount <- c_duckdb_profiling_info_get_child_count infoPtr
                assertBool "expected at least one child node" (childCount > 0)
                let firstChildIdx = 0
                childPtr <- c_duckdb_profiling_info_get_child infoPtr firstChildIdx
                assertBool "child pointer should be non-null" (childPtr /= nullPtr)

                childMetrics <- c_duckdb_profiling_info_get_metrics childPtr
                childCountIdx <- c_duckdb_get_map_size childMetrics
                assertBool "child node should expose metrics" (childCountIdx > 0)
                destroyDuckValue childMetrics

runStatement :: DuckDBConnection -> String -> IO ()
runStatement conn sql =
    withCString sql \sqlPtr ->
        alloca \resPtr -> do
            state <- c_duckdb_query conn sqlPtr resPtr
            if state == DuckDBSuccess
                then c_duckdb_destroy_result resPtr
                else do
                    errPtr <- c_duckdb_result_error resPtr
                    errMsg <-
                        if errPtr == nullPtr
                            then pure "unknown error"
                            else peekCString errPtr
                    c_duckdb_destroy_result resPtr
                    assertFailure ("duckdb_query failed: " <> errMsg)

collectMetrics :: DuckDBValue -> DuckDBIdx -> IO [(String, String)]
collectMetrics metricsVal entryCountIdx = do
    let entryCount = fromIntegral entryCountIdx :: Int
    forM [0 .. entryCount - 1] \i -> do
        let idx = fromIntegral i :: DuckDBIdx
        keyHandle <- c_duckdb_get_map_key metricsVal idx
        keyName <- duckValueToText keyHandle
        destroyDuckValue keyHandle

        valHandle <- c_duckdb_get_map_value metricsVal idx
        valText <- duckValueToString valHandle
        destroyDuckValue valHandle

        pure (keyName, valText)

duckValueToText :: DuckDBValue -> IO String
duckValueToText valHandle = do
    strPtr <- c_duckdb_get_varchar valHandle
    text <- peekCString strPtr
    c_duckdb_free (castPtr strPtr)
    pure text

duckValueToString :: DuckDBValue -> IO String
duckValueToString valHandle = do
    strPtr <- c_duckdb_value_to_string valHandle
    text <- peekCString strPtr
    c_duckdb_free (castPtr strPtr)
    pure text