packages feed

duckdb-simple-0.3.0.0: test-codec/Main.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Check the private codec's validation without constructing invalid native vectors.
module Main (main) where

import Control.Exception (IOException, try)
import Control.Monad (forM_)
import Data.Bits (shiftR, (.&.), (.|.))
import qualified Data.ByteString as BS
import Data.Text (Text)
import Data.Word (Word32, Word8)
import Database.DuckDB.Simple.FromField (FieldValue (..))
import Database.DuckDB.Simple.Variant (variantObject)
import Database.DuckDB.Simple.VariantCodec (decodeVariantPayload)
import Test.Tasty (TestTree, defaultMain, testGroup)
import Test.Tasty.HUnit

-- | Run malformed payload checks and boundary cases for the native format.
main :: IO ()
main =
    defaultMain $
        testGroup
            "VARIANT payload validation"
            [ rejected "missing root" [] [] [] []
            , rejected "unknown tag" [(34, 0)] [] [] []
            , rejected "offset outside blob" [(1, 1)] [] [] []
            , rejected "truncated BIGINT" [(6, 0)] [] [] (replicate 7 0)
            , rejected "truncated varint" [(16, 0)] [] [] [128]
            , rejected "varint overflows uint32" [(16, 0)] [] [] [255, 255, 255, 255, 16]
            , rejected "unterminated varint" [(16, 0)] [] [] [255, 255, 255, 255, 255, 0]
            , rejected "string outside blob" [(16, 0)] [] [] [3, 97, 98]
            , rejected "invalid UTF-8" [(16, 0)] [] [] [1, 255]
            , rejected "decimal scale exceeds precision" [(15, 0)] [] [] [2, 3, 1, 0]
            , rejected "decimal value exceeds precision" [(15, 0)] [] [] [1, 0, 100, 0]
            , rejected "BIGNUM header is truncated" [(31, 0)] [] [] [2, 128, 0]
            , rejected "BIGNUM length mismatch" [(31, 0)] [] [] [4, 128, 0, 2, 1]
            , rejected "BIT padding exceeds seven" [(32, 0)] [] [] [2, 8, 255]
            , rejected "BIT padding bits are absent" [(32, 0)] [] [] [2, 3, 21]
            , rejected "child value out of range" [(30, 0)] [(Nothing, 1)] [] [1, 0]
            , rejected "container range out of bounds" [(30, 0)] [] [] [1, 0]
            , rejected "object key is missing" [(29, 0), (1, 2)] [(Just 0, 1)] [] [1, 0]
            , rejected "array child has a key" [(30, 0), (1, 2)] [(Just 0, 1)] [(0, "x")] [1, 0]
            , rejected "object child has no key" [(29, 0), (1, 2)] [(Nothing, 1)] [] [1, 0]
            , rejected "duplicate dictionary index" [(1, 0)] [] [(0, "x"), (0, "y")] []
            , rejected "duplicate object key" [(29, 0), (1, 2)] [(Just 0, 1), (Just 1, 1)] [(0, "x"), (1, "x")] [2, 0]
            , rejected "cyclic child reference" [(30, 0)] [(Nothing, 0)] [] [1, 0]
            , testCase "empty values need no payload bytes" $
                decodeVariantPayload [(30, 0)] [] [] (BS.singleton 0) >>= (@?= FieldList [])
            , testCase "shared children decode without duplicate traversal" $
                decodeVariantPayload [(30, 0), (3, 2)] [(Nothing, 1), (Nothing, 1)] [] (BS.pack [2, 0, 7])
                    >>= (@?= FieldList [FieldInt8 7, FieldInt8 7])
            , testCase "length-aware keys preserve embedded NUL" $
                decodeVariantPayload [(29, 0), (1, 2)] [(Just 0, 1)] [(0, "a\0b")] (BS.pack [1, 0])
                    >>= (@?= variantObject [("a\0b", FieldBool True)])
            , testCase "deep acyclic containers are accepted" $
                forM_ [128, 129, 257, 1025] $ \count ->
                    let (values, children, bytes) = nested count
                     in decodeVariantPayload values children [] bytes >>= (@?= foldr (const (FieldList . pure)) (FieldInt8 7) [1 .. count - 1])
            ]

-- | Require a validation error for copied native payload data.
rejected :: String -> [(Word8, Word32)] -> [(Maybe Word32, Word32)] -> [(Word32, Text)] -> [Word8] -> TestTree
rejected label values children keys bytes =
    testCase label $ assertRejected (decodeVariantPayload values children keys (BS.pack bytes))

-- | Require a codec error.
assertRejected :: IO FieldValue -> Assertion
assertRejected action = do
    result <- try action
    case result of
        Left (_ :: IOException) -> pure ()
        Right value -> assertFailure ("expected payload rejection, got " <> show value)

-- | Construct a chain of arrays with one scalar leaf.
nested :: Int -> ([(Word8, Word32)], [(Maybe Word32, Word32)], BS.ByteString)
nested count =
    ( zip (replicate (count - 1) 30 <> [3]) offsets
    , [(Nothing, fromIntegral (n + 1)) | n <- [0 .. count - 2]]
    , BS.pack (concat payloads)
    )
  where
    payloads = [1 : varint (fromIntegral n) | n <- [0 .. count - 2]] <> [[7]]
    offsets = scanl (+) 0 (map (fromIntegral . length) payloads)
    varint :: Word32 -> [Word8]
    varint value
        | value < 128 = [fromIntegral value]
        | otherwise = fromIntegral (value .&. 127 .|. 128) : varint (value `shiftR` 7)