packages feed

dataframe-parquet-1.5.1.0: src/DataFrame/IO/Parquet/Writer/Encoder.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module DataFrame.IO.Parquet.Writer.Encoder (
    Encoder (..),
    buildEncoder,
) where

import Control.Monad.ST (stToIO)
import Data.Bits (shiftL, (.|.))
import Data.IORef (newIORef, readIORef, writeIORef)
import Data.Int (Int32, Int64)
import Data.Primitive.ByteArray (
    withMutableByteArrayContents,
    writeByteArray,
 )
import qualified Data.Text as T
import qualified Data.Text.Array as TA
import Data.Text.Internal (Text (Text))
import Data.Time.Calendar (toModifiedJulianDay)
import Data.Time.Clock (UTCTime (UTCTime), diffTimeToPicoseconds)
import Data.Type.Equality (TestEquality (..), (:~:) (Refl))
import qualified Data.Vector as VB
import qualified Data.Vector.Unboxed as VU
import Data.Word (Word8)
import DataFrame.IO.Parquet.Thrift
import DataFrame.IO.Utils.RandomAccess (
    MemoryBuffer (..),
    ensureCapacity,
    writeInteger64At,
    writeWord32At,
    writeWord64At,
 )
import DataFrame.Internal.Column (
    Column (..),
    Columnable,
    columnTypeString,
    hasElemType,
 )
import DataFrame.Internal.Column.Bitmap (
    Bitmap,
    bitmapTestBit,
 )
import DataFrame.Internal.Data.PackedText (
    PackedTextData (..),
    offAt,
    selAt,
 )
import Foreign (plusPtr)
import GHC.Float (castDoubleToWord64, castFloatToWord32)
import Pinch (enum, putField)
import Type.Reflection (typeRep)

data Encoder = Encoder
    { encType :: !ThriftType
    , convertedType :: !(Maybe ConvertedType)
    , logicalType :: !(Maybe LogicalType)
    , encodeValue :: !(MemoryBuffer -> Int -> Int -> IO (Int, Bool))
    , finishValues :: !(MemoryBuffer -> Int -> IO Int)
    }

buildEncoder :: Column -> IO Encoder
buildEncoder col
    | hasElemType @Int32 col =
        pure $
            scalarEncoder @Int32
                (INT32 enum)
                Nothing
                Nothing
                (\buffer pos v -> writeWord32At buffer pos (fromIntegral v) >> pure (pos + 4))
                col
    | hasElemType @Int64 col =
        pure $
            scalarEncoder @Int64
                (INT64 enum)
                Nothing
                Nothing
                (\buffer pos v -> writeWord64At buffer pos (fromIntegral v) >> pure (pos + 8))
                col
    -- Ints in GHC can be 32 bit or 64 bit integers depending on the
    -- underlying computers architecture. So we'll do 64bit integers
    -- to cover all our bases
    | hasElemType @Int col =
        pure $
            scalarEncoder @Int
                (INT64 enum)
                Nothing
                Nothing
                (\buffer pos v -> writeWord64At buffer pos (fromIntegral v) >> pure (pos + 8))
                col
    | hasElemType @Integer col =
        pure $
            scalarEncoder @Integer
                (INT64 enum)
                Nothing
                Nothing
                writeInteger64At
                col
    | hasElemType @Float col =
        pure $
            scalarEncoder @Float
                (FLOAT enum)
                Nothing
                Nothing
                ( \buffer pos v -> writeWord32At buffer pos (castFloatToWord32 v) >> pure (pos + 4)
                )
                col
    | hasElemType @Double col =
        pure $
            scalarEncoder @Double
                (DOUBLE enum)
                Nothing
                Nothing
                ( \buffer pos v -> writeWord64At buffer pos (castDoubleToWord64 v) >> pure (pos + 8)
                )
                col
    | hasElemType @Bool col = boolEncoder col
    | hasElemType @T.Text col = pure (textEncoder col)
    | hasElemType @UTCTime col = pure (timestampEncoder col)
    | otherwise =
        error ("writeParquet: unsupported column type " <> columnTypeString col)

scalarEncoder ::
    forall a.
    (Columnable a) =>
    ThriftType ->
    Maybe ConvertedType ->
    Maybe LogicalType ->
    (MemoryBuffer -> Int -> a -> IO Int) ->
    Column ->
    Encoder
scalarEncoder tt conv logical writePrim col =
    Encoder tt conv logical (columnWriter @a col writePrim) (\_ pos -> pure pos)
{-# INLINEABLE scalarEncoder #-}
{-# SPECIALIZE scalarEncoder ::
    ThriftType ->
    Maybe ConvertedType ->
    Maybe LogicalType ->
    (MemoryBuffer -> Int -> Int32 -> IO Int) ->
    Column ->
    Encoder
    #-}
{-# SPECIALIZE scalarEncoder ::
    ThriftType ->
    Maybe ConvertedType ->
    Maybe LogicalType ->
    (MemoryBuffer -> Int -> Int64 -> IO Int) ->
    Column ->
    Encoder
    #-}
{-# SPECIALIZE scalarEncoder ::
    ThriftType ->
    Maybe ConvertedType ->
    Maybe LogicalType ->
    (MemoryBuffer -> Int -> Float -> IO Int) ->
    Column ->
    Encoder
    #-}
{-# SPECIALIZE scalarEncoder ::
    ThriftType ->
    Maybe ConvertedType ->
    Maybe LogicalType ->
    (MemoryBuffer -> Int -> Double -> IO Int) ->
    Column ->
    Encoder
    #-}
{-# SPECIALIZE scalarEncoder ::
    ThriftType ->
    Maybe ConvertedType ->
    Maybe LogicalType ->
    (MemoryBuffer -> Int -> Int -> IO Int) ->
    Column ->
    Encoder
    #-}
{-# SPECIALIZE scalarEncoder ::
    ThriftType ->
    Maybe ConvertedType ->
    Maybe LogicalType ->
    (MemoryBuffer -> Int -> Integer -> IO Int) ->
    Column ->
    Encoder
    #-}

columnWriter ::
    forall a.
    (Columnable a) =>
    Column ->
    (MemoryBuffer -> Int -> a -> IO Int) ->
    MemoryBuffer ->
    Int ->
    Int ->
    IO (Int, Bool)
columnWriter col writePrim = case col of
    BoxedColumn bitmap (values :: VB.Vector b) ->
        case testEquality (typeRep @a) (typeRep @b) of
            Just Refl -> writeFrom bitmap (VB.unsafeIndex values)
            Nothing -> mismatch
    UnboxedColumn bitmap (values :: VU.Vector b) ->
        case testEquality (typeRep @a) (typeRep @b) of
            Just Refl -> writeFrom bitmap (VU.unsafeIndex values)
            Nothing -> mismatch
    _ -> mismatch
  where
    writeFrom bitmap at buffer pos row
        | isPresent bitmap row = do
            pos' <- writePrim buffer pos (at row)
            pure (pos', True)
        | otherwise = pure (pos, False)
    mismatch =
        error
            ("writeParquet: incompatible column representation for " <> columnTypeString col)
{-# INLINEABLE columnWriter #-}
{-# SPECIALIZE columnWriter ::
    Column ->
    (MemoryBuffer -> Int -> Int32 -> IO Int) ->
    MemoryBuffer ->
    Int ->
    Int ->
    IO (Int, Bool)
    #-}
{-# SPECIALIZE columnWriter ::
    Column ->
    (MemoryBuffer -> Int -> Int64 -> IO Int) ->
    MemoryBuffer ->
    Int ->
    Int ->
    IO (Int, Bool)
    #-}
{-# SPECIALIZE columnWriter ::
    Column ->
    (MemoryBuffer -> Int -> Float -> IO Int) ->
    MemoryBuffer ->
    Int ->
    Int ->
    IO (Int, Bool)
    #-}
{-# SPECIALIZE columnWriter ::
    Column ->
    (MemoryBuffer -> Int -> Double -> IO Int) ->
    MemoryBuffer ->
    Int ->
    Int ->
    IO (Int, Bool)
    #-}
{-# SPECIALIZE columnWriter ::
    Column ->
    (MemoryBuffer -> Int -> Bool -> IO Int) ->
    MemoryBuffer ->
    Int ->
    Int ->
    IO (Int, Bool)
    #-}
{-# SPECIALIZE columnWriter ::
    Column ->
    (MemoryBuffer -> Int -> UTCTime -> IO Int) ->
    MemoryBuffer ->
    Int ->
    Int ->
    IO (Int, Bool)
    #-}
{-# SPECIALIZE columnWriter ::
    Column ->
    (MemoryBuffer -> Int -> Int -> IO Int) ->
    MemoryBuffer ->
    Int ->
    Int ->
    IO (Int, Bool)
    #-}
{-# SPECIALIZE columnWriter ::
    Column ->
    (MemoryBuffer -> Int -> Integer -> IO Int) ->
    MemoryBuffer ->
    Int ->
    Int ->
    IO (Int, Bool)
    #-}

isPresent :: Maybe Bitmap -> Int -> Bool
isPresent Nothing _ = True
isPresent (Just bitmap) row = bitmapTestBit bitmap row
{-# INLINE isPresent #-}

boolEncoder :: Column -> IO Encoder
boolEncoder col = do
    bitsRef <- newIORef (0 :: Word8)
    countRef <- newIORef (0 :: Int)
    let addBit buffer pos value = do
            bits <- readIORef bitsRef
            count <- readIORef countRef
            let bits' = if value then bits .|. ((1 :: Word8) `shiftL` count) else bits
                count' = count + 1
            if count' == 8
                then do
                    arr <- readIORef buffer.arrayRef
                    writeByteArray arr pos bits'
                    writeIORef bitsRef 0
                    writeIORef countRef 0
                    pure (pos + 1)
                else do
                    writeIORef bitsRef bits'
                    writeIORef countRef count'
                    pure pos
        finish buffer pos = do
            count <- readIORef countRef
            pos' <-
                if count > 0
                    then do
                        bits <- readIORef bitsRef
                        arr <- readIORef buffer.arrayRef
                        writeByteArray arr pos bits
                        pure (pos + 1)
                    else pure pos
            writeIORef bitsRef 0
            writeIORef countRef 0
            pure pos'
    pure
        (Encoder (BOOLEAN enum) Nothing Nothing (columnWriter @Bool col addBit) finish)

textEncoder :: Column -> Encoder
textEncoder col =
    Encoder
        (BYTE_ARRAY enum)
        (Just (UTF8 enum))
        (Just (LT_STRING (putField StringType)))
        writePresent
        (\_ pos -> pure pos)
  where
    writePresent = case col of
        BoxedColumn bitmap (values :: VB.Vector a) ->
            case testEquality (typeRep @T.Text) (typeRep @a) of
                Just Refl -> writeBoxed bitmap values
                Nothing -> mismatch
        PackedText bitmap packed -> writePacked bitmap packed
        _ -> mismatch
    writeBoxed bitmap values buffer pos row
        | isPresent bitmap row = do
            let Text bytes offset count = VB.unsafeIndex values row
            pos' <- writeTextSlice buffer pos bytes offset count
            pure (pos', True)
        | otherwise = pure (pos, False)
    writePacked bitmap packed buffer pos row
        | isPresent bitmap row = do
            let baseRow = maybe row (`selAt` row) packed.ptSel
                start = offAt packed.ptOffsets baseRow
                end = offAt packed.ptOffsets (baseRow + 1)
            pos' <- writeTextSlice buffer pos packed.ptBytes start (end - start)
            pure (pos', True)
        | otherwise = pure (pos, False)
    writeTextSlice buffer pos bytes offset count = do
        writeIORef buffer.positionRef pos
        _ <- ensureCapacity buffer (pos + 4 + count)
        writeWord32At buffer pos (fromIntegral count)
        arr <- readIORef buffer.arrayRef
        withMutableByteArrayContents arr $ \ptr ->
            stToIO (TA.copyToPointer bytes offset (ptr `plusPtr` (pos + 4)) count)
        pure (pos + 4 + count)
    mismatch =
        error
            ("writeParquet: incompatible text representation for " <> columnTypeString col)

timestampEncoder :: Column -> Encoder
timestampEncoder col =
    Encoder
        (INT64 enum)
        (Just (TIMESTAMP_MICROS enum))
        (Just timestampLogical)
        (columnWriter @UTCTime col writeMicros)
        (\_ pos -> pure pos)
  where
    writeMicros buffer pos t = do
        writeWord64At buffer pos (fromIntegral (utcToMicros t))
        pure (pos + 8)

timestampLogical :: LogicalType
timestampLogical =
    LT_TIMESTAMP
        ( putField
            TimestampType
                { timestamp_isAdjustedToUTC = putField True
                , timestamp_unit = putField (MICROS (putField MicroSeconds))
                }
        )

utcToMicros :: UTCTime -> Int64
utcToMicros (UTCTime day dt) =
    fromIntegral
        ( (toModifiedJulianDay day - 40587) * 86400 * 1000000
            + diffTimeToPicoseconds dt `div` 1000000
        )
{-# INLINE utcToMicros #-}