dataframe-parquet-1.6.0.0: src/DataFrame/IO/Parquet/Dictionary.hs
{-# LANGUAGE BangPatterns #-}
module DataFrame.IO.Parquet.Dictionary (
DictVals (..),
readDictVals,
dictText,
dictInt32,
dictInt64,
dictFloat,
dictDouble,
word32At,
word64At,
decodeRLEBitPackedHybrid,
) where
import Data.Bits
import qualified Data.ByteString as BS
import qualified Data.ByteString.Unsafe as BSU
import Data.Int (Int32, Int64)
import qualified Data.Text as T
import Data.Text.Encoding
import Data.Time (UTCTime)
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as VU
import Data.Word
import DataFrame.IO.Parquet.Binary (readUVarInt)
import DataFrame.IO.Parquet.Thrift (ThriftType (..))
import DataFrame.IO.Parquet.Time (int96ToUTCTime)
import DataFrame.Internal.Binary (littleEndianWord32)
import GHC.Float
-- | A decoded dictionary page with unboxed numeric entries.
data DictVals
= DBool !(VU.Vector Bool)
| DInt32 !(VU.Vector Int32)
| DInt64 !(VU.Vector Int64)
| DInt96 !(V.Vector UTCTime)
| DFloat !(VU.Vector Float)
| DDouble !(VU.Vector Double)
| DText !(VU.Vector Int) !(VU.Vector Int) !BS.ByteString
deriving (Show, Eq)
-- | Decode the values from a dictionary page.
readDictVals :: ThriftType -> BS.ByteString -> Int32 -> Maybe Int32 -> DictVals
readDictVals (BOOLEAN _) bs count _ =
let n = min (fromIntegral count) (8 * BS.length bs)
in DBool $ VU.generate n $ \i ->
(BSU.unsafeIndex bs (i `shiftR` 3) `shiftR` (i .&. 7)) .&. 1 == 1
readDictVals (INT32 _) bs _ _ =
DInt32 $ VU.generate (BS.length bs `div` 4) $ \i -> fromIntegral (word32At bs (4 * i))
readDictVals (INT64 _) bs _ _ =
DInt64 $ VU.generate (BS.length bs `div` 8) $ \i -> fromIntegral (word64At bs (8 * i))
readDictVals (INT96 _) bs _ _ =
DInt96 $ V.generate (BS.length bs `div` 12) $ \i ->
int96ToUTCTime (BSU.unsafeTake 12 (BSU.unsafeDrop (12 * i) bs))
readDictVals (FLOAT _) bs _ _ =
DFloat $ VU.generate (BS.length bs `div` 4) $ \i -> castWord32ToFloat (word32At bs (4 * i))
readDictVals (DOUBLE _) bs _ _ =
DDouble $ VU.generate (BS.length bs `div` 8) $ \i -> castWord64ToDouble (word64At bs (8 * i))
readDictVals (BYTE_ARRAY _) bs _ _ = textDict bs
readDictVals (FIXED_LEN_BYTE_ARRAY _) bs _ (Just len) =
let w = fromIntegral len
n = if w <= 0 then 0 else BS.length bs `div` w
in DText (VU.generate n (* w)) (VU.replicate n w) bs
readDictVals t _ _ _ = error $ "Unsupported dictionary type: " ++ show t
-- | Length-prefixed byte-array entries, in two passes.
textDict :: BS.ByteString -> DictVals
textDict bs = DText starts lens bs
where
!len = BS.length bs
count !off !n
| off + 4 > len = n
| otherwise = count (off + 4 + fromIntegral (word32At bs off)) (n + 1)
!n = count 0 0
positions = VU.unfoldrExactN n step 0
step off = let l = fromIntegral (word32At bs off) in ((off + 4, l), off + 4 + l)
(starts, lens) = VU.unzip positions
-- | Dictionary entry @i@ as 'T.Text' (lenient decode, for the boxed paths).
dictText :: DictVals -> Int -> T.Text
dictText (DText starts lens bs) i =
decodeUtf8Lenient
( BSU.unsafeTake
(VU.unsafeIndex lens i)
(BSU.unsafeDrop (VU.unsafeIndex starts i) bs)
)
dictText d _ = error ("dictText: wrong dict type, got " ++ show d)
dictInt32 :: DictVals -> Maybe (VU.Vector Int32)
dictInt32 (DInt32 ds) = Just ds
dictInt32 _ = Nothing
dictInt64 :: DictVals -> Maybe (VU.Vector Int64)
dictInt64 (DInt64 ds) = Just ds
dictInt64 _ = Nothing
dictFloat :: DictVals -> Maybe (VU.Vector Float)
dictFloat (DFloat ds) = Just ds
dictFloat _ = Nothing
dictDouble :: DictVals -> Maybe (VU.Vector Double)
dictDouble (DDouble ds) = Just ds
dictDouble _ = Nothing
word32At :: BS.ByteString -> Int -> Word32
word32At bs o =
fromIntegral (BSU.unsafeIndex bs o)
.|. (fromIntegral (BSU.unsafeIndex bs (o + 1)) `unsafeShiftL` 8)
.|. (fromIntegral (BSU.unsafeIndex bs (o + 2)) `unsafeShiftL` 16)
.|. (fromIntegral (BSU.unsafeIndex bs (o + 3)) `unsafeShiftL` 24)
{-# INLINE word32At #-}
word64At :: BS.ByteString -> Int -> Word64
word64At bs o =
fromIntegral (word32At bs o)
.|. (fromIntegral (word32At bs (o + 4)) `unsafeShiftL` 32)
{-# INLINE word64At #-}
unpackBitPacked :: Int -> Int -> BS.ByteString -> ([Word32], BS.ByteString)
unpackBitPacked bw count bs
| count <= 0 = ([], bs)
| BS.null bs = ([], bs)
| otherwise =
let totalBytes = (bw * count + 7) `div` 8
chunk = BS.take totalBytes bs
rest = BS.drop totalBytes bs
in (extractBits bw count chunk, rest)
-- | LSB-first bit accumulator that reads each byte once.
extractBits :: Int -> Int -> BS.ByteString -> [Word32]
extractBits bw count bs = go 0 (0 :: Word64) 0 count
where
!mask = if bw == 32 then maxBound else (1 `shiftL` bw) - 1 :: Word64
!len = BS.length bs
go !byteIdx !acc !accBits !remaining
| remaining <= 0 = []
| accBits >= bw =
fromIntegral (acc .&. mask)
: go byteIdx (acc `shiftR` bw) (accBits - bw) (remaining - 1)
| byteIdx >= len = []
| otherwise =
let b = fromIntegral (BSU.unsafeIndex bs byteIdx) :: Word64
in go (byteIdx + 1) (acc .|. (b `shiftL` accBits)) (accBits + 8) remaining
decodeRLEBitPackedHybrid :: Int -> BS.ByteString -> ([Word32], BS.ByteString)
decodeRLEBitPackedHybrid bitWidth bs
| bitWidth == 0 = ([0], bs)
| BS.null bs = ([], bs)
| otherwise =
-- Evaluated inside the guard: a where clause would force it too early.
let (hdr64, afterHdr) = readUVarInt bs
isPacked = (hdr64 .&. 1) == 1
in if isPacked
then
let groups = fromIntegral (hdr64 `shiftR` 1) :: Int
totalVals = groups * 8
in unpackBitPacked bitWidth totalVals afterHdr
else
let mask = if bitWidth == 32 then maxBound else (1 `shiftL` bitWidth) - 1
runLen = fromIntegral (hdr64 `shiftR` 1) :: Int
nBytes = (bitWidth + 7) `div` 8 :: Int
word32 = littleEndianWord32 (BS.take 4 afterHdr)
value = word32 .&. mask
in (replicate runLen value, BS.drop nBytes afterHdr)