wireform-parquet-0.1.0.0: src/Parquet/Variant/Arrow.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UnboxedSums #-}
{-# LANGUAGE UnboxedTuples #-}
{- | Semantic reconstruction of canonical @arrow.parquet.variant@ arrays.
Compile field names, storage profiles and primitive conversions once. Each
present row uses one original metadata dictionary and one depth/value budget
across its typed nodes and binary fallback fragments. Null storage slots,
present Variant nulls and missing object fields remain distinct.
See https://arrow.apache.org/docs/format/CanonicalExtensions.html#parquet-variant
and https://parquet.apache.org/docs/file-format/types/variantshredding/.
-}
module Parquet.Variant.Arrow (
VariantArrowReader,
compileVariantArrowReader,
compileVariantArrowReaderWithLimits,
variantArrowLength,
variantArrowAt,
variantArrowAt#,
) where
import Arrow.Column qualified as C
import Arrow.Types qualified as A
import Arrow.Write.Columns (validateColumns)
import Control.Monad (unless)
import Data.Int (Int32, Int64)
import Data.Map.Strict qualified as M
import Data.Text (Text)
import Data.Vector qualified as V
import Data.Vector.Mutable qualified as MV
import Data.Vector.Storable qualified as S
import Foreign.Storable (Storable)
import GHC.Exts (Int (I#), Int#, isTrue#, runRW#, (+#), (==#))
import GHC.ST (ST (..))
import Parquet.Variant qualified as B
import Parquet.Variant.Array (BytesReader (..), alignVariantColumns, compileBytes)
import Parquet.Variant.Schema qualified as Schema
data VariantArrowReader = VariantArrowReader
{ variantArrowLength :: {-# UNPACK #-} !Int
, readerValidity :: !(Maybe C.Validity)
, readerMetadata :: !BytesReader
, readerNode :: !NodePlan
, readerLimits :: !B.VariantLimits
}
newtype PrimitiveReader = PrimitiveReader {primitiveAt# :: Int# -> (# String | B.Variant #)}
newtype RangeReader = RangeReader {rangeAt# :: Int# -> (# Int#, Int# #)}
data NodePlan = NodePlan !(Maybe C.Validity) !BytesReader !(Maybe TypedPlan)
data TypedPlan
= PrimitivePlan !(Maybe C.Validity) !PrimitiveReader
| ObjectPlan !(Maybe C.Validity) !(V.Vector ObjectField)
| ArrayPlan !(Maybe C.Validity) !RangeReader !NodePlan
data ObjectField = ObjectField
{ objectName :: !Text
, objectPlan :: !NodePlan
}
type NodeResult# = (# String | (# B.Variant, Int# #) | Int# #)
type PresentResult# = (# String | (# B.Variant, Int# #) #)
compileVariantArrowReader :: A.Field -> C.ColumnArray -> Either String VariantArrowReader
compileVariantArrowReader = compileVariantArrowReaderWithLimits B.defaultVariantLimits Schema.defaultVariantSchemaLimits
{- | Independent limits bound schema compilation and each reconstructed tree.
Arrow buffer shapes are checked once. Required-child validity is checked only
where its containing Variant/object/array is present; null ancestors mask
arbitrary children, including malformed Variant payload bytes.
-}
compileVariantArrowReaderWithLimits :: B.VariantLimits -> Schema.VariantSchemaLimits -> A.Field -> C.ColumnArray -> Either String VariantArrowReader
compileVariantArrowReaderWithLimits limits schemaLimits field input = do
unless (B.variantMaxDepth limits >= 0 && B.variantMaxValues limits > 0 && B.variantMaxDictionaryEntries limits >= 0) (Left "Parquet.Variant.Arrow: invalid resource limits")
Schema.validateVariantFieldWithLimits schemaLimits field
column <- alignVariantColumns field input
validateColumns (V.singleton field {A.fieldChildren = V.map relax (A.fieldChildren field)}) (V.singleton column)
metadata <- namedColumn "metadata" column >>= compileBytes
plan <- compileNode field column
pure (VariantArrowReader (C.columnLength column) (C.validity column) metadata plan limits)
where
relax current = current {A.fieldNullable = True, A.fieldChildren = V.map relax (A.fieldChildren current)}
namedColumn :: Text -> C.ColumnArray -> Either String C.ColumnArray
namedColumn name (C.ColStruct _ _ children) = maybe (Left "Parquet.Variant.Arrow: missing named child") (Right . snd) (V.find ((== name) . fst) children)
namedColumn _ _ = Left "Parquet.Variant.Arrow: expected struct storage"
compileNode :: A.Field -> C.ColumnArray -> Either String NodePlan
compileNode field column = do
let children = A.fieldChildren field
fallback <- case V.find ((== "value") . A.fieldName) children of
Nothing -> pure (BytesReader (\_ -> (# | () #)))
Just _ -> namedColumn "value" column >>= compileBytes
typed <- case V.find ((== "typed_value") . A.fieldName) children of
Nothing -> pure Nothing
Just child -> Just <$> (namedColumn "typed_value" column >>= compileTyped child)
-- Preserve required-child checks at visible rows without scanning payloads
-- beneath null parents. Usually no required child has a validity bitmap;
-- only the exceptional case needs one combined mask at compile time.
required <-
V.mapM
( \child ->
if not (A.fieldNullable child) && A.fieldName child /= "metadata"
then C.validity <$> namedColumn (A.fieldName child) column
else pure Nothing
)
children
let requiredMasks = V.mapMaybe id required
visible =
if V.null requiredMasks
then C.validity column
else
C.validityGenerate
(C.columnLength column)
( \index ->
C.isValidAt (C.validity column) index && V.all (\mask -> C.bitAt (C.validityBits mask) index) requiredMasks
)
pure (NodePlan visible fallback typed)
compileTyped :: A.Field -> C.ColumnArray -> Either String TypedPlan
compileTyped field column = case (A.fieldType field, column) of
(A.AStruct, C.ColStruct _ validity columns) -> do
-- Name alignment has already bound every object once, before shape
-- validation. Positional compilation now needs no second name map.
children <- V.zipWithM (\child (_, value) -> ObjectField (A.fieldName child) <$> compileNode child value) (A.fieldChildren field) columns
pure (ObjectPlan validity children)
(_, C.ColList validity offsets child) -> list validity (offsetsRange offsets) child
(_, C.ColLargeList validity offsets child) -> list validity (offsetsRange offsets) child
(_, C.ColListView validity offsets sizes child) -> list validity (viewRange offsets sizes) child
_ -> PrimitivePlan (C.validity column) <$> compilePrimitive field column
where
list validity ranges child = do
plan <- compileNode (V.head (A.fieldChildren field)) child
pure (ArrayPlan validity ranges plan)
offsetsRange :: (Storable a, Integral a) => S.Vector a -> RangeReader
offsetsRange offsets = RangeReader $ \index ->
case fromIntegral (S.unsafeIndex offsets (I# index)) of
I# start -> case fromIntegral (S.unsafeIndex offsets (I# (index +# 1#))) - I# start of
I# count -> (# start, count #)
{-# INLINE offsetsRange #-}
viewRange :: (Storable a, Integral a) => S.Vector a -> S.Vector a -> RangeReader
viewRange offsets sizes = RangeReader $ \index ->
case fromIntegral (S.unsafeIndex offsets (I# index)) of
I# start -> case fromIntegral (S.unsafeIndex sizes (I# index)) of
I# count -> (# start, count #)
{-# INLINE viewRange #-}
compilePrimitive :: A.Field -> C.ColumnArray -> Either String PrimitiveReader
compilePrimitive field column = case column of
C.ColInt8 _ xs -> pure (primitive B.VInt8 xs)
C.ColInt16 _ xs -> pure (primitive B.VInt16 xs)
C.ColInt32 _ xs -> pure (primitive B.VInt32 xs)
C.ColInt64 _ xs -> pure (primitive B.VInt64 xs)
C.ColFloat _ xs -> pure (primitive B.VFloat xs)
C.ColDouble _ xs -> pure (primitive B.VDouble xs)
C.ColDate32 _ xs -> pure (primitive B.VDate xs)
C.ColTime64 _ xs -> pure $ PrimitiveReader $ \index ->
let value = S.unsafeIndex xs (I# index)
in if value >= 0 && value < 86400000000 then (# | B.VTime value #) else (# "Parquet.Variant.Arrow: time outside one day" | #)
C.ColTimestamp _ xs -> pure (primitive timestamp xs)
C.ColDecimal32 precision scale _ xs ->
let !bound = fromInteger (10 ^ precision) :: Int32
in pure $ PrimitiveReader $ \index ->
let !value = S.unsafeIndex xs (I# index)
in if value > negate bound && value < bound then (# | B.VDecimal4 (fromIntegral scale) value #) else badDecimal#
C.ColDecimal64 precision scale _ xs ->
let !bound = fromInteger (10 ^ precision) :: Int64
in pure $ PrimitiveReader $ \index ->
let !value = S.unsafeIndex xs (I# index)
in if value > negate bound && value < bound then (# | B.VDecimal8 (fromIntegral scale) value #) else badDecimal#
C.ColDecimal128 precision scale _ xs ->
let !bound = 10 ^ precision
in pure $ PrimitiveReader $ \index ->
let !value = C.decimal128ToInteger (S.unsafeIndex xs (I# index))
in if value > negate bound && value < bound then (# | B.VDecimal16 (fromIntegral scale) value #) else badDecimal#
_
| A.fieldType field == A.ABool
, Just values <- C.asBool column ->
pure $ PrimitiveReader $ \index -> case C.boolArrayAt values (I# index) of
Just value -> (# | B.VBool value #)
Nothing -> missingPrimitive#
| A.fieldType field `elem` [A.AUtf8, A.ALargeUtf8, A.AUtf8View] ->
pure $ PrimitiveReader $ \index -> case C.anyTextAt column (I# index) of
Just value -> (# | B.VString value #)
Nothing -> missingPrimitive#
| otherwise ->
pure $ PrimitiveReader $ \index -> case C.anyBytesAt column (I# index) of
Just value -> (# | if A.fieldType field == A.AFixedSizeBinary 16 then B.VUuid value else B.VBinary value #)
Nothing -> missingPrimitive#
where
timestamp = case A.fieldType field of
A.ATimestamp A.Microsecond (Just "UTC") -> B.VTimestamp
A.ATimestamp A.Nanosecond (Just "UTC") -> B.VTimestampNanos
A.ATimestamp A.Microsecond Nothing -> B.VTimestampNtz
_ -> B.VTimestampNtzNanos
primitive :: Storable a => (a -> B.Variant) -> S.Vector a -> PrimitiveReader
primitive make values = PrimitiveReader $ \index -> let !value = make (S.unsafeIndex values (I# index)) in (# | value #)
{-# INLINE primitive #-}
badDecimal# :: (# String | B.Variant #)
badDecimal# = (# "Parquet.Variant.Arrow: decimal exceeds declared precision" | #)
missingPrimitive# :: (# String | B.Variant #)
missingPrimitive# = (# "Parquet.Variant.Arrow: missing typed primitive" | #)
variantArrowAt :: VariantArrowReader -> Int -> Either String (Maybe B.Variant)
variantArrowAt reader (I# index) = case variantArrowAt# reader index of
(# message | | #) -> Left message
(# | value | #) -> Right (Just value)
(# | | () #) -> Right Nothing
variantArrowAt# :: VariantArrowReader -> Int# -> (# String | B.Variant | () #)
variantArrowAt# reader index
| I# index < 0 || I# index >= variantArrowLength reader = (# "Parquet.Variant.Arrow: row index out of range" | | #)
| not (C.isValidAt (readerValidity reader) (I# index)) = (# | | () #)
| otherwise = case bytesAt# (readerMetadata reader) index of
(# | () #) -> (# "Parquet.Variant.Arrow: present root has null metadata" | | #)
(# bytes | #) -> case B.compileVariantMetadataWithLimits (readerLimits reader) bytes of
Left message -> (# message | | #)
Right dictionary -> case B.variantMaxValues (readerLimits reader) of
I# budget -> case required# (readerLimits reader) dictionary (readerNode reader) 0# budget index of
(# message | #) -> (# message | | #)
(# | (# value, _ #) #) -> (# | value | #)
{-# INLINE variantArrowAt# #-}
charge# :: B.VariantLimits -> Int# -> Int# -> (# String | Int# #)
charge# limits depth budget
| I# depth > B.variantMaxDepth limits = (# "Parquet.Variant.Arrow: nesting depth limit exceeded" | #)
| I# budget <= 0 = (# "Parquet.Variant.Arrow: value count limit exceeded" | #)
| otherwise = (# | budget +# (-1#) #)
{-# INLINE charge# #-}
required# :: B.VariantLimits -> B.VariantMetadata -> NodePlan -> Int# -> Int# -> Int# -> PresentResult#
required# limits dictionary plan depth budget index = case readNode# limits dictionary plan depth budget index of
(# message | | #) -> (# message | #)
(# | (# value, left #) | #) -> (# | (# value, left #) #)
(# | | left #) -> case charge# limits depth left of
(# message | #) -> (# message | #)
(# | remaining #) -> (# | (# B.VNull, remaining #) #)
{-# INLINE required# #-}
readNode# :: B.VariantLimits -> B.VariantMetadata -> NodePlan -> Int# -> Int# -> Int# -> NodeResult#
readNode# limits dictionary (NodePlan validity fallback typed) depth budget index
| not (C.isValidAt validity (I# index)) = (# "Parquet.Variant.Arrow: null shredded wrapper or required child" | | #)
| otherwise = case typed of
Just plan | typedPresent plan -> case plan of
PrimitivePlan _ primitive -> case bytesAt# fallback index of
(# _ | #) -> conflict#
(# | () #) -> case charge# limits depth budget of
(# message | #) -> (# message | | #)
(# | left #) -> case primitiveAt# primitive index of
(# message | #) -> (# message | | #)
(# | !value #) -> (# | (# value, left #) | #)
ArrayPlan _ ranges child -> case bytesAt# fallback index of
(# _ | #) -> conflict#
(# | () #) -> case charge# limits depth budget of
(# message | #) -> (# message | | #)
(# | left #) -> case rangeAt# ranges index of
(# start, count #) -> case readArray# limits dictionary child (depth +# 1#) left start count of
(# message | #) -> (# message | | #)
(# | (# value, remaining #) #) -> (# | (# value, remaining #) | #)
ObjectPlan _ fields -> case bytesAt# fallback index of
(# | () #) -> case charge# limits depth budget of
(# message | #) -> (# message | | #)
(# | left #) -> object fields M.empty left
(# bytes | #) -> case B.decodeVariantFragment# limits dictionary depth budget bytes of
(# message | #) -> (# message | | #)
(# | (# B.VObject rest, left #) #)
| V.any (\field -> M.member (objectName field) rest) fields -> (# "Parquet.Variant.Arrow: object field appears in typed and fallback storage" | | #)
| otherwise -> object fields rest left
_ -> (# "Parquet.Variant.Arrow: partial fallback must be an object" | | #)
_ -> case bytesAt# fallback index of
(# | () #) -> (# | | budget #)
(# bytes | #) -> case B.decodeVariantFragment# limits dictionary depth budget bytes of
(# message | #) -> (# message | | #)
(# | (# value, left #) #) -> case (typed, value) of
(Just ObjectPlan {}, B.VObject {}) -> (# "Parquet.Variant.Arrow: object is missing its typed storage" | | #)
(Just ArrayPlan {}, B.VArray {}) -> (# "Parquet.Variant.Arrow: array is missing its typed storage" | | #)
_ -> (# | (# value, left #) | #)
where
typedPresent plan = C.isValidAt (case plan of PrimitivePlan v _ -> v; ObjectPlan v _ -> v; ArrayPlan v _ _ -> v) (I# index)
object :: V.Vector ObjectField -> M.Map Text B.Variant -> Int# -> NodeResult#
object fields rest left = case readObject# limits dictionary fields (depth +# 1#) left index 0# rest of
(# message | #) -> (# message | | #)
(# | (# value, remaining #) #) -> (# | (# value, remaining #) | #)
conflict# :: NodeResult#
conflict# = (# "Parquet.Variant.Arrow: value and typed_value conflict" | | #)
-- A dictionary omission can be detected from storage presence alone. Do
-- not construct a large typed subtree only to reject its field name later.
nodePresent# :: NodePlan -> Int# -> (# String | Bool #)
nodePresent# (NodePlan validity fallback typed) index
| not (C.isValidAt validity (I# index)) = (# "Parquet.Variant.Arrow: null shredded wrapper or required child" | #)
| otherwise = case typed of
Just plan | C.isValidAt (case plan of PrimitivePlan v _ -> v; ObjectPlan v _ -> v; ArrayPlan v _ _ -> v) (I# index) -> (# | True #)
_ -> case bytesAt# fallback index of
(# _ | #) -> (# | True #)
(# | () #) -> (# | False #)
{-# INLINE nodePresent# #-}
readObject# :: B.VariantLimits -> B.VariantMetadata -> V.Vector ObjectField -> Int# -> Int# -> Int# -> Int# -> M.Map Text B.Variant -> PresentResult#
readObject# limits dictionary fields depth budget index fieldIndex values
| isTrue# (fieldIndex ==# count) = (# | (# B.VObject values, budget #) #)
| otherwise = case V.unsafeIndex fields (I# fieldIndex) of
field
| not (B.metadataContains (objectName field) dictionary) -> case nodePresent# (objectPlan field) index of
(# message | #) -> (# message | #)
(# | True #) -> (# "Parquet.Variant.Arrow: typed object field missing from shared dictionary" | #)
(# | False #) -> readObject# limits dictionary fields depth budget index (fieldIndex +# 1#) values
| otherwise -> case readNode# limits dictionary (objectPlan field) depth budget index of
(# message | | #) -> (# message | #)
(# | | left #) -> readObject# limits dictionary fields depth left index (fieldIndex +# 1#) values
(# | (# value, left #) | #) -> readObject# limits dictionary fields depth left index (fieldIndex +# 1#) (M.insert (objectName field) value values)
where
!(I# count) = V.length fields
readArray# :: B.VariantLimits -> B.VariantMetadata -> NodePlan -> Int# -> Int# -> Int# -> Int# -> PresentResult#
readArray# limits dictionary plan depth budget start count
| I# count > I# budget = (# "Parquet.Variant.Arrow: value count limit exceeded" | #)
| I# count > 0 && I# depth > B.variantMaxDepth limits = (# "Parquet.Variant.Arrow: nesting depth limit exceeded" | #)
| otherwise = runRW# $ \state -> case MV.unsafeNew (I# count) of
ST allocate -> case allocate state of
(# state1, output #) ->
let loop offset left state2
| isTrue# (offset ==# count) = case V.unsafeFreeze output of
ST freeze -> case freeze state2 of (# _, array #) -> (# | (# B.VArray array, left #) #)
| otherwise = case required# limits dictionary plan depth left (start +# offset) of
(# message | #) -> (# message | #)
(# | (# !value, remaining #) #) -> case MV.unsafeWrite output (I# offset) value of
ST write -> case write state2 of (# state3, () #) -> loop (offset +# 1#) remaining state3
in loop 0# budget state1