packages feed

lsm-tree 1.0.0.1 → 1.0.0.2

raw patch · 14 files changed

+702/−280 lines, 14 filesdep ~basedep ~containersdep ~io-classes

Dependency ranges changed: base, containers, io-classes, io-sim, tasty

Files

CHANGELOG.md view
@@ -1,5 +1,30 @@ # Revision history for `lsm-tree` +## 1.0.0.2 -- 2026-04-24++### Breaking changes++None++### New features++None++### Minor changes++* Support `io-classes ^>=1.9` and `^>=1.10`. See [PR+  #819](https://github.com/IntersectMBO/lsm-tree/pull/819).+* Support `ghc-9.14`. See [PR+  #836](https://github.com/IntersectMBO/lsm-tree/pull/836).+* Support `containers-0.8`. See [PR+  #836](https://github.com/IntersectMBO/lsm-tree/pull/836).++### Bug fixes++* Fix a bug where `lookups` with a large number of input keys would sometimes+  return incorrect lookup results. See [PR+  #841](https://github.com/IntersectMBO/lsm-tree/pull/841).+ ## 1.0.0.1 -- 2025-12-03  * PATCH: support `filepath-1.4`. See PR
bench/macro/utxo-bench.hs view
@@ -38,21 +38,23 @@  import           Control.Applicative ((<**>)) import           Control.Concurrent (getNumCapabilities)-import           Control.Concurrent.Async-import           Control.Concurrent.MVar+import           Control.Concurrent.Async (waitBoth, withAsyncOn)+import           Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar) import           Control.DeepSeq (force)-import           Control.Exception+import           Control.Exception (evaluate) import           Control.Monad (forM_, unless, void, when) import           Control.Monad.Trans.State.Strict (runState, state)-import           Control.Tracer+import           Control.Tracer (Contravariant (contramap), Tracer, nullTracer,+                     squelchUnless, stdoutTracer) import qualified Data.ByteString.Short as BS import qualified Data.Foldable as Fold import qualified Data.IntSet as IS-import           Data.IORef+import           Data.IORef (IORef, modifyIORef, modifyIORef', newIORef,+                     readIORef, writeIORef) import qualified Data.List.NonEmpty as NE import           Data.Map.Strict (Map) import qualified Data.Map.Strict as Map-import           Data.Monoid+import           Data.Monoid (Dual (Dual, getDual)) import qualified Data.Primitive as P import qualified Data.Vector as V import           Data.Void (Void)@@ -64,11 +66,13 @@ import qualified System.Clock as Clock import qualified System.FS.API as FS import qualified System.FS.BlockIO.IO as FsIO-import           System.IO+import           System.IO (BufferMode (NoBuffering), Handle,+                     IOMode (WriteMode), hPutStr, hPutStrLn, hSetBuffering,+                     stdout, withFile) import           System.Mem (performMajorGC) import qualified System.Random as Random import           Text.Printf (printf)-import           Text.Show.Pretty+import           Text.Show.Pretty (ppShow)  import           Database.LSMTree.Extras (groupsOfN) 
lsm-tree.cabal view
@@ -1,6 +1,6 @@ cabal-version:   3.4 name:            lsm-tree-version:         1.0.0.1+version:         1.0.0.2 synopsis:        Log-structured merge-trees description:   This package contains an efficient implementation of on-disk key–value storage, implemented as a log-structured merge-tree, LSM-tree or LSMT.@@ -502,7 +502,7 @@   type:     git   location: https://github.com/IntersectMBO/lsm-tree   subdir:   lsm-tree-  tag:      lsm-tree-1.0.0.1+  tag:      lsm-tree-1.0.0.2  common warnings   ghc-options:@@ -528,6 +528,7 @@     ExplicitNamespaces     GADTs     LambdaCase+    OverloadedRecordDot     RecordWildCards     RoleAnnotations     ViewPatterns@@ -540,12 +541,12 @@     Database.LSMTree.Simple    build-depends:-    , base                    >=4.16   && <4.22+    , base                    >=4.16   && <4.23     , blockio                 ^>=0.1     , contra-tracer           ^>=0.1   || ^>=0.2     , deepseq                 ^>=1.4   || ^>=1.5     , fs-api                  ^>=0.4-    , io-classes              ^>=1.6   || ^>=1.7 || ^>=1.8.0.1+    , io-classes              ^>=1.6   || ^>=1.7 || ^>=1.8.0.1 || ^>=1.9 || ^>=1.10     , io-classes:strict-mvar     , lsm-tree:control     , lsm-tree:core@@ -617,19 +618,19 @@     Database.LSMTree.Internal.WriteBufferWriter    build-depends:-    , base                    >=4.16      && <4.22+    , base                    >=4.16      && <4.23     , bitvec                  ^>=1.1     , blockio                 ^>=0.1     , bloomfilter-blocked     ^>=0.1     , bytestring              ^>=0.11.4.0 || ^>=0.12.1.0     , cborg                   ^>=0.2.10.0-    , containers              ^>=0.6      || ^>=0.7+    , containers              ^>=0.6      || ^>=0.7      || ^>=0.8     , contra-tracer           ^>=0.1      || ^>=0.2     , crc32c                  ^>=0.2.1     , deepseq                 ^>=1.4      || ^>=1.5     , filepath                ^>=1.4      || ^>=1.5     , fs-api                  ^>=0.4-    , io-classes              ^>=1.6      || ^>=1.7      || ^>=1.8.0.1+    , io-classes              ^>=1.6      || ^>=1.7      || ^>=1.8.0.1 || ^>=1.9 || ^>=1.10     , io-classes:strict-mvar     , lsm-tree:control     , lsm-tree:kmerge@@ -663,7 +664,7 @@     Database.LSMTree.Extras.UTxO    build-depends:-    , base                    >=4.16 && <4.22+    , base                    >=4.16 && <4.23     , bitvec     , blockio     , bytestring@@ -780,7 +781,7 @@     , io-classes     , io-classes:strict-mvar     , io-classes:strict-stm-    , io-sim+    , io-sim                  <1.10     , lsm-tree     , lsm-tree:control     , lsm-tree:core@@ -798,7 +799,7 @@     , safe-wild-cards     , semialign     , split-    , tasty+    , tasty                   <1.5.3.0     , tasty-golden     , tasty-hunit     , tasty-quickcheck@@ -1026,7 +1027,7 @@   hs-source-dirs: test   main-is:        kmerge-test.hs   build-depends:-    , base              >=4.16 && <4.22+    , base              >=4.16 && <4.23     , deepseq     , heaps     , lsm-tree:kmerge@@ -1045,7 +1046,7 @@   main-is:        kmerge-test.hs   cpp-options:    -DKMERGE_BENCHMARKS   build-depends:-    , base              >=4.16 && <4.22+    , base              >=4.16 && <4.23     , deepseq     , heaps     , lsm-tree:kmerge@@ -1063,7 +1064,7 @@   hs-source-dirs: test   main-is:        map-range-test.hs   build-depends:-    , base              >=4.16 && <4.22+    , base              >=4.16 && <4.23     , bytestring     , containers     , lsm-tree:core@@ -1099,6 +1100,7 @@     Test.FormatPage     Test.ScheduledMerges     Test.ScheduledMerges.RunSizes+    Test.ScheduledMergesDL     Test.ScheduledMergesQLS    build-depends:@@ -1127,9 +1129,9 @@     Control.RefCount    build-depends:-    , base                   >=4.16 && <4.22+    , base                   >=4.16 && <4.23     , deepseq                ^>=1.4 || ^>=1.5-    , io-classes             ^>=1.6 || ^>=1.7 || ^>=1.8.0.1+    , io-classes             ^>=1.6 || ^>=1.7 || ^>=1.8.0.1 || ^>=1.9 || ^>=1.10     , io-classes:strict-stm     , primitive              ^>=0.9 
src-core/Database/LSMTree/Internal/Arena.hs view
@@ -1,6 +1,8 @@-{-# LANGUAGE CPP             #-}-{-# LANGUAGE LambdaCase      #-}-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE CPP                   #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE LambdaCase            #-}+{-# LANGUAGE NoFieldSelectors      #-}+{-# LANGUAGE OverloadedRecordDot   #-} {-# OPTIONS_HADDOCK not-home #-} module Database.LSMTree.Internal.Arena (     ArenaManager,@@ -105,7 +107,7 @@             curr <- newBlock >>= newMVar             free <- newMutVar []             full <- newMutVar []-            pure Arena {..}+            pure Arena { curr = curr, free = free, full = full}  {-# SPECIALISE     closeArena :: ArenaManager s -> Arena s -> ST s ()@@ -132,10 +134,10 @@ #ifndef NO_IGNORE_ASSERTS scrambleArena _ = pure () #else-scrambleArena Arena {..} = do-    readMVar curr >>= scrambleBlock-    readMutVar full >>= mapM_ scrambleBlock-    readMutVar free >>= mapM_ scrambleBlock+scrambleArena arena = do+    readMVar arena.curr >>= scrambleBlock+    readMutVar arena.full >>= mapM_ scrambleBlock+    readMutVar arena.free >>= mapM_ scrambleBlock  {-# SPECIALISE     scrambleBlock :: Block s -> ST s ()@@ -157,18 +159,18 @@   #-} -- | Reset arena, i.e. return used blocks to free list. resetArena :: PrimMonad m => Arena (PrimState m) -> m ()-resetArena Arena {..} = do-    Block off mba <- takeMVar curr+resetArena arena = do+    Block off mba <- takeMVar arena.curr      -- reset current block     writePrimVar off 0      -- move full block to free blocks.     -- block's offset will be reset in 'newBlockWithFree'-    full' <- atomicModifyMutVar' full $ \xs -> ([], xs)-    atomicModifyMutVar' free $ \xs -> (full' <> xs, ())+    full' <- atomicModifyMutVar' arena.full $ \xs -> ([], xs)+    atomicModifyMutVar' arena.free $ \xs -> (full' <> xs, ()) -    putMVar curr $! Block off mba+    putMVar arena.curr $! Block off mba  -- | Create unmanaged arena. --@@ -205,9 +207,9 @@   #-} -- TODO!? this is not async exception safe allocateFromArena' :: PrimMonad m => Arena (PrimState m)-> Size -> Alignment -> m (Offset, MutableByteArray (PrimState m))-allocateFromArena' arena@Arena { .. } !size !alignment = do+allocateFromArena' arena !size !alignment = do     -- take current block, lock the arena-    curr'@(Block off mba) <- takeMVar curr+    curr'@(Block off mba) <- takeMVar arena.curr      off' <- readPrimVar off     let !ali = alignment - 1@@ -219,18 +221,18 @@         -- * update offset         writePrimVar off end         -- * release lock-        putMVar curr curr'+        putMVar arena.curr curr'         -- * return data         pure (off'', mba)      else do         -- doesn't fit into current block:         -- * move current block into full-        atomicModifyMutVar' full (\xs -> (curr' : xs, ()))+        atomicModifyMutVar' arena.full (\xs -> (curr' : xs, ()))         -- * allocate new block-        new <- newBlockWithFree free+        new <- newBlockWithFree arena.free         -- * set new block as current, release the lock-        putMVar curr new+        putMVar arena.curr new         -- * go again         allocateFromArena' arena size alignment 
src-core/Database/LSMTree/Internal/BloomFilter.hs view
@@ -89,7 +89,7 @@ unpackRunIxKeyIx :: RunIxKeyIx -> (Int, Int) unpackRunIxKeyIx (MkRunIxKeyIx c) =     ( fromIntegral (c `unsafeShiftR` 16)-    , fromIntegral (c .&. 0xfff)+    , fromIntegral (c .&. 0xffff)     ) {-# INLINE unpackRunIxKeyIx #-} 
src-core/Database/LSMTree/Internal/Index/Compact.hs view
@@ -233,15 +233,15 @@   entries both for the first and subsequent pages of a larger-than-page entry.   The natural thing to do is have each of these subsequent primary index   entries contain the same key prefix value. This means a binary search will-  find the /last/ entry in a run of equal prefix values.+  find the /last/ entry in a sequence of equal prefix values. -  What leads to complexity is that we will /also/ get runs of equal values if+  What leads to complexity is that we will /also/ get sequences of equal values if   we have clashes between pages (as discussed above). So in the general case-  we may have a run of equal values made up of a mixture of clashes and+  we may have a sequence of equal values made up of a mixture of clashes and   larger-than-page entries.    So the general situation is that after a binary search we have found the-  end of what may turn out to be a run of clashes and larger-than-page values+  end of what may turn out to be a sequence of clashes and larger-than-page values   and we must disambigutate and return the appropriate single page (for the   ordinary case) or an interval of pages (for the LTP case). @@ -251,13 +251,13 @@   counter-intuitive but it leads to a simpler mathematical definition (below).    The search algorithm involves searching backwards in the clash bits to find-  the beginning of the run of entries that are involved. To establish which-  entry within the run is the right one to return, we can consult the tie+  the beginning of the sequence of entries that are involved. To establish which+  entry within the sequence is the right one to return, we can consult the tie   breaker map by looking for the biggest entry that is less than or equal to-  the full search key. This may then point to an index within the run of+  the full search key. This may then point to an index within the sequence of   clashing entries, in which case this is the right entry, but it may also   point to an earlier and thus irrelevant entry, in which case the first entry-  in the run is the right one.+  in the sequence is the right one.    Note that this second case also covers the case of a single non-clashing LTP. 
src-prototypes/ScheduledMerges.hs view
@@ -1,8 +1,6 @@-{-# LANGUAGE DataKinds       #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE UnboxedTuples   #-} -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-partial-fields #-}  -- | A prototype of an LSM with explicitly scheduled incremental merges.@@ -106,7 +104,7 @@ import           Data.Functor.Contravariant import           Data.Map.Strict (Map) import qualified Data.Map.Strict as Map-import           Data.Maybe (catMaybes)+import           Data.Maybe (catMaybes, maybeToList) import           Data.Primitive.Types import           Data.STRef @@ -150,7 +148,7 @@ -- | The levels of the table, from most to least recently inserted. data LSMContent s =     LSMContent-      Buffer          -- ^ write buffer is level 0 of the table, in-memory+      WriteBuffer     -- ^ write buffer is level 0 of the table, in-memory       (Levels s)      -- ^ \"regular\" levels 1+, on disk in real implementation       (UnionLevel s)  -- ^ a potential last level @@ -163,7 +161,11 @@ -- | A level is a sequence of resident runs at this level, prefixed by an -- incoming run, which is usually multiple runs that are being merged. Once -- completed, the resulting run will become a resident run at this level.+--+-- Levels can also be empty, although we currently never create empty levels.+-- We plan to make use of them when migrating a completed union level. data Level s = Level !(IncomingRun s) ![Run]+             | EmptyLevel  -- | We represent single runs specially, rather than putting them in as a -- 'CompletedMerge'. This is for two reasons: to see statically that it's a@@ -173,8 +175,17 @@ data IncomingRun s = Merging !MergePolicyForLevel                              !NominalDebt !(STRef s NominalCredit)                              !(MergingRun LevelMergeType s)-                   | Single  !Run+                   | Single  !SingleRunOrigin !Run +-- | Additional information about the origin of a 'Single' run. This allows us+-- to have stronger invariants.+data SingleRunOrigin = FromWriteBuffer+                     | FromLevellingLevel+                       -- | A former union level that was completed (merged down+                       -- to a single run) and became the last regular level.+                     | FromMigratedUnion+  deriving stock (Eq, Show)+ -- | The merge policy for a LSM level can be either tiering or levelling. -- In this design we use levelling for the last level, and tiering for -- all other levels. The first level always uses tiering however, even if@@ -288,18 +299,34 @@                      -> PendingMerge s pattern PendingMerge mt prs ts <- (pendingContent -> (mt, prs, ts)) -type Run    = Map Key Entry-type Buffer = Map Key Entry--bufferToRun :: Buffer -> Run-bufferToRun = id+newtype Run = Run { runEntries :: Map Key Entry }+  deriving newtype Show  runSize :: Run -> Int-runSize = Map.size+runSize = Map.size . runEntries -bufferSize :: Buffer -> Int-bufferSize = Map.size+lookupRun :: Key -> Run -> Maybe Entry+lookupRun k = Map.lookup k . runEntries +newtype WriteBuffer = WriteBuffer { bufferEntries :: Map Key Entry }++emptyWriteBuffer :: WriteBuffer+emptyWriteBuffer = WriteBuffer Map.empty++writeBufferSize :: WriteBuffer -> Int+writeBufferSize = Map.size . bufferEntries++insertWriteBuffer :: Key -> Entry -> WriteBuffer -> WriteBuffer+insertWriteBuffer k e = WriteBuffer . Map.insertWith combine k e . bufferEntries++lookupWriteBuffer :: Key -> WriteBuffer -> Maybe Entry+lookupWriteBuffer k = Map.lookup k . bufferEntries++-- | Flush a write buffer. In the real implementation, this involves IO.+-- Note that we should never flush an empty write buffer.+flushWriteBuffer :: WriteBuffer -> Run+flushWriteBuffer (WriteBuffer m) = assert (not (null m)) (Run m)+ type Entry = Update Value Blob  newtype Key = K Int@@ -340,9 +367,12 @@     levelsInvariant :: Int -> Levels s -> ST s ()     levelsInvariant !_ [] = pure () +    levelsInvariant !_ (EmptyLevel : ls) = do+      assertST $ not (null ls)  -- last level shouldn't be empty+     levelsInvariant !ln (Level ir rs : ls) = do       mrs <- case ir of-        Single r ->+        Single _ r ->           pure (CompletedMerge r)         Merging mp _ _ (MergingRun mt _ ref) -> do           assertST $ ln > 1  -- no merges on level 1@@ -356,24 +386,35 @@        levelsInvariant (ln+1) ls -    -- All runs within a level "proper" (as opposed to the incoming runs+    -- All regular runs within a level "proper" (as opposed to the incoming runs     -- being merged) should be of the correct size for the level.     expectedRunLengths :: Int -> [Run] -> [Level s] -> ST s ()     expectedRunLengths ln rs ls =       case mergePolicyForLevel ln ls ul of-        -- Levels using levelling have only one (incoming) run, which almost-        -- always consists of an ongoing merge. The exception is when a-        -- levelling run becomes too large and is promoted, in that case-        -- initially there's no merge, but it is still represented as an-        -- 'IncomingRun', using 'Single'. Thus there are no other resident runs.-        LevelLevelling -> assertST $ null rs && null ls-        -- Runs in tiering levels usually fit that size, but they can be one-        -- larger, if a run has been held back (creating a (T+1)-way merge).-        LevelTiering   -> assertST $ all (\r -> runToLevelNumber LevelTiering conf r `elem` [ln, ln+1]) rs-        -- (This is actually still not really true, but will hold in practice.-        -- In the pathological case, all runs passed to the next level can be-        -- factor ((T+1)/T) too large, and there the same holding back can lead to-        -- factor ((T+2)/T) etc., until at level 12 a run is two levels too large.+        LevelLevelling -> do+          -- Levelling can only occur on the last level.+          assertST $ null ls+          -- Levels using levelling have only one (incoming) run, which almost+          -- always consists of an ongoing merge. The exception is when a+          -- levelling run becomes too large and is promoted, in that case+          -- initially there's no merge, but it is still represented as an+          -- 'IncomingRun', using 'Single'. Thus there are no other resident+          -- runs.+          assertST $ null rs+        LevelTiering -> do+          -- There are no empty runs in tiering levels, as they are either:+          -- 1. a mid-level, so deletes can't be dropped and merges can't result+          --    in empty runs+          -- 2. the first level, so their runs come directly from flushing the+          --    write buffer.+          assertST $ all (\r -> runSize r > 0) rs+          -- Runs in tiering levels usually fit that level's size, but they can+          -- be slightly larger if a run has been held back (creating a+          -- (T+1)-way merge).+          --+          -- TODO: Holding back runs can theoretically result in runs that are+          -- more than one size too large. See issue #829.+          assertST $ all (\r -> runToLevelNumber LevelTiering conf r `elem` [ln, ln+1]) rs      -- Incoming runs being merged also need to be of the right size, but the     -- conditions are more complicated.@@ -383,12 +424,23 @@       case mergePolicyForLevel ln ls ul of         LevelLevelling -> do           case (ir, mrs) of-            -- A single incoming run (which thus didn't need merging) must be-            -- of the expected size range already-            (Single r, m) -> do+            -- A single incoming run (which didn't need merging) in a levelling+            -- level can be created one of two ways:+            -- 1. when a levelling level is full, so the run gets promoted to+            --    the new last level+            -- 2. when the union level gets migrated.+            --+            -- In the latter case, it can be arbitrarily small.+            (Single origin r, m) -> do               assertST $ case m of CompletedMerge{} -> True                                    OngoingMerge{}   -> False-              assertST $ runToLevelNumber LevelLevelling conf r == ln+              case origin of+                FromWriteBuffer ->+                  assertST False  -- we don't flush to levelling levels+                FromLevellingLevel ->+                  assertST $ runToLevelNumber LevelLevelling conf r == ln+                FromMigratedUnion ->+                  assertST $ runToLevelNumber LevelLevelling conf r <= ln              -- A completed merge for levelling can be of almost any size at all!             -- It can be smaller, due to deletions in the last level. But it@@ -415,9 +467,10 @@           case (ir, mrs, mergeTypeForLevel ls ul) of             -- A single incoming run (which thus didn't need merging) must be             -- of the expected size already-            (Single r, m, _) -> do+            (Single origin r, m, _) -> do               assertST $ case m of CompletedMerge{} -> True                                    OngoingMerge{}   -> False+              assertST $ origin == FromWriteBuffer               assertST $ runToLevelNumber LevelTiering conf r == ln              -- A completed last level run can be of almost any smaller size due@@ -891,7 +944,7 @@     assertST $ length runs > 1     -- in some cases, no merging is required at all     (debt, state) <- case filter (\r -> runSize r > 0) runs of-      []  -> let (r:_) = runs -- just reuse the empty input+      []  -> let r = head runs -- just reuse the empty input               in pure (runSize r, CompletedMerge r)       [r] -> pure (runSize r, CompletedMerge r)       rs  -> do@@ -904,8 +957,10 @@  mergek :: IsMergeType t => t -> [Run] -> Run mergek t =-      (if isLastLevel t then Map.filter (/= Delete) else id)+      Run+    . (if isLastLevel t then Map.filter (/= Delete) else id)     . Map.unionsWith (if isUnion t then combineUnion else combine)+    . map runEntries  -- | Combines two entries that have been performed after another. Therefore, the -- newer one overwrites the old one (or modifies it for 'Mupsert'). Only take a@@ -992,7 +1047,7 @@   | otherwise = do       traceWith tr $ NewTableEvent tid conf       c   <- newSTRef 0-      lsm <- newSTRef (LSMContent Map.empty [] NoUnion)+      lsm <- newSTRef (LSMContent emptyWriteBuffer [] NoUnion)       pure (LSMHandle tid c conf lsm)  inserts :: Tracer (ST s) Event -> LSM s -> [(Key, Value, Maybe Blob)] -> ST s ()@@ -1026,19 +1081,23 @@ update tr (LSMHandle tid scr conf lsmr) k entry = do     traceWith tr $ UpdateEvent tid k entry     sc <- readSTRef scr-    content@(LSMContent wb ls unionLevel) <- readSTRef lsmr+    content@(LSMContent wb ls ul) <- readSTRef lsmr     modifySTRef' scr (+1)     supplyCreditsLevels (NominalCredit 1) ls     invariant conf content-    let wb' = Map.insertWith combine k entry wb-    if bufferSize wb' >= maxWriteBufferSize conf+    let wb' = insertWriteBuffer k entry wb+    if writeBufferSize wb' >= maxWriteBufferSize conf       then do-        ls' <- increment (LevelEvent tid >$< tr) sc conf (bufferToRun wb') ls unionLevel-        let content' = LSMContent Map.empty ls' unionLevel+        -- The merging tree might have become completed. If so, we want to+        -- migrate it now, so it can become part of new merges.+        (ls', ul') <- migrateUnionLevel (LevelEvent tid >$< tr) sc conf ls ul+        let r = flushWriteBuffer wb'+        ls'' <- increment (LevelEvent tid >$< tr) sc conf r ls' ul'+        let content' = LSMContent emptyWriteBuffer ls'' ul'         invariant conf content'         writeSTRef lsmr content'       else-        writeSTRef lsmr (LSMContent wb' ls unionLevel)+        writeSTRef lsmr (LSMContent wb' ls ul)  supplyMergeCredits :: LSM s -> NominalCredit -> ST s () supplyMergeCredits (LSMHandle _ scr conf lsmr) credits = do@@ -1099,7 +1158,7 @@       Just tree -> do         debt <- fst <$> remainingDebtMergingTree tree         Union tree <$> newSTRef debt-    lsmr <- newSTRef (LSMContent Map.empty [] unionLevel)+    lsmr <- newSTRef (LSMContent emptyWriteBuffer [] unionLevel)     c    <- newSTRef 0     pure (LSMHandle childTid c conf lsmr) @@ -1116,12 +1175,17 @@  -- | Return the current union debt. This debt can be reduced until it is paid -- off using 'supplyUnionCredits'.+--+-- As long as there is a union level, there is a non-zero debt. This makes it+-- clear that 'supplyUnionCredits' should still be called to trigger migration,+-- even if the merging tree itself has been completed. This becomes necessary+-- when the tree has been completed by operations on another table via sharing. remainingUnionDebt :: LSM s -> ST s UnionDebt remainingUnionDebt (LSMHandle _ _ _conf lsmr) = do     LSMContent _ _ ul <- readSTRef lsmr     UnionDebt <$> case ul of       NoUnion      -> pure 0-      Union tree d -> checkedUnionDebt tree d+      Union tree d -> (+1) <$> checkedUnionDebt tree d  -- | Credits are used to pay off 'UnionDebt', completing a 'union' in the -- process.@@ -1142,43 +1206,31 @@ -- This function returns any surplus of union credits as /leftover/ credits when -- a union has finished. In particular, if the returned number of credits is -- non-negative, then the union is finished.-supplyUnionCredits :: LSM s -> UnionCredits -> ST s UnionCredits-supplyUnionCredits (LSMHandle _ scr conf lsmr) (UnionCredits credits)-  | credits <= 0 = pure (UnionCredits 0)-  | otherwise = do-    content@(LSMContent _ _ ul) <- readSTRef lsmr-    UnionCredits <$> case ul of-      NoUnion ->-        pure credits-      Union tree debtRef -> do-        modifySTRef' scr (+1)-        _debt <- checkedUnionDebt tree debtRef  -- just to make sure it's checked-        c' <- supplyCreditsMergingTree credits tree-        debt' <- checkedUnionDebt tree debtRef-        when (debt' > 0) $-          assertST $ c' == 0  -- should have spent these credits-        invariant conf content-        pure c'---- TODO: At some point the completed merging tree should to moved into the--- regular levels, so it can be merged with other runs and last level merges can--- happen again to drop deletes. Also, lookups then don't need to handle the--- merging tree any more. There are two possible strategies:------ 1. As soon as the merging tree completes, move the resulting run to the---    regular levels. However, its size does generally not fit the last level,---    which requires relaxing 'invariant' and adjusting 'increment'.------    If the run is much larger than the resident and incoming runs of the last---    level, it should also not be included into a merge yet, as that merge---    would be expensive, but offer very little potential for compaction (the---    run from the merging tree is already compacted after all). So it needs to---    be bumped to the next level instead.------ 2. Initially leave the completed run in the union level. Then every time a---    new last level merge is created in 'increment', check if there is a---    completed run in the union level with a size that fits the new merge. If---    yes, move it over.+supplyUnionCredits :: Tracer (ST s) Event -> LSM s -> UnionCredits -> ST s UnionCredits+supplyUnionCredits tr (LSMHandle tid scr conf lsmr) (UnionCredits credits) = do+    traceWith tr $ SupplyUnionCreditsEvent tid credits+    if credits <= 0+      then+        pure (UnionCredits 0)+      else do+        content@(LSMContent wb ls ul) <- readSTRef lsmr+        UnionCredits <$> case ul of+          NoUnion ->+            pure credits+          Union tree debtRef -> do+            invariant conf content+            sc <- readSTRef scr+            modifySTRef' scr (+1)+            _debt <- checkedUnionDebt tree debtRef  -- make sure it's checked+            c' <- supplyCreditsMergingTree credits tree+            debt' <- checkedUnionDebt tree debtRef+            when (debt' > 0) $+              assertST $ c' == 0  -- we should have spent these credits+            (ls', ul') <- migrateUnionLevel (LevelEvent tid >$< tr) sc conf ls ul+            let content' = LSMContent wb ls' ul'+            invariant conf content'+            writeSTRef lsmr content'+            pure c'  -- | Like 'remainingDebtMergingTree', but additionally asserts that the debt -- never increases.@@ -1190,6 +1242,53 @@     writeSTRef debtRef debt     pure debt +-- | At some point the completed merging tree should become part of the regular+-- levels, so it can be merged with other runs. Otherwise, we could never+-- perform a last level merge, which is especially important for compaction, as+-- it allows us to drop deletes. Also, lookups then don't need to consider the+-- merging tree any more.+--+-- We can do this as soon as the tree is completed by appending a new regular+-- level containing only the run that resulted from the tree merge. If the+-- run is too large to fit into the level directly after the existing ones, we+-- first add empty levels.+migrateUnionLevel :: forall s. Tracer (ST s) (EventAt EventDetail)+                  -> Counter -> LSMConfig -> Levels s -> UnionLevel s+                  -> ST s (Levels s, UnionLevel s)+migrateUnionLevel _ _ _ ls NoUnion = do+    -- nothing to do+    pure (ls, NoUnion)+migrateUnionLevel tr sc conf ls ul@(Union t _) =+    getCompletedMergingTree t >>= \case+      Nothing ->+        -- Still in progress, leave it.+        pure (ls, ul)+      Just r -> do+        -- Before migration, there is usually a last regular (i.e. non-union)+        -- level, which uses tiering. We could potentially add the completed+        -- union (or rather the run it resulted in) directly to the resident+        -- tiering runs of that last regular level, but that would clash with+        -- some invariants. Instead, we always create a new last level, which+        -- only makes a small difference, but keeps the invariants simpler.+        --+        -- Also note even empty runs get migrated. This doesn't violate any+        -- invariants, as levelling levels can already contain empty runs.+        -- Dropping the run would be more complicated since the previous level+        -- then suddenly becomes the last one while potentially still containing+        -- midlevel merges.+        let levelNo = maximum+              [ 2              -- First level is tiering, don't migrate there.+              , length ls + 1  -- Must come after existing levels.+              , runToLevelNumber LevelLevelling conf r+                               -- Run must not be too large for the level.+              ]+        let emptyLevels = replicate (levelNo - 1 - length ls) EmptyLevel+        let level = Level (Single FromMigratedUnion r) []+        let levels = ls ++ emptyLevels ++ [level]+        assertST $ length levels == levelNo+        traceWith tr $ EventAt sc levelNo $ UnionLevelMigratedEvent r+        pure (levels, NoUnion)+ ------------------------------------------------------------------------------- -- Lookups --@@ -1212,9 +1311,9 @@ -- -- In the real implementation, this is done not on an individual 'LookupAcc', -- but one for each key, i.e. @Vector (Maybe Entry)@.-doLookup :: Buffer -> [Run] -> UnionLevel s -> Key -> ST s (LookupResult Value Blob)+doLookup :: WriteBuffer -> [Run] -> UnionLevel s -> Key -> ST s (LookupResult Value Blob) doLookup wb runs ul k = do-    let acc0 = lookupBatch (Map.lookup k wb) k runs+    let acc0 = lookupBatch (lookupWriteBuffer k wb) k runs     case ul of       NoUnion ->         pure (convertAcc acc0)@@ -1222,7 +1321,7 @@         treeBatches <- buildLookupTree tree         let treeResults = lookupBatch Nothing k <$> treeBatches         pure $ convertAcc $ foldLookupTree $-          if null wb && null runs+          if writeBufferSize wb == 0 && null runs           then treeResults           else LookupNode MergeLevel [LookupBatch acc0, treeResults ]   where@@ -1239,7 +1338,7 @@ -- In a real implementation, this would take all keys at once and be in IO. lookupBatch :: LookupAcc -> Key -> [Run] -> LookupAcc lookupBatch acc k rs =-    let entries = [entry | r <- rs, Just entry <- [Map.lookup k r]]+    let entries = [entry | r <- rs, Just entry <- [lookupRun k r]]     in foldl (updateAcc combine) acc entries  data LookupTree a = LookupBatch a@@ -1333,11 +1432,12 @@ -- inserting without calling 'supplyUnionCredits'. supplyCreditsLevels :: NominalCredit -> Levels s -> ST s () supplyCreditsLevels nominalDeposit =-  traverse_ $ \(Level ir _rs) -> do-    case ir of-      Single{} -> pure ()-      Merging _mp nominalDebt nominalCreditVar-              mr@(MergingRun _  physicalDebt _) -> do+  traverse_ $ \lvl -> do+    case lvl of+      EmptyLevel -> pure ()+      Level Single{} _ -> pure ()+      Level (Merging _mp nominalDebt nominalCreditVar+                     mr@(MergingRun _  physicalDebt _)) _ -> do          nominalCredit       <- depositNominalCredit                                  nominalDebt nominalCreditVar nominalDeposit@@ -1438,9 +1538,17 @@       where         tr' = contramap (EventAt sc ln) tr +    go !ln incoming (EmptyLevel : ls) = do+        assertST $ mergePolicyForLevel ln ls ul == LevelTiering  -- mid level+        traceWith tr' LevelWasEmptyEvent+        ir <- newLevelMerge tr' conf ln LevelTiering (mergeTypeFor ls) incoming+        pure (Level ir [] : ls)+      where+        tr' = contramap (EventAt sc ln) tr+     go !ln incoming (Level ir rs : ls) = do       r <- case ir of-        Single r -> do+        Single _ r -> do           traceWith tr' $ SingleRunCompletedEvent r           pure r         Merging mergePolicy _ _ mr -> do@@ -1511,10 +1619,18 @@               -> LSMConfig               -> Int -> MergePolicyForLevel -> LevelMergeType               -> [Run] -> ST s (IncomingRun s)-newLevelMerge tr _ _ _ _ [r] =  do+newLevelMerge tr _ ln mergePolicy _ [r] = do+    origin <-+      if ln <= 1+        then do+          assertST $ ln == 1+          pure FromWriteBuffer+        else do+          assertST $ mergePolicy == LevelLevelling+          pure FromLevellingLevel     traceWith tr $ NewSingleRunEvent r-    pure (Single r)-newLevelMerge tr conf@LSMConfig{..} level mergePolicy mergeType rs = do+    pure (Single origin r)+newLevelMerge tr conf@LSMConfig{..} ln mergePolicy mergeType rs = do     mergingRun@(MergingRun _ physicalDebt _) <- newMergingRun mergeType rs     traceWith tr NewLevelMergeEvent {                    mergePolicy,@@ -1530,7 +1646,7 @@     -- The nominal debt equals the minimum of credits we will supply before we     -- expect the merge to complete. This is the same as the number of updates     -- in a run that gets moved to this level.-    nominalDebt = NominalDebt (levelNumberToMaxRunSize LevelTiering conf level)+    nominalDebt = NominalDebt (levelNumberToMaxRunSize LevelTiering conf ln)      -- The physical debt is the number of actual merge steps we will need to     -- perform before the merge is complete. This is always the sum of the@@ -1548,16 +1664,16 @@         LevelLevelling ->           -- Incoming runs, which may be slightly overfull with respect to the           -- previous level-          configSizeRatio * levelNumberToMaxRunSize LevelTiering conf level+          configSizeRatio * levelNumberToMaxRunSize LevelTiering conf ln               -- The single run that was already on this level-            + levelNumberToMaxRunSize LevelLevelling conf level+            + levelNumberToMaxRunSize LevelLevelling conf ln         LevelTiering   ->           -- Incoming runs, which may be slightly overfull with respect to the           -- previous level-          configSizeRatio * levelNumberToMaxRunSize LevelTiering conf level+          configSizeRatio * levelNumberToMaxRunSize LevelTiering conf ln               -- Held back run that is underfull with respect to the current               -- level-            + levelNumberToMaxRunSize LevelTiering conf (level - 1)+            + levelNumberToMaxRunSize LevelTiering conf (ln - 1)  ------------------------------------------------------------------------------- -- MergingTree abstraction@@ -1600,24 +1716,19 @@  -- | Ensures that the merge contains more than one input, avoiding creating a -- pending merge where possible.-newPendingLevelMerge :: [IncomingRun s]+newPendingLevelMerge :: [PreExistingRun s]                      -> Maybe (MergingTree s)                      -> ST s (Maybe (MergingTree s)) newPendingLevelMerge [] t = pure t-newPendingLevelMerge [Single r] Nothing =+newPendingLevelMerge [PreExistingRun r] Nothing =     Just . MergingTree <$> newSTRef (CompletedTreeMerge r)-newPendingLevelMerge [Merging{}] Nothing =+newPendingLevelMerge [PreExistingMergingRun{}] Nothing =     -- This case should never occur. If there is a single entry in the list,     -- there can only be one level in the input table. At level 1 there are no-    -- merging runs, so it must be a PreExistingRun.+    -- merging runs, it must be a Single run flushed from a write buffer.     error "newPendingLevelMerge: singleton Merging run"-newPendingLevelMerge irs tree = do-    let prs = map incomingToPreExistingRun irs-        st  = PendingTreeMerge (PendingLevelMerge prs tree)-    Just . MergingTree <$> newSTRef st-  where-    incomingToPreExistingRun (Single         r) = PreExistingRun r-    incomingToPreExistingRun (Merging _ _ _ mr) = PreExistingMergingRun mr+newPendingLevelMerge prs t = do+    Just . MergingTree <$> newSTRef (PendingTreeMerge (PendingLevelMerge prs t))  -- | Ensures that the merge contains more than one input. newPendingUnionMerge :: [MergingTree s] -> ST s (Maybe (MergingTree s))@@ -1629,15 +1740,20 @@  contentToMergingTree :: LSMContent s -> ST s (Maybe (MergingTree s)) contentToMergingTree (LSMContent wb ls ul) =-    newPendingLevelMerge (buffers ++ levels) trees+    newPendingLevelMerge (maybeToList buffer ++ levels) trees   where     -- flush the write buffer (but this should not modify the content)-    buffers-      | bufferSize wb == 0 = []-      | otherwise          = [Single (bufferToRun wb)]+    buffer+      | writeBufferSize wb == 0 = Nothing+      | otherwise               = Just (PreExistingRun (flushWriteBuffer wb)) -    levels = flip concatMap ls $ \(Level ir rs) -> ir : map Single rs+    levels = flip concatMap ls $ \case+               EmptyLevel  -> []+               Level ir rs -> toPreExisting ir : map PreExistingRun rs +    toPreExisting (Single  _      r) = PreExistingRun r+    toPreExisting (Merging _ _ _ mr) = PreExistingMergingRun mr+     trees = case ul of         NoUnion   -> Nothing         Union t _ -> Just t@@ -1791,6 +1907,12 @@ expectCompletedMergingTree :: HasCallStack => MergingTree s -> ST s Run expectCompletedMergingTree = expectInvariant . isCompletedMergingTree +getCompletedMergingTree :: MergingTree s -> ST s (Maybe Run)+getCompletedMergingTree t =+    evalInvariant (isCompletedMergingTree t) >>= \case+      Left _  -> pure Nothing+      Right r -> pure (Just r)+ ------------------------------------------------------------------------------- -- Measurements --@@ -1799,7 +1921,7 @@              | MNode TreeMergeType [MTree r]   deriving stock (Eq, Foldable, Functor, Show) -allLevels :: LSM s -> ST s (Buffer, [[Run]], Maybe (MTree Run))+allLevels :: LSM s -> ST s (WriteBuffer, [[Run]], Maybe (MTree Run)) allLevels (LSMHandle _ _ _conf lsmr) = do     LSMContent wb ls ul <- readSTRef lsmr     rs <- flattenLevels ls@@ -1812,11 +1934,12 @@ flattenLevels = mapM flattenLevel  flattenLevel :: Level s -> ST s [Run]+flattenLevel EmptyLevel    = pure [] flattenLevel (Level ir rs) = (++ rs) <$> flattenIncomingRun ir  flattenIncomingRun :: IncomingRun s -> ST s [Run] flattenIncomingRun = \case-    Single r         -> pure [r]+    Single _ r       -> pure [r]     Merging _ _ _ mr -> flattenMergingRun mr  flattenMergingRun :: MergingRun t s -> ST s [Run]@@ -1851,8 +1974,9 @@     (wb, levels, tree) <- allLevels lsm     let r = mergek               MergeLevel-              (wb : concat levels ++ toList (mergeTree <$> tree))-    pure (Map.mapMaybe justInsert r)+              (Run (bufferEntries wb)  -- we don't flush, but treat wb as a run+                : concat levels ++ toList (mergeTree <$> tree))+    pure (Map.mapMaybe justInsert (runEntries r))   where     mergeTree :: MTree Run -> Run     mergeTree (MLeaf r)     = r@@ -1862,7 +1986,7 @@     justInsert  Delete      = Nothing     justInsert (Mupsert v)  = Just (v, Nothing) -type Representation = (Run, [LevelRepresentation], Maybe (MTree Run))+type Representation = (WriteBuffer, [LevelRepresentation], Maybe (MTree Run))  type LevelRepresentation =     (Maybe (MergePolicyForLevel, NominalDebt, NominalCredit,@@ -1879,7 +2003,9 @@     pure (wb, levels, tree)  dumpLevel :: Level s -> ST s LevelRepresentation-dumpLevel (Level (Single r) rs) =+dumpLevel EmptyLevel =+    pure (Nothing, [])+dumpLevel (Level (Single _ r) rs) =     pure (Nothing, (r:rs)) dumpLevel (Level (Merging mp nd ncv (MergingRun mt _ ref)) rs) = do     mrs <- readSTRef ref@@ -1892,31 +2018,30 @@ representationShape :: Representation                     -> (Int, [([Int], [Int])], Maybe (MTree Int)) representationShape (wb, levels, tree) =-    (summaryRun wb, map summaryLevel levels, fmap (fmap summaryRun) tree)+    (writeBufferSize wb, map summaryLevel levels, fmap (fmap runSize) tree)   where     summaryLevel (mmr, rs) =       let (ongoing, complete) = summaryMR mmr-      in (ongoing, complete <> map summaryRun rs)--    summaryRun = runSize+      in (ongoing, complete <> map runSize rs)      summaryMR = \case       Nothing                          -> ([], [])-      Just (_, _, _, _, CompletedMerge r)    -> ([], [summaryRun r])-      Just (_, _, _, _, OngoingMerge _ rs _) -> (map summaryRun rs, [])+      Just (_, _, _, _, CompletedMerge r)    -> ([], [runSize r])+      Just (_, _, _, _, OngoingMerge _ rs _) -> (map runSize rs, [])  ------------------------------------------------------------------------------- -- Tracing --  -- TODO: these events are incomplete, in particular we should also trace what--- happens in the union level.+-- happens in the union level. Somewhat related: issue #445. data Event =     NewTableEvent TableId LSMConfig   | UpdateEvent TableId Key Entry   | LookupEvent TableId Key   | DuplicateEvent TableId TableId   | UnionsEvent TableId [TableId]+  | SupplyUnionCreditsEvent TableId Credit   | LevelEvent TableId (EventAt EventDetail)   deriving stock Show @@ -1946,9 +2071,12 @@       }   | SingleRunCompletedEvent Run +  | UnionLevelMigratedEvent Run+   | RunTooSmallForLevelEvent MergePolicyForLevel Run   | LevelIsFullEvent MergePolicyForLevel   | LevelIsNotFullEvent MergePolicyForLevel+  | LevelWasEmptyEvent   deriving stock Show  -------------------------------------------------------------------------------@@ -1974,8 +2102,26 @@       , (1, pure Delete)       ] +instance QC.Arbitrary Run where+  arbitrary = Run <$> QC.arbitrary+  shrink = map Run . QC.shrink . runEntries+ instance QC.Arbitrary LevelMergeType where   arbitrary = QC.elements [MergeMidLevel, MergeLastLevel]  instance QC.Arbitrary TreeMergeType where   arbitrary = QC.elements [MergeLevel, MergeUnion]++instance QC.Arbitrary LSMConfig where+  arbitrary = do+      configMaxWriteBufferSize <- QC.chooseInt (1, 10)+      configSizeRatio <- QC.chooseInt (2, 8)+      pure LSMConfig {configMaxWriteBufferSize, configSizeRatio}+  shrink (LSMConfig size ratio) =+      [ LSMConfig size' ratio'+      | (size', ratio') <- QC.liftShrink2 QC.shrink shrinkSizeRatio (size, ratio)+      , size' >= 1+      ]+    where+      shrinkSizeRatio 4 = []+      shrinkSizeRatio _ = [4]  -- try shrinking to four, the default ratio
test-prototypes/Main.hs view
@@ -5,6 +5,7 @@ import qualified Test.FormatPage import qualified Test.ScheduledMerges import qualified Test.ScheduledMerges.RunSizes+import qualified Test.ScheduledMergesDL import qualified Test.ScheduledMergesQLS  main :: IO ()@@ -13,4 +14,5 @@     , Test.ScheduledMerges.tests     , Test.ScheduledMerges.RunSizes.tests     , Test.ScheduledMergesQLS.tests+    , Test.ScheduledMergesDL.tests     ]
test-prototypes/Test/ScheduledMerges.hs view
@@ -13,7 +13,8 @@ import           ScheduledMerges as LSM  import qualified Test.QuickCheck as QC-import           Test.QuickCheck (Arbitrary (arbitrary, shrink), Property)+import           Test.QuickCheck (Arbitrary (arbitrary, shrink), Property,+                     (.&&.)) import           Test.QuickCheck.Exception (isDiscard) import           Test.Tasty import           Test.Tasty.HUnit (HasCallStack, testCase)@@ -24,7 +25,7 @@ tests = testGroup "Test.ScheduledMerges"     [ testCase "test_regression_empty_run" test_regression_empty_run     , testCase "test_merge_again_with_incoming" test_merge_again_with_incoming-    , testProperty "prop_union" prop_union+    , testProperty "prop_union_complete" prop_union_complete     , testGroup "T"         [ localOption (QuickCheckTests 1000) $  -- super quick, run more             testProperty "Arbitrary satisfies invariant" prop_arbitrarySatisfiesInvariant@@ -176,35 +177,114 @@ -- properties -- --- | Supplying enough credits for the remaining debt completes the union merge.-prop_union :: [[(LSM.Key, LSM.Entry)]] -> Property-prop_union kess = length (filter (not . null) kess) > 1 QC.==>+-- | Supplying enough credits for the remaining debt completes the union merge+-- (as externally observable through 'LSM.remainingUnionDebt'). However, a+-- special union level remains.+prop_union_complete :: LSMConfig -> NestedUnionData -> Property+prop_union_complete conf nestedUnionData =     QC.ioProperty $ runWithTracer $ \tr ->       stToIO $ do-        ts <- traverse (uncurry $ mkTable tr) (zip [LSM.TableId 0..] kess)-        t <- LSM.unions tr (LSM.TableId (length kess)) ts+        tidCounter <- newSTRef (LSM.TableId 0)+        t <- mkNestedUnion tr conf tidCounter nestedUnionData +        rep <- dumpRepresentation t         debt@(UnionDebt x) <- LSM.remainingUnionDebt t-        _ <- LSM.supplyUnionCredits t (UnionCredits x)++        leftovers <- LSM.supplyUnionCredits tr t (UnionCredits x)++        rep' <- dumpRepresentation t         debt' <- LSM.remainingUnionDebt t -        rep <- dumpRepresentation t         pure $ QC.counterexample (show (debt, debt')) $ QC.conjoin-          [ debt =/= UnionDebt 0-          , debt' === UnionDebt 0-          , hasUnionWith isCompleted rep+          [ QC.counterexample "before" $+              -- The input is a non-empty list of structurally non-empty tables+              -- (i.e. they have runs or at least a non-empty write buffer).+              -- Therefore there must be a merging tree.+              debt =/= UnionDebt 0+              .&&. hasUnionLevelWith (not . isCompleted) rep+          , QC.counterexample "after" $+              debt' === UnionDebt 0+              .&&. hasNoUnionLevel rep'+          , QC.counterexample "leftovers" $+              leftovers >= 0           ]   where     isCompleted = \case         MLeaf{} -> True         MNode{} -> False -mkTable :: Tracer (ST s) Event -> LSM.TableId -> [(LSM.Key, LSM.Entry)] -> ST s (LSM s)-mkTable tr tid ks = do-    t <- LSM.new tr tid++-- | For simplicity, this is not a recursive structure. We just nest once, or+-- not at all if there is just a single 'UnionData'.+newtype NestedUnionData = NestedUnionData [UnionData]+  deriving stock Show++instance Arbitrary NestedUnionData where+  arbitrary = do+    numUnionInputs <- QC.chooseInt (1, 10)+    NestedUnionData <$> QC.vectorOf numUnionInputs arbitrary++  shrink (NestedUnionData unionInputs) =+    [ NestedUnionData unionInputs'+    | unionInputs' <- shrink unionInputs+    , not (null unionInputs')+    ]++-- | Inputs to a union, plus some extra updates to perform on the result.+-- Note that we want at least two inputs, so there is some merging required.+data UnionData = UnionData [(LSM.Key, LSM.Entry)] [TableData]+  deriving stock Show++unionDataInvariant :: UnionData -> Bool+unionDataInvariant (UnionData _ tableInputs) = length tableInputs >= 2++instance Arbitrary UnionData where+  arbitrary = do+    numUnionInputs <- QC.oneof [pure 2, QC.chooseInt (3, 6)]+    UnionData <$> arbitrary <*> QC.vectorOf numUnionInputs arbitrary++  shrink (UnionData kes tableInputs) =+    [ data'+    | (kes', tableInputs') <- shrink (kes, tableInputs)+    , let data' = UnionData kes' tableInputs'+    , unionDataInvariant data'+    ]++newtype TableData = TableData [(LSM.Key, LSM.Entry)]+  deriving stock Show+  deriving Arbitrary+    via QC.NonEmptyList (LSM.Key, LSM.Entry)++mkNestedUnion :: Tracer (ST s) Event -> LSMConfig -> STRef s LSM.TableId+              -> NestedUnionData -> ST s (LSM s)+mkNestedUnion tr conf tidCounter (NestedUnionData unionInputs) = do+    tid <- freshTableId tidCounter+    ts <- traverse (mkUnion tr conf tidCounter) unionInputs+    LSM.unions tr tid ts++mkUnion :: Tracer (ST s) Event -> LSMConfig -> STRef s LSM.TableId+        -> UnionData -> ST s (LSM s)+mkUnion tr conf tidCounter (UnionData kes tableInputs) = do+    tid <- freshTableId tidCounter+    ts <- traverse (mkTable tr conf tidCounter) tableInputs+    table <- LSM.unions tr tid ts+    LSM.updates tr table kes+    pure table++mkTable :: Tracer (ST s) Event -> LSMConfig -> STRef s LSM.TableId+        -> TableData -> ST s (LSM s)+mkTable tr conf tidCounter (TableData ks) = do+    tid <- freshTableId tidCounter+    t <- LSM.newWith tr tid conf     LSM.updates tr t ks     pure t +freshTableId :: STRef s LSM.TableId -> ST s LSM.TableId+freshTableId ref = do+    tid <- readSTRef ref+    modifySTRef' ref succ+    pure tid+ ------------------------------------------------------------------------------- -- tests for MergingTree --@@ -448,7 +528,7 @@ -- genMergeCreditForRuns :: [NonEmptyRun] -> QC.Gen (MergeDebt, MergeCredit) genMergeCreditForRuns rs = do-      let totalDebt    = sum (map (length . getNonEmptyRun) rs)+      let totalDebt    = sum (map (runSize . getNonEmptyRun) rs)       suppliedCredits <- QC.chooseInt (0, totalDebt-1)       unspentCredits  <- QC.chooseInt (0, min (mergeBatchSize-1) suppliedCredits)       let spentCredits = suppliedCredits - unspentCredits@@ -472,7 +552,7 @@ shrinkMergeCreditForRuns rs' MergeCredit {spentCredits, unspentCredits} =     [ assert (mergeDebtInvariant md' mc')       (md', mc')-    | let totalDebt'    = sum (map (length . getNonEmptyRun) rs')+    | let totalDebt'    = sum (map (runSize . getNonEmptyRun) rs')     , suppliedCredits' <- shrink (min (spentCredits+unspentCredits)                                       (totalDebt'-1))     , unspentCredits'  <- shrink (min unspentCredits suppliedCredits')@@ -487,8 +567,8 @@     ]  instance Arbitrary NonEmptyRun where-  arbitrary = NonEmptyRun <$> (arbitrary `QC.suchThat` (not . null))-  shrink (NonEmptyRun r) = [NonEmptyRun r' | r' <- shrink r, not (null r')]+  arbitrary = NonEmptyRun <$> (arbitrary `QC.suchThat` (\r -> runSize r > 0))+  shrink (NonEmptyRun r) = [NonEmptyRun r' | r' <- shrink r, runSize r' > 0]  prop_arbitrarySatisfiesInvariant :: T -> Property prop_arbitrarySatisfiesInvariant t =@@ -551,10 +631,16 @@         , "actual shape:   " <> show shape         ] -hasUnionWith :: (MTree Int -> Bool) -> Representation -> Property-hasUnionWith p rep = do+hasNoUnionLevel :: Representation -> Property+hasNoUnionLevel rep = do     let (_, _, shape) = representationShape rep-    QC.counterexample "expected suitable Union" $+    QC.counterexample "expected no union level" $+      Nothing === shape++hasUnionLevelWith :: (MTree Int -> Bool) -> Representation -> Property+hasUnionLevelWith p rep = do+    let (_, _, shape) = representationShape rep+    QC.counterexample "expected suitable union level" $       QC.counterexample (show shape) $         case shape of           Nothing -> False
+ test-prototypes/Test/ScheduledMergesDL.hs view
@@ -0,0 +1,47 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module Test.ScheduledMergesDL (tests) where++import           ScheduledMerges as LSM+import           Test.ScheduledMergesQLS as QLS (Action (..), Model, prop_LSM)++import           Test.QuickCheck (NonNegative (..), Property)+import           Test.QuickCheck.DynamicLogic+import           Test.QuickCheck.StateModel.Lockstep hiding (ModelOp)+import           Test.Tasty (TestTree, testGroup)+import           Test.Tasty.QuickCheck (testProperty)++tests :: TestTree+tests = testGroup "Test.ScheduledMergesDL" [+      testProperty "prop_empty_union" prop_empty_union+    ]++instance DynLogicModel (Lockstep Model)++prop_empty_union :: Property+prop_empty_union = forAllDL dl_empty_union prop_LSM++-- | Create a table with an empty run in the union level, then run arbitrary+-- actions to see if all invariants still hold.+dl_empty_union :: DL (Lockstep Model) ()+dl_empty_union = do+    -- Create a union level that will result in an empty run. This is easy to+    -- achieve if the inputs to the union only contain deletes (which will get+    -- removed in a last level merge, which the union merges are).+    tInput <- action $ ANew conf+    _ <- action $ ADelete (unsafeMkGVar tInput opIdentity) (Right (K 0))+    tUnion <- action $ AUnions [unsafeMkGVar tInput opIdentity, unsafeMkGVar tInput opIdentity]++    -- Perform some actions so tUnion gets regular levels as well.+    anyActions_++    -- Now complete the union.+    _ <- action $ ASupplyUnion (unsafeMkGVar tUnion opIdentity) (NonNegative (UnionCredits 100))++    anyActions_+  where+    conf =+      LSMConfig+        { configMaxWriteBufferSize = 2+        , configSizeRatio = 2+        }
test-prototypes/Test/ScheduledMergesQLS.hs view
@@ -3,7 +3,12 @@ {-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} -module Test.ScheduledMergesQLS (tests) where+module Test.ScheduledMergesQLS (+    tests+  , Model+  , prop_LSM+  , Action (..)+  ) where  import           Control.Monad.Reader (ReaderT (..)) import           Control.Monad.ST@@ -404,7 +409,7 @@     AUnions vars        -> do       tid <- incrTidVar tidVar       stToIO $ unions tr tid (map lookUpVar vars)-    ASupplyUnion var c  -> stToIO $ supplyUnionCredits (lookUpVar var) (getNonNegative c) >> pure ()+    ASupplyUnion var c  -> stToIO $ supplyUnionCredits tr (lookUpVar var) (getNonNegative c) >> pure ()     ADump      var      -> stToIO $ logicalValue (lookUpVar var)   where     lookUpVar :: ModelVar Model a -> a
test/Database/LSMTree/Model/Session.hs view
@@ -598,7 +598,7 @@       })  openTableFromSnapshot ::-     forall k v b m.(+     forall k v b m. (        MonadState Model m      , MonadError Err m      , C k v b
test/Test/Database/LSMTree/Internal/BloomFilter.hs view
@@ -44,6 +44,8 @@         prop_total_deserialisation_whitebox     , testProperty "bloomQueries (bulk)" $         prop_bloomQueries+    , testProperty "prop_packUnpack_RunIxKeyIx" prop_packUnpack_RunIxKeyIx+    , testProperty "prop_packUnpack_RunIxKeyIx_limits" prop_packUnpack_RunIxKeyIx_limits     ]  testSalt :: Bloom.Salt@@ -168,3 +170,50 @@        ===         map (\(RunIxKeyIx rix kix) -> (rix, kix))             (VP.toList (bloomQueries testSalt (V.fromList filters') (V.fromList keys')))++{-------------------------------------------------------------------------------+  RunIxKeyIx+-------------------------------------------------------------------------------}++-- | Test that 'RunIx' and 'KeyIx' roundtrip through the 'RunIxKeyIx' pattern+-- synonym+--+-- More specifically, if we apply a 'RunIxKeyIx' pattern synonym to a pair of+-- 'RunIx' and 'KeyIx', and then pattern match on it agains, then we would+-- expect to get the same 'RunIx' and 'KeyIx' out as the ones we put in.+--+-- There used to be a bug where this went wrong because of a typo in a bit-mask.+-- This property test should ensure that we catch such mistakes in the future.+-- See PR #841 for more information.+--+-- <PR https://github.com/IntersectMBO/lsm-tree/pull/841>+--+prop_packUnpack_RunIxKeyIx :: Int_0xffff -> Int_0xffff -> Property+prop_packUnpack_RunIxKeyIx r k =+    case RunIxKeyIx r.unwrap k.unwrap of+      RunIxKeyIx r' k' -> r.unwrap === r' .&&. k.unwrap === k'++-- | A variant of 'prop_packUnpack_RunIxKeyIx' applied to 'RunIx' and 'KeyIx'+-- that are close to their upper bounds.+prop_packUnpack_RunIxKeyIx_limits :: Property+prop_packUnpack_RunIxKeyIx_limits = conjoin [+      prop_packUnpack_RunIxKeyIx 0xffff       0xffff+    , prop_packUnpack_RunIxKeyIx (0xffff - 1) 0xffff+    , prop_packUnpack_RunIxKeyIx 0xffff       (0xffff - 1)+    , prop_packUnpack_RunIxKeyIx (0xffff - 1) (0xffff - 1)+    ]++-- | An Int in the inclusive range @(0, 0xffff)@+newtype Int_0xffff = Int_0xffff { unwrap :: Int }+  deriving stock (Show, Eq)+  deriving newtype Num++instance Arbitrary Int_0xffff where+  arbitrary = Int_0xffff <$> chooseInt (0, 0xffff)+  shrink x = [+        Int_0xffff y+      | y <- shrink x.unwrap+      , 0 <= y+      , y < 0xffff+      ]+
test/Test/Database/LSMTree/Internal/Snapshot/Codec/Golden.hs view
@@ -7,9 +7,11 @@   , Annotation   ) where +import           Codec.CBOR.Read (deserialiseFromBytes) import           Codec.CBOR.Write (toLazyByteString) import           Control.Monad (when)-import qualified Data.ByteString.Lazy as BSL (writeFile)+import           Control.Monad.Class.MonadThrow (Exception (displayException))+import qualified Data.ByteString.Lazy as BSL import qualified Data.Set as Set import           Data.Typeable import qualified Data.Vector as V@@ -38,14 +40,22 @@ import qualified Test.Tasty as Tasty import           Test.Tasty (TestTree, testGroup) import qualified Test.Tasty.Golden as Au+import           Test.Tasty.HUnit (Assertion, assertEqual, assertFailure,+                     testCase) import           Test.Tasty.QuickCheck (testProperty)  tests :: TestTree tests =-    handleOutputFiles $-    testGroup "Test.Database.LSMTree.Internal.Snapshot.Codec.Golden" $-         concat (forallSnapshotTypes snapshotCodecGoldenTest)-      ++ [testProperty "prop_noUnexpectedOrMissingGoldenFiles" prop_noUnexpectedOrMissingGoldenFiles]+    testGroup "Test.Database.LSMTree.Internal.Snapshot.Codec.Golden" [+        handleOutputFiles $+        testGroup "Generate golden files" $+            concat (forallSnapshotTypes snapshotCodecGoldenTest) +++          [ testProperty "prop_noUnexpectedOrMissingGoldenFiles" prop_noUnexpectedOrMissingGoldenFiles+          ]+      , testGroup "Backwards compatibility" [+            testCase "test_compatTableConfigV0" test_compatTableConfigV0+          ]+      ]  {-------------------------------------------------------------------------------   Configuration@@ -74,12 +84,12 @@   -> [TestTree] snapshotCodecGoldenTest proxy = [       go annotation datum-    | (annotation, datum) <- enumGoldenAnnotated' proxy+    | (annotation, datum) <- enumGoldenAnnotated' proxy v     ]   where+    v = currentSnapshotVersion     go ann datum =-      let v = currentSnapshotVersion-          outputFilePath = goldenDataFilePath </> filePathOutput proxy ann v+      let outputFilePath = goldenDataFilePath </> filePathOutput proxy ann v           goldenFilePath = goldenDataFilePath </> filePathGolden proxy ann v            -- IO actions@@ -99,10 +109,11 @@ -- | Check that are no missing or unexpected files in the output directory prop_noUnexpectedOrMissingGoldenFiles :: Property prop_noUnexpectedOrMissingGoldenFiles = once $ ioProperty $ do-    let expectedFiles = Set.fromList $ concat $ forallSnapshotTypes $ \p -> concat [-            filePathsGolden p v-          | v <- supportedVersions p-          ]+    let expectedFiles = Set.fromList $ concat $ forallSnapshotTypes $ \p ->+          concat [+              filePathsGolden p v+            | v <- allCompatibleSnapshotVersions+            ]      let hfs = ioHasFS goldenDataMountPoint     actualDirectoryEntries <- FS.listDirectory hfs (FS.mkFsPath [])@@ -122,6 +133,43 @@     pure $ propMissing .&&. propUnexpected  {-------------------------------------------------------------------------------+  Backwards compatibility tests+-------------------------------------------------------------------------------}++test_compatTableConfigV0 :: Assertion+test_compatTableConfigV0 =+    assertGoldenFileDecodesTo (Proxy @TableConfig) "A" V0 $+      TableConfig {+          confMergePolicy = singGolden V0+        , confMergeSchedule = singGolden V0+        , confSizeRatio = singGolden V0+        , confWriteBufferAlloc = singGolden V0+        , confBloomFilterAlloc = singGolden V0+        , confFencePointerIndex = singGolden V0+        , confDiskCachePolicy = singGolden V0+          -- MergeBatchSize not included in V0, decoder uses WriteBufferAlloc+        , confMergeBatchSize = MergeBatchSize magicNumber2+        }++-- | For types that changed their snapshot format between versions, we should+-- also test that we can in fact still decode the old format.+assertGoldenFileDecodesTo ::+     (DecodeVersioned a, Eq a, Show a, Typeable a)+  => Proxy a+  -> String+  -> SnapshotVersion+  -> a+  -> Assertion+assertGoldenFileDecodesTo proxy ann v expected = do+    let fp = goldenDataFilePath </> filePathGolden proxy ann v+    lbs <- BSL.readFile fp+    case deserialiseFromBytes (decodeVersioned v) lbs of+      Left err ->+        assertFailure $ "Error decoding " ++ fp ++ ": " ++ displayException err+      Right (_, decoded) ->+        assertEqual "" expected decoded++{-------------------------------------------------------------------------------   Mapping -------------------------------------------------------------------------------} @@ -206,31 +254,32 @@   --   -- The default implementation is to return a singleton list containing   -- 'singGolden'.-  enumGolden :: [a]-  enumGolden = [ singGolden ]+  enumGolden :: SnapshotVersion -> [a]+  enumGolden v = [ singGolden v ]    -- | Enumerated values with an annotation for naming purposes. The enumeration   -- should be /shallow/, and the annotations should be unique.   --   -- The default implementation is to annotate 'enumGolden' with capital letters   -- starting with @\'A\'@.-  enumGoldenAnnotated :: [(Annotation, a)]-  enumGoldenAnnotated = zip [[c] | c <- ['A' .. 'Z']] enumGolden+  enumGoldenAnnotated :: SnapshotVersion -> [(Annotation, a)]+  enumGoldenAnnotated v = zip [[c] | c <- ['A' .. 'Z']] (enumGolden v)    -- | A singleton enumerated value. This is mainly useful for superclass   -- instances.   --   -- The default implementation is to take the 'head' of 'enumGoldenAnnotated'.-  singGolden :: a-  singGolden = snd $ head enumGoldenAnnotated--  supportedVersions :: Proxy a -> [SnapshotVersion]-  supportedVersions _ = allCompatibleSnapshotVersions+  singGolden :: SnapshotVersion -> a+  singGolden v = snd $ head (enumGoldenAnnotated v)  type Annotation = String -enumGoldenAnnotated' :: EnumGolden a => Proxy a -> [(Annotation, a)]-enumGoldenAnnotated' _ = enumGoldenAnnotated+enumGoldenAnnotated' ::+     EnumGolden a+  => Proxy a+  -> SnapshotVersion+  -> [(Annotation, a)]+enumGoldenAnnotated' _ v = enumGoldenAnnotated v  {-------------------------------------------------------------------------------   Enumeration class: names and file paths@@ -250,7 +299,7 @@   -> [String] filePathsGolden p v = [       filePathGolden p annotation v-    | (annotation, _) <- enumGoldenAnnotated' p+    | (annotation, _) <- enumGoldenAnnotated' p v     ]  filePathOutput :: Typeable a => Proxy a -> String -> SnapshotVersion -> String@@ -264,13 +313,14 @@ -------------------------------------------------------------------------------}  instance EnumGolden SnapshotMetaData where-  singGolden = SnapshotMetaData singGolden singGolden singGolden singGolden singGolden+  singGolden v = SnapshotMetaData (singGolden v) (singGolden v) (singGolden v)+                                  (singGolden v) (singGolden v)     where       _coveredAllCases = \case         SnapshotMetaData{} -> ()  instance EnumGolden SnapshotLabel where-  enumGolden = [+  enumGolden _v = [         SnapshotLabel "UserProvidedLabel"       , SnapshotLabel ""       ]@@ -279,47 +329,48 @@         SnapshotLabel{} -> ()  instance EnumGolden TableConfig where-  singGolden = TableConfig singGolden singGolden singGolden singGolden-                           singGolden singGolden singGolden singGolden+  singGolden v = TableConfig (singGolden v) (singGolden v) (singGolden v)+                             (singGolden v) (singGolden v) (singGolden v)+                             (singGolden v) (singGolden v)     where       _coveredAllCases = \case         TableConfig{} -> ()  instance EnumGolden MergePolicy where-  singGolden = LazyLevelling+  singGolden _v = LazyLevelling     where       _coveredAllCases = \case         LazyLevelling{} -> ()   instance EnumGolden SizeRatio where-  singGolden = Four+  singGolden _v = Four     where       _coveredAllCases = \case         Four{} -> ()  instance EnumGolden WriteBufferAlloc where-  singGolden = AllocNumEntries magicNumber2+  singGolden _v = AllocNumEntries magicNumber2     where       _coveredAllCases = \case         AllocNumEntries{} -> ()  instance EnumGolden BloomFilterAlloc where-  enumGolden = [ AllocFixed magicNumber3, AllocRequestFPR pi ]+  enumGolden _v = [ AllocFixed magicNumber3, AllocRequestFPR pi ]     where       _coveredAllCases = \case         AllocFixed{} -> ()         AllocRequestFPR{} -> ()  instance EnumGolden FencePointerIndexType where-  enumGolden = [ CompactIndex, OrdinaryIndex ]+  enumGolden _v = [ CompactIndex, OrdinaryIndex ]     where       _coveredAllCases = \case         CompactIndex{} -> ()         OrdinaryIndex{} -> ()  instance EnumGolden DiskCachePolicy where-  enumGolden = [ DiskCacheAll, DiskCacheLevelOneTo magicNumber3, DiskCacheNone ]+  enumGolden _v = [ DiskCacheAll, DiskCacheLevelOneTo magicNumber3, DiskCacheNone ]     where       _coveredAllCases = \case         DiskCacheAll{} -> ()@@ -327,32 +378,34 @@         DiskCacheNone{} -> ()  instance EnumGolden MergeSchedule where-  enumGolden = [ OneShot, Incremental ]+  enumGolden _v = [ OneShot, Incremental ]     where       _coveredAllCases = \case         OneShot{} -> ()         Incremental{} -> ()  instance EnumGolden MergeBatchSize where-  enumGolden = map MergeBatchSize [ 1, 1000 ]-  supportedVersions _ = [V1]+  enumGolden v+    | v >= V1   = map MergeBatchSize [ 1, 1000 ]+    | otherwise = []  instance EnumGolden (SnapLevels SnapshotRun) where-  singGolden = SnapLevels singGolden+  singGolden v = SnapLevels (singGolden v)     where       _coveredAllCases = \case         SnapLevels{} -> ()  instance EnumGolden (SnapLevel SnapshotRun) where-  singGolden = SnapLevel singGolden singGolden+  singGolden v = SnapLevel (singGolden v) (singGolden v)     where       _coveredAllCases = \case         SnapLevel{} -> ()  instance EnumGolden (SnapIncomingRun SnapshotRun) where-  enumGolden = [-        SnapIncomingMergingRun singGolden singGolden singGolden singGolden-      , SnapIncomingSingleRun singGolden+  enumGolden v = [+        SnapIncomingMergingRun (singGolden v) (singGolden v) (singGolden v)+                               (singGolden v)+      , SnapIncomingSingleRun (singGolden v)       ]     where       _coveredAllCases = \case@@ -360,30 +413,30 @@         SnapIncomingSingleRun{} -> ()  instance EnumGolden MergePolicyForLevel where-  enumGolden = [ LevelTiering, LevelLevelling ]+  enumGolden _v = [ LevelTiering, LevelLevelling ]     where       _coveredAllCases = \case         LevelTiering -> ()         LevelLevelling -> ()  instance EnumGolden LevelMergeType where-  enumGolden = [ MergeMidLevel, MergeLastLevel ]+  enumGolden _v = [ MergeMidLevel, MergeLastLevel ]     where       _coveredAllCases = \case         MergeMidLevel{} -> ()         MergeLastLevel{} -> ()  instance EnumGolden (SnapMergingTree SnapshotRun) where-  singGolden = SnapMergingTree singGolden+  singGolden v = SnapMergingTree (singGolden v)     where       _coveredAllCases = \case         SnapMergingTree{} -> ()  instance EnumGolden (SnapMergingTreeState SnapshotRun) where-  enumGolden = [-        SnapCompletedTreeMerge singGolden-      , SnapPendingTreeMerge singGolden-      , SnapOngoingTreeMerge singGolden+  enumGolden v = [+        SnapCompletedTreeMerge (singGolden v)+      , SnapPendingTreeMerge (singGolden v)+      , SnapOngoingTreeMerge (singGolden v)       ]     where       _coveredAllCases = \case@@ -392,9 +445,9 @@         SnapOngoingTreeMerge{} -> ()  instance EnumGolden (SnapPendingMerge SnapshotRun) where-  enumGolden = [-        SnapPendingLevelMerge singGolden singGolden-      , SnapPendingUnionMerge singGolden+  enumGolden v = [+        SnapPendingLevelMerge (singGolden v) (singGolden v)+      , SnapPendingUnionMerge (singGolden v)       ]     where       _coveredAllCases = \case@@ -402,9 +455,9 @@         SnapPendingUnionMerge{} -> ()  instance EnumGolden (SnapPreExistingRun SnapshotRun) where-  enumGolden = [-        SnapPreExistingRun singGolden-      , SnapPreExistingMergingRun singGolden+  enumGolden v = [+        SnapPreExistingRun (singGolden v)+      , SnapPreExistingMergingRun (singGolden v)       ]     where       _coveredAllCases = \case@@ -412,43 +465,44 @@         SnapPreExistingMergingRun{} -> ()  instance EnumGolden TreeMergeType where-  enumGolden = [ MergeLevel, MergeUnion ]+  enumGolden _v = [ MergeLevel, MergeUnion ]     where       _coveredAllCases = \case         MergeLevel{} -> ()         MergeUnion{} -> ()  instance EnumGolden a => EnumGolden (Maybe a) where-  enumGolden = [ Just singGolden, Nothing ]+  enumGolden v = [ Just (singGolden v), Nothing ]     where       _coveredAllCases = \case         Just{} -> ()         Nothing{} -> ()  instance EnumGolden a => EnumGolden (V.Vector a) where-  enumGolden = [-      V.fromList [ singGolden, singGolden ]+  enumGolden v = [+      V.fromList [ singGolden v, singGolden v ]     , mempty-    , V.fromList [ singGolden ]+    , V.fromList [ singGolden v ]     ]  instance EnumGolden a => EnumGolden [a] where-  enumGolden = [-      [singGolden, singGolden]+  enumGolden v = [+      [ singGolden v, singGolden v ]     , []-    , [singGolden]+    , [ singGolden v]     ]  instance EnumGolden RunParams where-  singGolden = RunParams singGolden singGolden singGolden+  singGolden v = RunParams (singGolden v) (singGolden v) (singGolden v)     where       _coveredAllCases = \case         RunParams{} -> ()  instance EnumGolden t => EnumGolden (SnapMergingRun t SnapshotRun) where-  enumGolden = [-        SnapCompletedMerge singGolden singGolden-      , SnapOngoingMerge singGolden singGolden singGolden singGolden+  enumGolden v = [+        SnapCompletedMerge (singGolden v) (singGolden v)+      , SnapOngoingMerge (singGolden v) (singGolden v) (singGolden v)+                         (singGolden v)       ]     where       _coveredAllCases = \case@@ -456,7 +510,7 @@         SnapOngoingMerge{} -> ()  instance EnumGolden RunBloomFilterAlloc where-  enumGolden = [+  enumGolden _v = [         RunAllocFixed magicNumber3       , RunAllocRequestFPR pi       ]@@ -466,13 +520,13 @@         RunAllocRequestFPR{} -> ()  instance EnumGolden RunNumber where-  singGolden = RunNumber magicNumber3+  singGolden _v = RunNumber magicNumber3     where       _coveredAllCases = \case         RunNumber{} -> ()  instance EnumGolden IndexType where-  enumGolden = [+  enumGolden _v = [         Compact       , Ordinary       ]@@ -482,7 +536,7 @@         Ordinary{} -> ()  instance EnumGolden RunDataCaching where-  enumGolden = [+  enumGolden _v = [         CacheRunData       , NoCacheRunData       ]@@ -492,31 +546,31 @@         NoCacheRunData{} -> ()  instance EnumGolden SnapshotRun where-  singGolden = SnapshotRun singGolden singGolden singGolden+  singGolden v = SnapshotRun (singGolden v) (singGolden v) (singGolden v)     where       _coveredAllCases = \case         SnapshotRun{} -> ()  instance EnumGolden MergeDebt where-  singGolden = MergeDebt magicNumber2+  singGolden _v = MergeDebt magicNumber2     where       _coveredAllCases = \case         MergeDebt{} -> ()  instance EnumGolden MergeCredits where-  singGolden = MergeCredits magicNumber2+  singGolden _v = MergeCredits magicNumber2     where       _coveredAllCases = \case         MergeCredits{} -> ()  instance EnumGolden NominalDebt where-  singGolden = NominalDebt magicNumber2+  singGolden _v = NominalDebt magicNumber2     where       _coveredAllCases = \case         NominalDebt{} -> ()  instance EnumGolden NominalCredits where-  singGolden = NominalCredits magicNumber1+  singGolden _v = NominalCredits magicNumber1     where       _coveredAllCases = \case         NominalCredits{} -> ()