wireform-arrow-0.1.0.0: test/Test/Arrow/Vector.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{- | Properties of "Arrow.Vector" against the boxed "Data.Vector" model.
For every element type the generic API ('VG.fromList', 'VG.generate',
slices at arbitrary offsets, 'VG.map', 'VG.filter', 'VG.concat',
'VG.thaw' / 'VG.freeze' / 'VG.modify', growth, 'VG.force', 'Eq', 'Ord')
must agree with the same operation on a list. Mutable operations run on
a slice of a larger vector so bitmaps sit at bit offsets that are not a
multiple of 8 and var-length rows share their store with neighbours.
The column conversions are checked against the logical row model of
"Test.Arrow.Gen" on generated columns, on the same columns after an IPC
round trip (decoded columns alias the input; one input lives on the
GHC heap and one in malloc memory, which takes the text copy path), and
on slices of both.
-}
module Test.Arrow.Vector (tests) where
import Arrow.Column
import Arrow.Stream (decodeArrowStream, defaultReadOptions, defaultWriteOptions, encodeArrowStream)
import Arrow.Types (ArrowType (..), DictionaryEncoding (..), Field (..), IntervalUnit (..), Precision (..), defaultField, defaultLeafField, defaultSchema)
import Arrow.Vector (Element, Vector, maybeValues)
import Control.Monad (forM_)
import Control.Monad.ST (ST, runST)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Internal qualified as BSI
import Data.Foldable (foldl')
import Data.Either (isLeft)
import Data.Int (Int32, Int64)
import Data.List (uncons)
import Data.Maybe (isNothing)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Data.Vector qualified as V
import Data.Vector.Generic qualified as VG
import Data.Vector.Generic.Mutable qualified as GM
import Foreign.ForeignPtr (newForeignPtr)
import Foreign.Marshal.Alloc (finalizerFree, mallocBytes)
import Foreign.Marshal.Utils (copyBytes)
import Foreign.Ptr (castPtr)
import Hedgehog
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Test.Arrow.Gen (Value (..), columnValues, genColumnFor)
tests :: IO Bool
tests =
checkParallel $
Group
"Arrow.Vector"
[ ("Int8", laws (Gen.int8 Range.linearBounded))
, ("Int16", laws (Gen.int16 Range.linearBounded))
, ("Int32", laws i32)
, ("Int64", laws i64)
, ("Int", laws (Gen.int Range.linearBounded))
, ("Word8", laws (Gen.word8 Range.linearBounded))
, ("Word16", laws (Gen.word16 Range.linearBounded))
, ("Word32", laws (Gen.word32 Range.linearBounded))
, ("Word64", laws (Gen.word64 Range.linearBounded))
, ("Word", laws (Gen.word Range.linearBounded))
, ("Float", laws (Gen.float (Range.linearFrac (-1e6) 1e6)))
, ("Double", laws (Gen.double (Range.linearFrac (-1e12) 1e12)))
, ("Float16", laws (Float16 <$> Gen.word16 Range.linearBounded))
, ("IntervalDayTime", laws (IntervalDayTime <$> i32 <*> i32))
, ("IntervalMonthDayNano", laws (IntervalMonthDayNano <$> i32 <*> i32 <*> i64))
, ("Decimal128", laws genDecimal128)
, ("Decimal256", laws (decimal256FromInteger <$> Gen.integral (Range.linearFrom 0 (negate (2 ^ (255 :: Int))) (2 ^ (255 :: Int) - 1))))
, ("Bool", laws Gen.bool)
, ("Text", laws genText)
, ("ByteString", laws genBytes)
, ("Maybe Int64", laws (maybeOf i64))
, ("Maybe Bool", laws (maybeOf Gen.bool))
, ("Maybe Decimal128", laws (maybeOf genDecimal128))
, ("Maybe Text", laws (maybeOf genText))
, ("Maybe ByteString", laws (maybeOf genBytes))
, ("Maybe (Maybe Int32)", laws (maybeOf (maybeOf i32)))
, ("Vector (Maybe Int32)", laws (genVec (maybeOf i32)))
, ("Maybe (Vector (Maybe Int32))", laws (maybeOf (genVec (maybeOf i32))))
, ("Vector Text", laws (genVec genText))
, ("Maybe (Vector (Maybe Text))", laws (maybeOf (genVec (maybeOf genText))))
, ("Vector (Vector Int64)", laws (genVec (genVec i64)))
, ("new initialises: Nothing, False, empty rows", prop_newInitialises)
, ("unsafeThaw of a converted text vector keeps rows apart", prop_unsafeThawConverted)
, ("conversions agree with the row model on generated, decoded and sliced columns", prop_conversions)
]
-- ============================================================
-- Generators
-- ============================================================
i32 :: Gen Int32
i32 = Gen.int32 Range.linearBounded
i64 :: Gen Int64
i64 = Gen.int64 Range.linearBounded
genDecimal128 :: Gen Decimal128
genDecimal128 = decimal128FromInteger <$> Gen.integral (Range.linearFrom 0 (negate (2 ^ (127 :: Int))) (2 ^ (127 :: Int) - 1))
maybeOf :: Gen a -> Gen (Maybe a)
maybeOf g = Gen.frequency [(1, pure Nothing), (3, Just <$> g)]
-- | Often a suffix of a longer text, so rows start inside their array.
genText :: Gen Text
genText = do
t <- Gen.text (Range.linear 0 10) Gen.unicode
k <- Gen.int (Range.linear 0 (T.length t))
pure (T.drop k t)
genBytes :: Gen ByteString
genBytes = do
b <- Gen.bytes (Range.linear 0 10)
k <- Gen.int (Range.linear 0 (BS.length b))
pure (BS.drop k b)
-- | Often a slice of a longer vector.
genVec :: Element a => Gen a -> Gen (Vector a)
genVec g = do
xs <- Gen.list (Range.linear 0 6) g
k <- Gen.int (Range.linear 0 (length xs))
pure (VG.drop k (VG.fromList xs))
-- ============================================================
-- Generic API against the list model
-- ============================================================
data Op a
= Write Int a
| Set Int Int a
| -- | destination, source, length (the ranges may overlap)
Move Int Int Int
| -- | copy from a separate mutable vector
CopyIn Int [a]
| -- | copy from an immutable vector
CopyFrom Int [a]
deriving stock (Show)
genOps :: Int -> Gen a -> Gen [Op a]
genOps n g
| n <= 0 = pure []
| otherwise = Gen.list (Range.linear 0 8) (Gen.choice [write, set, move, copy CopyIn, copy CopyFrom])
where
write = Write <$> Gen.int (Range.constant 0 (n - 1)) <*> g
set = do
s <- Gen.int (Range.constant 0 n)
l <- Gen.int (Range.constant 0 (n - s))
Set s l <$> g
move = do
l <- Gen.int (Range.constant 0 n)
Move <$> Gen.int (Range.constant 0 (n - l)) <*> Gen.int (Range.constant 0 (n - l)) <*> pure l
copy k = do
l <- Gen.int (Range.constant 0 n)
k <$> Gen.int (Range.constant 0 (n - l)) <*> Gen.list (Range.singleton l) g
applyOp :: Element a => VG.Mutable Vector s a -> Op a -> ST s ()
applyOp mv = \case
Write i x -> GM.write mv i x
Set s l x -> GM.set (GM.slice s l mv) x
Move d s l -> GM.move (GM.slice d l mv) (GM.slice s l mv)
CopyIn d xs -> do
src <- VG.thaw (VG.fromList xs)
GM.copy (GM.slice d (length xs) mv) src
CopyFrom d xs -> VG.copy (GM.slice d (length xs) mv) (VG.fromList xs)
modelOp :: [a] -> Op a -> [a]
modelOp xs = \case
Write i x -> splice i 1 [x]
Set s l x -> splice s l (replicate l x)
Move d s l -> splice d l (take l (drop s xs))
CopyIn d ys -> splice d (length ys) ys
CopyFrom d ys -> splice d (length ys) ys
where
splice at l ys = take at xs ++ ys ++ drop (at + l) xs
laws :: (Element a, Ord a, Show a) => Gen a -> Property
laws gen = withTests 150 . property $ do
xs <- forAll (Gen.list (Range.linear 0 70) gen)
ys <- forAll (Gen.list (Range.linear 0 20) gen)
y <- forAll gen
let n = length xs
v = VG.fromList xs
w = VG.fromList ys
bv = V.fromList xs
-- construction, indexing, conversion to and from boxed vectors
VG.toList v === xs
VG.length v === n
map (v VG.!) [0 .. n - 1] === xs
VG.convert v === bv
VG.convert bv === v
VG.generate n (bv V.!) === v
VG.toList (VG.unfoldr uncons xs `asTypeOf` v) === xs
VG.toList (VG.replicate 11 y `asTypeOf` v) === replicate 11 y
VG.toList (VG.snoc (VG.cons y v) y) === [y] ++ xs ++ [y]
-- slices at any offset, and slices of slices
i <- forAll (Gen.int (Range.linear 0 n))
k <- forAll (Gen.int (Range.linear 0 (n - i)))
let s = VG.slice i k v
sm = take k (drop i xs)
VG.toList s === sm
j <- forAll (Gen.int (Range.linear 0 k))
l <- forAll (Gen.int (Range.linear 0 (k - j)))
VG.toList (VG.slice j l s) === take l (drop j sm)
-- map, filter and concat over a slice
keep <- V.fromList <$> forAll (Gen.list (Range.singleton k) Gen.bool)
VG.toList (VG.imap (\ix x -> if keep V.! ix then x else y) s) === zipWith (\b x -> if b then x else y) (V.toList keep) sm
VG.toList (VG.ifilter (\ix _ -> keep V.! ix) s) === map snd (filter fst (zip (V.toList keep) sm))
VG.toList (VG.concat [s, w, v, s]) === sm ++ ys ++ xs ++ sm
VG.toList (v <> w) === xs ++ ys
-- force copies only the referenced rows and keeps them
VG.force s === s
VG.toList (VG.force s) === sm
-- mutable operations on a window of a larger vector
ops <- forAll (genOps k gen)
let modified = VG.modify (\mv -> forM_ ops (applyOp (GM.slice i k mv))) v
VG.toList modified === take i xs ++ foldl' modelOp sm ops ++ drop (i + k) xs
let thawed = runST $ do
mv <- VG.thaw s
forM_ ops (applyOp mv)
VG.freeze mv
VG.toList thawed === foldl' modelOp sm ops
VG.toList s === sm
-- growing keeps the rows and appends writable slots
extra <- forAll (Gen.int (Range.linear 0 20))
let grown = runST $ do
mv <- VG.thaw s
mv' <- GM.grow mv extra
forM_ [k .. k + extra - 1] $ \ix -> GM.write mv' ix y
VG.freeze mv'
VG.toList grown === sm ++ replicate extra y
-- Eq and Ord are the list's
(v == w) === (xs == ys)
compare v w === compare xs ys
compare s w === compare sm ys
(VG.fromList sm == s) === True
prop_newInitialises :: Property
prop_newInitialises = withTests 50 . property $ do
k <- forAll (Gen.int (Range.linear 0 70))
let fresh :: Element a => Vector a
fresh = runST (GM.new k >>= VG.freeze)
assert (VG.all isNothing (fresh :: Vector (Maybe Int64)))
assert (VG.all isNothing (fresh :: Vector (Maybe Text)))
assert (VG.all isNothing (fresh :: Vector (Maybe (Vector Int32))))
assert (VG.all not (fresh :: Vector Bool))
assert (VG.all T.null (fresh :: Vector Text))
assert (VG.all BS.null (fresh :: Vector ByteString))
assert (VG.all VG.null (fresh :: Vector (Vector Int64)))
{- | A utf8 column converts with starts and ends sharing one offsets
buffer; mutating the unsafely thawed vector must still treat rows as
independent.
-}
prop_unsafeThawConverted :: Property
prop_unsafeThawConverted = withTests 100 . property $ do
rows <- forAll (Gen.list (Range.linear 1 20) genText)
x <- forAll genText
i <- forAll (Gen.int (Range.linear 0 (length rows - 1)))
converted <- evalEither (toTextVector (fromTexts (V.fromList rows)))
let values = maybeValues converted
written = runST $ do
mv <- VG.unsafeThaw values
GM.write mv i x
VG.unsafeFreeze mv
VG.toList written === take i rows ++ [x] ++ drop (i + 1) rows
-- ============================================================
-- Conversions against the row model
-- ============================================================
-- | Fields whose columns some conversion accepts; lists hold int32.
genConvField :: Gen Field
genConvField = do
nullable <- Gen.bool
let leaf = defaultLeafField "c" nullable
list ty = defaultField "c" nullable ty (V.singleton (defaultLeafField "item" True (AInt 32 True)))
Gen.choice
[ leaf <$> Gen.element [AInt 8 True, AInt 16 False, AInt 32 True, AInt 64 True, AInt 64 False, AFloatingPoint Half, AFloatingPoint Single, AFloatingPoint DoublePrecision, ADecimal 38 4, ADecimal256 76 10, AInterval DayTime, AInterval MonthDayNano]
, leaf <$> Gen.element [ABool, AUtf8, ALargeUtf8, ABinary, ALargeBinary, AUtf8View, ABinaryView]
, leaf . AFixedSizeBinary <$> Gen.int (Range.linear 0 4)
, list <$> Gen.element [AList, ALargeList, AListView, ALargeListView]
, list . AFixedSizeList <$> Gen.int (Range.linear 0 3)
, do
ty <- Gen.element [AUtf8, ABinary, ALargeUtf8]
idx <- AInt <$> Gen.element [8, 16, 32, 64] <*> Gen.bool
pure (leaf ty) {fieldDictionary = Just (DictionaryEncoding 0 idx False)}
]
prop_conversions :: Property
prop_conversions = withTests 400 . property $ do
f <- forAll genConvField
n <- forAll (Gen.int (Range.linear 0 40))
c <- forAll (genColumnFor f n)
bytes <- evalEither (encodeArrowStream defaultWriteOptions (defaultSchema (V.singleton f)) [V.singleton c])
heap <- decodedFrom bytes
malloced <- evalIO (mallocCopy bytes) >>= decodedFrom
forM_ [c, heap, malloced] $ \col -> do
checkConversions f col
i <- forAll (Gen.int (Range.linear 0 n))
k <- forAll (Gen.int (Range.linear 0 (n - i)))
checkConversions f (sliceColumnArray i k col)
where
decodedFrom bs = do
(_, batches) <- evalEither (decodeArrowStream defaultReadOptions bs)
case batches of
[batch] | V.length batch == 1 -> pure (V.head batch)
_ -> annotate "expected one batch of one column" >> failure
-- | A copy of the bytes in malloc memory (not the GHC heap).
mallocCopy :: ByteString -> IO ByteString
mallocCopy bs = do
let len = BS.length bs
p <- mallocBytes (max 1 len)
BS.useAsCStringLen bs $ \(src, _) -> copyBytes p (castPtr src) len
fp <- newForeignPtr finalizerFree p
pure (BSI.BS fp len)
checkConversions :: Field -> ColumnArray -> PropertyT IO ()
checkConversions f c = do
model <- evalEither (columnValues c)
let rows = V.toList model
ty = fieldType f
textKind = ty `elem` [AUtf8, ALargeUtf8, AUtf8View]
bytesKind = textKind || ty `elem` [ABinary, ALargeBinary, ABinaryView] || isFixedBinary ty
listKind = ty `elem` [AList, ALargeList, AListView, ALargeListView] || isFixedList ty
case c of
ColPrim t _ _ -> withPrim t $ do
arr <- maybe (annotate "asPrim" >> failure) pure (asPrim t c)
let v = toMaybeVector arr
VG.length v === length rows
-- every row through the generic index path, rebuilt as a column
evalEither (columnValues (fromMaybes t (VG.convert v))) >>= (=== model)
evalEither (columnValues (fromMaybeVector t v)) >>= (=== model)
evalEither (columnValues (fromMaybeVector t (VG.force v))) >>= (=== model)
ColBool {} -> do
v <- evalEither (toBoolVector c)
map (maybe VNull VBool) (VG.toList v) === rows
evalEither (columnValues (fromBoolVector (VG.force v))) >>= (=== model)
_ -> success
if textKind
then do
v <- evalEither (toTextVector c)
map (maybe VNull VText) (VG.toList v) === rows
VG.force v === v
else assert (isLeft (toTextVector c))
if bytesKind
then do
v <- evalEither (toBytesVector c)
map (maybe VNull VBytes) (VG.toList v) === map asBytes rows
VG.force v === v
else success
if listKind
then do
v <- evalEither (toListVector int32s c)
map (maybe VNull (VList . map (maybe VNull (VInt . toInteger)) . VG.toList)) (VG.toList v) === rows
VG.force v === v
else success
where
int32s ch = maybe (Left ("not an int32 child: " ++ columnTag ch)) (Right . toMaybeVector) (asPrim PInt32 ch)
asBytes = \case
VText t -> VBytes (TE.encodeUtf8 t)
other -> other
isFixedBinary = \case
AFixedSizeBinary _ -> True
_ -> False
isFixedList = \case
AFixedSizeList _ -> True
_ -> False