wireform-parquet-0.1.0.0: test/VariantWriteRegression.hs
{-# LANGUAGE OverloadedStrings #-}
module VariantWriteRegression (tests) where
import Arrow.Column qualified as C
import Arrow.Types qualified as A
import Control.Exception (evaluate)
import Control.Monad (unless)
import Data.ByteString qualified as BS
import Data.Either (isLeft)
import Data.Int (Int32)
import Data.Map.Strict qualified as M
import Data.Text qualified as T
import Data.Vector qualified as V
import Data.Vector.Storable qualified as S
import GHC.Conc (getAllocationCounter)
import Hedgehog qualified as H
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Parquet.Arrow qualified as Arrow
import Parquet.Nested qualified as N
import Parquet.Read qualified as Read
import Parquet.Types qualified as P
import Parquet.Variant qualified as B
import Parquet.Variant.Arrow qualified as VariantArrow
import Parquet.Variant.Parquet qualified as VP
import Parquet.Variant.Schema qualified as Schema
import Parquet.Variant.Shred qualified as Shred
import VariantRegression qualified as Cases
load :: Either String a -> IO a
load = either fail pure
file :: Schema.VariantStorageSchema -> V.Vector N.NestedRow -> Either String Read.ParquetFile
file storage rows = N.buildNestedFile (V.singleton ("v", N.NSOptional (N.NSVariantTyped storage))) (V.singleton rows) >>= Read.loadParquetFile
leaf :: BS.ByteString -> N.NestedRow
leaf = N.NRLeaf . N.LvBinary
tests :: IO ()
tests = do
let check name good = unless good (fail ("typed Variant writer: " ++ name))
metadata = leaf (BS.pack [1, 0, 0])
int32 = A.defaultLeafField "typed_value" True (A.AInt 32 True)
-- Canonical fields are accessed by name, including when the column's
-- named children are in a different order than its declared Field.
plain <- load (Schema.variantField "v" True Nothing)
named <- load (C.mkStruct 1 Nothing (V.fromList [("value", C.fromByteStrings (V.singleton (BS.singleton 0))), ("metadata", C.fromByteStrings (V.singleton (BS.pack [1, 0, 0])))]))
namedSchema <- load (Arrow.arrowFieldToNestedSchema plain)
namedRows <- load (Arrow.columnArrayToNestedRows plain named)
namedBytes <- load (N.buildNestedFile (V.singleton ("v", namedSchema)) (V.singleton namedRows))
namedFile <- load (Read.loadParquetFile namedBytes)
namedValues <- load (VP.readVariantColumn namedFile 0 "v")
check "permuted metadata/value buffers bind by name before row lowering" (namedValues == V.singleton (Just B.VNull))
duplicate <- load (C.mkStruct 1 Nothing (V.fromList [("metadata", C.fromByteStrings (V.singleton (BS.pack [1, 0, 0]))), ("metadata", C.fromByteStrings (V.singleton (BS.singleton 0)))]))
check "duplicate named Variant children rejected before binding" (isLeft (Arrow.columnArrayToNestedRows plain duplicate) && isLeft (VariantArrow.compileVariantArrowReader plain duplicate))
let elementFields = V.fromList [A.defaultLeafField "value" True A.ABinary, int32]
elementField = A.defaultField "element" False A.AStruct elementFields
listField = A.defaultField "typed_value" True A.AList (V.singleton elementField)
namedField = (A.defaultField "v" True A.AStruct (V.fromList [A.defaultLeafField "metadata" False A.ABinary, listField])) {A.fieldMetadata = Schema.variantExtensionMetadata}
elementColumns <- load (C.mkStruct 2 Nothing (V.fromList [("typed_value", C.fromMaybes C.PInt32 (V.fromList [Just 3, Nothing])), ("value", C.fromMaybeByteStrings (V.fromList [Nothing, Just (BS.singleton 0)]))]))
listColumn <- load (C.mkList Nothing (S.fromList [0, 2 :: Int32]) elementColumns)
nestedNames <- load (C.mkStruct 1 Nothing (V.fromList [("typed_value", listColumn), ("metadata", C.fromByteStrings (V.singleton (BS.pack [1, 0, 0])))]))
directReader <- load (VariantArrow.compileVariantArrowReader namedField nestedNames)
directValue <- load (VariantArrow.variantArrowAt directReader 0)
let expectedNames = Just (B.VArray (V.fromList [B.VInt32 3, B.VNull]))
check "reader aligns different child types recursively before shape validation" (directValue == expectedNames)
nestedSchema <- load (Arrow.arrowFieldToNestedSchema namedField)
nestedRows <- load (Arrow.columnArrayToNestedRows namedField nestedNames)
nestedBytes <- load (N.buildNestedFile (V.singleton ("v", nestedSchema)) (V.singleton nestedRows))
nestedFile <- load (Read.loadParquetFile nestedBytes)
nestedValues <- load (VP.readVariantColumn nestedFile 0 "v")
check "writer aligns nested list element wrappers by name" (nestedValues == V.singleton expectedNames)
field <- load (Schema.variantField "v" True (Just int32))
storage <- load (Schema.compileVariantStorageSchema field)
let row value = N.NRStruct (V.fromList [metadata, N.NRNull, value])
rows = V.fromList [row (N.NRLeaf (N.LvInt32 7)), N.NRNull, row N.NRNull]
native <- load (file storage rows)
actual <- load (VP.readVariantColumn native 0 "v")
check "primitive, null root and implicit Variant null stay distinct" (actual == V.fromList [Just (B.VInt32 7), Nothing, Just B.VNull])
check "native annotation emitted" (P.seLogicalType (P.fmSchema (Read.pfFooter native) V.! 1) == Just (P.LTVariant 1))
let conflict = N.NRStruct (V.fromList [metadata, leaf (BS.singleton 0), N.NRLeaf (N.LvInt32 7)])
check "conflicting primitive storage rejected" (isLeft (file storage (V.singleton conflict)))
let wrapper = A.defaultField "element" False A.AStruct (V.singleton int32)
array = A.defaultField "typed_value" True A.AList (V.singleton wrapper)
root = (A.defaultField "v" True A.AStruct (V.fromList [A.defaultLeafField "metadata" False A.ABinary, array])) {A.fieldMetadata = Schema.variantExtensionMetadata}
elements = V.fromList [N.NRStruct (V.singleton (N.NRLeaf (N.LvInt32 3))), N.NRStruct (V.singleton N.NRNull), N.NRStruct (V.singleton N.NRNull)]
arrayRow = N.NRStruct (V.fromList [metadata, N.NRList elements])
arrays <- load (Schema.compileVariantStorageSchema root)
arrayFile <- load (file arrays (V.singleton arrayRow))
arrayValues <- load (VP.readVariantColumn arrayFile 0 "v")
check "typed-only arrays gain explicit null fallbacks without losing elements" (arrayValues == V.singleton (Just (B.VArray (V.fromList [B.VInt32 3, B.VNull, B.VNull]))))
forLimits <- load (Schema.compileVariantStorageSchemaWithLimits (B.VariantLimits 10 3 0) Schema.defaultVariantSchemaLimits root)
check "one budget covers typed array and materialized null elements" (isLeft (file forLimits (V.singleton arrayRow)))
noDepth <- load (Schema.compileVariantStorageSchemaWithLimits (B.VariantLimits 0 100 0) Schema.defaultVariantSchemaLimits root)
check "missing array elements still obey nesting depth" (isLeft (file noDepth (V.singleton arrayRow)))
let identified = field {A.fieldMetadata = A.fieldMetadata field `V.snoc` ("PARQUET:field_id", "-7"), A.fieldChildren = V.imap (\index child -> child {A.fieldMetadata = A.fieldMetadata child `V.snoc` ("PARQUET:field_id", if index == 0 then "11" else if index == 1 then "12" else "13")}) (A.fieldChildren field)}
ids <- load (Schema.compileVariantStorageSchema identified)
withIds <- load (file ids (V.singleton (row (N.NRLeaf (N.LvInt32 1)))))
check "field IDs preserved through native schema" (V.map P.seFieldId (V.tail (P.fmSchema (Read.pfFooter withIds))) == V.fromList [Just (-7), Just 11, Just 12, Just 13])
recovered <- load (Arrow.parquetFileArrowSchemaChecked withIds)
let recoveredField = V.head (A.arrowFields recovered)
check "native Arrow schema retains root and child field IDs" (V.elem ("PARQUET:field_id", "-7") (A.fieldMetadata recoveredField) && V.elem ("PARQUET:field_id", "13") (A.fieldMetadata (A.fieldChildren recoveredField V.! 2)))
let invalidId = field {A.fieldMetadata = A.fieldMetadata field V.++ V.fromList [("PARQUET:field_id", "1"), ("PARQUET:field_id", "2")]}
check "duplicate field IDs rejected" (isLeft (Schema.compileVariantStorageSchema invalidId))
-- Semantic selection must preserve all primitive tags, not just values
-- that happen to fit the target's numeric carrier.
target <- load (Schema.variantField "v" True (Just (A.defaultLeafField "typed_value" True (A.AInt 64 True))))
semantic <- load (Shred.compileVariantShredder target)
semanticRows <- V.mapM (load . Shred.shredVariant semantic . Just) Cases.values
semanticBytes <- load (N.buildNestedFile (V.singleton ("v", Shred.variantShredSchema semantic)) (V.singleton semanticRows))
semanticFile <- load (Read.loadParquetFile semanticBytes)
semanticValues <- load (VP.readVariantColumn semanticFile 0 "v")
actualBytes <- V.mapM (load . traverse B.encodeVariant) semanticValues
expectedBytes <- V.mapM (load . traverse B.encodeVariant) (Just <$> Cases.values)
check "semantic shreds preserve every Variant primitive/container/IEEE tag" (actualBytes == expectedBytes)
let decimalField = A.defaultLeafField "typed_value" True (A.ADecimal32 8 2)
decimalTarget <- load (Schema.variantField "v" True (Just decimalField))
decimalShred <- load (Shred.compileVariantShredder decimalTarget)
let decimalValues = V.fromList [B.VDecimal4 2 1, B.VDecimal4 3 1, B.VDecimal4 2 100000000, B.VDecimal8 2 1]
decimalRows <- V.mapM (load . Shred.shredVariant decimalShred . Just) decimalValues
decimalBytes <- load (N.buildNestedFile (V.singleton ("v", Shred.variantShredSchema decimalShred)) (V.singleton decimalRows))
decimalFile <- load (Read.loadParquetFile decimalBytes)
decimalOutput <- load (VP.readVariantColumn decimalFile 0 "v")
check "decimal scale, precision overflow and width mismatch use exact fallback" (decimalOutput == (Just <$> decimalValues))
emptyDictionary <- load (B.buildVariantMetadata V.empty)
objectTarget <- load (Schema.variantField "v" True (Just (A.defaultField "typed_value" True A.AStruct (V.singleton (A.defaultField "a" False A.AStruct (V.singleton int32))))))
objectShred <- load (Shred.compileVariantShredder objectTarget)
check "fully shredded names must exist in the supplied dictionary" (isLeft (Shred.shredVariantWithMetadata objectShred emptyDictionary (Just (B.VObject (M.singleton "a" (B.VInt32 1))))))
passed <-
H.check
( H.withTests
500
( H.property $ do
number <- H.forAll (Gen.int32 (Range.linear (-1000000) 1000000))
present <- H.forAll Gen.bool
remainder <- H.forAll Gen.bool
dictionary <- H.evalEither (B.buildVariantMetadata (V.fromList ["a", "rest"]))
let wrap = A.defaultField "a" False A.AStruct (V.singleton int32)
object = A.defaultField "typed_value" True A.AStruct (V.singleton wrap)
objectField <- H.evalEither (Schema.variantField "v" True (Just object))
objectStorage <- H.evalEither (Schema.compileVariantStorageSchema objectField)
fragment <- H.evalEither (B.encodeVariantValue dictionary (B.VObject (if remainder then M.singleton "rest" (B.VInt8 9) else M.empty)))
let child = N.NRStruct (V.singleton (if present then N.NRLeaf (N.LvInt32 number) else N.NRNull))
objectRow = N.NRStruct (V.fromList [leaf (B.metadataBytes dictionary), if remainder then leaf fragment else N.NRNull, N.NRStruct (V.singleton child)])
expected = (if present then M.insert "a" (B.VInt32 number) else id) (if remainder then M.singleton "rest" (B.VInt8 9) else M.empty)
encoded <- H.evalEither (file objectStorage (V.singleton objectRow))
decoded <- H.evalEither (VP.readVariantColumn encoded 0 "v")
decoded H.=== V.singleton (Just (B.VObject expected))
)
)
check "generated partial objects with shared dictionaries" passed
dictionary <- load (B.buildVariantMetadata (V.singleton (T.replicate (1024 * 1024) "a")))
let bytes = B.metadataBytes dictionary
count = fromIntegral (maxBound :: Int32) `quot` (BS.length bytes + 4) + 1
oversized = V.replicate count (N.NRStruct (V.fromList [leaf bytes, N.NRNull, N.NRLeaf (N.LvInt32 1)]))
_ <- evaluate (BS.length bytes)
_ <- evaluate (V.length oversized)
before <- getAllocationCounter
rejected <- evaluate (isLeft (N.shred (N.NSOptional (N.NSVariantTyped storage)) oversized))
after <- getAllocationCounter
check "all byte preflights precede repeated dictionary decoding" rejected
check ("oversized typed page allocation: " ++ show (before - after)) (before - after < 8 * 1024 * 1024)
putStrLn "Typed Variant native writing, dictionaries, nulls, field IDs, budgets and preflight regressions passed"