diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,30 @@
 # Revision history for `lsm-tree`
 
+## 1.1.1.0 -- 2026-07-21
+
+### Breaking changes
+
+None
+
+### New features
+
+None
+
+### Minor changes
+
+* Support `data-elevator-0.3`. See [issue
+  #856](https://github.com/IntersectMBO/lsm-tree/issues/856) and [PR
+  #857](https://github.com/IntersectMBO/lsm-tree/pull/857).
+* Support `ghc-9.14`. See [issue
+  #813](https://github.com/IntersectMBO/lsm-tree/issues/813) and [PR
+  #859](https://github.com/IntersectMBO/lsm-tree/pull/859).
+* Drop support for `random < 1.2`. See [PR
+  #865](https://github.com/IntersectMBO/lsm-tree/pull/865).
+
+### Bug fixes
+
+None
+
 ## 1.1.0.0 -- 2026-05-13
 
 ### Breaking changes
diff --git a/bench-unions/Bench/Unions.hs b/bench-unions/Bench/Unions.hs
--- a/bench-unions/Bench/Unions.hs
+++ b/bench-unions/Bench/Unions.hs
@@ -159,13 +159,13 @@
     case P.runPrimArray $ do
            v <- P.newPrimArray 5
            let g0 = Random.mkStdGen (fromIntegral seed)
-           let (!w0, !g1) = Random.uniform g0
+           let (!w0, !g1) = Random.uniform_compat g0
            P.writePrimArray v 0 w0
-           let (!w1, !g2) = Random.uniform g1
+           let (!w1, !g2) = Random.uniform_compat g1
            P.writePrimArray v 1 w1
-           let (!w2, !g3) = Random.uniform g2
+           let (!w2, !g3) = Random.uniform_compat g2
            P.writePrimArray v 2 w2
-           let (!w3, _g4) = Random.uniform g3
+           let (!w3, _g4) = Random.uniform_compat g3
            P.writePrimArray v 3 w3
            P.writePrimArray v 4 0x3d3d3d3d3d3d3d3d -- ========
            case v of
@@ -461,7 +461,7 @@
 deriveSetupRNGs gOpts amount =
     let  -- g1 is reserved for the run command
         (_g1, !g2) = deriveInitialForkedRNGs gOpts
-    in  NE.fromList $ take amount $ List.unfoldr (Just . Random.splitGen) g2
+    in  NE.fromList $ take amount $ List.unfoldr (Just . Random.splitGen_compat) g2
 
 deriveRunRNG :: GlobalOpts -> Random.StdGen
 deriveRunRNG gOpts =
@@ -470,7 +470,7 @@
     in  g1
 
 deriveInitialForkedRNGs :: GlobalOpts -> (Random.StdGen, Random.StdGen)
-deriveInitialForkedRNGs = Random.splitGen . Random.mkStdGen . seed
+deriveInitialForkedRNGs = Random.splitGen_compat . Random.mkStdGen . seed
 
 -------------------------------------------------------------------------------
 -- Batch generation
diff --git a/bench/macro/lsm-tree-bench-bloomfilter.hs b/bench/macro/lsm-tree-bench-bloomfilter.hs
--- a/bench/macro/lsm-tree-bench-bloomfilter.hs
+++ b/bench/macro/lsm-tree-bench-bloomfilter.hs
@@ -24,6 +24,8 @@
 import           Text.Printf (printf)
 
 import           Database.LSMTree.Extras.Orphans ()
+import           Database.LSMTree.Extras.Random (splitGen_compat,
+                     uniform_compat)
 import           Database.LSMTree.Internal.Assertions (fromIntegralChecked)
 import qualified Database.LSMTree.Internal.BloomFilter as Bloom
 import           Database.LSMTree.Internal.Serialise (SerialisedKey,
@@ -236,7 +238,7 @@
          when (i .&. 0xFFFF == 0) (unsafeIOToST $ putStr ".")
          -- insert n elements into filter b
          let k :: Word256
-             (!k, !rng') = uniform rng
+             (!k, !rng') = uniform_compat rng
          Bloom.insert mb (serialiseKey k)
          pure rng'
       )
@@ -257,9 +259,9 @@
     go !rng !n
       | n <= 0    = ()
       | otherwise =
-        let (!rng'', !rng') = splitGen rng
+        let (!rng'', !rng') = splitGen_compat rng
             ks  :: VP.Vector Word256
-            !ks  = VP.unfoldrExactN b uniform rng'
+            !ks  = VP.unfoldrExactN b uniform_compat rng'
             ks' :: V.Vector SerialisedKey
             !ks' = V.map serialiseKey (V.convert ks)
         in action ks' `seq` go rng'' (n-b)
diff --git a/bench/macro/lsm-tree-bench-lookups.hs b/bench/macro/lsm-tree-bench-lookups.hs
--- a/bench/macro/lsm-tree-bench-lookups.hs
+++ b/bench/macro/lsm-tree-bench-lookups.hs
@@ -20,6 +20,7 @@
 import qualified Data.Vector.Primitive as VP
 import qualified Data.Vector.Unboxed.Mutable as VUM
 import           Database.LSMTree.Extras.Orphans ()
+import           Database.LSMTree.Extras.Random (uniform_compat)
 import           Database.LSMTree.Extras.UTxO
 import           Database.LSMTree.Internal.Arena (ArenaManager, newArenaManager,
                      withArena)
@@ -384,9 +385,9 @@
 
     -- return runs
     runs <- V.fromList <$> mapM (Run.fromBuilder refCtx) rbs
-    let blooms  = V.map (\(DeRef r) -> Run.runFilter   r) runs
-        indexes = V.map (\(DeRef r) -> Run.runIndex    r) runs
-        handles = V.map (\(DeRef r) -> Run.runKOpsFile r) runs
+    let blooms  = V.map (\(DeRef r) -> r.bloomFilter) runs
+        indexes = V.map (\(DeRef r) -> r.index      ) runs
+        handles = V.map (\(DeRef r) -> r.kOpsFile   ) runs
     pure $!! (runs, blooms, indexes, handles)
 
 genLookupBatch :: StdGen -> Int -> (V.Vector SerialisedKey, StdGen)
@@ -404,7 +405,7 @@
           !res <- V.unsafeFreeze mres
           pure (res, rng)
       | otherwise = do
-          let (!k, !rng') = uniform @UTxOKey @StdGen rng
+          let (!k, !rng') = uniform_compat @UTxOKey @StdGen rng
               !sk = serialiseKey k
           VM.write mres i $! sk
           go rng' (i+1) mres
@@ -548,7 +549,7 @@
       | i == n-1 = pure g
       | otherwise = do
           when (i .&. 0xFFFF == 0) (unsafeIOToPrim $ putStr ".")
-          let (!x, !g') = uniform g
+          let (!x, !g') = uniform_compat g
           VGM.unsafeWrite vec i x
           loop (i+1) g'
 
diff --git a/bench/macro/utxo-bench.hs b/bench/macro/utxo-bench.hs
--- a/bench/macro/utxo-bench.hs
+++ b/bench/macro/utxo-bench.hs
@@ -79,6 +79,7 @@
 -- We should be able to write this benchmark
 -- using only use public lsm-tree interface
 import qualified Database.LSMTree as LSM
+import qualified Database.LSMTree.Extras.Random as Random
 
 -------------------------------------------------------------------------------
 -- Table configuration
@@ -114,13 +115,13 @@
     case P.runPrimArray $ do
            v <- P.newPrimArray 5
            let g0 = Random.mkStdGen (fromIntegral seed)
-           let (!w0, !g1) = Random.uniform g0
+           let (!w0, !g1) = Random.uniform_compat g0
            P.writePrimArray v 0 w0
-           let (!w1, !g2) = Random.uniform g1
+           let (!w1, !g2) = Random.uniform_compat g1
            P.writePrimArray v 1 w1
-           let (!w2, !g3) = Random.uniform g2
+           let (!w2, !g3) = Random.uniform_compat g2
            P.writePrimArray v 2 w2
-           let (!w3, _g4) = Random.uniform g3
+           let (!w3, _g4) = Random.uniform_compat g3
            P.writePrimArray v 3 w3
            P.writePrimArray v 4 0x3d3d3d3d3d3d3d3d -- ========
            case v of
diff --git a/bench/micro/Bench/Database/LSMTree.hs b/bench/micro/Bench/Database/LSMTree.hs
--- a/bench/micro/Bench/Database/LSMTree.hs
+++ b/bench/micro/Bench/Database/LSMTree.hs
@@ -17,6 +17,7 @@
 import           Database.LSMTree hiding (withTable)
 import           Database.LSMTree.Extras
 import           Database.LSMTree.Extras.Orphans ()
+import           Database.LSMTree.Extras.Random (uniform_compat)
 import           Database.LSMTree.Internal.Assertions (fromIntegralChecked)
 import qualified Database.LSMTree.Internal.RawBytes as RB
 import           GHC.Generics (Generic)
@@ -116,7 +117,7 @@
 
       customRandomEntries :: Int -> V.Vector (K, Word64, ShortByteString)
       customRandomEntries n = V.unfoldrExactN n f (mkStdGen 17)
-        where f !g = let (!k, !g') = uniform g
+        where f !g = let (!k, !g') = uniform_compat g
                     in  ((k, v, b), g')
               -- The exact value does not matter much, so we pick an arbitrary
               -- hardcoded one.
@@ -207,7 +208,7 @@
 
       customRandomEntries :: Int -> V.Vector (K, V2, Maybe B2)
       customRandomEntries n = V.unfoldrExactN n f (mkStdGen 17)
-        where f !g = let (!k, !g') = uniform g
+        where f !g = let (!k, !g') = uniform_compat g
                     in  ((k, v, Nothing), g')
               -- The exact value does not matter much, so we pick an arbitrary
               -- hardcoded one.
@@ -257,7 +258,7 @@
 
       randomInserts :: Int -> V.Vector (K, V2, Maybe Void)
       randomInserts n = V.unfoldrExactN n f (mkStdGen 17)
-        where f !g = let (!k, !g') = uniform g
+        where f !g = let (!k, !g') = uniform_compat g
                     in  ((k, v, Nothing), g')
               -- The exact value does not matter much, so we pick an arbitrary
               -- hardcoded one.
@@ -414,7 +415,7 @@
 -- | Random keys, default values @1@
 randomEntries :: Int -> V.Vector (K, V3)
 randomEntries n = V.unfoldrExactN n f (mkStdGen 17)
-  where f !g = let (!k, !g') = uniform g
+  where f !g = let (!k, !g') = uniform_compat g
                in  ((k, 1), g')
 
 -- | Like 'randomEntries', but also returns groups of size 'm'
diff --git a/bench/micro/Bench/Database/LSMTree/Internal/BloomFilter.hs b/bench/micro/Bench/Database/LSMTree/Internal/BloomFilter.hs
--- a/bench/micro/Bench/Database/LSMTree/Internal/BloomFilter.hs
+++ b/bench/micro/Bench/Database/LSMTree/Internal/BloomFilter.hs
@@ -57,8 +57,8 @@
   -> IO (Bloom SerialisedKey, [SerialisedKey])
 elemEnv fpr nbloom nelemsPositive nelemsNegative = do
     let g = mkStdGen 100
-        (g1, g') = R.splitGen g
-        (g2, g3) = R.splitGen g'
+        (g1, g') = splitGen_compat g
+        (g2, g3) = splitGen_compat g'
 
     let (xs, ys1) = splitAt nbloom
                   $ uniformWithoutReplacement    @UTxOKey g1  (nbloom + nelemsNegative)
diff --git a/bench/micro/Bench/Database/LSMTree/Internal/Lookup.hs b/bench/micro/Bench/Database/LSMTree/Internal/Lookup.hs
--- a/bench/micro/Bench/Database/LSMTree/Internal/Lookup.hs
+++ b/bench/micro/Bench/Database/LSMTree/Internal/Lookup.hs
@@ -16,8 +16,8 @@
 import qualified Data.Vector as V
 import           Database.LSMTree.Extras.Orphans ()
 import           Database.LSMTree.Extras.Random (frequency, randomByteStringR,
-                     sampleUniformWithReplacement, shuffle,
-                     uniformWithoutReplacement)
+                     sampleUniformWithReplacement, shuffle, splitGen_compat,
+                     uniformWithoutReplacement, uniform_compat)
 import           Database.LSMTree.Extras.UTxO
 import           Database.LSMTree.Internal.Arena (ArenaManager, closeArena,
                      newArena, newArenaManager, withArena)
@@ -91,9 +91,9 @@
 benchLookups :: Config -> Benchmark
 benchLookups conf@Config{name} =
     withEnv $ \ ~(_dir, arenaManager, _hasFS, hasBlockIO, _refCtx, wbblobs, rs, ks) ->
-      env ( pure ( V.map (\(DeRef r) -> Run.runFilter   r) rs
-                 , V.map (\(DeRef r) -> Run.runIndex    r) rs
-                 , V.map (\(DeRef r) -> Run.runKOpsFile r) rs
+      env ( pure ( V.map (\(DeRef r) -> r.bloomFilter) rs
+                 , V.map (\(DeRef r) -> r.index      ) rs
+                 , V.map (\(DeRef r) -> r.kOpsFile   ) rs
                  )
           ) $ \ ~(blooms, indexes, kopsFiles) ->
         bgroup name [
@@ -248,9 +248,9 @@
         , V.Vector SerialisedKey
         )
 lookupsEnv g nentries npos nneg = do
-    let  (g1, g')  = R.splitGen g
-         (g2, g'') = R.splitGen g'
-         (g3, g4)  = R.splitGen g''
+    let  (g1, g')  = splitGen_compat g
+         (g2, g'') = splitGen_compat g'
+         (g3, g4)  = splitGen_compat g''
     let (keys, negLookups) = splitAt nentries
                            $ uniformWithoutReplacement @UTxOKey g1 (nentries + nneg)
         posLookups         = sampleUniformWithReplacement g2 npos keys
@@ -267,14 +267,14 @@
 
 randomEntry :: StdGen -> (Entry UTxOValue ByteString, StdGen)
 randomEntry g = frequency [
-      (20, \g' -> let (!v, !g'') = uniform g' in (Insert v, g''))
-    , (1,  \g' -> let (!v, !g'') = uniform g'
+      (20, \g' -> let (!v, !g'') = uniform_compat g' in (Insert v, g''))
+    , (1,  \g' -> let (!v, !g'') = uniform_compat g'
                       -- The size of the blobs doesn't matter for the benchmark,
                       -- as it only deals with the blob references. So we make
                       -- them tiny to not slow down the setup.
                       (!b, !g''') = randomByteStringR (0, 100) g''
                   in  (InsertWithBlob v b, g'''))
-    , (2,  \g' -> let (!v, !g'') = uniform g' in (Upsert v, g''))
+    , (2,  \g' -> let (!v, !g'') = uniform_compat g' in (Upsert v, g''))
     , (2,  \g' -> (Delete, g'))
     ] g
 
diff --git a/bench/micro/Bench/Database/LSMTree/Internal/Merge.hs b/bench/micro/Bench/Database/LSMTree/Internal/Merge.hs
--- a/bench/micro/Bench/Database/LSMTree/Internal/Merge.hs
+++ b/bench/micro/Bench/Database/LSMTree/Internal/Merge.hs
@@ -36,8 +36,7 @@
 import qualified System.FS.BlockIO.IO as FS
 import qualified System.FS.IO as FS
 import           System.IO.Temp
-import qualified System.Random as R
-import           System.Random (StdGen, mkStdGen, uniform, uniformR)
+import           System.Random (StdGen, mkStdGen, uniformR)
 
 benchmarks :: Benchmark
 benchmarks = bgroup "Bench.Database.LSMTree.Internal.Merge" [
@@ -345,22 +344,22 @@
 
 configWord64 :: Config
 configWord64 = defaultConfig {
-    randomKey    = first serialiseKey . uniform @Word64 @_
-  , randomValue  = first serialiseValue . uniform @Word64 @_
+    randomKey    = first serialiseKey . R.uniform_compat @Word64 @_
+  , randomValue  = first serialiseValue . R.uniform_compat @Word64 @_
   , randomBlob   = first serialiseBlob . R.randomByteStringR (0, 0x2000)  -- up to 8 kB
   }
 
 configUTxO :: Config
 configUTxO = defaultConfig {
-    randomKey    = first serialiseKey . uniform @UTxOKey @_
-  , randomValue  = first serialiseValue . uniform @UTxOValue @_
+    randomKey    = first serialiseKey . R.uniform_compat @UTxOKey @_
+  , randomValue  = first serialiseValue . R.uniform_compat @UTxOValue @_
   }
 
 configUTxOStaking :: Config
 configUTxOStaking = defaultConfig {
     fmupserts    = 1
-  , randomKey    = first serialiseKey . uniform @UTxOKey @_
-  , randomValue  = first serialiseValue . uniform @Word64 @_
+  , randomKey    = first serialiseKey . R.uniform_compat @UTxOKey @_
+  , randomValue  = first serialiseValue . R.uniform_compat @Word64 @_
   , mergeResolve = Just (onDeserialisedValues ((+) @Word64))
   }
 
@@ -410,7 +409,7 @@
         zipWith
           (randomRunData config)
           nentries
-          (List.unfoldr (Just . R.splitGen) rng0)
+          (List.unfoldr (Just . R.splitGen_compat) rng0)
 
 -- | Generate keys and entries to insert into the write buffer.
 -- They are already serialised to exclude the cost from the benchmark.
@@ -425,7 +424,7 @@
       (R.withoutReplacement g1 runentries randomKey)
       (R.withReplacement g2 runentries randomEntry)
   where
-    (g1, g2) = R.splitGen g0
+    (g1, g2) = R.splitGen_compat g0
 
     randomEntry :: Rnd (Entry SerialisedValue SerialisedBlob)
     randomEntry = R.frequency
diff --git a/bench/micro/Bench/Database/LSMTree/Internal/Serialise.hs b/bench/micro/Bench/Database/LSMTree/Internal/Serialise.hs
--- a/bench/micro/Bench/Database/LSMTree/Internal/Serialise.hs
+++ b/bench/micro/Bench/Database/LSMTree/Internal/Serialise.hs
@@ -3,13 +3,14 @@
   ) where
 
 import           Criterion.Main
+import           Database.LSMTree.Extras.Random (uniform_compat)
 import           Database.LSMTree.Extras.UTxO
 import           Database.LSMTree.Internal.Serialise.Class
 import           System.Random
 
 benchmarks :: Benchmark
 benchmarks = bgroup "Bench.Database.LSMTree.Internal.Serialise" [
-      env (pure $ fst $ uniform (mkStdGen 12)) $ \(k :: UTxOKey) ->
+      env (pure $ fst $ uniform_compat (mkStdGen 12)) $ \(k :: UTxOKey) ->
         bgroup "UTxOKey" [
             bench "serialiseKey" $ whnf serialiseKey k
           , bench "serialiseKeyRoundtrip" $ whnf serialiseKeyRoundtrip k
diff --git a/bench/micro/Bench/Database/LSMTree/Internal/WriteBuffer.hs b/bench/micro/Bench/Database/LSMTree/Internal/WriteBuffer.hs
--- a/bench/micro/Bench/Database/LSMTree/Internal/WriteBuffer.hs
+++ b/bench/micro/Bench/Database/LSMTree/Internal/WriteBuffer.hs
@@ -10,14 +10,15 @@
 import           Data.Maybe (fromMaybe, isJust, isNothing)
 import           Data.Word (Word64)
 import           Database.LSMTree.Extras.Orphans ()
-import           Database.LSMTree.Extras.Random (frequency, randomByteStringR)
+import           Database.LSMTree.Extras.Random (frequency, randomByteStringR,
+                     uniform_compat)
 import           Database.LSMTree.Extras.UTxO
 import           Database.LSMTree.Internal.BlobRef (BlobSpan (..))
 import           Database.LSMTree.Internal.Entry
 import           Database.LSMTree.Internal.Serialise
 import           Database.LSMTree.Internal.WriteBuffer (WriteBuffer)
 import qualified Database.LSMTree.Internal.WriteBuffer as WB
-import           System.Random (StdGen, mkStdGen, uniform)
+import           System.Random (StdGen, mkStdGen)
 
 benchmarks :: Benchmark
 benchmarks = bgroup "Bench.Database.LSMTree.Internal.WriteBuffer" [
@@ -168,14 +169,14 @@
 
 configWord64 :: Config
 configWord64 = defaultConfig {
-    randomKey    = first serialiseKey . uniform @Word64 @_
-  , randomValue  = first serialiseValue . uniform @Word64 @_
+    randomKey    = first serialiseKey . uniform_compat @Word64 @_
+  , randomValue  = first serialiseValue . uniform_compat @Word64 @_
   }
 
 configUTxO :: Config
 configUTxO = defaultConfig {
-    randomKey    = first serialiseKey . uniform @UTxOKey @_
-  , randomValue  = first serialiseValue . uniform @UTxOValue @_
+    randomKey    = first serialiseKey . uniform_compat @UTxOKey @_
+  , randomValue  = first serialiseValue . uniform_compat @UTxOValue @_
   }
 
 envInputKOps :: Config -> InputKOps
@@ -219,6 +220,6 @@
 
 randomBlobSpan :: Rnd BlobSpan
 randomBlobSpan !g =
-  let (off, !g')  = uniform g
-      (len, !g'') = uniform g'
+  let (off, !g')  = uniform_compat g
+      (len, !g'') = uniform_compat g'
   in (BlobSpan off len, g'')
diff --git a/lsm-tree.cabal b/lsm-tree.cabal
--- a/lsm-tree.cabal
+++ b/lsm-tree.cabal
@@ -1,6 +1,6 @@
 cabal-version:   3.4
 name:            lsm-tree
-version:         1.1.0.0
+version:         1.1.1.0
 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.1.0.0
+  tag:      lsm-tree-1.1.1.0
 
 common warnings
   ghc-options:
@@ -513,6 +513,10 @@
 
   ghc-options: -Werror=missing-deriving-strategies
 
+  -- In ghc-9.14 the `pattern` namespace specifier is deprecated.
+  if impl(ghc >=9.14)
+    ghc-options: -Wno-pattern-namespace-specifier
+
 common wno-x-partial
   if impl(ghc >=9.8)
     -- No errors for x-partial functions. We might remove this in the future if
@@ -551,7 +555,7 @@
     , lsm-tree:control
     , lsm-tree:core
     , primitive               ^>=0.9
-    , random                  ^>=1.0   || ^>=1.1 || ^>=1.2     || ^>=1.3
+    , random                  ^>=1.2   || ^>=1.3
     , text                    ^>=2.1.1
     , vector                  ^>=0.13
 
@@ -644,7 +648,7 @@
 
   if impl(ghc >=9.4)
     other-modules: Database.LSMTree.Internal.StrictArray
-    build-depends: data-elevator ^>=0.1.0.2 || ^>=0.2
+    build-depends: data-elevator ^>=0.1.0.2 || ^>=0.2 || ^>=0.3
     cpp-options:   -DHAVE_STRICT_ARRAY
 
 library extras
@@ -755,6 +759,7 @@
     Test.Util.Orphans
     Test.Util.PrettyProxy
     Test.Util.QC
+    Test.Util.QC.Compat
     Test.Util.QLS
     Test.Util.RawPage
     Test.Util.TypeFamilyWrappers
diff --git a/src-core/Database/LSMTree/Internal/Arena.hs b/src-core/Database/LSMTree/Internal/Arena.hs
--- a/src-core/Database/LSMTree/Internal/Arena.hs
+++ b/src-core/Database/LSMTree/Internal/Arena.hs
@@ -2,7 +2,6 @@
 {-# LANGUAGE DuplicateRecordFields #-}
 {-# LANGUAGE LambdaCase            #-}
 {-# LANGUAGE NoFieldSelectors      #-}
-{-# LANGUAGE OverloadedRecordDot   #-}
 {-# OPTIONS_HADDOCK not-home #-}
 module Database.LSMTree.Internal.Arena (
     ArenaManager,
diff --git a/src-core/Database/LSMTree/Internal/MergeSchedule.hs b/src-core/Database/LSMTree/Internal/MergeSchedule.hs
--- a/src-core/Database/LSMTree/Internal/MergeSchedule.hs
+++ b/src-core/Database/LSMTree/Internal/MergeSchedule.hs
@@ -220,9 +220,9 @@
       lvls
     pure $! LevelsCache_ {
         cachedRuns      = rs
-      , cachedFilters   = mapStrict (\(DeRef r) -> Run.runFilter   r) rs
-      , cachedIndexes   = mapStrict (\(DeRef r) -> Run.runIndex    r) rs
-      , cachedKOpsFiles = mapStrict (\(DeRef r) -> Run.runKOpsFile r) rs
+      , cachedFilters   = mapStrict (\(DeRef r) -> r.bloomFilter) rs
+      , cachedIndexes   = mapStrict (\(DeRef r) -> r.index      ) rs
+      , cachedKOpsFiles = mapStrict (\(DeRef r) -> r.kOpsFile   ) rs
       }
   where
     dupRun r = withRollback reg (dupRef r) releaseRef
diff --git a/src-core/Database/LSMTree/Internal/MergingTree/Lookup.hs b/src-core/Database/LSMTree/Internal/MergingTree/Lookup.hs
--- a/src-core/Database/LSMTree/Internal/MergingTree/Lookup.hs
+++ b/src-core/Database/LSMTree/Internal/MergingTree/Lookup.hs
@@ -109,15 +109,17 @@
     -- dropped before we duplicated the reference.
     withMVar (MT.mergeState mt) $ \case
       MT.CompletedTreeMerge r ->
-        LookupBatch . V.singleton <$!> dupRun r
+        LookupBatch <$!> (dupRun r >>= V.singletonMStrict)
       MT.OngoingTreeMerge mr -> do
         !rs <- withRollback reg (MR.duplicateRuns mr) (V.mapM_ releaseRef)
         ty <- MR.mergeType mr
-        pure $ case ty of
-          Nothing            -> LookupBatch rs  -- just one run
-          Just MR.MergeLevel -> LookupBatch rs  -- combine runs
-          Just MR.MergeUnion -> mkLookupNode MR.MergeUnion  -- separate
-                                  (LookupBatch . V.singleton <$!> rs)
+        case ty of
+          Nothing            -> pure $ LookupBatch rs  -- just one run
+          Just MR.MergeLevel -> pure $ LookupBatch rs  -- combine runs
+          Just MR.MergeUnion -> do
+            !rs' <- mapM V.singletonMStrict rs
+            pure $ mkLookupNode MR.MergeUnion  -- separate
+                                  (LookupBatch <$!> rs')
       MT.PendingTreeMerge (MT.PendingLevelMerge prs Nothing) -> do
         LookupBatch . V.concatMap id <$!>  -- combine runs
           V.mapMStrict duplicatePreExistingRun prs
@@ -136,6 +138,6 @@
     dupRun r = withRollback reg (dupRef r) releaseRef
 
     duplicatePreExistingRun (MT.PreExistingRun r) =
-        V.singleton <$!> dupRun r
+        dupRun r >>= V.singletonMStrict
     duplicatePreExistingRun (MT.PreExistingMergingRun mr) =
         withRollback reg (MR.duplicateRuns mr) (V.mapM_ releaseRef)
diff --git a/src-core/Database/LSMTree/Internal/Run.hs b/src-core/Database/LSMTree/Internal/Run.hs
--- a/src-core/Database/LSMTree/Internal/Run.hs
+++ b/src-core/Database/LSMTree/Internal/Run.hs
@@ -1,15 +1,17 @@
-{-# LANGUAGE DataKinds          #-}
-{-# LANGUAGE DeriveAnyClass     #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE DerivingVia        #-}
-{-# LANGUAGE RecordWildCards    #-}
+{-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE DeriveAnyClass        #-}
+{-# LANGUAGE DerivingStrategies    #-}
+{-# LANGUAGE DerivingVia           #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+
 {-# OPTIONS_HADDOCK not-home #-}
 
 -- | Runs of sorted key\/value data.
 module Database.LSMTree.Internal.Run (
     -- * Run
-    Run (Run, runIndex, runHasFS, runHasBlockIO, runRunDataCaching,
-         runBlobFile, runFilter, runKOpsFile)
+    Run (Run, index, hasFS, hasBlockIO, dataCaching,
+         blobFile, bloomFilter, kOpsFile)
   , RunFsPaths
   , size
   , sizeInPages
@@ -68,75 +70,74 @@
 -- | The in-memory representation of a completed LSM run.
 --
 data Run m h = Run {
-      runNumEntries     :: !NumEntries
+      numEntries  :: !NumEntries
       -- | The reference count for the LSM run. This counts the
       -- number of references from LSM handles to this run. When
       -- this drops to zero the open files will be closed.
-    , runRefCounter     :: !(RefCounter m)
+    , refCounter  :: !(RefCounter m)
       -- | The file system paths for all the files used by the run.
-    , runRunFsPaths     :: !RunFsPaths
+    , fsPaths     :: !RunFsPaths
       -- | The bloom filter for the set of keys in this run.
-    , runFilter         :: !(Bloom SerialisedKey)
+    , bloomFilter :: !(Bloom SerialisedKey)
       -- | The in-memory index mapping keys to page numbers in the
       -- Key\/Ops file. In future we may support alternative index
       -- representations.
-    , runIndex          :: !Index
+    , index       :: !Index
       -- | The file handle for the Key\/Ops file. This file is opened
       -- read-only and is accessed in a page-oriented way, i.e. only
       -- reading whole pages, at page offsets. It will be opened with
       -- 'O_DIRECT' on supported platforms.
-    , runKOpsFile       :: !(FS.Handle h)
+    , kOpsFile    :: !(FS.Handle h)
       -- | The file handle for the BLOBs file. This file is opened
       -- read-only and is accessed in a normal style using buffered
       -- I\/O, reading arbitrary file offset and length spans.
-    , runBlobFile       :: !(Ref (BlobFile m h))
-    , runRunDataCaching :: !RunDataCaching
-    , runHasFS          :: !(HasFS m h)
-    , runHasBlockIO     :: !(HasBlockIO m h)
+    , blobFile    :: !(Ref (BlobFile m h))
+    , dataCaching :: !RunDataCaching
+    , hasFS       :: !(HasFS m h)
+    , hasBlockIO  :: !(HasBlockIO m h)
     }
 
 -- | Shows only the 'runRunFsPaths' field.
 instance Show (Run m h) where
-  showsPrec _ run = showString "Run { runRunFsPaths = " . showsPrec 0 (runRunFsPaths run) .  showString " }"
+  showsPrec _ run = showString "Run { fsPaths = " . showsPrec 0 run.fsPaths .  showString " }"
 
 instance NFData h => NFData (Run m h) where
-  rnf (Run a b c d e f g h i j) =
-      rnf a `seq` rwhnf b `seq` rnf c `seq` rnf d `seq` rnf e `seq`
-      rnf f `seq` rnf g `seq` rnf h `seq` rwhnf i `seq` rwhnf j
+  rnf (Run numEntries refCounter fsPaths bloomFilter index kOpsFile blobFile dataCaching hasFS hasBlockIO) =
+    rnf numEntries `seq` rwhnf refCounter `seq` rnf fsPaths `seq`
+    rnf bloomFilter `seq` rnf index `seq` rnf kOpsFile `seq`
+    rnf blobFile `seq` rnf dataCaching `seq` rwhnf hasFS `seq` rwhnf hasBlockIO
 
 instance RefCounted m (Run m h) where
-    getRefCounter = runRefCounter
+    getRefCounter r = r.refCounter
 
 size :: Ref (Run m h) -> NumEntries
-size (DeRef run) = runNumEntries run
+size (DeRef run) = run.numEntries
 
 sizeInPages :: Ref (Run m h) -> NumPages
-sizeInPages (DeRef run) = Index.sizeInPages (runIndex run)
+sizeInPages (DeRef run) = Index.sizeInPages run.index
 
 runFsPaths :: Ref (Run m h) -> RunFsPaths
-runFsPaths (DeRef r) = runRunFsPaths r
+runFsPaths (DeRef r) = r.fsPaths
 
 runFsPathsNumber :: Ref (Run m h) -> RunNumber
 runFsPathsNumber = Paths.runNumber . runFsPaths
 
 -- | See 'openFromDisk'
 runIndexType :: Ref (Run m h) -> IndexType
-runIndexType (DeRef r) = Index.indexToIndexType (runIndex r)
+runIndexType (DeRef r) = Index.indexToIndexType r.index
 
 -- | See 'openFromDisk'
 runDataCaching :: Ref (Run m h) -> RunDataCaching
-runDataCaching (DeRef r) = runRunDataCaching r
+runDataCaching (DeRef r) = r.dataCaching
 
 
 -- | Helper function to make a 'WeakBlobRef' that points into a 'Run'.
 mkRawBlobRef :: Run m h -> BlobSpan -> RawBlobRef m h
-mkRawBlobRef Run{runBlobFile} blobspan =
-    BlobRef.mkRawBlobRef runBlobFile blobspan
+mkRawBlobRef run = BlobRef.mkRawBlobRef run.blobFile
 
 -- | Helper function to make a 'WeakBlobRef' that points into a 'Run'.
 mkWeakBlobRef :: Ref (Run m h) -> BlobSpan -> WeakBlobRef m h
-mkWeakBlobRef (DeRef Run{runBlobFile}) blobspan =
-    BlobRef.mkWeakBlobRef runBlobFile blobspan
+mkWeakBlobRef (DeRef run) blobspan = BlobRef.mkWeakBlobRef run.blobFile blobspan
 
 {-# SPECIALISE finaliser ::
      HasFS IO h
@@ -227,7 +228,18 @@
     setRunDataCaching runHasBlockIO runKOpsFile runRunDataCaching
     newRef refCtx
            (finaliser runHasFS runKOpsFile runBlobFile runRunFsPaths)
-           (\runRefCounter -> Run { .. })
+           (\refCounter -> Run {
+                numEntries  = runNumEntries
+              , refCounter  = refCounter
+              , fsPaths     = runRunFsPaths
+              , bloomFilter = runFilter
+              , index       = runIndex
+              , kOpsFile    = runKOpsFile
+              , blobFile    = runBlobFile
+              , dataCaching = runRunDataCaching
+              , hasFS       = runHasFS
+              , hasBlockIO  = runHasBlockIO
+            })
 
 {-# SPECIALISE fromWriteBuffer ::
      HasFS IO h
@@ -336,10 +348,17 @@
     setRunDataCaching hbio runKOpsFile runRunDataCaching
     newRef refCtx (finaliser fs runKOpsFile runBlobFile runRunFsPaths) $ \runRefCounter ->
       Run {
-        runHasFS = fs
-      , runHasBlockIO = hbio
-      , ..
-      }
+          numEntries  = runNumEntries
+        , refCounter  = runRefCounter
+        , fsPaths     = runRunFsPaths
+        , bloomFilter = runFilter
+        , index       = runIndex
+        , kOpsFile    = runKOpsFile
+        , blobFile    = runBlobFile
+        , dataCaching = runRunDataCaching
+        , hasFS       = fs
+        , hasBlockIO  = hbio
+        }
   where
     -- Note: all file data for this path is evicted from the page cache /if/ the
     -- caching argument is 'NoCacheRunData'.
diff --git a/src-core/Database/LSMTree/Internal/RunReader.hs b/src-core/Database/LSMTree/Internal/RunReader.hs
--- a/src-core/Database/LSMTree/Internal/RunReader.hs
+++ b/src-core/Database/LSMTree/Internal/RunReader.hs
@@ -1,3 +1,6 @@
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+
 {-# OPTIONS_HADDOCK not-home #-}
 
 -- | A run that is being read incrementally.
@@ -74,19 +77,19 @@
 data RunReader m h = RunReader {
       -- | The disk page currently being read. If it is 'Nothing', the reader
       -- is considered closed.
-      readerCurrentPage    :: !(MutVar (PrimState m) (Maybe RawPage))
+      currentPage    :: !(MutVar (PrimState m) (Maybe RawPage))
       -- | The index of the entry to be returned by the next call to 'next'.
-    , readerCurrentEntryNo :: !(PrimVar (PrimState m) Word16)
+    , currentEntryNo :: !(PrimVar (PrimState m) Word16)
       -- | Read mode file handle into the run's k\/ops file. We rely on it to
       -- track the position of the next disk page to read, instead of keeping
       -- a counter ourselves. Also, the run's handle is supposed to be opened
       -- with @O_DIRECT@, which is counterproductive here.
-    , readerKOpsHandle     :: !(FS.Handle h)
+    , kOpsHandle     :: !(FS.Handle h)
       -- | The blob file from the run this reader is reading from.
-    , readerBlobFile       :: !(Ref (BlobFile m h))
-    , readerRunDataCaching :: !Run.RunDataCaching
-    , readerHasFS          :: !(HasFS m h)
-    , readerHasBlockIO     :: !(HasBlockIO m h)
+    , blobFile       :: !(Ref (BlobFile m h))
+    , dataCaching    :: !Run.RunDataCaching
+    , hasFS          :: !(HasFS m h)
+    , hasBlockIO     :: !(HasBlockIO m h)
     }
 
 data OffsetKey = NoOffsetKey | OffsetKey !SerialisedKey
@@ -101,30 +104,31 @@
   => OffsetKey
   -> Ref (Run.Run m h)
   -> m  (RunReader m h)
-new !offsetKey
-    readerRun@(DeRef Run.Run {
-      runBlobFile,
-      runRunDataCaching = readerRunDataCaching,
-      runHasFS          = readerHasFS,
-      runHasBlockIO     = readerHasBlockIO,
-      runIndex          = index
-    }) = do
-    (readerKOpsHandle :: FS.Handle h) <-
-      FS.hOpen readerHasFS (runKOpsPath (Run.runFsPaths readerRun)) FS.ReadMode >>= \h -> do
-        fileSize <- FS.hGetSize readerHasFS h
+new !offsetKey readerRun@(DeRef run) = do
+    (kOpsHandle :: FS.Handle h) <-
+      FS.hOpen run.hasFS (runKOpsPath (Run.runFsPaths readerRun)) FS.ReadMode >>= \h -> do
+        fileSize <- FS.hGetSize run.hasFS h
         let fileSizeInPages = fileSize `div` toEnum pageSize
         let indexedPages = getNumPages $ Run.sizeInPages readerRun
         assert (indexedPages == fileSizeInPages) $ pure h
     -- Advise the OS that this file is being read sequentially, which will
     -- double the readahead window in response (only for this file descriptor)
-    FS.hAdviseAll readerHasBlockIO readerKOpsHandle FS.AdviceSequential
+    FS.hAdviseAll run.hasBlockIO kOpsHandle FS.AdviceSequential
 
-    (page, entryNo) <- seekFirstEntry readerKOpsHandle
+    (page, entryNo) <- seekFirstEntry kOpsHandle
 
-    readerBlobFile <- dupRef runBlobFile
-    readerCurrentEntryNo <- newPrimVar entryNo
-    readerCurrentPage <- newMutVar page
-    let reader = RunReader {..}
+    blobFile <- dupRef run.blobFile
+    currentEntryNo <- newPrimVar entryNo
+    currentPage <- newMutVar page
+    let reader = RunReader {
+            currentPage = currentPage
+          , currentEntryNo = currentEntryNo
+          , kOpsHandle = kOpsHandle
+          , blobFile = blobFile
+          , dataCaching = run.dataCaching
+          , hasFS = run.hasFS
+          , hasBlockIO = run.hasBlockIO
+          }
 
     when (isNothing page) $
       close reader
@@ -134,13 +138,13 @@
         case offsetKey of
           NoOffsetKey -> do
             -- Load first page from disk, if it exists.
-            firstPage <- readDiskPage readerHasFS readerKOpsHandle
+            firstPage <- readDiskPage run.hasFS readerKOpsHandle
             pure (firstPage, 0)
           OffsetKey offset -> do
             -- Use the index to find the page number for the key (if it exists).
-            let PageSpan pageNo pageEnd = Index.search offset index
-            seekToDiskPage readerHasFS pageNo readerKOpsHandle
-            readDiskPage readerHasFS readerKOpsHandle >>= \case
+            let PageSpan pageNo pageEnd = Index.search offset run.index
+            seekToDiskPage run.hasFS pageNo readerKOpsHandle
+            readDiskPage run.hasFS readerKOpsHandle >>= \case
               Nothing ->
                 pure (Nothing, 0)
               Just foundPage -> do
@@ -159,8 +163,8 @@
                     -- page and the first key in the next page.
                     -- Thus the reader should be initialised to return keys
                     -- starting from the next (non-overflow) page.
-                    seekToDiskPage readerHasFS (nextPageNo pageEnd) readerKOpsHandle
-                    nextPage <- readDiskPage readerHasFS readerKOpsHandle
+                    seekToDiskPage run.hasFS (nextPageNo pageEnd) readerKOpsHandle
+                    nextPage <- readDiskPage run.hasFS readerKOpsHandle
                     pure (nextPage, 0)
 
 {-# SPECIALISE close ::
@@ -173,12 +177,12 @@
      (MonadSTM m, MonadMask m, PrimMonad m)
   => RunReader m h
   -> m ()
-close RunReader{..} = do
-    when (readerRunDataCaching == Run.NoCacheRunData) $
+close r = do
+    when (r.dataCaching == Run.NoCacheRunData) $
       -- drop the file from the OS page cache
-      FS.hDropCacheAll readerHasBlockIO readerKOpsHandle
-    FS.hClose readerHasFS readerKOpsHandle
-    releaseRef readerBlobFile
+      FS.hDropCacheAll r.hasBlockIO r.kOpsHandle
+    FS.hClose r.hasFS r.kOpsHandle
+    releaseRef r.blobFile
     --TODO: arguably we should have distinct finish and close and require that
     -- readers are _always_ closed, even after they have been drained.
     -- This would allow BlobRefs to remain valid until the reader is closed.
@@ -247,12 +251,12 @@
      (MonadMask m, MonadSTM m, MonadST m)
   => RunReader m h
   -> m (Result m h)
-next reader@RunReader {..} = do
-    readMutVar readerCurrentPage >>= \case
+next reader = do
+    readMutVar reader.currentPage >>= \case
       Nothing ->
         pure Empty
       Just page -> do
-        entryNo <- readPrimVar readerCurrentEntryNo
+        entryNo <- readPrimVar reader.currentEntryNo
         go entryNo page
   where
     go :: Word16 -> RawPage -> m (Result m h)
@@ -261,26 +265,26 @@
         case rawPageIndex page entryNo of
           IndexNotPresent -> do
             -- if it is past the last one, load a new page from disk, try again
-            newPage <- readDiskPage readerHasFS readerKOpsHandle
-            stToIO $ writeMutVar readerCurrentPage newPage
+            newPage <- readDiskPage reader.hasFS reader.kOpsHandle
+            stToIO $ writeMutVar reader.currentPage newPage
             case newPage of
               Nothing -> do
                 close reader
                 pure Empty
               Just p -> do
-                writePrimVar readerCurrentEntryNo 0
+                writePrimVar reader.currentEntryNo 0
                 go 0 p  -- try again on the new page
           IndexEntry key entry -> do
-            modifyPrimVar readerCurrentEntryNo (+1)
-            let entry' = fmap (BlobRef.mkRawBlobRef readerBlobFile) entry
+            modifyPrimVar reader.currentEntryNo (+1)
+            let entry' = fmap (BlobRef.mkRawBlobRef reader.blobFile) entry
             let rawEntry = Entry entry'
             pure (ReadEntry key rawEntry)
           IndexEntryOverflow key entry lenSuffix -> do
             -- TODO: we know that we need the next page, could already load?
-            modifyPrimVar readerCurrentEntryNo (+1)
+            modifyPrimVar reader.currentEntryNo (+1)
             let entry' :: E.Entry SerialisedValue (RawBlobRef m h)
-                entry' = fmap (BlobRef.mkRawBlobRef readerBlobFile) entry
-            overflowPages <- readOverflowPages readerHasFS readerKOpsHandle lenSuffix
+                entry' = fmap (BlobRef.mkRawBlobRef reader.blobFile) entry
+            overflowPages <- readOverflowPages reader.hasFS reader.kOpsHandle lenSuffix
             let rawEntry = mkEntryOverflow entry' page lenSuffix overflowPages
             pure (ReadEntry key rawEntry)
 
diff --git a/src-core/Database/LSMTree/Internal/Unsafe.hs b/src-core/Database/LSMTree/Internal/Unsafe.hs
--- a/src-core/Database/LSMTree/Internal/Unsafe.hs
+++ b/src-core/Database/LSMTree/Internal/Unsafe.hs
@@ -1,6 +1,6 @@
-{-# LANGUAGE CPP                 #-}
-{-# LANGUAGE DataKinds           #-}
-{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE CPP       #-}
+{-# LANGUAGE DataKinds #-}
+
 {-# OPTIONS_HADDOCK not-home #-}
 
 -- | This module brings together the internal parts to provide an API in terms
@@ -1235,9 +1235,9 @@
           resolve
           (tableSessionSalt tEnv)
           runs
-          (V.mapStrict (\(DeRef r) -> Run.runFilter   r) runs)
-          (V.mapStrict (\(DeRef r) -> Run.runIndex    r) runs)
-          (V.mapStrict (\(DeRef r) -> Run.runKOpsFile r) runs)
+          (V.mapStrict (\(DeRef r) -> r.bloomFilter) runs)
+          (V.mapStrict (\(DeRef r) -> r.index      ) runs)
+          (V.mapStrict (\(DeRef r) -> r.kOpsFile   ) runs)
           ks
 
 {-# SPECIALISE rangeLookup ::
diff --git a/src-core/Database/LSMTree/Internal/Vector.hs b/src-core/Database/LSMTree/Internal/Vector.hs
--- a/src-core/Database/LSMTree/Internal/Vector.hs
+++ b/src-core/Database/LSMTree/Internal/Vector.hs
@@ -16,6 +16,7 @@
     binarySearchL,
     unsafeInsertWithMStrict,
     unfoldrNM',
+    singletonMStrict,
 ) where
 
 import           Control.Monad
@@ -136,3 +137,10 @@
             (Just !a,  !b') -> do
               VM.unsafeWrite vec n a
               go vec (n+1) b'
+
+{-# INLINE singletonMStrict #-}
+singletonMStrict :: PrimMonad m => a -> m (V.Vector a)
+singletonMStrict !x = do
+  mv <- VM.unsafeNew 1
+  VM.unsafeWrite mv 0 $! x
+  V.unsafeFreeze mv
diff --git a/src-extras/Database/LSMTree/Extras/Random.hs b/src-extras/Database/LSMTree/Extras/Random.hs
--- a/src-extras/Database/LSMTree/Extras/Random.hs
+++ b/src-extras/Database/LSMTree/Extras/Random.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE CPP          #-}
 
 module Database.LSMTree.Extras.Random (
     -- * Sampling from uniform distributions
@@ -14,6 +15,9 @@
   , shuffle
     -- * Generators for specific data types
   , randomByteStringR
+    -- * Compatibility
+  , splitGen_compat
+  , uniform_compat
   ) where
 
 import qualified Data.ByteString as BS
@@ -21,7 +25,7 @@
 import           Data.Ord (comparing)
 import qualified Data.Set as Set
 import qualified System.Random as R
-import           System.Random (StdGen, Uniform, uniform, uniformR)
+import           System.Random (StdGen, Uniform, uniformR)
 import           Text.Printf (printf)
 
 {-------------------------------------------------------------------------------
@@ -29,17 +33,16 @@
 -------------------------------------------------------------------------------}
 
 uniformWithoutReplacement :: (Ord a, Uniform a) => StdGen -> Int -> [a]
-uniformWithoutReplacement rng n = withoutReplacement rng n uniform
+uniformWithoutReplacement rng n = withoutReplacement rng n uniform_compat
 
 uniformWithReplacement :: Uniform a => StdGen -> Int -> [a]
-uniformWithReplacement rng n = withReplacement rng n uniform
+uniformWithReplacement rng n = withReplacement rng n uniform_compat
 
 sampleUniformWithoutReplacement :: Ord a => StdGen -> Int -> [a] -> [a]
 sampleUniformWithoutReplacement rng0 n (Set.fromList -> xs0)
   | n > Set.size xs0 =
       error $
-        printf "sampleUniformWithoutReplacement: n > length xs0 for n=%d, \
-               \ length xs0=%d"
+        printf "sampleUniformWithoutReplacement: n > length xs0 for n=%d, length xs0=%d"
                n
                (Set.size xs0)
   | otherwise =
@@ -115,6 +118,39 @@
 -- | Generates a random bytestring. Its length is uniformly distributed within
 -- the provided range.
 randomByteStringR :: (Int, Int) -> StdGen -> (BS.ByteString, StdGen)
+#if MIN_VERSION_random(1,3,0)
 randomByteStringR range g =
     let (!l, !g')  = uniformR range g
     in  R.uniformByteString l g'
+#else
+-- MIN_VERSION_random(1,2,0)
+randomByteStringR range g =
+    let (!l, !g')  = uniformR range g
+    in  R.genByteString l g'
+#endif
+
+{-------------------------------------------------------------------------------
+  Compatibility
+-------------------------------------------------------------------------------}
+
+-- | Alternative to @splitGen@ that is also compatible with versions of
+-- random<1.3
+--
+-- Uses @split@ on @random<1.3@, and @splitGen@ on @random>=1.3@. The former
+-- function is deprecated on @random>=1.3@.
+splitGen_compat :: StdGen -> (StdGen, StdGen)
+#if MIN_VERSION_random(1,3,0)
+splitGen_compat = R.splitGen
+#else
+splitGen_compat = R.split
+#endif
+
+-- | Alternative to @uniform@ that is also compatible with versions of
+-- random<1.3
+--
+-- The order of type variables is different on random<1.3 and random>=1.3. This
+-- is inconvenient for type application, so we use this compatibility function
+-- to ensure that the type variables always have the same ordering.
+uniform_compat :: (Uniform a, R.RandomGen g) => g -> (a, g)
+uniform_compat = R.uniform
+
diff --git a/test/Database/LSMTree/Model/Session.hs b/test/Database/LSMTree/Model/Session.hs
--- a/test/Database/LSMTree/Model/Session.hs
+++ b/test/Database/LSMTree/Model/Session.hs
@@ -496,7 +496,7 @@
   , innerBlob :: !(Model.BlobRef b)
   }
 
-deriving stock instance Show b => Show (BlobRef b)
+deriving stock instance Show (BlobRef b)
 
 retrieveBlobs ::
      forall m b. ( MonadState Model m
diff --git a/test/Test/Database/LSMTree/Internal.hs b/test/Test/Database/LSMTree/Internal.hs
--- a/test/Test/Database/LSMTree/Internal.hs
+++ b/test/Test/Database/LSMTree/Internal.hs
@@ -22,11 +22,12 @@
 import           Test.Tasty
 import           Test.Tasty.QuickCheck
 import           Test.Util.FS
+import           Test.Util.QC.Compat (withNumTests_compat)
 
 tests :: TestTree
 tests = testGroup "Test.Database.LSMTree.Internal" [
       testGroup "Cursor" [
-          testProperty "prop_roundtripCursor" $ withMaxSuccess 500 $
+          testProperty "prop_roundtripCursor" $ withNumTests_compat 500 $
             prop_roundtripCursor
         ]
     ]
diff --git a/test/Test/Database/LSMTree/Internal/BloomFilter.hs b/test/Test/Database/LSMTree/Internal/BloomFilter.hs
--- a/test/Test/Database/LSMTree/Internal/BloomFilter.hs
+++ b/test/Test/Database/LSMTree/Internal/BloomFilter.hs
@@ -30,6 +30,7 @@
                      FileFormat (..))
 import           Database.LSMTree.Internal.Serialise (SerialisedKey,
                      serialiseKey)
+import           Test.Util.QC.Compat (withNumTests_compat)
 
 --TODO: add a golden test for the BloomFilter format vs the 'formatVersion'
 -- to ensure we don't change the format without conciously bumping the version.
@@ -38,9 +39,9 @@
     [ testProperty "roundtrip" roundtrip_prop
       -- a specific case: 300 bits is just under 5x 64 bit words
     , testProperty "roundtrip-3-300" $ roundtrip_prop (Positive (Small 3)) (Positive 300)
-    , testProperty "total-deserialisation" $ withMaxSuccess 10000 $
+    , testProperty "total-deserialisation" $ withNumTests_compat 10000 $
         prop_total_deserialisation
-    , testProperty "total-deserialisation-whitebox" $ withMaxSuccess 10000 $
+    , testProperty "total-deserialisation-whitebox" $ withNumTests_compat 10000 $
         prop_total_deserialisation_whitebox
     , testProperty "bloomQueries (bulk)" $
         prop_bloomQueries
diff --git a/test/Test/Database/LSMTree/Internal/Index/Compact.hs b/test/Test/Database/LSMTree/Internal/Index/Compact.hs
--- a/test/Test/Database/LSMTree/Internal/Index/Compact.hs
+++ b/test/Test/Database/LSMTree/Internal/Index/Compact.hs
@@ -50,6 +50,7 @@
 import           Test.Util.Arbitrary (noTags,
                      prop_arbitraryAndShrinkPreserveInvariant)
 import           Test.Util.Orphans ()
+import           Test.Util.QC.Compat (withNumTests_compat)
 import           Text.Printf (printf)
 
 tests :: TestTree
@@ -123,9 +124,9 @@
           prop_roundtrip_chunks
       , testProperty "prop_roundtrip" $
           prop_roundtrip @TestKey
-      , testProperty "prop_total_deserialisation" $ withMaxSuccess 10000
+      , testProperty "prop_total_deserialisation" $ withNumTests_compat 10000
           prop_total_deserialisation
-      , testProperty "prop_total_deserialisation_whitebox" $ withMaxSuccess 10000
+      , testProperty "prop_total_deserialisation_whitebox" $ withNumTests_compat 10000
           prop_total_deserialisation_whitebox
       ]
   ]
diff --git a/test/Test/Database/LSMTree/Internal/Lookup.hs b/test/Test/Database/LSMTree/Internal/Lookup.hs
--- a/test/Test/Database/LSMTree/Internal/Lookup.hs
+++ b/test/Test/Database/LSMTree/Internal/Lookup.hs
@@ -341,9 +341,9 @@
         testSalt
         wb wbblobs
         runs
-        (V.map (\(DeRef r) -> Run.runFilter   r) runs)
-        (V.map (\(DeRef r) -> Run.runIndex    r) runs)
-        (V.map (\(DeRef r) -> Run.runKOpsFile r) runs)
+        (V.map (\(DeRef r) -> r.bloomFilter) runs)
+        (V.map (\(DeRef r) -> r.index) runs)
+        (V.map (\(DeRef r) -> r.kOpsFile) runs)
         keys
     pure $ modelres === realres
   where
diff --git a/test/Test/Database/LSMTree/Internal/Merge.hs b/test/Test/Database/LSMTree/Internal/Merge.hs
--- a/test/Test/Database/LSMTree/Internal/Merge.hs
+++ b/test/Test/Database/LSMTree/Internal/Merge.hs
@@ -127,20 +127,15 @@
     vals = concatMap (bifoldMap pure mempty . snd) kops
     isLarge = not . uncurry entryWouldFitInPage
 
-    getRunContent run@(DeRef Run.Run {
-                         Run.runFilter,
-                         Run.runIndex,
-                         Run.runKOpsFile,
-                         Run.runBlobFile
-                       }) = do
+    getRunContent run@(DeRef r) = do
       runSize         <- evaluate (Run.size run)
       runKOps         <- readKOps Nothing run
-      kopsFileContent <- FS.hGetAll fs runKOpsFile
-      blobFileContent <- withRef runBlobFile $
+      kopsFileContent <- FS.hGetAll fs r.kOpsFile
+      blobFileContent <- withRef r.blobFile $
                          FS.hGetAll fs . BlobFile.blobFileHandle
       pure ( runSize
-             , runFilter
-             , runIndex
+             , r.bloomFilter
+             , r.index
              , runKOps
              , kopsFileContent
              , blobFileContent
diff --git a/test/Test/Database/LSMTree/Internal/MergingTree.hs b/test/Test/Database/LSMTree/Internal/MergingTree.hs
--- a/test/Test/Database/LSMTree/Internal/MergingTree.hs
+++ b/test/Test/Database/LSMTree/Internal/MergingTree.hs
@@ -176,9 +176,9 @@
             resolveVal
             testSalt
             runs
-            (fmap (\(DeRef r) -> Run.runFilter   r) runs)
-            (fmap (\(DeRef r) -> Run.runIndex    r) runs)
-            (fmap (\(DeRef r) -> Run.runKOpsFile r) runs)
+            (fmap (\(DeRef r) -> r.bloomFilter) runs)
+            (fmap (\(DeRef r) -> r.index      ) runs)
+            (fmap (\(DeRef r) -> r.kOpsFile   ) runs)
             keys
 
 type SerialisedEntry = Entry SerialisedValue SerialisedBlob
diff --git a/test/Test/Database/LSMTree/Internal/RawBytes.hs b/test/Test/Database/LSMTree/Internal/RawBytes.hs
--- a/test/Test/Database/LSMTree/Internal/RawBytes.hs
+++ b/test/Test/Database/LSMTree/Internal/RawBytes.hs
@@ -9,9 +9,10 @@
 import           Database.LSMTree.Internal.RawBytes (RawBytes (RawBytes))
 import qualified Database.LSMTree.Internal.RawBytes as RB
 import           Test.QuickCheck (Property, classify, collect, mapSize,
-                     withDiscardRatio, withMaxSuccess, (.||.), (===), (==>))
+                     withDiscardRatio, (.||.), (===), (==>))
 import           Test.Tasty (TestTree, testGroup)
 import           Test.Tasty.QuickCheck (testProperty)
+import           Test.Util.QC.Compat (withNumTests_compat)
 
 -- * Tests
 
@@ -40,7 +41,7 @@
 
 twoBlocksProp :: String -> RawBytes -> RawBytes -> Property -> Property
 twoBlocksProp msgAddition block1 block2
-    = withMaxSuccess 10000 .
+    = withNumTests_compat 10000 .
       classify (block1 == block2) ("equal blocks" ++ msgAddition)
 
 withFirstBlockSizeInfo :: RawBytes -> Property -> Property
diff --git a/test/Test/Database/LSMTree/Internal/Run.hs b/test/Test/Database/LSMTree/Internal/Run.hs
--- a/test/Test/Database/LSMTree/Internal/Run.hs
+++ b/test/Test/Database/LSMTree/Internal/Run.hs
@@ -223,8 +223,8 @@
       Run.size written @=? Run.size loaded
       withRef written $ \written' ->
         withRef loaded $ \loaded' -> do
-          runFilter written' @=? runFilter loaded'
-          runIndex  written' @=? runIndex  loaded'
+          written'.bloomFilter @=? loaded'.bloomFilter
+          written'.index       @=? loaded'.index
 
       writtenKOps <- readKOps Nothing written
       loadedKOps  <- readKOps Nothing loaded
diff --git a/test/Test/Database/LSMTree/Internal/RunBloomFilterAlloc.hs b/test/Test/Database/LSMTree/Internal/RunBloomFilterAlloc.hs
--- a/test/Test/Database/LSMTree/Internal/RunBloomFilterAlloc.hs
+++ b/test/Test/Database/LSMTree/Internal/RunBloomFilterAlloc.hs
@@ -36,7 +36,7 @@
 import qualified Database.LSMTree.Internal.Entry as LSMT
 import           Database.LSMTree.Internal.RunAcc (RunBloomFilterAlloc (..),
                      newMBloom)
-import           System.Random hiding (Seed)
+import           System.Random (StdGen, Uniform, mkStdGen)
 import           Test.QuickCheck
 import           Test.Tasty (TestTree, testGroup)
 import           Test.Tasty.QuickCheck
diff --git a/test/Test/Database/LSMTree/Internal/RunReader.hs b/test/Test/Database/LSMTree/Internal/RunReader.hs
--- a/test/Test/Database/LSMTree/Internal/RunReader.hs
+++ b/test/Test/Database/LSMTree/Internal/RunReader.hs
@@ -198,7 +198,7 @@
       Reader.next reader >>= \case
         Reader.Empty -> pure []
         Reader.ReadEntry key e -> do
-          let fs = Reader.readerHasFS reader
+          let fs = reader.hasFS
           e' <- traverse (readRawBlobRef fs) $ Reader.toFullEntry e
           ((key, e') :) <$> go reader
 
diff --git a/test/Test/Database/LSMTree/Resolve.hs b/test/Test/Database/LSMTree/Resolve.hs
--- a/test/Test/Database/LSMTree/Resolve.hs
+++ b/test/Test/Database/LSMTree/Resolve.hs
@@ -9,6 +9,7 @@
 import           Database.LSMTree.Extras.Generators ()
 import           Test.Tasty
 import           Test.Tasty.QuickCheck
+import           Test.Util.QC.Compat (withNumTests_compat)
 
 tests :: TestTree
 tests = testGroup "Test.Database.LSMTree.Resolve"
@@ -19,9 +20,9 @@
      forall v. (Show v, Arbitrary v, NFData v, SerialiseValue v, ResolveValue v)
   => [TestTree]
 allProperties =
-    [ testProperty "prop_resolveValidOutput" $ withMaxSuccess 1000 $
+    [ testProperty "prop_resolveValidOutput" $ withNumTests_compat 1000 $
         prop_resolveValidOutput @v
-    , testProperty "prop_resolveAssociativity" $ withMaxSuccess 1000 $
+    , testProperty "prop_resolveAssociativity" $ withNumTests_compat 1000 $
         prop_resolveAssociativity @v
     ]
 
diff --git a/test/Test/Util/QC/Compat.hs b/test/Test/Util/QC/Compat.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Util/QC/Compat.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE CPP       #-}
+{-# LANGUAGE DataKinds #-}
+
+-- disabled because imports depend on CPP
+{-# OPTIONS_GHC -Wno-unused-imports #-}
+
+module Test.Util.QC.Compat (
+    withNumTests_compat
+  ) where
+
+import           GHC.TypeLits
+import qualified Test.QuickCheck as QC
+import           Test.QuickCheck (Property, Testable)
+
+-- | Alternative to @withNumTests@ that is also compatible with versions of
+-- QuickCheck<2.18
+--
+-- Uses @withMaxSuccess@ on @QuickCheck<2.18@, and @withNumTests@ on
+-- @QuickCheck>=2.18@. The former function is deprecated on @QuickCheck>=2.18@.
+withNumTests_compat :: Testable prop => Int -> prop -> Property
+#if defined(MIN_VERSION_QuickCheck)
+-- version macros are available and can be used as usual
+# if MIN_VERSION_QuickCheck(2,18,0)
+withNumTests_compat = QC.withNumTests
+# else
+withNumTests_compat = QC.withMaxSuccess
+# endif
+#else
+-- MIN_VERSION_QuickCheck should always be defined as long as we are using Cabal
+-- as our build system
+withNumTests_compat = (undefined :: TypeError (Text "withNumTests_compat: MIN_VERSION_QuickCheck is unexpectedly undefined"))
+#endif
