packages feed

dataframe-parquet-1.6.0.0: src/DataFrame/IO/Parquet/Page.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE ScopedTypeVariables #-}

module DataFrame.IO.Parquet.Page (
    -- Types
    PageDecoder,
    UnboxedPageDecoder,
    -- Per-type decoders
    boolDecoder,
    int32Decoder,
    int64Decoder,
    int96Decoder,
    floatDecoder,
    doubleDecoder,
    byteArrayDecoder,
    fixedLenByteArrayDecoder,
    -- Page iteration
    foldColumnPagesM,
    foldColumnDataPagesM,
    RawPage,
    decodeRequiredNumericInto,
    appendStringPageIO,
    appendNullableStringPageIO,
) where

import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.ST (RealWorld, stToIO)
import Data.Bits (shiftL, shiftR, (.&.), (.|.))
import qualified Data.ByteString as BS
import qualified Data.ByteString.Unsafe as BSU
import Data.Int (Int32, Int64)
import Data.Maybe (fromJust, fromMaybe)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8Lenient)
import Data.Time (UTCTime)
import qualified Data.Vector as VB
import qualified Data.Vector.Generic as VG
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM
import Data.Word (Word8)
import DataFrame.IO.Parquet.Decompress (decompressData)
import DataFrame.IO.Parquet.Dictionary (
    DictVals (..),
    dictText,
    readDictVals,
    word32At,
    word64At,
 )
import DataFrame.IO.Parquet.Encoding (
    decodeDictIndices,
    decodeRLEBitPackedHybrid,
    rleHybridFold,
 )
import DataFrame.IO.Parquet.Levels (readLevelsV1, readLevelsV2)
import DataFrame.IO.Parquet.Thrift (
    ColumnChunk (..),
    ColumnMetaData (..),
    CompressionCodec,
    DataPageHeader (..),
    DataPageHeaderV2 (..),
    DictionaryPageHeader (..),
    Encoding (..),
    PageHeader (..),
    PageType (..),
    ThriftType (..),
    unField,
 )
import DataFrame.IO.Parquet.Time (int96ToUTCTime)
import DataFrame.IO.Parquet.Utils (ColumnDescription (..))
import DataFrame.IO.Utils.RandomAccess (RandomAccess (..), Range (Range))
import DataFrame.Internal.Binary (littleEndianInt32)
import DataFrame.Internal.Column.Builder (
    TextBuilder,
    appendNull,
    appendTextSliceFromPtr,
 )
import Foreign.Ptr (Ptr, castPtr, plusPtr)
import Foreign.Storable (peekByteOff)
import GHC.Float (castWord32ToFloat, castWord64ToDouble)
import Pinch (decodeWithLeftovers)
import qualified Pinch

-- | A type-specific page decoder.
type PageDecoder a =
    Maybe DictVals -> Encoding -> Int -> BS.ByteString -> VB.Vector a

type UnboxedPageDecoder a =
    Maybe DictVals -> Encoding -> Int -> BS.ByteString -> VU.Vector a

boolDecoder :: UnboxedPageDecoder Bool
boolDecoder mDict enc nPresent bs = case enc of
    -- PLAIN bools are bit-packed; index the bits directly, avoiding a list.
    PLAIN _ ->
        VU.generate nPresent $ \i ->
            (BSU.unsafeIndex bs (i `shiftR` 3) `shiftR` (i .&. 7)) .&. 1 == 1
    RLE_DICTIONARY _ -> gatherDict "boolDecoder" getBool mDict nPresent bs
    PLAIN_DICTIONARY _ -> gatherDict "boolDecoder" getBool mDict nPresent bs
    _ -> error ("boolDecoder: unsupported encoding " ++ show enc)
  where
    getBool (DBool ds) = Just ds
    getBool _ = Nothing

int32Decoder :: UnboxedPageDecoder Int32
int32Decoder mDict enc nPresent bs = case enc of
    PLAIN _ -> readNInt32 nPresent bs
    RLE_DICTIONARY _ -> gatherDict "int32Decoder" getInt32 mDict nPresent bs
    PLAIN_DICTIONARY _ -> gatherDict "int32Decoder" getInt32 mDict nPresent bs
    _ -> error ("int32Decoder: unsupported encoding " ++ show enc)
  where
    getInt32 (DInt32 ds) = Just ds
    getInt32 _ = Nothing

int64Decoder :: UnboxedPageDecoder Int64
int64Decoder mDict enc nPresent bs = case enc of
    PLAIN _ -> readNInt64 nPresent bs
    RLE_DICTIONARY _ -> gatherDict "int64Decoder" getInt64 mDict nPresent bs
    PLAIN_DICTIONARY _ -> gatherDict "int64Decoder" getInt64 mDict nPresent bs
    _ -> error ("int64Decoder: unsupported encoding " ++ show enc)
  where
    getInt64 (DInt64 ds) = Just ds
    getInt64 _ = Nothing

int96Decoder :: PageDecoder UTCTime
int96Decoder mDict enc nPresent bs = case enc of
    PLAIN _ -> VB.fromList (readNInt96 nPresent bs)
    RLE_DICTIONARY _ -> lookupDict mDict nPresent bs getInt96
    PLAIN_DICTIONARY _ -> lookupDict mDict nPresent bs getInt96
    _ -> error ("int96Decoder: unsupported encoding " ++ show enc)
  where
    getInt96 (DInt96 ds) i = VB.unsafeIndex ds i
    getInt96 d _ = error ("int96Decoder: wrong dict type, got " ++ show d)

floatDecoder :: UnboxedPageDecoder Float
floatDecoder mDict enc nPresent bs = case enc of
    PLAIN _ -> readNFloat nPresent bs
    RLE_DICTIONARY _ -> gatherDict "floatDecoder" getFloat mDict nPresent bs
    PLAIN_DICTIONARY _ -> gatherDict "floatDecoder" getFloat mDict nPresent bs
    _ -> error ("floatDecoder: unsupported encoding " ++ show enc)
  where
    getFloat (DFloat ds) = Just ds
    getFloat _ = Nothing

doubleDecoder :: UnboxedPageDecoder Double
doubleDecoder mDict enc nPresent bs = case enc of
    PLAIN _ -> readNDouble nPresent bs
    RLE_DICTIONARY _ -> gatherDict "doubleDecoder" getDouble mDict nPresent bs
    PLAIN_DICTIONARY _ -> gatherDict "doubleDecoder" getDouble mDict nPresent bs
    _ -> error ("doubleDecoder: unsupported encoding " ++ show enc)
  where
    getDouble (DDouble ds) = Just ds
    getDouble _ = Nothing

byteArrayDecoder :: PageDecoder T.Text
byteArrayDecoder mDict enc nPresent bs = case enc of
    PLAIN _ -> VB.fromList (readNTexts nPresent bs)
    RLE_DICTIONARY _ -> lookupDict mDict nPresent bs getText
    PLAIN_DICTIONARY _ -> lookupDict mDict nPresent bs getText
    _ -> error ("byteArrayDecoder: unsupported encoding " ++ show enc)
  where
    getText = dictText

fixedLenByteArrayDecoder :: Int -> PageDecoder T.Text
fixedLenByteArrayDecoder len mDict enc nPresent bs = case enc of
    PLAIN _ -> VB.fromList (readNFixedTexts len nPresent bs)
    RLE_DICTIONARY _ -> lookupDict mDict nPresent bs getText
    PLAIN_DICTIONARY _ -> lookupDict mDict nPresent bs getText
    _ -> error ("fixedLenByteArrayDecoder: unsupported encoding " ++ show enc)
  where
    getText = dictText

-- | Decode RLE\/bit-packed indices and look each up in the dictionary.
lookupDict ::
    Maybe DictVals ->
    Int ->
    BS.ByteString ->
    (DictVals -> Int -> a) ->
    VB.Vector a
lookupDict mDict nPresent bs f = case mDict of
    Nothing -> error "Dictionary-encoded page but no dictionary page seen"
    Just dict ->
        let (idxs, _) = decodeDictIndices nPresent bs
         in VB.generate nPresent (f dict . VU.unsafeIndex idxs)

-- | Decode dictionary indices and gather from an unboxed dictionary.
gatherDict ::
    (VU.Unbox a) =>
    String ->
    (DictVals -> Maybe (VU.Vector a)) ->
    Maybe DictVals ->
    Int ->
    BS.ByteString ->
    VU.Vector a
gatherDict who pick mDict nPresent bs = case mDict of
    Nothing -> error (who ++ ": dictionary-encoded page but no dictionary page seen")
    Just dict -> case pick dict of
        Nothing -> error (who ++ ": wrong dict type, got " ++ show dict)
        Just ds -> case BS.uncons bs of
            Nothing -> error (who ++ ": empty index stream")
            Just (w0, rest) ->
                let (idxs, _) = decodeRLEBitPackedHybrid (fromIntegral w0) nPresent rest
                 in VU.map (VU.unsafeIndex ds . fromIntegral) idxs
{-# INLINE gatherDict #-}

-- | Read the raw (compressed) byte range for a column chunk.
readChunkBytes ::
    (RandomAccess m) =>
    ColumnChunk ->
    m (CompressionCodec, ThriftType, BS.ByteString)
readChunkBytes columnChunk = do
    let meta = fromJust . unField $ columnChunk.cc_meta_data
        codec = unField meta.cmd_codec
        pType = unField meta.cmd_type
        dataOffset = fromIntegral . unField $ meta.cmd_data_page_offset
        dictOffset = fromIntegral <$> unField meta.cmd_dictionary_page_offset
        offset = fromMaybe dataOffset dictOffset
        compLen = fromIntegral . unField $ meta.cmd_total_compressed_size
    rawBytes <- readBytes (Range offset compLen)
    return (codec, pType, rawBytes)

-- | A decoded DATA page handed to a fold step.
type RawPage =
    (Maybe DictVals, Encoding, Int, BS.ByteString, VU.Vector Int, VU.Vector Int)

-- | Left-fold a monadic step over every DATA page of every column chunk.
{-# INLINEABLE foldColumnDataPagesM #-}
foldColumnDataPagesM ::
    forall m acc.
    (RandomAccess m, MonadIO m) =>
    ColumnDescription ->
    [ColumnChunk] ->
    (acc -> RawPage -> m acc) ->
    acc ->
    m acc
foldColumnDataPagesM description chunks step = goChunks chunks
  where
    maxDef = fromIntegral description.maxDefinitionLevel :: Int
    maxRep = fromIntegral description.maxRepetitionLevel :: Int

    goChunks [] !acc = return acc
    goChunks (cc : ccs) !acc = do
        (codec, pType, rawBytes) <- readChunkBytes cc
        acc' <- goPages Nothing codec pType rawBytes acc
        goChunks ccs acc'

    goPages dict codec pType bs !acc
        | BS.null bs = return acc
        | otherwise = case parsePageHeader bs of
            Left e -> error ("foldColumnDataPagesM: failed to parse page header: " ++ e)
            Right (rest, hdr) -> do
                let compSz = fromIntegral . unField $ hdr.ph_compressed_page_size
                    uncmpSz = fromIntegral . unField $ hdr.ph_uncompressed_page_size
                    (pageData, rest') = BS.splitAt compSz rest
                case unField hdr.ph_type of
                    DICTIONARY_PAGE _ -> do
                        let dictHdr =
                                fromMaybe
                                    (error "DICTIONARY_PAGE: missing dictionary page header")
                                    (unField hdr.ph_dictionary_page_header)
                            numVals = unField dictHdr.diph_num_values
                        decompressed <- liftIO $ decompressData uncmpSz codec pageData
                        let d = readDictVals pType decompressed numVals description.typeLength
                        goPages (Just d) codec pType rest' acc
                    DATA_PAGE _ -> do
                        let dph =
                                fromMaybe
                                    (error "DATA_PAGE: missing data page header")
                                    (unField hdr.ph_data_page_header)
                            n = fromIntegral . unField $ dph.dph_num_values
                            enc = unField dph.dph_encoding
                        decompressed <- liftIO $ decompressData uncmpSz codec pageData
                        let (defLvls, repLvls, nPresent, valBytes) =
                                readLevelsV1 n maxDef maxRep decompressed
                        acc' <- step acc (dict, enc, nPresent, valBytes, defLvls, repLvls)
                        goPages dict codec pType rest' acc'
                    DATA_PAGE_V2 _ -> do
                        let dph2 =
                                fromMaybe
                                    (error "DATA_PAGE_V2: missing data page header v2")
                                    (unField hdr.ph_data_page_header_v2)
                            n = fromIntegral . unField $ dph2.dph2_num_values
                            enc = unField dph2.dph2_encoding
                            defLen = unField dph2.dph2_definition_levels_byte_length
                            repLen = unField dph2.dph2_repetition_levels_byte_length
                            -- V2 levels are never compressed, only the values.
                            isCompressed = fromMaybe True (unField dph2.dph2_is_compressed)
                            (defLvls, repLvls, nPresent, compValBytes) =
                                readLevelsV2 n maxDef maxRep repLen defLen pageData
                        valBytes <-
                            if isCompressed
                                then liftIO $ decompressData uncmpSz codec compValBytes
                                else pure compValBytes
                        acc' <- step acc (dict, enc, nPresent, valBytes, defLvls, repLvls)
                        goPages dict codec pType rest' acc'
                    INDEX_PAGE _ -> goPages dict codec pType rest' acc

-- | Left-fold over per-page value triples with a given decoder.
{-# INLINEABLE foldColumnPagesM #-}
foldColumnPagesM ::
    forall m v a acc.
    (RandomAccess m, MonadIO m, VG.Vector v a) =>
    ColumnDescription ->
    (Maybe DictVals -> Encoding -> Int -> BS.ByteString -> v a) ->
    [ColumnChunk] ->
    (acc -> (v a, VU.Vector Int, VU.Vector Int) -> m acc) ->
    acc ->
    m acc
foldColumnPagesM description decoder chunks step =
    foldColumnDataPagesM description chunks $
        \acc (dict, enc, nPresent, valBytes, defLvls, repLvls) ->
            step acc (decoder dict enc nPresent valBytes, defLvls, repLvls)

-- | Append a non-nullable BYTE_ARRAY page's strings into a 'TextBuilder'.
appendStringPageIO ::
    TextBuilder RealWorld ->
    Maybe DictVals ->
    Encoding ->
    Int ->
    BS.ByteString ->
    IO ()
appendStringPageIO builder mDict enc nPresent bs = case enc of
    PLAIN _ -> BSU.unsafeUseAsCStringLen bs $ \(cptr, _len) -> do
        let p = castPtr cptr :: Ptr Word8
            go !_ !i | i >= nPresent = pure ()
            go !off !i = do
                len <- readLenLE p off
                stToIO (appendTextSliceFromPtr builder (p `plusPtr` (off + 4)) len)
                go (off + 4 + len) (i + 1)
        go 0 0
    RLE_DICTIONARY _ -> dictAppend
    PLAIN_DICTIONARY _ -> dictAppend
    _ -> error ("appendStringPageIO: unsupported encoding " ++ show enc)
  where
    dictAppend = case mDict of
        Just (DText starts lens dictBytes) ->
            BSU.unsafeUseAsCStringLen dictBytes $ \(cptr, _) -> do
                let p = castPtr cptr :: Ptr Word8
                    (idxs, _) = decodeDictIndices nPresent bs
                VU.forM_ idxs $ \i ->
                    stToIO
                        ( appendTextSliceFromPtr
                            builder
                            (p `plusPtr` VU.unsafeIndex starts i)
                            (VU.unsafeIndex lens i)
                        )
        Just d -> error ("appendStringPageIO: wrong dict type, got " ++ show d)
        Nothing -> error "appendStringPageIO: dictionary-encoded page but no dictionary seen"

-- | Read a little-endian 4-byte length prefix from a raw page pointer.
readLenLE :: Ptr Word8 -> Int -> IO Int
readLenLE p off = do
    b0 <- peekByteOff p off :: IO Word8
    b1 <- peekByteOff p (off + 1) :: IO Word8
    b2 <- peekByteOff p (off + 2) :: IO Word8
    b3 <- peekByteOff p (off + 3) :: IO Word8
    pure $
        fromIntegral b0
            .|. (fromIntegral b1 `shiftL` 8)
            .|. (fromIntegral b2 `shiftL` 16)
            .|. (fromIntegral b3 `shiftL` 24)
{-# INLINE readLenLE #-}

-- | Append a nullable BYTE_ARRAY page into a 'TextBuilder'.
appendNullableStringPageIO ::
    TextBuilder RealWorld ->
    Int ->
    Maybe DictVals ->
    Encoding ->
    Int ->
    BS.ByteString ->
    VU.Vector Int ->
    IO ()
appendNullableStringPageIO builder maxDef mDict enc nPresent bs defs = case enc of
    PLAIN _ -> BSU.unsafeUseAsCStringLen bs $ \(cptr, _len) -> do
        let p = castPtr cptr :: Ptr Word8
            go !_ !i | i >= nDefs = pure ()
            go !off !i
                | VU.unsafeIndex defs i == maxDef = do
                    len <- readLenLE p off
                    stToIO (appendTextSliceFromPtr builder (p `plusPtr` (off + 4)) len)
                    go (off + 4 + len) (i + 1)
                | otherwise = stToIO (appendNull builder) >> go off (i + 1)
        go 0 0
    RLE_DICTIONARY _ -> dictAppend
    PLAIN_DICTIONARY _ -> dictAppend
    _ -> error ("appendNullableStringPageIO: unsupported encoding " ++ show enc)
  where
    nDefs = VU.length defs
    dictAppend = case mDict of
        Just (DText starts lens dictBytes) -> BSU.unsafeUseAsCStringLen dictBytes $ \(cptr, _) -> do
            let p = castPtr cptr :: Ptr Word8
                (idxs, _) = decodeDictIndices nPresent bs
                go !_ !i | i >= nDefs = pure ()
                go !j !i
                    | VU.unsafeIndex defs i == maxDef = do
                        let d = VU.unsafeIndex idxs j
                        stToIO
                            ( appendTextSliceFromPtr
                                builder
                                (p `plusPtr` VU.unsafeIndex starts d)
                                (VU.unsafeIndex lens d)
                            )
                        go (j + 1) (i + 1)
                    | otherwise = stToIO (appendNull builder) >> go j (i + 1)
            go 0 0
        Just d -> error ("appendNullableStringPageIO: wrong dict type, got " ++ show d)
        Nothing ->
            error
                "appendNullableStringPageIO: dictionary-encoded page but no dictionary seen"

parsePageHeader :: BS.ByteString -> Either String (BS.ByteString, PageHeader)
parsePageHeader = decodeWithLeftovers Pinch.compactProtocol

readNInt32 :: Int -> BS.ByteString -> VU.Vector Int32
readNInt32 n bs = VU.generate n $ \i -> fromIntegral (word32At bs (4 * i))

readNInt64 :: Int -> BS.ByteString -> VU.Vector Int64
readNInt64 n bs = VU.generate n $ \i -> fromIntegral (word64At bs (8 * i))

readNInt96 :: Int -> BS.ByteString -> [UTCTime]
readNInt96 0 _ = []
readNInt96 n bs = int96ToUTCTime (BS.take 12 bs) : readNInt96 (n - 1) (BS.drop 12 bs)

readNFloat :: Int -> BS.ByteString -> VU.Vector Float
readNFloat n bs = VU.generate n $ \i -> castWord32ToFloat (word32At bs (4 * i))

readNDouble :: Int -> BS.ByteString -> VU.Vector Double
readNDouble n bs = VU.generate n $ \i -> castWord64ToDouble (word64At bs (8 * i))

readNTexts :: Int -> BS.ByteString -> [T.Text]
readNTexts 0 _ = []
readNTexts n bs =
    let len = fromIntegral . littleEndianInt32 . BS.take 4 $ bs
        text = decodeUtf8Lenient . BS.take len . BS.drop 4 $ bs
     in text : readNTexts (n - 1) (BS.drop (4 + len) bs)

readNFixedTexts :: Int -> Int -> BS.ByteString -> [T.Text]
readNFixedTexts _ 0 _ = []
readNFixedTexts len n bs =
    decodeUtf8Lenient (BS.take len bs)
        : readNFixedTexts len (n - 1) (BS.drop len bs)

-- | Decode a required numeric column chunk straight into @out[off ..]@.
decodeRequiredNumericInto ::
    forall a m.
    (VU.Unbox a, RandomAccess m, MonadIO m) =>
    (DictVals -> Maybe (VU.Vector a)) ->
    (BS.ByteString -> Int -> a) ->
    Int ->
    ColumnDescription ->
    ColumnChunk ->
    VUM.IOVector a ->
    Int ->
    m Int
decodeRequiredNumericInto pick plainAt width description chunk out off0 = do
    (codec, pType, rawBytes) <- readChunkBytes chunk
    goPages Nothing codec pType rawBytes off0
  where
    goPages ::
        Maybe (VU.Vector a) ->
        CompressionCodec ->
        ThriftType ->
        BS.ByteString ->
        Int ->
        m Int
    goPages dict codec pType bs !off
        | BS.null bs = return off
        | otherwise = case parsePageHeader bs of
            Left e -> error ("decodeRequiredNumericInto: failed to parse page header: " ++ e)
            Right (rest, hdr) -> do
                let compSz = fromIntegral . unField $ hdr.ph_compressed_page_size
                    uncmpSz = fromIntegral . unField $ hdr.ph_uncompressed_page_size
                    (pageData, rest') = BS.splitAt compSz rest
                case unField hdr.ph_type of
                    DICTIONARY_PAGE _ -> do
                        let dictHdr =
                                fromMaybe
                                    (error "DICTIONARY_PAGE: missing dictionary page header")
                                    (unField hdr.ph_dictionary_page_header)
                            numVals = unField dictHdr.diph_num_values
                        decompressed <- liftIO $ decompressData uncmpSz codec pageData
                        let d = readDictVals pType decompressed numVals description.typeLength
                        case pick d of
                            Nothing -> error "decodeRequiredNumericInto: dictionary page mistyped"
                            Just !ds -> goPages (Just ds) codec pType rest' off
                    DATA_PAGE _ -> do
                        let dph =
                                fromMaybe
                                    (error "DATA_PAGE: missing data page header")
                                    (unField hdr.ph_data_page_header)
                            n = fromIntegral . unField $ dph.dph_num_values
                            enc = unField dph.dph_encoding
                        decompressed <- liftIO $ decompressData uncmpSz codec pageData
                        liftIO (writePage dict enc n decompressed off)
                        goPages dict codec pType rest' (off + n)
                    DATA_PAGE_V2 _ -> do
                        let dph2 =
                                fromMaybe
                                    (error "DATA_PAGE_V2: missing data page header v2")
                                    (unField hdr.ph_data_page_header_v2)
                            n = fromIntegral . unField $ dph2.dph2_num_values
                            enc = unField dph2.dph2_encoding
                            levelBytes =
                                fromIntegral (unField dph2.dph2_definition_levels_byte_length)
                                    + fromIntegral (unField dph2.dph2_repetition_levels_byte_length)
                            isCompressed = fromMaybe True (unField dph2.dph2_is_compressed)
                            compValBytes = BS.drop levelBytes pageData
                        valBytes <-
                            if isCompressed
                                then liftIO $ decompressData (uncmpSz - levelBytes) codec compValBytes
                                else pure compValBytes
                        liftIO (writePage dict enc n valBytes off)
                        goPages dict codec pType rest' (off + n)
                    INDEX_PAGE _ -> goPages dict codec pType rest' off

    writePage ::
        Maybe (VU.Vector a) -> Encoding -> Int -> BS.ByteString -> Int -> IO ()
    writePage dict enc n vals off = case enc of
        PLAIN _ ->
            let go !i
                    | i >= n = pure ()
                    | otherwise = do
                        VUM.unsafeWrite out (off + i) (plainAt vals (width * i))
                        go (i + 1)
             in go 0
        RLE_DICTIONARY _ -> gather
        PLAIN_DICTIONARY _ -> gather
        _ -> error ("decodeRequiredNumericInto: unsupported encoding " ++ show enc)
      where
        gather = case dict of
            Nothing -> error "decodeRequiredNumericInto: dictionary page missing"
            Just ds -> case BS.uncons vals of
                Nothing -> error "decodeRequiredNumericInto: empty index stream"
                Just (w0, rest) ->
                    stToIO $
                        rleHybridFold (fromIntegral w0) n rest $ \i w ->
                            VUM.unsafeWrite out (off + i) (VU.unsafeIndex ds (fromIntegral w))
{-# INLINE decodeRequiredNumericInto #-}