dataframe-parquet-1.2.0.0: src/DataFrame/IO/Parquet/Utils.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
module DataFrame.IO.Parquet.Utils (
ColumnDescription (..),
generateColumnDescriptions,
getColumnNames,
foldNonNullable,
foldNonNullableUnboxed,
foldNullable,
foldNullableUnboxed,
foldRepeated,
foldRepeatedUnboxed,
) where
import Control.Monad.IO.Class (MonadIO (..))
import Data.Int (Int32)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Vector as VB
import qualified Data.Vector.Mutable as VBM
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM
import Data.Word (Word8)
import DataFrame.IO.Parquet.Levels (
stitchList,
stitchList2,
stitchList3,
)
import DataFrame.IO.Parquet.Thrift (
ConvertedType (..),
FieldRepetitionType (..),
LogicalType (..),
SchemaElement (..),
ThriftType,
unField,
)
import DataFrame.Internal.Column (
Column (..),
Columnable,
buildBitmapFromValid,
fromList,
)
{- | A left-fold driver over a column's per-page triples
@(values, def-levels, rep-levels)@, as produced by
'DataFrame.IO.Parquet.Page.foldColumnPagesM'. Generic in the accumulator so a
single driver serves every fold strategy below.
-}
type PageFold m v a =
forall acc.
(acc -> (v a, VU.Vector Int, VU.Vector Int) -> m acc) -> acc -> m acc
data ColumnDescription = ColumnDescription
{ colElementType :: !(Maybe ThriftType)
, maxDefinitionLevel :: !Int32
, maxRepetitionLevel :: !Int32
, colLogicalType :: !(Maybe LogicalType)
, colConvertedType :: !(Maybe ConvertedType)
, typeLength :: !(Maybe Int32)
}
deriving (Show, Eq)
levelContribution :: Maybe FieldRepetitionType -> (Int, Int)
levelContribution = \case
Just (REPEATED _) -> (1, 1)
Just (OPTIONAL _) -> (1, 0)
_ -> (0, 0) -- REQUIRED or absent
data SchemaTree = SchemaTree SchemaElement [SchemaTree]
buildTree :: [SchemaElement] -> (SchemaTree, [SchemaElement])
buildTree [] = error "buildTree: schema ended unexpectedly"
buildTree (se : rest) =
let n = fromIntegral $ fromMaybe 0 (unField (num_children se)) :: Int
(children, rest') = buildChildren n rest
in (SchemaTree se children, rest')
-- | Build a forest of sibling trees from a flat depth-first element list.
buildForest :: [SchemaElement] -> ([SchemaTree], [SchemaElement])
buildForest [] = ([], [])
buildForest xs =
let (tree, rest') = buildTree xs
(siblings, rest'') = buildForest rest'
in (tree : siblings, rest'')
-- | Build exactly @n@ child trees, each consuming only its own subtree.
buildChildren :: Int -> [SchemaElement] -> ([SchemaTree], [SchemaElement])
buildChildren 0 xs = ([], xs)
buildChildren n xs =
let (child, rest') = buildTree xs
(siblings, rest'') = buildChildren (n - 1) rest'
in (child : siblings, rest'')
collectLeaves :: Int -> Int -> SchemaTree -> [ColumnDescription]
collectLeaves defAcc repAcc (SchemaTree se children) =
let (dInc, rInc) = levelContribution (unField (repetition_type se))
defLevel = defAcc + dInc
repLevel = repAcc + rInc
in case children of
[] ->
-- leaf: emit a description
let pType = unField (schematype se)
in [ ColumnDescription
pType
(fromIntegral defLevel)
(fromIntegral repLevel)
(unField (logicalType se))
(unField (converted_type se))
(unField (type_length se))
]
_ ->
-- internal node: recurse into children
concatMap (collectLeaves defLevel repLevel) children
generateColumnDescriptions :: [SchemaElement] -> [ColumnDescription]
generateColumnDescriptions [] = []
generateColumnDescriptions (_ : rest) =
-- drop schema root
let (forest, _) = buildForest rest
in concatMap (collectLeaves 0 0) forest
getColumnNames :: [SchemaElement] -> [Text]
getColumnNames [] = []
getColumnNames schemaElements =
let (forest, _) = buildForest schemaElements
in go forest [] False
where
isRepeated se = case unField (repetition_type se) of
Just (REPEATED _) -> True
_ -> False
go [] _ _ = []
go (SchemaTree se children : rest) path skipThis =
case children of
-- Leaf node
[] ->
let newPath = if skipThis then path else path ++ [unField (name se)]
fullName = T.intercalate "." newPath
in fullName : go rest path skipThis
-- REPEATED intermediate: skip this name; skip single child too
_
| isRepeated se ->
let skipChildren = length children == 1
childLeaves = go children path skipChildren
in childLeaves ++ go rest path skipThis
-- Name-skipped intermediate: recurse with skip cleared
_
| skipThis ->
let childLeaves = go children path False
in childLeaves ++ go rest path skipThis
-- Normal intermediate: add name to path, recurse
_ ->
let subPath = path ++ [unField (name se)]
childLeaves = go children subPath False
in childLeaves ++ go rest path skipThis
{- | Fold a column's value pages into a non-nullable 'Column'.
Pre-allocates a mutable vector of @totalRows@ and fills it page-by-page via a
single streaming left fold ('PageFold'), avoiding any intermediate list or
concatenation allocation. Only one page's values are live at a time.
-}
{-# INLINEABLE foldNonNullable #-}
foldNonNullable ::
forall m a.
(MonadIO m, Columnable a) =>
Int ->
PageFold m VB.Vector a ->
m Column
foldNonNullable totalRows runPages = do
mv <- liftIO $ VBM.unsafeNew totalRows
_ <-
runPages
( \off (chunk, _, _) -> liftIO $ do
let n = VB.length chunk
VB.copy (VBM.unsafeSlice off n mv) chunk
return (off + n)
)
(0 :: Int)
v <- liftIO $ VB.unsafeFreeze mv
return (BoxedColumn Nothing v)
{-# INLINEABLE foldNonNullableUnboxed #-}
foldNonNullableUnboxed ::
forall m a.
(MonadIO m, Columnable a, VU.Unbox a) =>
Int ->
PageFold m VU.Vector a ->
m Column
foldNonNullableUnboxed totalRows runPages = do
mv <- liftIO $ VUM.unsafeNew totalRows
_ <-
runPages
( \off (chunk, _, _) -> liftIO $ do
let n = VU.length chunk
go i
| i >= n = return ()
| otherwise = do
VUM.unsafeWrite
mv
(off + i)
(VU.unsafeIndex chunk i)
go (i + 1)
go 0
return (off + n)
)
(0 :: Int)
dat <- liftIO $ VU.unsafeFreeze mv
return (UnboxedColumn Nothing dat)
{- | Fold a column's (values, def-levels) pages into a nullable 'Column'.
Pre-allocates the output buffer and a valid-mask vector of @totalRows@, then
scatters values inline during a single streaming left fold ('PageFold').
A 'hasNull' flag is accumulated during the scatter so the
'buildBitmapFromValid' call is skipped entirely when all values are present.
-}
{-# INLINEABLE foldNullable #-}
foldNullable ::
forall m a.
(MonadIO m, Columnable a) =>
Int ->
Int ->
PageFold m VB.Vector a ->
m Column
foldNullable maxDef totalRows runPages = do
-- null slots hold an error thunk, guarded by bitmap.
--
-- IMPORTANT: 'VBM.unsafeWrite' for boxed vectors stores a *pointer* to
-- the value without evaluating it, so unsupported-encoding error thunks
-- would be silently swallowed into the column data and only fire lazily
-- when user code reads a cell. The '!v' bang pattern forces each value
-- to WHNF before the write, surfacing decoder errors immediately.
mvDat <-
liftIO $ VBM.replicate totalRows (error "parquet: null slot accessed")
mvValid <- liftIO (VUM.new totalRows :: IO (VUM.IOVector Word8))
(_, hasNull) <-
runPages
( \(rowOff, anyNull) (vals, defs, _) -> liftIO $ do
let nDefs = VU.length defs
go i j acc
| i >= nDefs = return acc
| VU.unsafeIndex defs i == maxDef = do
let !v = VB.unsafeIndex vals j
VBM.unsafeWrite mvDat (rowOff + i) v
VUM.unsafeWrite mvValid (rowOff + i) 1
go (i + 1) (j + 1) acc
| otherwise = go (i + 1) j True
newNull <- go 0 0 False
return (rowOff + nDefs, anyNull || newNull)
)
(0 :: Int, False)
dat <- liftIO $ VB.unsafeFreeze mvDat
maybeBm <-
if hasNull
then do
validV <- liftIO $ VU.unsafeFreeze mvValid
return (Just (buildBitmapFromValid validV))
else return Nothing
return (BoxedColumn maybeBm dat)
{-# INLINEABLE foldNullableUnboxed #-}
foldNullableUnboxed ::
forall m a.
(MonadIO m, Columnable a, VU.Unbox a) =>
Int ->
Int ->
PageFold m VU.Vector a ->
m Column
foldNullableUnboxed maxDef totalRows runPages = do
-- zero-init means null slots silently hold 0, guarded by bitmap.
mvDat <- liftIO $ VUM.new totalRows
mvValid <- liftIO (VUM.new totalRows :: IO (VUM.IOVector Word8))
(_, hasNull) <-
runPages
( \(rowOff, anyNull) (vals, defs, _) -> liftIO $ do
let !nDefs = VU.length defs
go !i !j !acc
| i >= nDefs = pure acc
| VU.unsafeIndex defs i == maxDef = do
VUM.unsafeWrite mvDat (rowOff + i) (VU.unsafeIndex vals j)
VUM.unsafeWrite mvValid (rowOff + i) 1
go (i + 1) (j + 1) acc
| otherwise = go (i + 1) j True
!newNull <- go 0 0 False
return (rowOff + nDefs, anyNull || newNull)
)
(0 :: Int, False)
dat <- liftIO $ VU.unsafeFreeze mvDat
maybeBm <-
if hasNull
then do
validV <- liftIO $ VU.unsafeFreeze mvValid
return (Just (buildBitmapFromValid validV))
else return Nothing
return (UnboxedColumn maybeBm dat)
{- | Fold a stream of (values, def-levels, rep-levels) triples into a
repeated (list) 'Column' using Dremel-style level stitching.
The stitching function is selected by @maxRep@:
* @maxRep == 1@ → 'stitchList' → @[Maybe [Maybe a]]@
* @maxRep == 2@ → 'stitchList2' → @[Maybe [Maybe [Maybe a]]]@
* @maxRep >= 3@ → 'stitchList3' → @[Maybe [Maybe [Maybe [Maybe a]]]]@
Threshold formula: @defT_r = maxDef - 2 * (maxRep - r)@.
-}
{-# INLINEABLE foldRepeated #-}
foldRepeated ::
forall m a.
( MonadIO m
, Columnable a
, Columnable (Maybe [Maybe a])
, Columnable (Maybe [Maybe [Maybe a]])
, Columnable (Maybe [Maybe [Maybe [Maybe a]]])
) =>
Int ->
Int ->
PageFold m VB.Vector a ->
m Column
foldRepeated maxRep maxDef runPages = do
chunks <- collectPages runPages
let allVals = VB.concat [vs | (vs, _, _) <- chunks]
allDefs = VU.concat [ds | (_, ds, _) <- chunks]
allReps = VU.concat [rs | (_, _, rs) <- chunks]
return $ case maxRep of
2 -> fromList (stitchList2 (maxDef - 2) maxDef allReps allDefs allVals)
3 ->
fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef allReps allDefs allVals)
_ -> fromList (stitchList maxDef allReps allDefs allVals)
{-# INLINEABLE foldRepeatedUnboxed #-}
foldRepeatedUnboxed ::
forall m a.
( MonadIO m
, Columnable a
, VU.Unbox a
, Columnable (Maybe [Maybe a])
, Columnable (Maybe [Maybe [Maybe a]])
, Columnable (Maybe [Maybe [Maybe [Maybe a]]])
) =>
Int ->
Int ->
PageFold m VU.Vector a ->
m Column
foldRepeatedUnboxed maxRep maxDef runPages = do
chunks <- collectPages runPages
let allVals = VB.convert $ VU.concat [vs | (vs, _, _) <- chunks]
allDefs = VU.concat [ds | (_, ds, _) <- chunks]
allReps = VU.concat [rs | (_, _, rs) <- chunks]
return $ case maxRep of
2 -> fromList (stitchList2 (maxDef - 2) maxDef allReps allDefs allVals)
3 ->
fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef allReps allDefs allVals)
_ -> fromList (stitchList maxDef allReps allDefs allVals)
{- | Collect all of a column's page triples into a list, in page order. Used by
the repeated/list folds, where Dremel level-stitching needs the full
concatenated rep/def/value arrays at once (so streaming gives no benefit here).
-}
collectPages ::
(Monad m) =>
PageFold m v a ->
m [(v a, VU.Vector Int, VU.Vector Int)]
collectPages runPages = reverse <$> runPages (\acc triple -> return (triple : acc)) []