packages feed

wireform-parquet-0.1.0.0: src/Parquet/Variant/Array.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UnboxedSums #-}
{-# LANGUAGE UnboxedTuples #-}

-- | Internal shared binary accessors for Variant reading and writing.
module Parquet.Variant.Array (BytesReader (..), compileBytes, alignVariantColumns) where

import Arrow.Column qualified as C
import Arrow.Types qualified as A
import Data.ByteString (ByteString)
import Data.Map.Strict qualified as M
import Data.Vector qualified as V
import Data.Vector.Storable qualified as S
import Foreign.Storable (Storable)
import GHC.Exts (Int (I#), Int#)
import Parquet.Variant.Schema qualified as Schema


newtype BytesReader = BytesReader {bytesAt# :: Int# -> (# ByteString | () #)}


newtype IndexReader = IndexReader {indexAt# :: Int# -> (# Int# | () #)}


-- Only metadata may be dictionary or run-end encoded. Resolve one logical
-- index at a time; no expanded metadata column is allocated.
compileBytes :: C.ColumnArray -> Either String BytesReader
compileBytes column = case column of
  C.ColDictionary _ keys values -> do
    entries <- compileBytes values
    indices <- compileIndices keys
    pure $ BytesReader $ \index -> case indexAt# indices index of
      (# | () #) -> (# | () #)
      (# key | #) -> bytesAt# entries key
  C.ColRunEndEncoded offset _ ends values -> do
    entries <- compileBytes values
    case ends of
      C.ColInt16 _ xs -> pure (runs entries offset xs)
      C.ColInt32 _ xs -> pure (runs entries offset xs)
      C.ColInt64 _ xs -> pure (runs entries offset xs)
      _ -> Left "Parquet.Variant.Arrow: invalid metadata run ends"
  C.ColBinary validity offsets bytes -> pure (variableBytes validity offsets bytes)
  C.ColLargeBinary validity offsets bytes -> pure (variableBytes validity offsets bytes)
  C.ColBinaryView {} -> pure $ BytesReader $ \index -> case C.anyBytesAt column (I# index) of
    Nothing -> (# | () #)
    Just bytes -> (# bytes | #)
  _ -> Left "Parquet.Variant.Arrow: expected binary storage"
  where
    runs :: (Storable a, Integral a) => BytesReader -> Int -> S.Vector a -> BytesReader
    runs entries offset ends = BytesReader $ \index ->
      let position = offset + I# index
          search !low !high
            | low == high = low
            | fromIntegral (S.unsafeIndex ends middle) <= position = search (middle + 1) high
            | otherwise = search low middle
            where
              middle = low + (high - low) `quot` 2
      in case search 0 (S.length ends) of I# run -> bytesAt# entries run


-- The public dictionary accessor returns a boxed index and must dispatch
-- on its integer type for each row. Bind the concrete key vector once.
compileIndices :: C.ColumnArray -> Either String IndexReader
compileIndices column = case column of
  C.ColInt8 v xs -> pure (indexKeys v xs)
  C.ColInt16 v xs -> pure (indexKeys v xs)
  C.ColInt32 v xs -> pure (indexKeys v xs)
  C.ColInt64 v xs -> pure (indexKeys v xs)
  C.ColUInt8 v xs -> pure (indexKeys v xs)
  C.ColUInt16 v xs -> pure (indexKeys v xs)
  C.ColUInt32 v xs -> pure (indexKeys v xs)
  C.ColUInt64 v xs -> pure (indexKeys v xs)
  _ -> Left "Parquet.Variant.Arrow: dictionary keys must be integers"


indexKeys :: (Storable a, Integral a) => Maybe C.Validity -> S.Vector a -> IndexReader
indexKeys validity values = IndexReader $ \index ->
  if C.isValidAt validity (I# index)
    then case fromIntegral (S.unsafeIndex values (I# index)) of I# key -> (# key | #)
    else (# | () #)
{-# INLINE indexKeys #-}


variableBytes :: (Storable a, Integral a) => Maybe C.Validity -> S.Vector a -> ByteString -> BytesReader
variableBytes validity offsets bytes =
  let array = C.BytesArray validity offsets bytes
  in BytesReader $ \index -> case C.unsafeBytesAt array (I# index) of
       Nothing -> (# | () #)
       Just value -> (# value | #)
{-# INLINE variableBytes #-}


-- Names carry Variant semantics even when the in-memory column order differs
-- from its field. Bind once before structural validation or positional row
-- lowering. Normal IPC columns take the linear already-ordered fast path;
-- a permuted wide object builds one name map. No value buffer is copied.
alignVariantColumns :: A.Field -> C.ColumnArray -> Either String C.ColumnArray
alignVariantColumns = go False
  where
    -- The common ordered case aliases the complete original column and
    -- never revalidates offsets or creates replacement container vectors.
    needed parent field column =
      let active = parent || Schema.isVariantField field
          fields = A.fieldChildren field
          one value = case V.toList fields of [item] -> needed active item value; _ -> False
      in case column of
           C.ColStruct _ _ columns
             | V.length columns == V.length fields ->
                 (active && V.or (V.zipWith (\item (name, _) -> A.fieldName item /= name) fields columns))
                   || V.or (V.zipWith (\item (_, value) -> needed active item value) fields columns)
           C.ColList _ _ values -> one values
           C.ColLargeList _ _ values -> one values
           C.ColListView _ _ _ values -> one values
           C.ColMap _ _ keys values -> case V.toList fields of
             [entries] -> case V.toList (A.fieldChildren entries) of
               [key, value] -> needed active key keys || needed active value values
               _ -> False
             _ -> False
           _ -> False
    go parent field column | not (needed parent field column) = Right column
    go parent field column =
      let active = parent || Schema.isVariantField field
          fields = A.fieldChildren field
          child value = case V.toList fields of
            [item] -> go active item value
            _ -> Left "Parquet.Variant: list storage requires one child field"
      in case column of
           C.ColStruct count validity columns -> do
             if V.length columns /= V.length fields then Left "Parquet.Variant: struct child count mismatch" else Right ()
             ordered <-
               if not active || V.and (V.zipWith (\item (name, _) -> A.fieldName item == name) fields columns)
                 then Right columns
                 else do
                   let named = M.fromList (V.toList columns)
                   if M.size named /= V.length columns then Left "Parquet.Variant: duplicate named storage child" else Right ()
                   V.mapM (\item -> maybe (Left "Parquet.Variant: missing named storage child") (\value -> Right (A.fieldName item, value)) (M.lookup (A.fieldName item) named)) fields
             aligned <- V.zipWithM (\item (name, value) -> (,) name <$> go active item value) fields ordered
             C.mkStruct count validity aligned
           C.ColList validity offsets values -> child values >>= C.mkList validity offsets
           C.ColLargeList validity offsets values -> child values >>= C.mkLargeList validity offsets
           C.ColListView validity offsets sizes values -> child values >>= C.mkListView validity offsets sizes
           C.ColMap validity offsets keys values -> case V.toList fields of
             [entries] -> case V.toList (A.fieldChildren entries) of
               [key, value] -> do
                 alignedKeys <- go active key keys
                 alignedValues <- go active value values
                 C.mkMap validity offsets alignedKeys alignedValues
               _ -> Left "Parquet.Variant: map entries require key and value fields"
             _ -> Left "Parquet.Variant: map storage requires entries"
           _ -> Right column