lsm-tree-1.0.0.0: test/Test/Database/LSMTree/Class.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE OverloadedStrings #-}
module Test.Database.LSMTree.Class (
tests
) where
import Control.Exception (SomeException, assert, try)
import Control.Monad (forM, when)
import Control.Monad.ST.Strict (runST)
import Control.Monad.Trans.State
import qualified Data.ByteString as BS
import Data.Foldable (toList)
import Data.Functor.Compose (Compose (..))
import qualified Data.List as List
import qualified Data.List.NonEmpty as NE
import qualified Data.Proxy as Proxy
import qualified Data.Vector as V
import qualified Data.Vector.Algorithms.Merge as VA
import Data.Word (Word64)
import qualified Database.LSMTree as R
import Database.LSMTree.Class
import Database.LSMTree.Extras.Generators ()
import qualified Database.LSMTree.Model.IO as ModelIO
import qualified System.FS.API as FS
import Test.Database.LSMTree.StateMachine ()
import Test.QuickCheck.Monadic (monadicIO, monitor, run)
import Test.Tasty (TestName, TestTree, testGroup)
import qualified Test.Tasty.QuickCheck as QC
import Test.Tasty.QuickCheck hiding (label)
import qualified Test.Util.FS as FS
tests :: TestTree
tests = testGroup "Test.Database.LSMTree.Class"
[ testGroup "Model" $ zipWith ($) (props tbl1) expectFailures1
, testGroup "Real" $ zipWith ($) (props tbl2) expectFailures2
]
where
tbl1 :: RunSetup ModelIO.Table IO
tbl1 = RunSetup $ \conf -> Setup {
testTableConfig = conf
, testWithSessionArgs = \action -> action ModelIO.NoSessionArgs
}
expectFailures1 = repeat False
tbl2 :: RunSetup R.Table IO
tbl2 = RunSetup $ \conf -> Setup {
testTableConfig = conf
, testWithSessionArgs = \action ->
FS.withTempIOHasBlockIO "R" $ \hfs hbio ->
action (SessionArgs hfs hbio (FS.mkFsPath []))
}
expectFailures2 = repeat False
props RunSetup {..} =
[ testProperty' "lookup-insert" $ prop_lookupInsert . runSetup
, testProperty' "lookup-insert-else" $ prop_lookupInsertElse . runSetup
, testProperty' "lookup-insert-blob" $ prop_lookupInsertBlob . runSetup
, testProperty' "lookup-delete" $ prop_lookupDelete . runSetup
, testProperty' "lookup-delete-else" $ prop_lookupDeleteElse . runSetup
, testProperty' "insert-insert" $ prop_insertInsert . runSetup
, testProperty' "insert-insert-blob" $ prop_insertInsertBlob . runSetup
, testProperty' "insert-commutes" $ prop_insertCommutes . runSetup
, testProperty' "insert-commutes-blob" $ prop_insertCommutesBlob . runSetup
, testProperty' "invalidated-blob-references" $ prop_updatesMayInvalidateBlobRefs . runSetup
, testProperty' "dup-insert-insert" $ prop_dupInsertInsert . runSetup
, testProperty' "dup-insert-comm" $ prop_dupInsertCommutes . runSetup
, testProperty' "dup-nochanges" $ prop_dupNoChanges . runSetup
, testProperty' "lookupRange-like-lookups" $ prop_lookupRangeLikeLookups . runSetup
, testProperty' "lookupRange-insert" $ prop_insertLookupRange . runSetup
, testProperty' "readCursor-sorted" $ prop_readCursorSorted . runSetup
, testProperty' "readCursor-num-results" $ prop_readCursorNumResults . runSetup
, testProperty' "readCursor-insert" $ prop_readCursorInsert . runSetup
, testProperty' "readCursor-delete" $ prop_readCursorDelete . runSetup
, testProperty' "readCursor-delete-else" $ prop_readCursorDeleteElse . runSetup
, testProperty' "readCursor-stable-view" $ prop_readCursorStableView . runSetup
, testProperty' "readCursor-offset" $ prop_readCursorOffset . runSetup
, testProperty' "snapshot-nochanges" $ prop_snapshotNoChanges . runSetup
, testProperty' "snapshot-nochanges2" $ prop_snapshotNoChanges2 . runSetup
, testProperty' "lookup-mupsert" $ prop_lookupUpdate . runSetup
, testProperty' "union" $ prop_union . runSetup
]
testProperty' :: forall a. Testable a => TestName -> a -> Bool -> TestTree
testProperty' name prop = \b ->
testProperty name ((if b then expectFailure else property) prop)
-------------------------------------------------------------------------------
-- test setup and helpers
-------------------------------------------------------------------------------
type Key = Word64
type Blob = BS.ByteString
newtype Value = Value BS.ByteString
deriving stock (Eq, Show)
deriving newtype (Arbitrary, Semigroup, SerialiseValue)
deriving ResolveValue via (R.ResolveViaSemigroup Value)
label :: SnapshotLabel
label = SnapshotLabel "Word64 ByteString ByteString"
type Proxy h = Setup h IO
newtype RunSetup h m = RunSetup {
runSetup :: TableConfig h -> Setup h m
}
data Setup h m = Setup {
testTableConfig :: TableConfig h
, testWithSessionArgs :: forall a. (SessionArgs (Session h) m -> m a) -> m a
}
-- | create session, table, and populate it with some data.
withSessionAndTableNew :: forall h m a.
( IsTable h
, IOLike m
)
=> Setup h m
-> [(Key, Update Value Blob)]
-> (Session h m -> h m Key Value Blob -> m a)
-> m a
withSessionAndTableNew Setup{..} ups action =
testWithSessionArgs $ \args ->
withSession args $ \sesh ->
withTableNew sesh testTableConfig $ \table -> do
updates table (V.fromList ups)
action sesh table
-- | Like 'retrieveBlobs' but works for any 'Traversable'.
--
-- Like 'partsOf' in @lens@ this uses state monad.
retrieveBlobsTrav ::
( IsTable h
, IOLike m
, SerialiseValue b
, Traversable t
, C_ b
)
=> proxy h
-> Session h m
-> t (BlobRef h m b)
-> m (t b)
retrieveBlobsTrav tbl ses brefs = do
blobs <- retrieveBlobs tbl ses (V.fromList $ toList brefs)
evalStateT (traverse (\_ -> state un) brefs) (V.toList blobs)
where
un [] = error "invalid traversal"
un (x:xs) = (x, xs)
lookupsWithBlobs :: forall h m k v b.
( IsTable h
, IOLike m
, C k v b
)
=> h m k v b
-> Session h m
-> V.Vector k
-> m (V.Vector (LookupResult v b))
lookupsWithBlobs tbl ses ks = do
res <- lookups tbl ks
getCompose <$> retrieveBlobsTrav (Proxy.Proxy @h) ses (Compose res)
rangeLookupWithBlobs :: forall h m k v b.
( IsTable h
, IOLike m
, C k v b
)
=> h m k v b
-> Session h m
-> Range k
-> m (V.Vector (Entry k v b))
rangeLookupWithBlobs tbl ses r = do
res <- rangeLookup tbl r
getCompose <$> retrieveBlobsTrav (Proxy.Proxy @h) ses (Compose res)
readCursorWithBlobs :: forall h m k v b proxy.
( IsTable h
, IOLike m
, C k v b
)
=> proxy h
-> Session h m
-> Cursor h m k v b
-> Int
-> m (V.Vector (Entry k v b))
readCursorWithBlobs tbl ses cursor n = do
res <- readCursor tbl n cursor
getCompose <$> retrieveBlobsTrav tbl ses (Compose res)
readCursorAllWithBlobs :: forall h m k v b proxy.
( IsTable h
, IOLike m
, C k v b
)
=> proxy h
-> Session h m
-> Cursor h m k v b
-> CursorReadSchedule
-> m [V.Vector (Entry k v b)]
readCursorAllWithBlobs tbl ses cursor = go . getCursorReadSchedule
where
go [] = error "readCursorAllWithBlobs: finite infinite list"
go (n : ns) = do
res <- readCursorWithBlobs tbl ses cursor n
if V.null res
then pure [res]
else (res :) <$> go ns
type CursorReadSchedule = InfiniteList (Positive Int)
getCursorReadSchedule :: CursorReadSchedule -> [Int]
getCursorReadSchedule = map getPositive . getInfiniteList
-------------------------------------------------------------------------------
-- implement classic QC tests for basic k/v properties
-------------------------------------------------------------------------------
-- | You can lookup what you inserted.
prop_lookupInsert ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key -> Value -> Property
prop_lookupInsert h ups k v = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
-- the main dish
inserts tbl (V.singleton (k, v, Nothing))
res <- lookupsWithBlobs tbl ses (V.singleton k)
pure $ res === V.singleton (Found v)
-- | Insert doesn't change the lookup results of other keys.
prop_lookupInsertElse ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key -> Value -> [Key] -> Property
prop_lookupInsertElse h ups k v testKeys = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
let testKeys' = V.fromList $ filter (/= k) testKeys
res1 <- lookupsWithBlobs tbl ses testKeys'
inserts tbl (V.singleton (k, v, Nothing))
res2 <- lookupsWithBlobs tbl ses testKeys'
pure $ res1 === res2
-- | You cannot lookup what you have just deleted
prop_lookupDelete ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key -> Property
prop_lookupDelete h ups k = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
deletes tbl (V.singleton k)
res <- lookupsWithBlobs tbl ses (V.singleton k)
pure $ res === V.singleton NotFound
-- | Delete doesn't change the lookup results of other keys
prop_lookupDeleteElse ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key -> [Key] -> Property
prop_lookupDeleteElse h ups k testKeys = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
let testKeys' = V.fromList $ filter (/= k) testKeys
res1 <- lookupsWithBlobs tbl ses testKeys'
deletes tbl (V.singleton k)
res2 <- lookupsWithBlobs tbl ses testKeys'
pure $ res1 === res2
-- | Last insert wins.
prop_insertInsert ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key -> Value -> Value -> Property
prop_insertInsert h ups k v1 v2 = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
inserts tbl (V.fromList [(k, v1, Nothing), (k, v2, Nothing)])
res <- lookupsWithBlobs tbl ses (V.singleton k)
pure $ res === V.singleton (Found v2)
-- | Inserts with different keys don't interfere.
prop_insertCommutes ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key -> Value -> Key -> Value -> Property
prop_insertCommutes h ups k1 v1 k2 v2 = k1 /= k2 ==> ioProperty do
withSessionAndTableNew h ups $ \ses tbl -> do
inserts tbl (V.fromList [(k1, v1, Nothing), (k2, v2, Nothing)])
res <- lookupsWithBlobs tbl ses (V.fromList [k1,k2])
pure $ res === V.fromList [Found v1, Found v2]
-------------------------------------------------------------------------------
-- implement classic QC tests for cursors
-------------------------------------------------------------------------------
-- | Cursor read results are sorted by key.
prop_readCursorSorted ::
forall h. IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Maybe Key
-> CursorReadSchedule
-> Property
prop_readCursorSorted h ups offset ns = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
res <- withCursor offset tbl $ \cursor -> do
V.concat <$> readCursorAllWithBlobs (Proxy.Proxy @h) ses cursor ns
let keys = map queryResultKey (V.toList res)
pure $ keys === List.sort keys
-- | Cursor reads return the requested number of results, until the end.
prop_readCursorNumResults ::
forall h. IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Maybe Key
-> CursorReadSchedule
-> Property
prop_readCursorNumResults h ups offset ns = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
res <- withCursor offset tbl $ \cursor -> do
readCursorAllWithBlobs (Proxy.Proxy @h) ses cursor ns
let elemsRead = map V.length res
let numFullReads = length res - 2
pure $ last elemsRead === 0
.&&. take numFullReads elemsRead
=== take numFullReads (getCursorReadSchedule ns)
-- | You can read what you inserted.
prop_readCursorInsert ::
forall h. IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> CursorReadSchedule
-> Key -> Value -> Property
prop_readCursorInsert h ups ns k v = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
inserts tbl (V.singleton (k, v, Nothing))
res <- withCursor Nothing tbl $ \cursor ->
V.concat <$> readCursorAllWithBlobs (Proxy.Proxy @h) ses cursor ns
pure $ V.find (\r -> queryResultKey r == k) res
=== Just (Entry k v)
-- | You can't read what you deleted.
prop_readCursorDelete ::
forall h. IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> CursorReadSchedule
-> Key -> Property
prop_readCursorDelete h ups ns k = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
deletes tbl (V.singleton k)
res <- withCursor Nothing tbl $ \cursor -> do
V.concat <$> readCursorAllWithBlobs (Proxy.Proxy @h) ses cursor ns
pure $ V.find (\r -> queryResultKey r == k) res === Nothing
-- | Updates don't change the cursor read results of other keys.
prop_readCursorDeleteElse ::
forall h. IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Maybe Key
-> CursorReadSchedule
-> [(Key, Update Value Blob)] -> Property
prop_readCursorDeleteElse h ups offset ns ups2 = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
res1 <- withCursor offset tbl $ \cursor -> do
V.concat <$> readCursorAllWithBlobs (Proxy.Proxy @h) ses cursor ns
updates tbl (V.fromList ups2)
res2 <- withCursor offset tbl $ \cursor -> do
V.concat <$> readCursorAllWithBlobs (Proxy.Proxy @h) ses cursor ns
let updatedKeys = map fst ups2
pure $ V.filter (\r -> queryResultKey r `notElem` updatedKeys) res1
=== V.filter (\r -> queryResultKey r `notElem` updatedKeys) res2
-- | Updates don't affect previously created cursors.
prop_readCursorStableView ::
forall h. IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Maybe Key
-> CursorReadSchedule
-> [(Key, Update Value Blob)] -> Property
prop_readCursorStableView h ups offset ns ups2 = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
res1 <- withCursor offset tbl $ \cursor -> do
readCursorAllWithBlobs (Proxy.Proxy @h) ses cursor ns
res2 <- withCursor offset tbl $ \cursor -> do
updates tbl (V.fromList ups2)
readCursorAllWithBlobs (Proxy.Proxy @h) ses cursor ns
pure $ res1 === res2
-- | Creating a cursor at an offset simply skips a prefix.
prop_readCursorOffset ::
forall h. IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key
-> CursorReadSchedule
-> Property
prop_readCursorOffset h ups offset ns = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
res1 <- withCursor (Just offset) tbl $ \cursor -> do
V.concat <$> readCursorAllWithBlobs (Proxy.Proxy @h) ses cursor ns
res2 <- withCursor Nothing tbl $ \cursor -> do
V.concat <$> readCursorAllWithBlobs (Proxy.Proxy @h) ses cursor ns
pure $ res1 === V.dropWhile ((< offset) . queryResultKey) res2
-------------------------------------------------------------------------------
-- implement classic QC tests for range lookups
-------------------------------------------------------------------------------
evalRange :: Ord k => Range k -> k -> Bool
evalRange (FromToExcluding lo hi) x = lo <= x && x < hi
evalRange (FromToIncluding lo hi) x = lo <= x && x <= hi
queryResultKey :: Entry k v b -> k
queryResultKey (Entry k _) = k
queryResultKey (EntryWithBlob k _ _ ) = k
queryResultFromLookup :: k -> LookupResult v b -> Maybe (Entry k v b)
queryResultFromLookup k = \case
NotFound -> Nothing
Found v -> Just (Entry k v)
FoundWithBlob v b -> Just (EntryWithBlob k v b)
-- | A range lookup behaves like many point lookups.
prop_lookupRangeLikeLookups ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Range Key
-> Property
prop_lookupRangeLikeLookups h ups r = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
res1 <- rangeLookupWithBlobs tbl ses r
let testKeys = V.fromList $ nubSort $ filter (evalRange r) $ map fst ups
res2 <- fmap (V.catMaybes . V.zipWith queryResultFromLookup testKeys) $
lookupsWithBlobs tbl ses testKeys
pure $ res1 === res2
where
nubSort = map NE.head . NE.group . List.sort
-- | Last insert wins.
prop_insertLookupRange ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key -> Value -> Range Key -> Property
prop_insertLookupRange h ups k v r = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
res <- rangeLookupWithBlobs tbl ses r
inserts tbl (V.singleton (k, v, Nothing))
res' <- rangeLookupWithBlobs tbl ses r
let p :: Entry Key Value b -> Bool
p rlr = queryResultKey rlr /= k
if evalRange r k
then pure $ vsortOn queryResultKey (V.cons (Entry k v) (V.filter p res)) === res'
else pure $ res === res'
where
vsortOn f vec = runST $ do
mvec <- V.thaw vec
VA.sortBy (\e1 e2 -> f e1 `compare` f e2) mvec
V.unsafeFreeze mvec
-------------------------------------------------------------------------------
-- implement classic QC tests for split-value BLOB retrieval
-------------------------------------------------------------------------------
-- | You can lookup what you inserted.
prop_lookupInsertBlob ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key -> Value -> Blob -> Property
prop_lookupInsertBlob h ups k v blob = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
-- the main dish
inserts tbl (V.singleton (k, v, Just blob))
res <- lookupsWithBlobs tbl ses (V.singleton k)
pure $ res === V.singleton (FoundWithBlob v blob)
-- | Last insert wins.
prop_insertInsertBlob ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key -> Value -> Value -> Maybe Blob -> Maybe Blob -> Property
prop_insertInsertBlob h ups k v1 v2 mblob1 mblob2 = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl -> do
inserts tbl (V.fromList [(k, v1, mblob1), (k, v2, mblob2)])
res <- lookupsWithBlobs tbl ses (V.singleton k)
pure $ res === case mblob2 of
Nothing -> V.singleton (Found v2)
Just blob2 -> V.singleton (FoundWithBlob v2 blob2)
-- | Inserts with different keys don't interfere.
prop_insertCommutesBlob ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key -> Value -> Maybe Blob
-> Key -> Value -> Maybe Blob -> Property
prop_insertCommutesBlob h ups k1 v1 mblob1 k2 v2 mblob2 = k1 /= k2 ==> ioProperty do
withSessionAndTableNew h ups $ \ses tbl -> do
inserts tbl (V.fromList [(k1, v1, mblob1), (k2, v2, mblob2)])
res <- lookupsWithBlobs tbl ses $ V.fromList [k1,k2]
pure $ res === case (mblob1, mblob2) of
(Nothing, Nothing) -> V.fromList [Found v1, Found v2]
(Just blob1, Nothing) -> V.fromList [FoundWithBlob v1 blob1, Found v2]
(Nothing, Just blob2) -> V.fromList [Found v1, FoundWithBlob v2 blob2]
(Just blob1, Just blob2) -> V.fromList [FoundWithBlob v1 blob1, FoundWithBlob v2 blob2]
-- | A blob reference may be invalidated by an update.
prop_updatesMayInvalidateBlobRefs ::
forall h. IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key -> Value -> Blob
-> [(Key, Update Value Blob)]
-> Property
prop_updatesMayInvalidateBlobRefs h ups k1 v1 blob1 ups' = monadicIO $ do
(res, blobs, res') <- run $ do
withSessionAndTableNew h ups $ \ses tbl -> do
inserts tbl (V.singleton (k1, v1, Just blob1))
res <- lookups tbl (V.singleton k1)
blobs <- getCompose <$> retrieveBlobsTrav (Proxy.Proxy @h) ses (Compose res)
updates tbl (V.fromList ups')
res' <- try @SomeException (getCompose <$> retrieveBlobsTrav (Proxy.Proxy @h) ses (Compose res))
pure (res, blobs, res')
case (V.toList res, V.toList blobs) of
([FoundWithBlob{}], [FoundWithBlob _ x])
| Left _ <- res' ->
monitor (QC.label "blob reference invalidated") >> pure True
| Right (V.toList -> [FoundWithBlob _ y]) <- res' ->
monitor (QC.label "blob reference valid") >> pure (x == y)
_ -> monitor (counterexample "insert before lookup failed, somehow...") >> pure False
-------------------------------------------------------------------------------
-- implement classic QC tests for monoidal updates
-------------------------------------------------------------------------------
-- | You can lookup what you inserted.
prop_lookupUpdate ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key -> Value -> Maybe Blob -> Value -> Property
prop_lookupUpdate h ups k v1 mb1 v2 = ioProperty $ do
withSessionAndTableNew h ups $ \s tbl -> do
-- the main dish
inserts tbl (V.singleton (k, v1, mb1))
upserts tbl (V.singleton (k, v2))
res <- lookupsWithBlobs tbl s (V.singleton k)
-- notice the order.
pure $ res === V.singleton (Found (resolve v2 v1))
-------------------------------------------------------------------------------
-- implement classic QC tests for monoidal table unions
-------------------------------------------------------------------------------
prop_union :: forall h.
IsTable h
=> Proxy h -> [(Key, Update Value Blob)] -> [(Key, Update Value Blob)]
-> [Key]
-> Positive (Small Int) -- ^ Number of batches for supplying union credits
-> Property
prop_union h ups1 ups2
(V.fromList -> testKeys)
(Positive (Small nSupplyBatches))
= ioProperty $ do
withSessionAndTableNew h ups1 $ \s tbl1 -> do
withTableNew s (testTableConfig h) $ \tbl2 -> do
updates tbl2 $ V.fromList ups2
-- union them.
withTableUnion tbl1 tbl2 $ \tbl3 -> do
propBegin <- compareLookups s tbl1 tbl2 tbl3
-- Supply union credits in @nSupplyBatches@ batches
props <- forM (reverse [1 .. nSupplyBatches]) $ \i -> do
-- Try to keep the batch sizes roughly the same size
UnionDebt debt <- remainingUnionDebt tbl3
_ <- supplyUnionCredits tbl3 (UnionCredits (debt `div` i))
-- In case @i == 0@, then @debt `div` i == debt@, so the last supply
-- should have finished the union.
when (i == 1) $ do
finalDebt <- remainingUnionDebt tbl3
assert (finalDebt == UnionDebt 0) $ pure ()
-- Check that the lookup results are still the same after each batch
-- of union credits.
compareLookups s tbl1 tbl2 tbl3
pure (propBegin .&&. conjoin props)
where
compareLookups s tbl1 tbl2 tbl3 = do
-- results in parts and the union table
res1 <- lookupsWithBlobs tbl1 s testKeys
res2 <- lookupsWithBlobs tbl2 s testKeys
res3 <- lookupsWithBlobs tbl3 s testKeys
let unionResult ::
LookupResult Value Blob
-> LookupResult Value Blob
-> LookupResult Value Blob
unionResult r@NotFound NotFound = r
unionResult NotFound r@(Found _) = r
unionResult NotFound r@(FoundWithBlob _ _) = r
unionResult r@(Found _) NotFound
= r
unionResult (Found v1) (Found v2)
= Found (resolve v1 v2)
unionResult (Found v1) (FoundWithBlob v2 _)
= Found (resolve v1 v2)
unionResult r@(FoundWithBlob _ _) NotFound
= r
unionResult (FoundWithBlob v1 b1) (Found v2)
= FoundWithBlob (resolve v1 v2) b1
unionResult (FoundWithBlob v1 b1) (FoundWithBlob v2 _b2)
= FoundWithBlob (resolve v1 v2) b1
pure $ V.zipWith unionResult res1 res2 === res3
-------------------------------------------------------------------------------
-- implement classic QC tests for snapshots
-------------------------------------------------------------------------------
-- changes to handle would not change the snapshot
prop_snapshotNoChanges :: forall h.
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> [(Key, Update Value Blob)] -> [Key] -> Property
prop_snapshotNoChanges h ups ups' testKeys = ioProperty $ do
withSessionAndTableNew h ups $ \ses tbl1 -> do
res <- lookupsWithBlobs tbl1 ses $ V.fromList testKeys
let name = R.toSnapshotName "foo"
saveSnapshot name label tbl1
updates tbl1 (V.fromList ups')
withTableFromSnapshot @h ses label name$ \tbl2 -> do
res' <- lookupsWithBlobs tbl2 ses $ V.fromList testKeys
pure $ res == res'
-- same snapshot may be opened multiple times,
-- and the handles are separate.
prop_snapshotNoChanges2 :: forall h.
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> [(Key, Update Value Blob)] -> [Key] -> Property
prop_snapshotNoChanges2 h ups ups' testKeys = ioProperty $ do
withSessionAndTableNew h ups $ \sess tbl0 -> do
let name = R.toSnapshotName "foo"
saveSnapshot name label tbl0
withTableFromSnapshot @h sess label name $ \tbl1 ->
withTableFromSnapshot @h sess label name $ \tbl2 -> do
res <- lookupsWithBlobs tbl1 sess $ V.fromList testKeys
updates tbl1 (V.fromList ups')
res' <- lookupsWithBlobs tbl2 sess $ V.fromList testKeys
pure $ res == res'
-------------------------------------------------------------------------------
-- implement classic QC tests for multiple writable tables
-- - results of insert/delete, monoidal update, lookups, and range lookups should
-- be equal if applied to two duplicated tables
-- - changes to one table should not cause any visible changes in any others
-------------------------------------------------------------------------------
-- | Last insert wins.
prop_dupInsertInsert ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key -> Value -> Value -> [Key] -> Property
prop_dupInsertInsert h ups k v1 v2 testKeys = ioProperty $ do
withSessionAndTableNew h ups $ \sess tbl1 -> do
withTableDuplicate tbl1 $ \tbl2 -> do
inserts tbl1 (V.fromList [(k, v1, Nothing), (k, v2, Nothing)])
inserts tbl2 (V.fromList [(k, v2, Nothing)])
res1 <- lookupsWithBlobs tbl1 sess $ V.fromList testKeys
res2 <- lookupsWithBlobs tbl2 sess $ V.fromList testKeys
pure $ res1 === res2
-- | Different key inserts commute.
prop_dupInsertCommutes ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> Key -> Value -> Key -> Value -> [Key] -> Property
prop_dupInsertCommutes h ups k1 v1 k2 v2 testKeys = k1 /= k2 ==> ioProperty do
withSessionAndTableNew h ups $ \sess tbl1 -> do
withTableDuplicate tbl1 $ \tbl2 -> do
inserts tbl1 (V.fromList [(k1, v1, Nothing), (k2, v2, Nothing)])
inserts tbl2 (V.fromList [(k2, v2, Nothing), (k1, v1, Nothing)])
res1 <- lookupsWithBlobs tbl1 sess $ V.fromList testKeys
res2 <- lookupsWithBlobs tbl2 sess $ V.fromList testKeys
pure $ res1 === res2
-- changes to one handle should not cause any visible changes in any others
prop_dupNoChanges ::
IsTable h
=> Proxy h -> [(Key, Update Value Blob)]
-> [(Key, Update Value Blob)] -> [Key] -> Property
prop_dupNoChanges h ups ups' testKeys = ioProperty $ do
withSessionAndTableNew h ups $ \sess tbl1 -> do
res <- lookupsWithBlobs tbl1 sess $ V.fromList testKeys
withTableDuplicate tbl1 $ \tbl2 -> do
updates tbl2 (V.fromList ups')
-- lookup tbl1 again.
res' <- lookupsWithBlobs tbl1 sess $ V.fromList testKeys
pure $ res == res'