packages feed

wireform-arrow-0.1.0.0: src/Arrow/Column/Builder.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}

{- | Streaming column builders in 'ST' or 'IO' ('PrimMonad').

Each builder appends rows into growable, pinned, 64-byte aligned
buffers (amortised doubling) and freezes into a 'ColumnArray' whose
buffers are those allocations: no per-row heap objects, no final
copy. The validity bitmap is only allocated once the first null is
appended, so a builder that never sees a null freezes with no
validity.

Values are valid by construction ('appendText' copies the bytes of a
'Text', which are UTF-8), so freezing runs no validation.

'freezeBuilder' hands the buffers to the column and resets the
builder to empty; appending afterwards starts a new column in fresh
memory and never touches the frozen one.

Appending more than 2^31 - 1 bytes to a 32-bit offset builder
('newUtf8Builder', 'newBinaryBuilder') is an error; use the large
variants for such data.
-}
module Arrow.Column.Builder (
  -- * Common interface
  ColumnBuilder (..),

  -- * Fixed width
  PrimBuilder,
  newPrimBuilder,
  appendPrim,
  appendPrimMaybe,

  -- * Booleans
  BoolBuilder,
  newBoolBuilder,
  appendBool,
  appendBoolMaybe,

  -- * Strings and bytes
  Utf8Builder,
  newUtf8Builder,
  newLargeUtf8Builder,
  appendText,
  appendTextMaybe,
  BinaryBuilder,
  newBinaryBuilder,
  newLargeBinaryBuilder,
  appendBytes,
  appendBytesMaybe,
) where

import Arrow.Column.Internal
import Control.Monad.Primitive (PrimMonad, PrimState, unsafeIOToPrim)
import Data.Bits (complement, unsafeShiftL, unsafeShiftR, (.&.), (.|.))
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Internal qualified as BSI
import Data.ByteString.Unsafe qualified as BSU
import Data.Int (Int32, Int64)
import Data.Primitive.MutVar (MutVar, newMutVar, readMutVar, writeMutVar)
import Data.Primitive.PrimArray (MutablePrimArray, newPrimArray, readPrimArray, setPrimArray, writePrimArray)
import Data.Text (Text)
import Data.Text.Foreign qualified as TF
import Data.Vector.Storable qualified as VS
import Data.Word (Word8)
import Foreign.ForeignPtr (ForeignPtr, castForeignPtr)
import GHC.ForeignPtr (unsafeWithForeignPtr)
import Foreign.Marshal.Utils (copyBytes, fillBytes)
import Foreign.Ptr (Ptr, castPtr, plusPtr)
import Foreign.Storable (Storable (..))


-- | Operations every builder supports.
class ColumnBuilder b where
  -- | Append a null row.
  appendNull :: PrimMonad m => b (PrimState m) -> m ()

  -- | Rows appended so far.
  builderLength :: PrimMonad m => b (PrimState m) -> m Int

  -- | Freeze into a column and reset the builder to empty.
  freezeBuilder :: PrimMonad m => b (PrimState m) -> m ColumnArray


-- ============================================================
-- Growable byte buffers
-- ============================================================

-- | A growable pinned byte buffer: the memory and its capacity in bytes.
data Grow s = Grow !(MutVar s (ForeignPtr Word8)) !(MutablePrimArray s Int)


{-# INLINABLE newGrow #-}
newGrow :: PrimMonad m => Int -> m (Grow (PrimState m))
newGrow cap0 = do
  let !cap = max 0 cap0
  fp <- unsafeIOToPrim $ do
    fp <- mallocAligned cap
    unsafeWithForeignPtr fp $ \p -> fillBytes p 0 cap
    pure fp
  ref <- newMutVar fp
  meta <- newPrimArray 1
  writePrimArray meta 0 cap
  pure (Grow ref meta)


-- | Make room for @need@ bytes, keeping the first @used@ bytes and zeroing the rest.
ensureGrow :: PrimMonad m => Grow (PrimState m) -> Int -> Int -> m ()
ensureGrow (Grow ref meta) !used !need = do
  cap <- readPrimArray meta 0
  if need <= cap
    then pure ()
    else do
      let !cap' = max need (max 64 (cap * 2))
      old <- readMutVar ref
      new <- unsafeIOToPrim $ do
        fp <- mallocAligned cap'
        unsafeWithForeignPtr fp $ \dst -> unsafeWithForeignPtr old $ \src -> do
          copyBytes dst src used
          fillBytes (dst `plusPtr` used) 0 (cap' - used)
        pure fp
      writeMutVar ref new
      writePrimArray meta 0 cap'
{-# INLINE ensureGrow #-}


-- | Hand the memory out and replace it with an empty allocation.
{-# INLINABLE takeGrow #-}
takeGrow :: PrimMonad m => Grow (PrimState m) -> m (ForeignPtr Word8)
takeGrow (Grow ref meta) = do
  fp <- readMutVar ref
  fresh <- unsafeIOToPrim (mallocAligned 0)
  writeMutVar ref fresh
  writePrimArray meta 0 0
  pure fp


-- | Validity under construction: the bits, plus [null count, materialised flag].
data ValidBuf s = ValidBuf !(Grow s) !(MutablePrimArray s Int)


{-# INLINABLE newValidBuf #-}
newValidBuf :: PrimMonad m => m (ValidBuf (PrimState m))
newValidBuf = do
  g <- newGrow 0
  meta <- newPrimArray 2
  setPrimArray meta 0 2 0
  pure (ValidBuf g meta)


writeBit :: PrimMonad m => Grow (PrimState m) -> Int -> Bool -> m ()
writeBit g@(Grow ref _) !i !b = do
  let !byte = i `unsafeShiftR` 3
  ensureGrow g byte (byte + 1)
  fp <- readMutVar ref
  unsafeIOToPrim $ unsafeWithForeignPtr fp $ \p -> do
    x <- peekByteOff p byte :: IO Word8
    let !m = 1 `unsafeShiftL` (i .&. 7)
    pokeByteOff p byte (if b then x .|. m else x .&. complement m)
{-# INLINE writeBit #-}


-- | Row @i@ is valid.
validAt :: PrimMonad m => ValidBuf (PrimState m) -> Int -> m ()
validAt (ValidBuf g meta) !i = do
  materialised <- readPrimArray meta 1
  if materialised == 0 then pure () else writeBit g i True
{-# INLINE validAt #-}


-- | Row @i@ is null; rows before it that were appended valid get their bits now.
{-# INLINE nullAt #-}
nullAt :: PrimMonad m => ValidBuf (PrimState m) -> Int -> m ()
nullAt (ValidBuf g@(Grow ref _) meta) !i = do
  materialised <- readPrimArray meta 1
  if materialised /= 0
    then pure ()
    else do
      let !full = i `unsafeShiftR` 3
      ensureGrow g 0 (full + 1)
      fp <- readMutVar ref
      unsafeIOToPrim $ unsafeWithForeignPtr fp $ \p -> do
        fillBytes p 0xFF full
        pokeByteOff p full ((1 `unsafeShiftL` (i .&. 7)) - 1 :: Word8)
      writePrimArray meta 1 1
  writeBit g i False
  n <- readPrimArray meta 0
  writePrimArray meta 0 (n + 1)


{-# INLINABLE freezeValid #-}
freezeValid :: PrimMonad m => ValidBuf (PrimState m) -> Int -> m (Maybe Validity)
freezeValid (ValidBuf g meta) !rows = do
  nulls <- readPrimArray meta 0
  fp <- takeGrow g
  setPrimArray meta 0 2 0
  pure $
    if nulls == 0
      then Nothing
      else Just (Validity (Bitmap (BSI.BS fp ((rows + 7) `unsafeShiftR` 3)) 0 rows) nulls)


-- ============================================================
-- Fixed width
-- ============================================================

-- | Builder for a fixed-width column of tag @PrimType a@.
data PrimBuilder a s = PrimBuilder !(PrimType a) !(Grow s) !(ValidBuf s) !(MutablePrimArray s Int)


-- | A builder with room for the given number of rows before it first grows.
{-# INLINABLE newPrimBuilder #-}
newPrimBuilder :: PrimMonad m => PrimType a -> Int -> m (PrimBuilder a (PrimState m))
newPrimBuilder t hint = do
  g <- newGrow (max 0 hint * primWidth t)
  vb <- newValidBuf
  len <- newPrimArray 1
  writePrimArray len 0 0
  pure (PrimBuilder t g vb len)


appendPrim :: (PrimMonad m, Storable a) => PrimBuilder a (PrimState m) -> a -> m ()
appendPrim (PrimBuilder _ g@(Grow ref _) vb lenRef) x = do
  n <- readPrimArray lenRef 0
  let !w = sizeOf x
  ensureGrow g (n * w) ((n + 1) * w)
  fp <- readMutVar ref
  unsafeIOToPrim $ unsafeWithForeignPtr fp $ \p -> pokeElemOff (castPtr p) n x
  validAt vb n
  writePrimArray lenRef 0 (n + 1)
{-# INLINE appendPrim #-}


appendPrimMaybe :: (PrimMonad m, Storable a) => PrimBuilder a (PrimState m) -> Maybe a -> m ()
appendPrimMaybe b = maybe (appendNull b) (appendPrim b)
{-# INLINE appendPrimMaybe #-}


instance ColumnBuilder (PrimBuilder a) where
  appendNull (PrimBuilder t g vb lenRef) = do
    n <- readPrimArray lenRef 0
    let !w = primWidth t
    ensureGrow g (n * w) ((n + 1) * w) -- new bytes are zeroed
    nullAt vb n
    writePrimArray lenRef 0 (n + 1)
  builderLength (PrimBuilder _ _ _ lenRef) = readPrimArray lenRef 0
  {-# INLINE appendNull #-}
  {-# INLINE builderLength #-}
  {-# INLINABLE freezeBuilder #-}
  freezeBuilder (PrimBuilder t g vb lenRef) = withPrim t $ do
    n <- readPrimArray lenRef 0
    fp <- takeGrow g
    v <- freezeValid vb n
    writePrimArray lenRef 0 0
    pure (ColPrim t v (VS.unsafeFromForeignPtr0 (castForeignPtr fp) n))


-- ============================================================
-- Booleans
-- ============================================================

data BoolBuilder s = BoolBuilder !(Grow s) !(ValidBuf s) !(MutablePrimArray s Int)


{-# INLINABLE newBoolBuilder #-}
newBoolBuilder :: PrimMonad m => Int -> m (BoolBuilder (PrimState m))
newBoolBuilder hint = do
  g <- newGrow ((max 0 hint + 7) `unsafeShiftR` 3)
  vb <- newValidBuf
  len <- newPrimArray 1
  writePrimArray len 0 0
  pure (BoolBuilder g vb len)


appendBool :: PrimMonad m => BoolBuilder (PrimState m) -> Bool -> m ()
appendBool (BoolBuilder g vb lenRef) x = do
  n <- readPrimArray lenRef 0
  writeBit g n x
  validAt vb n
  writePrimArray lenRef 0 (n + 1)
{-# INLINE appendBool #-}


appendBoolMaybe :: PrimMonad m => BoolBuilder (PrimState m) -> Maybe Bool -> m ()
appendBoolMaybe b = maybe (appendNull b) (appendBool b)
{-# INLINE appendBoolMaybe #-}


instance ColumnBuilder BoolBuilder where
  appendNull (BoolBuilder g vb lenRef) = do
    n <- readPrimArray lenRef 0
    writeBit g n False
    nullAt vb n
    writePrimArray lenRef 0 (n + 1)
  builderLength (BoolBuilder _ _ lenRef) = readPrimArray lenRef 0
  {-# INLINE appendNull #-}
  {-# INLINE builderLength #-}
  {-# INLINABLE freezeBuilder #-}
  freezeBuilder (BoolBuilder g vb lenRef) = do
    n <- readPrimArray lenRef 0
    -- A builder with no rows may never have allocated a byte.
    let !nbytes = (n + 7) `unsafeShiftR` 3
    ensureGrow g nbytes nbytes
    fp <- takeGrow g
    v <- freezeValid vb n
    writePrimArray lenRef 0 0
    pure (ColBool v (Bitmap (BSI.BS fp nbytes) 0 n))


-- ============================================================
-- Var-length
-- ============================================================

{- | Offsets, data, validity, [rows, data length], and the constructor
for the frozen column.
-}
data VarBuilder o s
  = VarBuilder
      !(Grow s)
      !(Grow s)
      !(ValidBuf s)
      !(MutablePrimArray s Int)
      !(Maybe Validity -> VS.Vector o -> ByteString -> ColumnArray)


{-# INLINABLE newVarBuilder #-}
newVarBuilder
  :: forall o m
   . (PrimMonad m, Offset o)
  => (Maybe Validity -> VS.Vector o -> ByteString -> ColumnArray)
  -> Int
  -> m (VarBuilder o (PrimState m))
newVarBuilder mk hint = do
  let !w = sizeOf (0 :: o)
  offs <- newGrow ((max 0 hint + 1) * w)
  ensureGrow offs 0 w -- offset 0, zeroed
  dat <- newGrow (max 0 hint * 8)
  vb <- newValidBuf
  meta <- newPrimArray 2
  setPrimArray meta 0 2 0
  pure (VarBuilder offs dat vb meta mk)


-- | Append one row of @len@ bytes copied by the action.
appendVar :: forall o m. (PrimMonad m, Offset o) => VarBuilder o (PrimState m) -> Int -> (Ptr Word8 -> IO ()) -> m ()
appendVar (VarBuilder offs@(Grow offRef _) dat@(Grow datRef _) vb meta _) !len copy = do
  n <- readPrimArray meta 0
  used <- readPrimArray meta 1
  let !end = used + len
      !w = sizeOf (0 :: o)
  if toInteger end > toInteger (maxBound :: o)
    then errorWithoutStackTrace "Arrow.Column.Builder: var-length data exceeds the offset width (use a large builder)"
    else pure ()
  ensureGrow dat used end
  fp <- readMutVar datRef
  unsafeIOToPrim $ unsafeWithForeignPtr fp $ \p -> copy (p `plusPtr` used)
  ensureGrow offs ((n + 1) * w) ((n + 2) * w)
  ofp <- readMutVar offRef
  unsafeIOToPrim $ unsafeWithForeignPtr ofp $ \p -> pokeElemOff (castPtr p) (n + 1) (fromIntegral end :: o)
  validAt vb n
  writePrimArray meta 0 (n + 1)
  writePrimArray meta 1 end
{-# INLINE appendVar #-}


{-# INLINE appendVarNull #-}
appendVarNull :: forall o m. (PrimMonad m, Offset o) => VarBuilder o (PrimState m) -> m ()
appendVarNull (VarBuilder offs@(Grow offRef _) _ vb meta _) = do
  n <- readPrimArray meta 0
  used <- readPrimArray meta 1
  let !w = sizeOf (0 :: o)
  ensureGrow offs ((n + 1) * w) ((n + 2) * w)
  ofp <- readMutVar offRef
  unsafeIOToPrim $ unsafeWithForeignPtr ofp $ \p -> pokeElemOff (castPtr p) (n + 1) (fromIntegral used :: o)
  nullAt vb n
  writePrimArray meta 0 (n + 1)


{-# INLINABLE freezeVar #-}
freezeVar :: forall o m. (PrimMonad m, Offset o) => VarBuilder o (PrimState m) -> m ColumnArray
freezeVar (VarBuilder offs dat vb meta mk) = do
  n <- readPrimArray meta 0
  used <- readPrimArray meta 1
  ofp <- takeGrow offs
  dfp <- takeGrow dat
  v <- freezeValid vb n
  setPrimArray meta 0 2 0
  -- Restart with offset 0 in place for the next column.
  ensureGrow offs 0 (sizeOf (0 :: o))
  pure (mk v (VS.unsafeFromForeignPtr0 (castForeignPtr ofp) (n + 1)) (BSI.BS dfp used))


-- | Builder for a utf8 column (32-bit offsets) or large utf8 column (64-bit).
newtype Utf8Builder o s = Utf8Builder (VarBuilder o s)


-- | Builder for a binary column (32-bit offsets) or large binary column (64-bit).
newtype BinaryBuilder o s = BinaryBuilder (VarBuilder o s)


newUtf8Builder :: PrimMonad m => Int -> m (Utf8Builder Int32 (PrimState m))
newUtf8Builder hint = Utf8Builder <$> newVarBuilder ColUtf8 hint


newLargeUtf8Builder :: PrimMonad m => Int -> m (Utf8Builder Int64 (PrimState m))
newLargeUtf8Builder hint = Utf8Builder <$> newVarBuilder ColLargeUtf8 hint


newBinaryBuilder :: PrimMonad m => Int -> m (BinaryBuilder Int32 (PrimState m))
newBinaryBuilder hint = BinaryBuilder <$> newVarBuilder ColBinary hint


newLargeBinaryBuilder :: PrimMonad m => Int -> m (BinaryBuilder Int64 (PrimState m))
newLargeBinaryBuilder hint = BinaryBuilder <$> newVarBuilder ColLargeBinary hint


-- | Append the UTF-8 bytes of a 'Text' (one copy, no validation needed).
appendText :: (PrimMonad m, Offset o) => Utf8Builder o (PrimState m) -> Text -> m ()
appendText (Utf8Builder vb) t = appendVar vb (TF.lengthWord8 t) (TF.unsafeCopyToPtr t)
{-# INLINE appendText #-}


appendTextMaybe :: (PrimMonad m, Offset o) => Utf8Builder o (PrimState m) -> Maybe Text -> m ()
appendTextMaybe b = maybe (appendNull b) (appendText b)
{-# INLINE appendTextMaybe #-}


appendBytes :: (PrimMonad m, Offset o) => BinaryBuilder o (PrimState m) -> ByteString -> m ()
appendBytes (BinaryBuilder vb) bs =
  appendVar vb (BS.length bs) $ \dst -> BSU.unsafeUseAsCStringLen bs $ \(src, l) -> copyBytes dst (castPtr src) l
{-# INLINE appendBytes #-}


appendBytesMaybe :: (PrimMonad m, Offset o) => BinaryBuilder o (PrimState m) -> Maybe ByteString -> m ()
appendBytesMaybe b = maybe (appendNull b) (appendBytes b)
{-# INLINE appendBytesMaybe #-}


instance Offset o => ColumnBuilder (Utf8Builder o) where
  appendNull (Utf8Builder vb) = appendVarNull vb
  {-# INLINE appendNull #-}
  {-# INLINE builderLength #-}
  {-# INLINE freezeBuilder #-}
  builderLength (Utf8Builder (VarBuilder _ _ _ meta _)) = readPrimArray meta 0
  freezeBuilder (Utf8Builder vb) = freezeVar vb


instance Offset o => ColumnBuilder (BinaryBuilder o) where
  appendNull (BinaryBuilder vb) = appendVarNull vb
  {-# INLINE appendNull #-}
  {-# INLINE builderLength #-}
  {-# INLINE freezeBuilder #-}
  builderLength (BinaryBuilder (VarBuilder _ _ _ meta _)) = readPrimArray meta 0
  freezeBuilder (BinaryBuilder vb) = freezeVar vb