wireform-parquet-0.1.0.0: src/Parquet/Variant/Shred.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedSums #-}
{-# LANGUAGE UnboxedTuples #-}
{- | Shred semantic Variant values to a declared canonical typed shape.
Exact Variant constructors and decimal scales select typed storage; a
mismatch stays in binary fallback with its original tag and coefficient.
-}
module Parquet.Variant.Shred (
VariantShredder,
compileVariantShredder,
compileVariantShredderWithLimits,
variantShredSchema,
shredVariant,
shredVariantWithMetadata,
) where
import Arrow.Column qualified as C
import Arrow.Types qualified as A
import Data.ByteString qualified as BS
import Data.ByteString.Internal qualified as BSI
import Data.Int (Int32, Int64)
import Data.Map.Internal qualified as MI
import Data.Map.Strict qualified as M
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Vector qualified as V
import Data.Word (byteSwap64)
import Foreign.Storable (pokeByteOff)
import GHC.ByteOrder (ByteOrder (..), targetByteOrder)
import GHC.Exts (Int (I#))
import Parquet.Nested.Generate qualified as Generate
import Parquet.Nested.Types
import Parquet.Variant qualified as B
import Parquet.Variant.Schema qualified as Schema
data VariantShredder = VariantShredder !VariantStorageSchema !NodePlan
data NodePlan = NodePlan
{ nodeFields :: {-# UNPACK #-} !Int
, nodeMetadata :: {-# UNPACK #-} !Int
, nodeValue :: {-# UNPACK #-} !Int
, nodeTyped :: {-# UNPACK #-} !Int
, nodePlan :: !(Maybe TypedPlan)
}
data TypedPlan
= PrimitivePlan !(B.Variant -> (# LeafValue | () #))
| ObjectPlan !(V.Vector ChildPlan) !(Set.Set Text)
| ArrayPlan !NodePlan
data ChildPlan = ChildPlan !Text !NodePlan
compileVariantShredder :: A.Field -> Either String VariantShredder
compileVariantShredder = compileVariantShredderWithLimits B.defaultVariantLimits Schema.defaultVariantSchemaLimits
compileVariantShredderWithLimits :: B.VariantLimits -> Schema.VariantSchemaLimits -> A.Field -> Either String VariantShredder
compileVariantShredderWithLimits limits schemaLimits field = do
Schema.validateVariantFieldWithLimits schemaLimits field
let complete = completeNode field
storage <- Schema.compileVariantStorageSchemaWithLimits limits schemaLimits complete
plan <- compileNode complete
pure (VariantShredder storage plan)
where
completeNode current =
let fields = A.fieldChildren current
normalized =
V.map
( \child -> case A.fieldName child of
"metadata" -> child {A.fieldType = A.ABinary, A.fieldDictionary = Nothing, A.fieldChildren = V.empty}
"value" -> child {A.fieldType = A.ABinary, A.fieldNullable = True}
"typed_value" -> completeTyped child
_ -> child
)
fields
full = if V.any ((== "value") . A.fieldName) fields then normalized else V.snoc normalized (A.defaultLeafField "value" True A.ABinary)
in current {A.fieldChildren = full}
completeTyped current =
current
{ A.fieldNullable = True
, A.fieldChildren = case A.fieldType current of
A.AStruct -> completeNode <$> A.fieldChildren current
typ | typ `elem` [A.AList, A.ALargeList, A.AListView] -> completeNode <$> A.fieldChildren current
_ -> A.fieldChildren current
}
variantShredSchema :: VariantShredder -> NestedSchema
variantShredSchema (VariantShredder storage _) =
(if A.fieldNullable (storageField storage) then NSOptional else NSRequired) (NSVariantTyped storage)
shredVariant :: VariantShredder -> Maybe B.Variant -> Either String NestedRow
shredVariant _ Nothing = Right NRNull
shredVariant shredder@(VariantShredder storage _) (Just value) = do
metadata <- B.buildVariantMetadataForWithLimits (storageLimits storage) value
shredVariantWithMetadata shredder metadata (Just value)
{- | Use one original dictionary for every fallback fragment and all fully
shredded names. The complete semantic tree is checked against one budget
before choosing any typed or binary branch. No scale conversion is done.
-}
shredVariantWithMetadata :: VariantShredder -> B.VariantMetadata -> Maybe B.Variant -> Either String NestedRow
shredVariantWithMetadata _ _ Nothing = Right NRNull
shredVariantWithMetadata (VariantShredder storage plan) metadata (Just value) = do
B.validateVariantWithMetadataWithLimits (storageLimits storage) metadata value
case nodeRow# (storageLimits storage) metadata plan (# value | #) of
(# message | #) -> Left message
(# | row #) -> Right row
compileNode :: A.Field -> Either String NodePlan
compileNode field = do
let fields = A.fieldChildren field
index name = maybe (-1) id (V.findIndex ((== name) . A.fieldName) fields)
typed = index "typed_value"
plan <- case fields V.!? typed of
Nothing -> pure Nothing
Just child -> Just <$> compileTyped child
pure (NodePlan (V.length fields) (index "metadata") (index "value") typed plan)
compileTyped :: A.Field -> Either String TypedPlan
compileTyped field = case A.fieldType field of
A.AStruct -> do
children <- V.mapM (\child -> ChildPlan (A.fieldName child) <$> compileNode child) (A.fieldChildren field)
pure (ObjectPlan children (Set.fromList (V.toList (A.fieldName <$> A.fieldChildren field))))
typ | typ `elem` [A.AList, A.ALargeList, A.AListView] -> ArrayPlan <$> compileNode (V.head (A.fieldChildren field))
_ -> PrimitivePlan <$> primitivePlan field
type RowResult# = (# String | NestedRow #)
nodeRow# :: B.VariantLimits -> B.VariantMetadata -> NodePlan -> (# B.Variant | () #) -> RowResult#
nodeRow# limits metadata plan value = case value of
(# | () #) -> finish NRNull NRNull
(# present | #) -> case (nodePlan plan, present) of
(Just (PrimitivePlan select), _) -> case select present of
(# leaf | #) -> finish NRNull (NRLeaf leaf)
(# | () #) -> fallback present
(Just (ArrayPlan element), B.VArray values) -> case V.length values of
I# count -> case Generate.generate# count (\index -> nodeRow# limits metadata element (# V.unsafeIndex values (I# index) | #)) of
(# message | #) -> (# message | #)
(# | rows #) -> finish NRNull (NRList rows)
(Just (ObjectPlan children names), B.VObject fields) -> case V.length children of
I# count -> case Generate.generate#
count
( \index -> case V.unsafeIndex children (I# index) of
ChildPlan name child -> nodeRow# limits metadata child (lookupValue# name fields)
) of
(# message | #) -> (# message | #)
(# | rows #) ->
let remainder = M.withoutKeys fields names
in if M.null remainder
then finish NRNull (NRStruct rows)
else case B.encodeVariantValueWithLimits limits metadata (B.VObject remainder) of
Left message -> (# message | #)
Right bytes -> finish (NRLeaf (LvBinary bytes)) (NRStruct rows)
_ -> fallback present
where
finish binary typed = case nodeFields plan of
I# count -> case Generate.generate#
count
( \index ->
(#
| if I# index == nodeMetadata plan
then NRLeaf (LvBinary (B.metadataBytes metadata))
else
if I# index == nodeValue plan
then binary
else
if I# index == nodeTyped plan
then typed
else NRNull
#)
) of
(# message | #) -> (# message | #)
(# | rows #) -> (# | NRStruct rows #)
{-# INLINE finish #-}
fallback present = case B.encodeVariantValueWithLimits limits metadata present of
Left message -> (# message | #)
Right bytes -> finish (NRLeaf (LvBinary bytes)) NRNull
{-# INLINE fallback #-}
-- Data.Map.lookup's boxed Maybe would allocate per shredded field. Keep the
-- same ordered-tree search while returning an unboxed finite result.
lookupValue# :: Text -> M.Map Text B.Variant -> (# B.Variant | () #)
lookupValue# key current = case current of
MI.Tip -> (# | () #)
MI.Bin _ name value left right -> case compare key name of
LT -> lookupValue# key left
GT -> lookupValue# key right
EQ -> (# value | #)
primitivePlan :: A.Field -> Either String (B.Variant -> (# LeafValue | () #))
primitivePlan field = do
_ <- Schema.variantTypedLeaf field
pure $ case A.fieldType field of
A.ABool -> \value -> case value of B.VBool x -> (# LvBool x | #); _ -> no# ()
A.AInt 8 True -> \value -> case value of B.VInt8 x -> (# LvInt32 (fromIntegral x) | #); _ -> no# ()
A.AInt 16 True -> \value -> case value of B.VInt16 x -> (# LvInt32 (fromIntegral x) | #); _ -> no# ()
A.AInt 32 True -> \value -> case value of B.VInt32 x -> (# LvInt32 x | #); _ -> no# ()
A.AInt 64 True -> \value -> case value of B.VInt64 x -> (# LvInt64 x | #); _ -> no# ()
A.AFloatingPoint A.Single -> \value -> case value of B.VFloat x -> (# LvFloat x | #); _ -> no# ()
A.AFloatingPoint A.DoublePrecision -> \value -> case value of B.VDouble x -> (# LvDouble x | #); _ -> no# ()
A.ADecimal32 precision scale ->
let !bound = fromInteger (10 ^ precision - 1) :: Int32
in \value -> case value of B.VDecimal4 s x | fromIntegral s == scale && x >= negate bound && x <= bound -> (# LvInt32 x | #); _ -> no# ()
A.ADecimal64 precision scale ->
let !bound = fromInteger (10 ^ precision - 1) :: Int64
in \value -> case value of B.VDecimal8 s x | fromIntegral s == scale && x >= negate bound && x <= bound -> (# LvInt64 x | #); _ -> no# ()
A.ADecimal precision scale ->
let !bound = 10 ^ precision - 1
in \value -> case value of B.VDecimal16 s x | fromIntegral s == scale && x >= negate bound && x <= bound -> (# LvFixedBinary (decimalBytes (C.decimal128FromInteger x)) | #); _ -> no# ()
A.ADate A.DateDay -> \value -> case value of B.VDate x -> (# LvInt32 x | #); _ -> no# ()
A.ATime A.Microsecond 64 -> \value -> case value of B.VTime x -> (# LvInt64 x | #); _ -> no# ()
A.ATimestamp A.Microsecond Nothing -> \value -> case value of B.VTimestampNtz x -> (# LvInt64 x | #); _ -> no# ()
A.ATimestamp A.Microsecond (Just "UTC") -> \value -> case value of B.VTimestamp x -> (# LvInt64 x | #); _ -> no# ()
A.ATimestamp A.Nanosecond Nothing -> \value -> case value of B.VTimestampNtzNanos x -> (# LvInt64 x | #); _ -> no# ()
A.ATimestamp A.Nanosecond (Just "UTC") -> \value -> case value of B.VTimestampNanos x -> (# LvInt64 x | #); _ -> no# ()
A.AFixedSizeBinary 16 -> \value -> case value of B.VUuid x -> (# LvFixedBinary x | #); _ -> no# ()
typ | typ `elem` [A.ABinary, A.ALargeBinary, A.ABinaryView] -> \value -> case value of B.VBinary x -> (# LvBinary x | #); _ -> no# ()
typ | typ `elem` [A.AUtf8, A.ALargeUtf8, A.AUtf8View] -> \value -> case value of B.VString x -> (# LvString x | #); _ -> no# ()
_ -> \_ -> no# ()
no# :: () -> (# LeafValue | () #)
no# () = (# | () #)
{-# INLINE no# #-}
decimalBytes :: C.Decimal128 -> BS.ByteString
decimalBytes (C.Decimal128 lo hi) = BSI.unsafeCreate 16 $ \pointer -> do
let big word = case targetByteOrder of LittleEndian -> byteSwap64 word; BigEndian -> word
pokeByteOff pointer 0 (big hi)
pokeByteOff pointer 8 (big lo)