packages feed

wireform-parquet-0.1.0.0: src/Parquet/Arrow/File.hs

{-# LANGUAGE OverloadedStrings #-}

{- | Compatibility with arrow-rs's experimental FILE extension. The extension
name is not yet a standardized canonical Arrow extension.
-}
module Parquet.Arrow.File (
  isFileField,
  fileSchemaFromField,
  fileMetadata,
  fieldIdMetadata,
) where

import Arrow.Types qualified as A
import Control.Monad (unless)
import Data.Int (Int32)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Lazy qualified as TL
import Data.Text.Lazy.Builder qualified as TB
import Data.Text.Lazy.Builder.Int qualified as TBI
import Data.Text.Read qualified as TR
import Data.Vector qualified as V
import Parquet.File qualified as F


isFileField :: A.Field -> Bool
isFileField field = V.elem ("ARROW:extension:name", "arrow.parquet.file") (A.fieldMetadata field)


fileMetadata :: V.Vector (Text, Text)
fileMetadata = V.fromList [("ARROW:extension:name", "arrow.parquet.file"), ("ARROW:extension:metadata", "")]


fieldIdMetadata :: Maybe Int32 -> V.Vector (Text, Text)
fieldIdMetadata = maybe V.empty (V.singleton . ("PARQUET:field_id",) . TL.toStrict . TB.toLazyText . TBI.decimal)


fileSchemaFromField :: A.Field -> Either String F.FileSchema
fileSchemaFromField field = do
  let names = values "ARROW:extension:name" field
      parameters = values "ARROW:extension:metadata" field
  unless
    (names == V.singleton "arrow.parquet.file" && V.length parameters <= 1 && V.all T.null parameters)
    (Left "Parquet.Arrow: FILE extension expects a unique name and absent/empty metadata")
  unless
    (A.fieldType field == A.AStruct && A.fieldDictionary field == Nothing)
    (Left "Parquet.Arrow: FILE extension requires struct storage")
  ident <- fieldId field
  children <- V.mapM child (A.fieldChildren field)
  F.withFileSchemaFieldId ident <$> F.mkFileSchema children
  where
    child value = do
      kind <- case A.fieldName value of
        "uri" -> Right F.FileUri
        "offset" -> Right F.FileOffset
        "size" -> Right F.FileSize
        "content_type" -> Right F.FileContentType
        "checksum" -> Right F.FileChecksumField
        "inline" -> Right F.FileInline
        _ -> Left "Parquet.Arrow: unrecognized FILE field name"
      let ty = A.fieldType value
          matches = case kind of
            F.FileOffset -> ty == A.AInt 64 True
            F.FileSize -> ty == A.AInt 64 True
            F.FileInline -> ty `elem` [A.ABinary, A.ALargeBinary, A.ABinaryView]
            _ -> ty `elem` [A.AUtf8, A.ALargeUtf8, A.AUtf8View]
      unless
        (matches && A.fieldNullable value && V.null (A.fieldChildren value) && A.fieldDictionary value == Nothing)
        (Left "Parquet.Arrow: FILE children must be nullable primitives with their specified types")
      F.FileFieldSpec kind <$> fieldId value


values :: Text -> A.Field -> V.Vector Text
values key = V.map snd . V.filter ((== key) . fst) . A.fieldMetadata


fieldId :: A.Field -> Either String (Maybe Int32)
fieldId field = case V.toList (values "PARQUET:field_id" field) of
  [] -> Right Nothing
  [encoded] -> case TR.signed TR.decimal encoded :: Either String (Integer, Text) of
    Right (number, remainder)
      | T.null remainder && number >= fromIntegral (minBound :: Int32) && number <= fromIntegral (maxBound :: Int32) -> Right (Just (fromInteger number))
    _ -> Left "Parquet.Arrow: FILE field ID must fit a signed 32-bit integer"
  _ -> Left "Parquet.Arrow: duplicate FILE field ID metadata"