packages feed

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

{-# LANGUAGE OverloadedStrings #-}

module DataFrame.IO.Parquet.Writer.Metadata (
    mkSchemaElem,
    rootSchemaElement,
    mkDataPageHeader,
    mkColumnChunk,
    mkRowGroup,
    writeFooter,
    magic,
) where

import qualified Data.ByteString as BS
import Data.Int (Int64)
import qualified Data.Text as T
import DataFrame.IO.Parquet.Thrift
import DataFrame.IO.Utils.RandomAccess (
    WritableBinaryHandle,
    flushBufferToFile,
    mallocBuffer,
    writeByteString,
    writeWord32LE,
 )
import Pinch (enum, putField)
import qualified Pinch

mkDataPageHeader :: Int -> Int -> Int -> PageHeader
mkDataPageHeader rows uncompressedSize compressedSize =
    PageHeader
        { ph_type = putField (DATA_PAGE enum)
        , ph_uncompressed_page_size = putField (fromIntegral uncompressedSize)
        , ph_compressed_page_size = putField (fromIntegral compressedSize)
        , ph_crc = putField Nothing
        , ph_data_page_header = putField (Just dph)
        , ph_index_page_header = putField Nothing
        , ph_dictionary_page_header = putField Nothing
        , ph_data_page_header_v2 = putField Nothing
        }
  where
    dph =
        DataPageHeader
            { dph_num_values = putField (fromIntegral rows)
            , dph_encoding = putField (PLAIN enum)
            , dph_definition_level_encoding = putField (RLE enum)
            , dph_repetition_level_encoding = putField (RLE enum)
            , dph_statistics = putField Nothing
            }

mkSchemaElem ::
    T.Text ->
    ThriftType ->
    Bool ->
    Maybe ConvertedType ->
    Maybe LogicalType ->
    SchemaElement
mkSchemaElem elementName elementType nullable converted logical =
    SchemaElement
        { schematype = putField (Just elementType)
        , type_length = putField Nothing
        , repetition_type =
            putField (Just (if nullable then OPTIONAL enum else REQUIRED enum))
        , name = putField elementName
        , num_children = putField Nothing
        , converted_type = putField converted
        , scale = putField Nothing
        , precision = putField Nothing
        , field_id = putField Nothing
        , logicalType = putField logical
        }

rootSchemaElement :: Int -> SchemaElement
rootSchemaElement count =
    SchemaElement
        { schematype = putField Nothing
        , type_length = putField Nothing
        , repetition_type = putField Nothing
        , name = putField "schema"
        , num_children = putField (Just (fromIntegral count))
        , converted_type = putField Nothing
        , scale = putField Nothing
        , precision = putField Nothing
        , field_id = putField Nothing
        , logicalType = putField Nothing
        }

mkColumnChunk ::
    CompressionCodec ->
    ThriftType ->
    T.Text ->
    Int64 ->
    Int ->
    Int64 ->
    Int ->
    ColumnChunk
mkColumnChunk codec columnType columnName offset compressedSize uncompressedSize rgRows =
    ColumnChunk
        { cc_file_path = putField Nothing
        , cc_file_offset = putField offset
        , cc_meta_data = putField (Just metadata)
        , cc_offset_index_offset = putField Nothing
        , cc_offset_index_length = putField Nothing
        , cc_column_index_offset = putField Nothing
        , cc_column_index_length = putField Nothing
        , cc_crypto_metadata = putField Nothing
        , cc_encrypted_column_metadata = putField Nothing
        }
  where
    metadata =
        ColumnMetaData
            { cmd_type = putField columnType
            , cmd_encodings = putField [PLAIN enum, RLE enum]
            , cmd_path_in_schema = putField [columnName]
            , cmd_codec = putField codec
            , cmd_num_values = putField (fromIntegral rgRows)
            , cmd_total_uncompressed_size = putField uncompressedSize
            , cmd_total_compressed_size = putField (fromIntegral compressedSize)
            , cmd_key_value_metadata = putField Nothing
            , cmd_data_page_offset = putField offset
            , cmd_index_page_offset = putField Nothing
            , cmd_dictionary_page_offset = putField Nothing
            , cmd_statistics = putField Nothing
            , cmd_encoding_stats = putField Nothing
            , cmd_bloom_filter_offset = putField Nothing
            , cmd_bloom_filter_length = putField Nothing
            }

mkRowGroup :: [ColumnChunk] -> Int64 -> Int64 -> Int -> RowGroup
mkRowGroup chunks totalCompressed totalUncompressed rgRows =
    RowGroup
        { rg_columns = putField chunks
        , rg_total_byte_size = putField totalUncompressed
        , rg_num_rows = putField (fromIntegral rgRows)
        , rg_sorting_columns = putField Nothing
        , rg_file_offset = putField Nothing
        , rg_total_compressed_size = putField (Just totalCompressed)
        , rg_ordinal = putField Nothing
        }

writeFooter ::
    WritableBinaryHandle ->
    [SchemaElement] ->
    Int ->
    [RowGroup] ->
    [(T.Text, T.Text)] ->
    IO ()
writeFooter output schemaElements numRows rowGroupMetadata keyValues = do
    let metadata =
            FileMetadata
                { version = putField 1
                , schema = putField schemaElements
                , num_rows = putField (fromIntegral numRows)
                , row_groups = putField rowGroupMetadata
                , key_value_metadata =
                    putField $
                        if null keyValues
                            then Nothing
                            else
                                Just
                                    [ KeyValue (putField k) (putField (Just v))
                                    | (k, v) <- keyValues
                                    ]
                , created_by = putField (Just "dataframe-parquet")
                , column_orders = putField Nothing
                , encryption_algorithm = putField Nothing
                , footer_signing_key_metadata = putField Nothing
                }
        footer = Pinch.encode Pinch.compactProtocol metadata
    buffer <- mallocBuffer (BS.length footer + 8)
    writeByteString buffer footer
    writeWord32LE buffer (fromIntegral (BS.length footer))
    writeByteString buffer magic
    flushBufferToFile output buffer

magic :: BS.ByteString
magic = "PAR1"