wireform-parquet-0.1.0.0: test/FileArrowRegression.hs
{-# LANGUAGE OverloadedStrings #-}
module FileArrowRegression (tests) where
import Arrow.Column qualified as C
import Arrow.Types qualified as A
import Control.Monad (forM_, unless)
import Data.ByteString qualified as BS
import Data.Either (isLeft, isRight)
import Data.Text (Text)
import Data.Vector qualified as V
import Parquet.Arrow qualified as Arrow
import Parquet.File qualified as F
import Parquet.Nested qualified as N
import Parquet.Read qualified as Read
import Parquet.Types qualified as P
load :: Either String a -> IO a
load = either fail pure
child :: Text -> A.ArrowType -> Text -> A.Field
child name ty ident = A.Field name True ty V.empty Nothing (V.singleton ("PARQUET:field_id", ident))
fileField :: V.Vector A.Field -> A.Field
fileField children =
A.Field
"f"
True
A.AStruct
children
Nothing
(V.fromList [("ARROW:extension:name", "arrow.parquet.file"), ("ARROW:extension:metadata", ""), ("PARQUET:field_id", "-7")])
tests :: IO ()
tests = do
let check name condition = unless condition (fail ("Arrow FILE regression: " ++ name))
uri = child "uri" A.AUtf8 "-2147483648"
inline = child "inline" A.ABinary "2147483647"
field = fileField (V.fromList [uri, inline])
check "known extension accepted" (isRight (Arrow.arrowFieldToNestedSchema field))
check "absent parameters accepted" (isRight (Arrow.arrowFieldToNestedSchema field {A.fieldMetadata = V.filter ((/= "ARROW:extension:metadata") . fst) (A.fieldMetadata field)}))
forM_ ["{}", "v2", " "] $ \parameters ->
check "unsupported extension metadata cannot disappear silently" (isLeft (Arrow.arrowFieldToNestedSchema field {A.fieldMetadata = V.map (\(key, value) -> (key, if key == "ARROW:extension:metadata" then parameters else value)) (A.fieldMetadata field)}))
forM_ ["ARROW:extension:name", "ARROW:extension:metadata", "PARQUET:field_id"] $ \key -> do
let pair = V.find ((== key) . fst) (A.fieldMetadata field)
check "duplicate reserved metadata rejected" (isLeft (Arrow.arrowFieldToNestedSchema field {A.fieldMetadata = A.fieldMetadata field V.++ maybe V.empty V.singleton pair}))
forM_ [uri {A.fieldNullable = False}, uri {A.fieldName = "URI"}, uri {A.fieldType = A.ABinary}, inline {A.fieldType = A.AUtf8}, child "size" (A.AInt 64 False) "0", child "offset" (A.AInt 32 True) "0"] $ \bad ->
check "extension validates names/nullability/types" (isLeft (Arrow.arrowFieldToNestedSchema (fileField (V.singleton bad))))
forM_ ["2147483648", "-2147483649", "1junk", "", " 1"] $ \ident ->
check "field IDs are bounded strict integers" (isLeft (Arrow.arrowFieldToNestedSchema (fileField (V.singleton (uri {A.fieldMetadata = V.singleton ("PARQUET:field_id", ident)})))))
check "duplicate names rejected" (isLeft (Arrow.arrowFieldToNestedSchema (fileField (V.fromList [uri, uri]))))
check "FILE extension cannot use primitive storage" (isLeft (Arrow.arrowFieldToNestedSchema field {A.fieldType = A.AUtf8, A.fieldChildren = V.empty}))
-- Each representation contains both short/inline and long/out-of-line view
-- strings. Parent nulls must mask arbitrary child storage in every case.
let texts = V.fromList [Just "../images/a%20b-long-relative.png", Just "masked", Nothing, Just ""]
bytes = V.fromList [Nothing, Just "masked", Just "", Just (BS.replicate 80 255)]
expected =
V.fromList
[ Just F.emptyFileReference {F.fileUri = Just "../images/a%20b-long-relative.png"}
, Nothing
, Just F.emptyFileReference {F.fileInline = Just ""}
, Just F.emptyFileReference {F.fileUri = Just "", F.fileInline = Just (BS.replicate 80 255)}
]
representations =
[ (A.AUtf8, A.ABinary, C.fromMaybeTexts texts, C.fromMaybeByteStrings bytes)
, (A.ALargeUtf8, A.ALargeBinary, C.fromMaybeLargeTexts texts, C.fromMaybeLargeByteStrings bytes)
, (A.AUtf8View, A.ABinaryView, C.fromMaybeUtf8View texts, C.fromMaybeBinaryView bytes)
]
forM_ representations $ \(textType, binaryType, textColumn, binaryColumn) -> do
let input = fileField (V.fromList [uri {A.fieldType = textType}, inline {A.fieldType = binaryType}])
column <- load (C.mkStruct 4 (C.validityFromBools (V.fromList [True, False, True, True])) (V.fromList [("uri", textColumn), ("inline", binaryColumn)]))
nested <- load (Arrow.arrowFieldToNestedSchema input)
rows <- load (Arrow.columnArrayToNestedRows input column)
check "FILE rows use typed references" (rows == V.map (maybe N.NRNull N.NRFile) expected)
encoded <- load (N.buildNestedFile (V.singleton ("f", nested)) (V.singleton rows))
file <- load (Read.loadParquetFile encoded)
let schema = P.fmSchema (Read.pfFooter file)
check "FILE and IDs written to native footer" (P.seLogicalType (schema V.! 1) == Just P.LTFile && V.map P.seFieldId (V.drop 1 schema) == V.fromList [Just (-7), Just minBound, Just maxBound])
output <- load (Arrow.parquetFileArrowSchemaChecked file)
let outputField = V.head (A.arrowFields output)
check "parent metadata has experimental FILE name and empty parameters" (A.fieldMetadata outputField == A.fieldMetadata field)
check "child IDs survive schema inference" (V.map A.fieldMetadata (A.fieldChildren outputField) == V.map A.fieldMetadata (A.fieldChildren field))
columns <- either (fail . show) pure (Arrow.parquetRowGroupToArrow output file 0)
nativeSchema <- load (F.parseFileSchema (schema V.! 1) (V.drop 2 schema))
reader <- load (F.compileFileReader nativeSchema (V.head columns))
actual <- load (V.generateM 4 (F.fileReferenceAt reader))
check "all storage representations retain semantic values" (actual == expected)
restored <- load (Arrow.arrowFieldToNestedSchema outputField)
check "FILE identity and parent/child IDs survive Arrow roundtrip" (restored == nested)
let plain = field {A.fieldMetadata = V.empty}
case Arrow.arrowFieldToNestedSchema plain of
Right (N.NSOptional N.NSStruct {}) -> pure ()
_ -> fail "Arrow FILE regression: plain struct must remain unannotated"
putStrLn "Arrow FILE extension, IDs, strict metadata and standard/large/view storage regressions passed."