packages feed

rocksdb-haskell-jprupp 2.1.7 → 2.2.0

raw patch · 7 files changed

+323/−101 lines, 7 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Database.RocksDB: iter :: (MonadIO m, MonadResource m) => DB -> m Iterator
- Database.RocksDB: iterCF :: (MonadIO m, MonadResource m) => DB -> ColumnFamily -> m Iterator
- Database.RocksDB: iterator :: (MonadIO m, MonadResource m) => DB -> Maybe ColumnFamily -> m Iterator
+ Database.RocksDB: createIteratorSnap :: DB -> Maybe Snapshot -> Maybe ColumnFamily -> IO (Iterator, Maybe ReadOpts)
+ Database.RocksDB: destroyReadOpts :: MonadIO m => ReadOpts -> m ()
+ Database.RocksDB: type ReadOpts = Ptr LReadOpts
+ Database.RocksDB: type Snapshot = Ptr LSnapshot
+ Database.RocksDB: withIterSnap :: DB -> Maybe Snapshot -> (Iterator -> IO a) -> IO a
+ Database.RocksDB: withIterSnapCF :: DB -> Maybe Snapshot -> ColumnFamily -> (Iterator -> IO a) -> IO a
- Database.RocksDB: createIterator :: MonadIO m => DB -> Maybe ColumnFamily -> m Iterator
+ Database.RocksDB: createIterator :: DB -> Maybe ColumnFamily -> IO Iterator
- Database.RocksDB: destroyIterator :: MonadIO m => Iterator -> m ()
+ Database.RocksDB: destroyIterator :: Iterator -> IO ()
- Database.RocksDB: iterEntry :: MonadIO m => Iterator -> m (Maybe (ByteString, ByteString))
+ Database.RocksDB: iterEntry :: Iterator -> IO (Maybe (ByteString, ByteString))
- Database.RocksDB: iterFirst :: MonadIO m => Iterator -> m ()
+ Database.RocksDB: iterFirst :: Iterator -> IO ()
- Database.RocksDB: iterGetError :: MonadIO m => Iterator -> m (Maybe ByteString)
+ Database.RocksDB: iterGetError :: Iterator -> IO (Maybe ByteString)
- Database.RocksDB: iterKey :: MonadIO m => Iterator -> m (Maybe ByteString)
+ Database.RocksDB: iterKey :: Iterator -> IO (Maybe ByteString)
- Database.RocksDB: iterLast :: MonadIO m => Iterator -> m ()
+ Database.RocksDB: iterLast :: Iterator -> IO ()
- Database.RocksDB: iterNext :: MonadIO m => Iterator -> m ()
+ Database.RocksDB: iterNext :: Iterator -> IO ()
- Database.RocksDB: iterPrev :: MonadIO m => Iterator -> m ()
+ Database.RocksDB: iterPrev :: Iterator -> IO ()
- Database.RocksDB: iterSeek :: MonadIO m => Iterator -> ByteString -> m ()
+ Database.RocksDB: iterSeek :: Iterator -> ByteString -> IO ()
- Database.RocksDB: iterValid :: MonadIO m => Iterator -> m Bool
+ Database.RocksDB: iterValid :: Iterator -> IO Bool
- Database.RocksDB: iterValue :: MonadIO m => Iterator -> m (Maybe ByteString)
+ Database.RocksDB: iterValue :: Iterator -> IO (Maybe ByteString)
- Database.RocksDB: withIter :: MonadUnliftIO m => DB -> (Iterator -> m a) -> m a
+ Database.RocksDB: withIter :: DB -> (Iterator -> IO a) -> IO a
- Database.RocksDB: withIterCF :: MonadUnliftIO m => DB -> ColumnFamily -> (Iterator -> m a) -> m a
+ Database.RocksDB: withIterCF :: DB -> ColumnFamily -> (Iterator -> IO a) -> IO a

Files

CHANGELOG.md view
@@ -1,3 +1,10 @@+## 2.2.0++ * Move Iterator module to plain IO.+ * Properly manage read options in createSnapshot.+ * Add nix flake for development.+ * API allows manual database lifecycle management.+ ## 2.1.7   * Simplify author name.
README.md view
@@ -1,2 +1,22 @@ This library provides Haskell bindings to [RocksDB](http://rocksdb.org)++## Development++### Stack++```bash+stack build+stack test+```++### Nix++```bash+nix develop+cabal build all+cabal test all+```++The shell includes GHC 9.8.4, cabal, haskell-language-server, fourmolu,+hlint, and hoogle.
rocksdb-haskell-jprupp.cabal view
@@ -1,13 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.38.0.+-- This file has been generated from package.yaml by hpack version 0.39.6. -- -- see: https://github.com/sol/hpack------ hash: bd91a467d6bd847de202b5a719feec0870396e6f5dcdb305c82ef81b2dbe33be  name:           rocksdb-haskell-jprupp-version:        2.1.7+version:        2.2.0 synopsis:       Haskell bindings for RocksDB description:    See README at <https://github.com/jprupp/rocksdb-haskell#readme> category:       Database, FFI
src/Database/RocksDB/Base.hs view
@@ -18,6 +18,8 @@     , BatchOp (..)     , Range     , ColumnFamily+    , Snapshot+    , ReadOpts      -- * Options     , Config (..)@@ -180,16 +182,21 @@     fst . snd <$> allocate (createSnapshot db) releaseSnapshot  -- | Manually create an unmanaged snapshot.+-- The returned 'DB' has 'readOpts' configured for the snapshot.+-- Use 'releaseSnapshot' to release both the snapshot and its read options. createSnapshot :: MonadIO m => DB -> m (DB, Snapshot) createSnapshot db@DB{rocksDB = db_ptr} = liftIO $ do     snap_ptr <- c_rocksdb_create_snapshot db_ptr-    withReadOpts (Just snap_ptr) $ \read_opts ->-        return (db{readOpts = read_opts}, snap_ptr)+    read_opts <- createReadOpts (Just snap_ptr)+    return (db{readOpts = read_opts}, snap_ptr)  -- | Function to release an unmanaged snapshot.+-- Also releases the read options associated with the snapshot DB. releaseSnapshot :: MonadIO m => (DB, Snapshot) -> m ()-releaseSnapshot (DB{rocksDB = db_ptr}, snap_ptr) =-    liftIO $ c_rocksdb_release_snapshot db_ptr snap_ptr+releaseSnapshot (DB{rocksDB = db_ptr, readOpts = read_opts}, snap_ptr) =+    liftIO $ do+        destroyReadOpts read_opts+        c_rocksdb_release_snapshot db_ptr snap_ptr  -- | Get a DB property. getProperty :: MonadIO m => DB -> Property -> m (Maybe ByteString)
src/Database/RocksDB/Internal.hs view
@@ -17,6 +17,8 @@     , withOptions     , withOptionsCF     , withReadOpts+    , createReadOpts+    , destroyReadOpts     , withWriteOpts      -- * Utilities@@ -95,13 +97,20 @@ withReadOpts :: MonadUnliftIO m => Maybe Snapshot -> (ReadOpts -> m a) -> m a withReadOpts maybe_snap_ptr =     bracket-    create_read_opts+    (createReadOpts maybe_snap_ptr)     (liftIO . c_rocksdb_readoptions_destroy)-  where-    create_read_opts = liftIO $ do-        read_opts_ptr <- c_rocksdb_readoptions_create-        forM_ maybe_snap_ptr $ c_rocksdb_readoptions_set_snapshot read_opts_ptr-        return read_opts_ptr++-- | Create read options without bracket management.+-- Caller is responsible for calling 'destroyReadOpts'.+createReadOpts :: MonadIO m => Maybe Snapshot -> m ReadOpts+createReadOpts maybe_snap_ptr = liftIO $ do+    read_opts_ptr <- c_rocksdb_readoptions_create+    forM_ maybe_snap_ptr $ c_rocksdb_readoptions_set_snapshot read_opts_ptr+    return read_opts_ptr++-- | Destroy read options created with 'createReadOpts'.+destroyReadOpts :: MonadIO m => ReadOpts -> m ()+destroyReadOpts = liftIO . c_rocksdb_readoptions_destroy  withWriteOpts :: MonadUnliftIO m => (WriteOpts -> m a) -> m a withWriteOpts =
src/Database/RocksDB/Iterator.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE TupleSections #-}  -- | -- Module      : Database.RocksDB.Iterator@@ -12,13 +13,17 @@ -- Iterating over key ranges. module Database.RocksDB.Iterator   ( Iterator,+    -- * Bracket-style iteration     withIter,     withIterCF,-    iter,-    iterCF,-    iterator,+    withIterSnap,+    withIterSnapCF,+    -- * Manual iterator management     createIterator,+    createIteratorSnap,     destroyIterator,+    destroyReadOpts,+    -- * Iterator operations     iterEntry,     iterFirst,     iterGetError,@@ -32,6 +37,7 @@   ) where +import Control.Exception (bracket) import Data.ByteString (ByteString) import Data.ByteString qualified as B import Data.ByteString.Unsafe qualified as BU@@ -41,8 +47,6 @@ import Foreign.C.Error (throwErrnoIfNull) import Foreign.C.String (CString) import Foreign.C.Types (CSize)-import UnliftIO-import UnliftIO.Resource  -- | Create 'Iterator' and use it. --@@ -51,78 +55,135 @@ -- however, specify an older 'Snapshot' in the 'ReadOptions'. -- -- Iterator should not be used after computation ends.-withIter :: (MonadUnliftIO m) => DB -> (Iterator -> m a) -> m a-withIter db = withIterCommon db Nothing+withIter :: DB -> (Iterator -> IO a) -> IO a+withIter db = withIterCommon db Nothing Nothing -withIterCF :: (MonadUnliftIO m) => DB -> ColumnFamily -> (Iterator -> m a) -> m a-withIterCF db cf = withIterCommon db (Just cf)+withIterCF :: DB -> ColumnFamily -> (Iterator -> IO a) -> IO a+withIterCF db cf = withIterCommon db Nothing (Just cf) --- | Variation on 'iterator' below.-iter :: (MonadIO m, MonadResource m) => DB -> m Iterator-iter db = iterator db Nothing+-- | Create 'Iterator' on a specific snapshot.+--+-- If 'Nothing' is passed, the iterator creates its own implicit snapshot.+-- If 'Just snapshot' is passed, the iterator uses the provided snapshot,+-- enabling consistent reads across multiple iterators and point queries.+--+-- Iterator should not be used after computation ends.+withIterSnap+    :: DB+    -> Maybe Snapshot+    -> (Iterator -> IO a)+    -> IO a+withIterSnap db msnap = withIterCommon db msnap Nothing -iterCF :: (MonadIO m, MonadResource m) => DB -> ColumnFamily -> m Iterator-iterCF db cf = iterator db (Just cf)+-- | Create 'Iterator' on a column family with a specific snapshot.+--+-- If 'Nothing' is passed for snapshot, the iterator creates its own implicit snapshot.+-- If 'Just snapshot' is passed, the iterator uses the provided snapshot.+withIterSnapCF+    :: DB+    -> Maybe Snapshot+    -> ColumnFamily+    -> (Iterator -> IO a)+    -> IO a+withIterSnapCF db msnap cf = withIterCommon db msnap (Just cf) -withIterCommon ::-  (MonadUnliftIO m) =>-  DB ->-  Maybe ColumnFamily ->-  (Iterator -> m a) ->-  m a-withIterCommon DB {rocksDB = rocks_db, readOpts = read_opts} mcf =-  bracket create_iterator destroy_iterator+withIterCommon+    :: DB+    -> Maybe Snapshot+    -> Maybe ColumnFamily+    -> (Iterator -> IO a)+    -> IO a+withIterCommon DB{rocksDB = rocks_db, readOpts = read_opts} msnap mcf f =+    case msnap of+        Nothing ->+            -- Use DB's readOpts (iterator creates implicit snapshot)+            bracket (create_iterator read_opts) destroy_iterator f+        Just snap ->+            -- Create temporary ReadOpts with the provided snapshot+            bracket create_read_opts destroy_read_opts $ \ro ->+                bracket (create_iterator ro) destroy_iterator f+          where+            create_read_opts = do+                ro <- c_rocksdb_readoptions_create+                c_rocksdb_readoptions_set_snapshot ro snap+                return ro+            destroy_read_opts = c_rocksdb_readoptions_destroy   where-    destroy_iterator = liftIO . c_rocksdb_iter_destroy-    create_iterator = liftIO $-      throwErrnoIfNull "create_iterator" $ case mcf of+    destroy_iterator = c_rocksdb_iter_destroy+    create_iterator ro =+        throwErrnoIfNull "create_iterator" $ case mcf of+            Just cf -> c_rocksdb_create_iterator_cf rocks_db ro cf+            Nothing -> c_rocksdb_create_iterator rocks_db ro++-- | Manually create unmanaged iterator.+createIterator :: DB -> Maybe ColumnFamily -> IO Iterator+createIterator DB{rocksDB = rocks_db, readOpts = read_opts} mcf =+    throwErrnoIfNull "create_iterator" $ case mcf of         Just cf -> c_rocksdb_create_iterator_cf rocks_db read_opts cf         Nothing -> c_rocksdb_create_iterator rocks_db read_opts --- | Iterator is not valid outside of 'ResourceT' context.-iterator ::-  (MonadIO m, MonadResource m) =>-  DB ->-  Maybe ColumnFamily ->-  m Iterator-iterator db mcf =-  snd <$> allocate (createIterator db mcf) destroyIterator+-- | Manually create unmanaged iterator on a specific snapshot.+--+-- If 'Nothing' is passed, behaves like 'createIterator'.+-- If 'Just snapshot' is passed, creates a 'ReadOpts' tied to that snapshot.+--+-- Returns both the 'Iterator' and the 'ReadOpts' that was created.+-- Both must be destroyed: use 'destroyIterator' for the iterator,+-- and 'destroyReadOpts' for the read options.+--+-- For automatic resource management, prefer 'withIterSnap'.+createIteratorSnap+    :: DB+    -> Maybe Snapshot+    -> Maybe ColumnFamily+    -> IO (Iterator, Maybe ReadOpts)+createIteratorSnap db msnap mcf = case msnap of+    Nothing -> (, Nothing) <$> createIterator db mcf+    Just snap -> do+        ro <- c_rocksdb_readoptions_create+        c_rocksdb_readoptions_set_snapshot ro snap+        it <- createIteratorWithOpts db ro mcf+        return (it, Just ro) --- | Manually create unmanaged iterator.-createIterator :: (MonadIO m) => DB -> Maybe ColumnFamily -> m Iterator-createIterator DB {rocksDB = rocks_db, readOpts = read_opts} mcf = liftIO $-  throwErrnoIfNull "create_iterator" $ case mcf of-    Just cf -> c_rocksdb_create_iterator_cf rocks_db read_opts cf-    Nothing -> c_rocksdb_create_iterator rocks_db read_opts+-- | Internal: create iterator with explicit ReadOpts.+createIteratorWithOpts+    :: DB+    -> ReadOpts+    -> Maybe ColumnFamily+    -> IO Iterator+createIteratorWithOpts DB{rocksDB = rocks_db} ro mcf =+    throwErrnoIfNull "create_iterator" $ case mcf of+        Just cf -> c_rocksdb_create_iterator_cf rocks_db ro cf+        Nothing -> c_rocksdb_create_iterator rocks_db ro  -- | Destroy unmanaged iterator.-destroyIterator :: (MonadIO m) => Iterator -> m ()-destroyIterator = liftIO . c_rocksdb_iter_destroy+destroyIterator :: Iterator -> IO ()+destroyIterator = c_rocksdb_iter_destroy  -- | An iterator is either positioned at a key/value pair, or not valid. This -- function returns /true/ iff the iterator is valid.-iterValid :: (MonadIO m) => Iterator -> m Bool-iterValid iter_ptr = liftIO $ do-  x <- c_rocksdb_iter_valid iter_ptr-  return (x /= 0)+iterValid :: Iterator -> IO Bool+iterValid iter_ptr = do+    x <- c_rocksdb_iter_valid iter_ptr+    return (x /= 0)  -- | Position at the first key in the source that is at or past target. The -- iterator is /valid/ after this call iff the source contains an entry that -- comes at or past target.-iterSeek :: (MonadIO m) => Iterator -> ByteString -> m ()-iterSeek iter_ptr key = liftIO $-  BU.unsafeUseAsCStringLen key $ \(key_ptr, klen) ->-    c_rocksdb_iter_seek iter_ptr key_ptr (intToCSize klen)+iterSeek :: Iterator -> ByteString -> IO ()+iterSeek iter_ptr key =+    BU.unsafeUseAsCStringLen key $ \(key_ptr, klen) ->+        c_rocksdb_iter_seek iter_ptr key_ptr (intToCSize klen)  -- | Position at the first key in the source. The iterator is /valid/ after this -- call iff the source is not empty.-iterFirst :: (MonadIO m) => Iterator -> m ()-iterFirst = liftIO . c_rocksdb_iter_seek_to_first+iterFirst :: Iterator -> IO ()+iterFirst = c_rocksdb_iter_seek_to_first  -- | Position at the last key in the source. The iterator is /valid/ after this -- call iff the source is not empty.-iterLast :: (MonadIO m) => Iterator -> m ()-iterLast = liftIO . c_rocksdb_iter_seek_to_last+iterLast :: Iterator -> IO ()+iterLast = c_rocksdb_iter_seek_to_last  -- | Moves to the next entry in the source. After this call, 'iterValid' is -- /true/ iff the iterator was not positioned at the last entry in the source.@@ -130,8 +191,8 @@ -- If the iterator is not valid, this function does nothing. Note that this is a -- shortcoming of the C API: an 'iterPrev' might still be possible, but we can't -- determine if we're at the last or first entry.-iterNext :: (MonadIO m) => Iterator -> m ()-iterNext iter_ptr = liftIO $ c_rocksdb_iter_next iter_ptr+iterNext :: Iterator -> IO ()+iterNext = c_rocksdb_iter_next  -- | Moves to the previous entry in the source. After this call, 'iterValid' is -- /true/ iff the iterator was not positioned at the first entry in the source.@@ -139,55 +200,55 @@ -- If the iterator is not valid, this function does nothing. Note that this is a -- shortcoming of the C API: an 'iterNext' might still be possible, but we can't -- determine if we're at the last or first entry.-iterPrev :: (MonadIO m) => Iterator -> m ()-iterPrev iter_ptr = liftIO $ c_rocksdb_iter_prev iter_ptr+iterPrev :: Iterator -> IO ()+iterPrev = c_rocksdb_iter_prev  -- | Return the key for the current entry if the iterator is currently -- positioned at an entry, ie. 'iterValid'.-iterKey :: (MonadIO m) => Iterator -> m (Maybe ByteString)-iterKey it = liftIO $ iterString it c_rocksdb_iter_key+iterKey :: Iterator -> IO (Maybe ByteString)+iterKey it = iterString it c_rocksdb_iter_key  -- | Return the value for the current entry if the iterator is currently -- positioned at an entry, ie. 'iterValid'.-iterValue :: (MonadIO m) => Iterator -> m (Maybe ByteString)-iterValue it = liftIO $ iterString it c_rocksdb_iter_value+iterValue :: Iterator -> IO (Maybe ByteString)+iterValue it = iterString it c_rocksdb_iter_value  -- | Return the current entry as a pair, if the iterator is currently positioned -- at an entry, ie. 'iterValid'.-iterEntry :: (MonadIO m) => Iterator -> m (Maybe (ByteString, ByteString))-iterEntry it = liftIO $ do-  mkey <- iterKey it-  mval <- iterValue it-  return $ (,) <$> mkey <*> mval+iterEntry :: Iterator -> IO (Maybe (ByteString, ByteString))+iterEntry it = do+    mkey <- iterKey it+    mval <- iterValue it+    return $ (,) <$> mkey <*> mval  -- | Check for errors -- -- Note that this captures somewhat severe errors such as a corrupted database.-iterGetError :: (MonadIO m) => Iterator -> m (Maybe ByteString)-iterGetError iter_ptr = liftIO $ alloca $ \err_ptr -> do-  poke err_ptr nullPtr-  c_rocksdb_iter_get_error iter_ptr err_ptr-  err_str <- peek err_ptr-  if err_str == nullPtr-    then return Nothing-    else Just <$> BU.unsafePackMallocCString err_str+iterGetError :: Iterator -> IO (Maybe ByteString)+iterGetError iter_ptr = alloca $ \err_ptr -> do+    poke err_ptr nullPtr+    c_rocksdb_iter_get_error iter_ptr err_ptr+    err_str <- peek err_ptr+    if err_str == nullPtr+        then return Nothing+        else Just <$> BU.unsafePackMallocCString err_str  -- -- Internal --  iterString ::-  Iterator ->-  (Iterator -> Ptr CSize -> IO CString) ->-  IO (Maybe ByteString)+    Iterator ->+    (Iterator -> Ptr CSize -> IO CString) ->+    IO (Maybe ByteString) iterString it f = do-  valid <- iterValid it-  if valid-    then alloca $ \len_ptr -> do-      str_ptr <- f it len_ptr-      if str_ptr == nullPtr-        then return Nothing-        else do-          len <- peek len_ptr-          Just <$> B.packCStringLen (str_ptr, cSizeToInt len)-    else return Nothing+    valid <- iterValid it+    if valid+        then alloca $ \len_ptr -> do+            str_ptr <- f it len_ptr+            if str_ptr == nullPtr+                then return Nothing+                else do+                    len <- peek len_ptr+                    Just <$> B.packCStringLen (str_ptr, cSizeToInt len)+        else return Nothing
test/Spec.hs view
@@ -3,6 +3,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-} +import Control.Concurrent (forkIO, killThread, threadDelay) import Control.Monad import Data.ByteString.Char8 qualified as C import Data.Default (def)@@ -106,3 +107,122 @@           iterNext itr -- But it remembers previous position           iterKey itr `shouldReturn` Just "b"           iterValue itr `shouldReturn` Just "bbb"+    describe "Snapshots" $ do+      it "getCF and iterator see same data during concurrent writes" $ \db -> do+        let cf = head $ columnFamilies db+        -- Initial data+        putCF db cf "key1" "val1"+        putCF db cf "key2" "val2"+        putCF db cf "key3" "val3"++        -- Create snapshot+        (snapDB, snap) <- createSnapshot db++        -- Start writer thread modifying the same keys+        started <- newEmptyMVar+        writerThread <- forkIO $ do+          putMVar started ()+          let loop n+                | n > 100 = pure ()+                | otherwise = do+                    let suffix = C.pack $ "-v" <> show n+                    putCF db cf "key1" ("val1" <> suffix)+                    putCF db cf "key2" ("val2" <> suffix)+                    putCF db cf "key3" ("val3" <> suffix)+                    threadDelay 500+                    loop (n + 1 :: Int)+          loop (0 :: Int)++        takeMVar started+        threadDelay 1000 -- Let writer get going++        -- Read via getCF on snapshot+        v1 <- getCF snapDB cf "key1"+        threadDelay 10000+        v2 <- getCF snapDB cf "key2"+        threadDelay 10000+        v3 <- getCF snapDB cf "key3"++        -- Read via iterator on same snapshot (using the same column family)+        iterEntries <- withIterCF snapDB cf $ \itr -> do+          iterFirst itr+          let collect acc = do+                valid <- iterValid itr+                if valid+                  then do+                    mentry <- iterEntry itr+                    iterNext itr+                    collect (mentry : acc)+                  else pure (reverse acc)+          catMaybes <$> collect []++        -- Clean up+        killThread writerThread+        releaseSnapshot (snapDB, snap)++        -- Verify: getCF and iterator see the same original values+        v1 `shouldBe` Just "val1"+        v2 `shouldBe` Just "val2"+        v3 `shouldBe` Just "val3"+        iterEntries `shouldBe` [("key1", "val1"), ("key2", "val2"), ("key3", "val3")]++      it "withIterSnap sees snapshot data" $ \db -> do+        put db "a" "1"+        put db "b" "2"+        (_, snap) <- createSnapshot db+        -- Modify after snapshot+        put db "a" "modified"+        put db "c" "3"+        -- Iterator with explicit snapshot should see original data+        entries <- withIterSnap db (Just snap) $ \itr -> do+          iterFirst itr+          let collect acc = do+                valid <- iterValid itr+                if valid+                  then do+                    mentry <- iterEntry itr+                    iterNext itr+                    collect (mentry : acc)+                  else pure (reverse acc)+          catMaybes <$> collect []+        entries `shouldBe` [("a", "1"), ("b", "2")]++      it "withIterSnapCF sees snapshot data on column family" $ \db -> do+        let cf = head $ columnFamilies db+        putCF db cf "x" "100"+        putCF db cf "y" "200"+        (_, snap) <- createSnapshot db+        -- Modify after snapshot+        putCF db cf "x" "modified"+        putCF db cf "z" "300"+        -- Iterator with explicit snapshot on CF+        entries <- withIterSnapCF db (Just snap) cf $ \itr -> do+          iterFirst itr+          let collect acc = do+                valid <- iterValid itr+                if valid+                  then do+                    mentry <- iterEntry itr+                    iterNext itr+                    collect (mentry : acc)+                  else pure (reverse acc)+          catMaybes <$> collect []+        entries `shouldBe` [("x", "100"), ("y", "200")]++      it "createIteratorSnap with manual management" $ \db -> do+        put db "m" "10"+        put db "n" "20"+        (_, snap) <- createSnapshot db+        put db "m" "changed"+        -- Manual iterator creation with snapshot+        (itr, mReadOpts) <- createIteratorSnap db (Just snap) Nothing+        iterFirst itr+        k1 <- iterKey itr+        v1 <- iterValue itr+        iterNext itr+        k2 <- iterKey itr+        v2 <- iterValue itr+        destroyIterator itr+        forM_ mReadOpts destroyReadOpts+        (k1, v1) `shouldBe` (Just "m", Just "10")+        (k2, v2) `shouldBe` (Just "n", Just "20")