packages feed

wireform-parquet-0.1.0.0: test/VariantArrowRegression.hs

{-# LANGUAGE OverloadedStrings #-}

module VariantArrowRegression (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 (Int64)
import Data.Map.Strict qualified as M
import Data.Text.Lazy qualified as TL
import Data.Text.Lazy.Builder qualified as TB
import Data.Text.Lazy.Builder.Int qualified as TBI
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.Variant qualified as B
import Parquet.Variant.Arrow qualified as R
import Parquet.Variant.Schema qualified as Schema


load :: Either String a -> IO a
load = either fail pure


check :: String -> Bool -> IO ()
check name ok = unless ok (fail ("Variant Arrow regression: " ++ name))


metadataField :: A.Field
metadataField = A.defaultLeafField "metadata" False A.ABinary


valueField :: A.Field
valueField = A.defaultLeafField "value" True A.ABinary


longField :: A.Field
longField = A.defaultLeafField "typed_value" True (A.AInt 64 True)


wrapper :: A.Field
wrapper = A.defaultField "a" False A.AStruct (V.fromList [valueField, longField])


root :: V.Vector A.Field -> A.Field
root children = (A.defaultField "v" True A.AStruct children) {A.fieldMetadata = Schema.variantExtensionMetadata}


-- A partial object whose fallback fragment uses a dictionary also containing
-- the completely shredded name a. Encoding that fragment with a new dictionary
-- would silently change its object IDs and lose/corrupt the rest field.
data PartialCase = PartialCase {partialField :: !A.Field, partialColumn :: !C.ColumnArray, partialExpected :: !B.Variant}


partial :: Int64 -> Either String PartialCase
partial number = do
  dictionary <- B.buildVariantMetadata (V.fromList ["a", "rest"])
  let remainder = B.VObject (M.singleton "rest" (B.VArray (V.fromList [B.VInt8 1, B.VInt8 2])))
  fallback <- B.encodeVariantValue dictionary remainder
  child <- C.mkStruct 1 Nothing (V.fromList [("value", C.fromMaybeByteStrings (V.singleton Nothing)), ("typed_value", C.fromMaybes C.PInt64 (V.singleton (Just number)))])
  typed <- C.mkStruct 1 Nothing (V.singleton ("a", child))
  let typedField = A.defaultField "typed_value" True A.AStruct (V.singleton wrapper)
      field = root (V.fromList [typedField, metadataField, valueField])
      expected = B.VObject (M.insert "a" (B.VInt64 number) (M.singleton "rest" (B.VArray (V.fromList [B.VInt8 1, B.VInt8 2]))))
  column <- C.mkStruct 1 Nothing (V.fromList [("typed_value", typed), ("metadata", C.fromMaybeByteStrings (V.singleton (Just (B.metadataBytes dictionary)))), ("value", C.fromMaybeByteStrings (V.singleton (Just fallback)))])
  pure (PartialCase field column expected)


tests :: IO ()
tests = do
  example <- load (partial minBound)
  let field = partialField example; column = partialColumn example; expected = partialExpected example
  reader <- load (R.compileVariantArrowReader field column)
  check "partial object reconstructs typed plus fallback with original IDs" (R.variantArrowAt reader 0 == Right (Just expected))
  check "row bounds reject" (isLeft (R.variantArrowAt reader (-1)) && isLeft (R.variantArrowAt reader 1))
  let compile limits = R.compileVariantArrowReaderWithLimits limits Schema.defaultVariantSchemaLimits field column
  limited <- load (compile (B.VariantLimits 500 4 100))
  check "typed and fallback siblings share one global count budget" (isLeft (R.variantArrowAt limited 0))
  exact <- load (compile (B.VariantLimits 2 5 2))
  check "exact tree count/depth/dictionary budgets succeed" (R.variantArrowAt exact 0 == Right (Just expected))
  shallow <- load (compile (B.VariantLimits 1 100 2))
  check "fallback fragments retain their position in typed tree depth" (isLeft (R.variantArrowAt shallow 0))
  let emptyMetadata = C.fromMaybeByteStrings (V.replicate 4 (Just (BS.pack [1, 0, 0])))
      binary = C.fromMaybeByteStrings (V.fromList [Nothing, Just (BS.singleton 0), Nothing, Just "poison"])
      typed = C.fromMaybes C.PInt64 (V.fromList [Nothing, Nothing, Just 34, Just 999])
      validity = C.validityFromBools (V.fromList [True, True, True, False])
      primitiveField = root (V.fromList [metadataField, valueField, longField])
  primitiveColumn <- load (C.mkStruct 4 validity (V.fromList [("metadata", emptyMetadata), ("value", binary), ("typed_value", typed)]))
  primitive <- load (R.compileVariantArrowReader primitiveField primitiveColumn)
  actual <- load (V.generateM 4 (R.variantArrowAt primitive))
  check "required missing root becomes Variant null; null root masks poison" (actual == V.fromList [Just B.VNull, Just B.VNull, Just (B.VInt64 34), Nothing])
  let requiredTyped = primitiveField {A.fieldChildren = V.fromList [metadataField, valueField, longField {A.fieldNullable = False}]}
      requiredValue = primitiveField {A.fieldChildren = V.fromList [metadataField, valueField {A.fieldNullable = False}, longField]}
  typedRequired <- load (R.compileVariantArrowReader requiredTyped primitiveColumn)
  check "visible required typed null is invalid instead of fallback" (isLeft (R.variantArrowAt typedRequired 0) && isLeft (R.variantArrowAt typedRequired 1))
  check "present required typed value remains valid" (R.variantArrowAt typedRequired 2 == Right (Just (B.VInt64 34)))
  valueRequired <- load (R.compileVariantArrowReader requiredValue primitiveColumn)
  check "visible required fallback null is invalid instead of typed value" (isLeft (R.variantArrowAt valueRequired 0) && isLeft (R.variantArrowAt valueRequired 2))
  check "present required fallback remains valid" (R.variantArrowAt valueRequired 1 == Right (Just B.VNull))
  maskedColumn <- load (C.mkStruct 1 (C.validityFromBools (V.singleton False)) (V.fromList [("metadata", C.fromMaybeByteStrings (V.singleton (Just "poison"))), ("value", C.fromMaybeByteStrings (V.singleton (Just "poison"))), ("typed_value", C.fromMaybes C.PInt64 (V.singleton Nothing))]))
  maskedReader <- load (R.compileVariantArrowReader requiredTyped maskedColumn)
  check "null parent masks null required typed child and poison payloads" (R.variantArrowAt maskedReader 0 == Right Nothing)
  check "non-nullable root cannot mask a null slot" (isLeft (R.compileVariantArrowReader primitiveField {A.fieldNullable = False} primitiveColumn))
  let missing = C.fromMaybeByteStrings (V.singleton Nothing)
      metadata = C.fromMaybeByteStrings (V.singleton (Just (BS.pack [1, 0, 0])))
  elements <- load (C.mkStruct 3 Nothing (V.singleton ("value", C.fromMaybeByteStrings (V.fromList [Just (BS.singleton 0), Nothing, Just (BS.pack [12, 127])]))))
  list <- load (C.mkList Nothing (S.fromList [0, 3]) elements)
  let elementField = A.defaultField "element" False A.AStruct (V.singleton valueField)
      listField = A.defaultField "typed_value" True A.AList (V.singleton elementField)
      arrayField = root (V.fromList [metadataField, listField])
  array <- load (C.mkStruct 1 Nothing (V.fromList [("metadata", metadata), ("typed_value", list)]))
  arrays <- load (R.compileVariantArrowReader arrayField array)
  check "missing array element materializes null and preserves cardinality" (R.variantArrowAt arrays 0 == Right (Just (B.VArray (V.fromList [B.VNull, B.VNull, B.VInt8 127]))))
  check "bad root schema is rejected before column traversal" (isLeft (R.compileVariantArrowReader arrayField {A.fieldMetadata = V.empty} array))
  let ordinaryRoot = root (V.fromList [metadataField, valueField])
  metadataMissing <- load (C.mkStruct 1 Nothing (V.fromList [("metadata", missing), ("value", missing)]))
  mm <- load (R.compileVariantArrowReader ordinaryRoot metadataMissing)
  check "present root requires metadata even when its value is missing" (isLeft (R.variantArrowAt mm 0))
  -- A fragment nested under a typed object starts at depth one. Resetting
  -- its binary decoder to depth zero would incorrectly admit this array.
  dict <- load (B.buildVariantMetadata (V.singleton "a"))
  payload <- load (B.encodeVariantValue dict (B.VArray (V.singleton B.VNull)))
  fragment <- load (C.mkStruct 1 Nothing (V.singleton ("value", C.fromMaybeByteStrings (V.singleton (Just payload)))))
  containing <- load (C.mkStruct 1 Nothing (V.singleton ("a", fragment)))
  let valueWrapper = A.defaultField "a" False A.AStruct (V.singleton valueField)
      containingField = A.defaultField "typed_value" True A.AStruct (V.singleton valueWrapper)
      nestedField = root (V.fromList [metadataField, containingField])
  nestedColumn <- load (C.mkStruct 1 Nothing (V.fromList [("metadata", C.fromMaybeByteStrings (V.singleton (Just (B.metadataBytes dict)))), ("typed_value", containing)]))
  tooShallow <- load (R.compileVariantArrowReaderWithLimits (B.VariantLimits 1 100 100) Schema.defaultVariantSchemaLimits nestedField nestedColumn)
  check "nested fallback decoder never resets its actual depth" (isLeft (R.variantArrowAt tooShallow 0))
  deepEnough <- load (R.compileVariantArrowReaderWithLimits (B.VariantLimits 2 3 1) Schema.defaultVariantSchemaLimits nestedField nestedColumn)
  check "nested fallback decoder counts one semantic root" (R.variantArrowAt deepEnough 0 == Right (Just (B.VObject (M.singleton "a" (B.VArray (V.singleton B.VNull))))))
  -- Directly sliced REE metadata retains a nonzero logical offset. No
  -- expansion or assumption that logical row zero is physical run zero.
  metadataValues <- load (C.mkBinary Nothing (S.fromList [0, 6, 9]) (BS.pack [112, 111, 105, 115, 111, 110, 1, 0, 0]))
  runs <- load (C.mkRunEndEncoded (C.primColumn C.PInt16 (S.fromList [2, 5])) metadataValues)
  let sliced = C.sliceColumnArray 1 4 runs
      reeField = A.defaultField "metadata" False A.ARunEndEncoded (V.fromList [A.defaultLeafField "run_ends" False (A.AInt 16 True), A.defaultLeafField "values" True A.ABinary])
      reeSchema = root (V.fromList [reeField, longField])
      rootValidity = C.validityFromBools (V.fromList [False, True, True, True])
  reeColumn <- load (C.mkStruct 4 rootValidity (V.fromList [("metadata", sliced), ("typed_value", C.primColumn C.PInt64 (S.fromList [999, 7, 8, 9]))]))
  reeReader <- load (R.compileVariantArrowReader reeSchema reeColumn)
  reeValues <- load (V.generateM 4 (R.variantArrowAt reeReader))
  check "sliced run-end metadata preserves offset and masks unreachable poison" (reeValues == V.fromList [Nothing, Just (B.VInt64 7), Just (B.VInt64 8), Just (B.VInt64 9)])
  -- Wide objects can share their all-missing child buffers. The schema
  -- work budget bounds their compiled shape; names must not be rebound by
  -- rescanning the whole column vector once for every sibling.
  emptyFieldColumn <- load (C.mkStruct 1 Nothing (V.singleton ("value", missing)))
  let width = 50000
      nameAt i = TL.toStrict (TB.toLazyText ("k" <> TBI.decimal i))
      wideFields = V.generate width (\i -> A.defaultField (nameAt i) False A.AStruct (V.singleton valueField))
      wideColumns = V.map (\child -> (A.fieldName child, emptyFieldColumn)) wideFields
      wideTyped = A.defaultField "typed_value" True A.AStruct wideFields
      wideSchema = root (V.fromList [metadataField, wideTyped])
  wideObject <- load (C.mkStruct 1 Nothing wideColumns)
  wideColumn <- load (C.mkStruct 1 Nothing (V.fromList [("metadata", metadata), ("typed_value", wideObject)]))
  wideReader <- load (R.compileVariantArrowReaderWithLimits (B.VariantLimits 1 1 0) Schema.defaultVariantSchemaLimits wideSchema wideColumn)
  check "wide all-missing object consumes only its semantic root" (R.variantArrowAt wideReader 0 == Right (Just (B.VObject M.empty)))
  -- Valid Arrow buffers, but a required shared dictionary name is absent.
  -- Reject by presence before allocating 100000 reconstructed values.
  let many = 100000
      elementType = A.defaultField "element" False A.AStruct (V.singleton longField)
      largeListType = A.defaultField "typed_value" True A.AList (V.singleton elementType)
      largeWrapperType = A.defaultField "a" False A.AStruct (V.singleton largeListType)
      largeObjectType = A.defaultField "typed_value" True A.AStruct (V.singleton largeWrapperType)
      hostileSchema = root (V.fromList [metadataField, largeObjectType])
  manyElements <- load (C.mkStruct many Nothing (V.singleton ("typed_value", C.primColumn C.PInt64 (S.replicate many 42))))
  manyList <- load (C.mkList Nothing (S.fromList [0, fromIntegral many]) manyElements)
  largeWrapper <- load (C.mkStruct 1 Nothing (V.singleton ("typed_value", manyList)))
  largeObject <- load (C.mkStruct 1 Nothing (V.singleton ("a", largeWrapper)))
  hostileColumn <- load (C.mkStruct 1 Nothing (V.fromList [("metadata", metadata), ("typed_value", largeObject)]))
  hostileReader <- load (R.compileVariantArrowReader hostileSchema hostileColumn)
  _ <- evaluate (R.variantArrowLength hostileReader)
  before <- getAllocationCounter
  rejected <- evaluate (isLeft (R.variantArrowAt hostileReader 0))
  after <- getAllocationCounter
  check "missing shared name rejects before typed subtree allocation" (rejected && before - after < 1024 * 1024)
  good <- H.check $ H.withTests 1000 $ H.property $ do
    number <- H.forAll (Gen.int64 Range.constantBounded)
    exampleCase <- H.evalEither (partial number)
    compiled <- H.evalEither (R.compileVariantArrowReader (partialField exampleCase) (partialColumn exampleCase))
    R.variantArrowAt compiled 0 H.=== Right (Just (partialExpected exampleCase))
  check "generated partial object semantic round trips" good
  putStrLn "Variant Arrow typed/fallback/global-budget/missing/null regressions passed"