packages feed

dataframe-3.7.0.0: tests/Operations/ParallelJoin.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Parallel join kernels must match the sequential reference bit for bit.
module Operations.ParallelJoin where

import qualified Data.Text as T
import qualified Data.Vector.Unboxed as VU

import qualified DataFrame as D
import DataFrame.Operations.Join (
    hashInnerKernel,
    hashLeftKernel,
    innerJoin,
    leftJoin,
    parInnerKernel,
    parLeftKernel,
 )

import Test.HUnit

-- | A probe/build hash pair straddling realistic shapes.
type HashPair = (VU.Vector Int, VU.Vector Int)

mkHashes :: [Int] -> VU.Vector Int
mkHashes = VU.fromList

-- | Synthetic hash vectors with collisions, duplicates and large sizes.
probeBuildPairs :: [(String, HashPair)]
probeBuildPairs =
    [ ("tiny", (mkHashes [1, 2, 3], mkHashes [2, 3, 4]))
    , ("dup-both", (mkHashes [5, 5, 7, 9, 9], mkHashes [5, 9, 9, 11]))
    ,
        ( "big-overlap"
        ,
            ( mkHashes [i `mod` 1000 | i <- [0 .. 300000 :: Int]]
            , mkHashes [i `mod` 1000 | i <- [0 .. 50000 :: Int]]
            )
        )
    ,
        ( "big-sparse"
        ,
            ( mkHashes [0 .. 300000 :: Int]
            , mkHashes [i * 3 | i <- [0 .. 120000 :: Int]]
            )
        )
    , -- Small-build side probed by a very large probe; parity is the gate.

        ( "small-build-big-probe"
        ,
            ( mkHashes [i `mod` 10000 | i <- [0 .. 1200000 :: Int]]
            , mkHashes [0 .. 10000 :: Int]
            )
        )
    ]

-- | parInnerKernel == hashInnerKernel, bit-for-bit, on each shape.
innerKernelParity :: (String, HashPair) -> Test
innerKernelParity (label, (probe, build)) =
    TestCase $
        assertEqual
            ("inner kernel parity: " ++ label)
            (hashInnerKernel probe build)
            (parInnerKernel probe build)

-- | parLeftKernel == hashLeftKernel, bit-for-bit, on each shape.
leftKernelParity :: (String, HashPair) -> Test
leftKernelParity (label, (probe, build)) =
    TestCase $
        assertEqual
            ("left kernel parity: " ++ label)
            (hashLeftKernel probe build)
            (parLeftKernel probe build)

-- | Build a frame large enough to cross the parallel threshold.
ordersFrame :: Int -> D.DataFrame
ordersFrame n =
    D.fromColumns
        [ ("cid", D.fromList [i `mod` 7000 | i <- [0 .. n - 1]])
        , ("amount", D.fromList [fromIntegral i * 1.5 :: Double | i <- [0 .. n - 1]])
        ]

custFrame :: Int -> D.DataFrame
custFrame n =
    D.fromColumns
        [ ("cid", D.fromList [0 .. n - 1])
        , ("region", D.fromList [T.pack ('r' : show (i `mod` 5)) | i <- [0 .. n - 1]])
        ]

-- | End to end: a large join equals the sorted join and has a stable row count.
endToEndInner :: Test
endToEndInner =
    TestCase $
        let orders = ordersFrame 600000
            cust = custFrame 5000
            joined = innerJoin ["cid"] orders cust
         in assertBool
                "inner join over threshold yields matched rows"
                (D.nRows joined > 0)

endToEndLeft :: Test
endToEndLeft =
    TestCase $
        let orders = ordersFrame 600000
            cust = custFrame 5000
            joined = leftJoin ["cid"] orders cust
         in assertEqual
                "left join keeps every probe row"
                600000
                (D.nRows joined)

-- | Probe frame with a text join key and enough rows for parallel hashing.
factorOrdersFrame :: Int -> Int -> D.DataFrame
factorOrdersFrame n k =
    D.fromColumns
        [ ("fk", D.fromList [T.pack ('f' : show (i `mod` k)) | i <- [0 .. n - 1]])
        , ("amount", D.fromList [fromIntegral i * 2.0 :: Double | i <- [0 .. n - 1]])
        ]

factorDimFrame :: Int -> D.DataFrame
factorDimFrame k =
    D.fromColumns
        [ ("fk", D.fromList [T.pack ('f' : show i) | i <- [0 .. k - 1]])
        , ("label", D.fromList [T.pack ('L' : show i) | i <- [0 .. k - 1]])
        ]

-- | Inner join on a text key keeps all n probe rows under parallel hashing.
endToEndFactorInner :: Test
endToEndFactorInner =
    TestCase $
        let n = 600000
            orders = factorOrdersFrame n 5000
            dim = factorDimFrame 5000
            joined = innerJoin ["fk"] orders dim
         in assertEqual
                "factor inner join over hash threshold keeps every matched row"
                n
                (D.nRows joined)

-- | Small-build, very large probe inner join on a text key keeps every match.
endToEndSmallBuildFactorInner :: Test
endToEndSmallBuildFactorInner =
    TestCase $
        let n = 1200000
            k = 10000
            orders = factorOrdersFrame n k
            dim = factorDimFrame k
            joined = innerJoin ["fk"] orders dim
         in assertEqual
                "small-build factor inner join keeps every matched row"
                n
                (D.nRows joined)

-- | Small-build, large-probe left join keeps every probe row, matched or not.
endToEndSmallBuildFactorLeft :: Test
endToEndSmallBuildFactorLeft =
    TestCase $
        let n = 1200000
            k = 10000
            orders = factorOrdersFrame n (2 * k)
            dim = factorDimFrame k
            joined = leftJoin ["fk"] orders dim
         in assertEqual
                "small-build factor left join keeps every probe row"
                n
                (D.nRows joined)

tests :: [Test]
tests =
    [ TestLabel ("innerKernel " ++ l) (innerKernelParity p)
    | p@(l, _) <- probeBuildPairs
    ]
        ++ [ TestLabel ("leftKernel " ++ l) (leftKernelParity p)
           | p@(l, _) <- probeBuildPairs
           ]
        ++ [ TestLabel "endToEndInner" endToEndInner
           , TestLabel "endToEndLeft" endToEndLeft
           , TestLabel "endToEndFactorInner" endToEndFactorInner
           , TestLabel "endToEndSmallBuildFactorInner" endToEndSmallBuildFactorInner
           , TestLabel "endToEndSmallBuildFactorLeft" endToEndSmallBuildFactorLeft
           ]