packages feed

rocksdb-query 0.6.0 → 0.6.1

raw patch · 4 files changed

+569/−35 lines, 4 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -4,6 +4,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 0.6.1+### Changed+- Documentation: the README explains the purpose and design of the library,+  with installation steps and a tutorial on modelling a schema, and the+  Haddock of every function says why it works the way it does.+- Build against rocksdb-haskell-jprupp 2.6.1 and Stackage nightly-2026-11-07.+ ## 0.6.0 ### Changed - Get rid of MonadIO and operate directly on IO monad.
README.md view
@@ -1,3 +1,412 @@-# RocksDB Query+# rocksdb-query -Helper functions to perform queries on RocksDB databases using serialized Haskell datatypes.+[![Hackage](https://img.shields.io/hackage/v/rocksdb-query.svg)](https://hackage.haskell.org/package/rocksdb-query)+[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jprupp/rocksdb-query/blob/master/LICENSE)++rocksdb-query stores and queries Haskell data types in+[RocksDB](https://rocksdb.org). It sits on top of+[rocksdb-haskell-jprupp](https://github.com/jprupp/rocksdb-haskell) and adds+three things:++- Keys and values are any types with a+  [cereal](https://hackage.haskell.org/package/cereal) `Serialize` instance.+- Empty type classes tie each key type to its value type, so the compiler stops+  you from reading a value of the wrong type.+- Prefix queries fetch every record whose key starts with a short key, as a list+  or as a [conduit](https://hackage.haskell.org/package/conduit) stream.++- [API documentation](https://hackage.haskell.org/package/rocksdb-query)+- [Changelog](https://github.com/jprupp/rocksdb-query/blob/master/CHANGELOG.md)+- [Report a bug](https://github.com/jprupp/rocksdb-query/issues)++## Contents++- [Why rocksdb-query](#why-rocksdb-query)+- [Design principles](#design-principles)+- [Installation](#installation)+- [Quick start](#quick-start)+- [Tutorial: modelling a schema](#tutorial-modelling-a-schema)+- [Concepts](#concepts)+- [Usage guide](#usage-guide)+- [API overview](#api-overview)+- [Designing keys](#designing-keys)+- [Development](#development)+- [Related projects](#related-projects)+- [License](#license)++## Why rocksdb-query++RocksDB is a key-value store of bytes. Real programs store records, and the code+in between is repetitive: encode the key, call `get`, decode the value, and+remember which kind of value lives under which kind of key. That last part is+where bugs hide. If you read a key as the wrong type, the bytes may still+decode, into nonsense, and you find out much later.++rocksdb-query turns that knowledge into types. You declare once that orders live+under order keys, and from then on the compiler checks every read and write.+It also gives you prefix queries, which is how a key-value store answers+questions like "all the orders of this customer" without a query language.++It was written for [Haskoin](https://github.com/jprupp/haskoin-store), whose+indices of the Bitcoin block chain are all modelled this way. It is small on+purpose: about thirty functions, all thin wrappers over+[rocksdb-haskell-jprupp](https://github.com/jprupp/rocksdb-haskell).++## Design principles++- **The schema is in the types.** A `KeyValue key value` instance says what is+  stored under a key type. Reads and writes that disagree do not compile.+- **You own the bytes.** Serialization is a `Serialize` instance that you+  control. That matters, because the byte order of keys decides what a prefix+  query returns and in which order.+- **Sorted keys instead of indices.** RocksDB has no secondary indices. A key+  that starts with what you look records up by turns a lookup into a seek and a+  short scan. Prefix queries make that pattern a one-liner.+- **Corrupt data is a bug.** If your program cannot decode data it wrote, it+  stops with `error` instead of handing you a `Left` to ignore. Carrying on+  would only spread the damage.+- **Stay thin.** Opening, tuning, snapshots and iterators remain+  `rocksdb-haskell-jprupp`'s job. This library adds types, not a new way to use+  RocksDB.++## Installation++### System library++You need the RocksDB C library, as for+[rocksdb-haskell-jprupp](https://github.com/jprupp/rocksdb-haskell#system-library):++```sh+sudo apt-get install librocksdb-dev   # Debian and Ubuntu+sudo dnf install rocksdb-devel        # Fedora+brew install rocksdb                  # macOS with Homebrew+```++### Step by step with Stack++1. Install [Stack](https://docs.haskellstack.org/) and RocksDB (above).+2. Create a project and enter it:++   ```sh+   stack new orders simple+   cd orders+   ```++3. Add `cereal`, `data-default`, `rocksdb-haskell-jprupp` and `rocksdb-query`+   to `build-depends` in `orders.cabal`.+4. Put the [quick start](#quick-start) program in `src/Main.hs` and run it with+   `stack run`. It creates a database in `/tmp/orders` and prints the records it+   reads back.++### With Cabal++```cabal+build-depends:+    base+  , cereal+  , data-default+  , rocksdb-haskell-jprupp+  , rocksdb-query+```++You open and close the database with `rocksdb-haskell-jprupp`, and write the+`Serialize` instances with `cereal`.++### With Stack++The package is in Stackage nightly snapshots. Otherwise add it to `stack.yaml`:++```yaml+extra-deps:+  - rocksdb-query-0.6.1+  - rocksdb-haskell-jprupp-2.6.1+```++## Quick start++Orders keyed by customer, then by order number:++```haskell+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE MultiParamTypeClasses #-}++import Data.Default (def)+import Data.Serialize (Serialize (..))+import Data.Word (Word32)+import Database.RocksDB (Config (..), withDB)+import Database.RocksDB.Query+import GHC.Generics (Generic)++data OrderKey+  = OrderKey Word32 Word32 -- customer, order+  | OrderKeyS Word32 -- customer only: a prefix of OrderKey+  deriving (Show, Generic)++data Order = Order {item :: String, qty :: Int}+  deriving (Show, Generic)++instance Serialize Order++-- The short key must serialize to a prefix of the full key, so write this+-- instance by hand instead of deriving it.+instance Serialize OrderKey where+  put (OrderKey c o) = put c >> put o+  put (OrderKeyS c) = put c+  get = OrderKey <$> get <*> get++instance Key OrderKey++instance KeyValue OrderKey Order++main :: IO ()+main = withDB "/tmp/orders" def {createIfMissing = True} $ \db -> do+  insert db (OrderKey 1 100) (Order "apple" 3)+  writeBatch+    db+    [ insertOp (OrderKey 1 101) (Order "pear" 1),+      insertOp (OrderKey 2 200) (Order "plum" 7)+    ]++  retrieve db (OrderKey 1 100) >>= print+  -- Just (Order {item = "apple", qty = 3})++  matchingAsList db (OrderKeyS 1) >>= mapM_ print+  -- (OrderKey 1 100,Order {item = "apple", qty = 3})+  -- (OrderKey 1 101,Order {item = "pear", qty = 1})++  remove db (OrderKey 2 200)+```++## Tutorial: modelling a schema++A small library system stores books and loans. This tutorial goes through the+decisions behind its keys.++### 1. List the questions++Before writing any type, list how the data will be read:++- get a book by its id;+- list the loans of a member, newest first;+- list every book on a shelf.++Each question becomes a key whose leading fields are what you look things up+by.++### 2. One tag byte per key type++The keys share one column family, so each type starts with its own byte, which+guarantees that a prefix of one type never matches keys of another:++```haskell+data BookKey = BookKey Word32 -- book id++instance Serialize BookKey where+  put (BookKey b) = putWord8 0x01 >> put b+  get = getWord8 >>= guard . (== 0x01) >> BookKey <$> get++instance Key BookKey+instance KeyValue BookKey Book+```++### 3. Keys that sort the way you read++Loans are listed per member, newest first. The key starts with the member, then+an inverted time, so that a larger time sorts first:++```haskell+data LoanKey+  = LoanKey Word32 Word64 Word32 -- member, maxBound - time, book+  | LoanKeyS Word32 -- member only++instance Serialize LoanKey where+  put (LoanKey m t b) = putWord8 0x02 >> put m >> put t >> put b+  put (LoanKeyS m) = putWord8 0x02 >> put m+  get = getWord8 >>= guard . (== 0x02) >> LoanKey <$> get <*> get <*> get++instance Key LoanKey+instance KeyValue LoanKey ()+```++The value is `()`: everything is in the key. That is common for indices.++### 4. Keep data and indices in step++A loan changes the book and adds an index entry. Write both in one batch, so a+crash can never leave one without the other:++```haskell+lend :: DB -> Word32 -> Word32 -> Word64 -> Book -> IO ()+lend db member book now b =+  writeBatch db+    [ insertOp (BookKey book) b {onLoan = True},+      insertOp (LoanKey member (maxBound - now) book) ()+    ]+```++### 5. Read++```haskell+loans <- matchingAsList db (LoanKeyS member)    -- newest first+page2 <- take 20 <$> matchingSkipAsList db (LoanKeyS member) lastKey+```++The same approach answers the shelf question with a `ShelfKey shelf book`+index.++## Concepts++### Typed keys++`Key` and `KeyValue` have no methods. They are declarations:++```haskell+instance Key OrderKey             -- OrderKey can be deleted+instance KeyValue OrderKey Order  -- values under OrderKey are Orders+```++`retrieve`, `insert` and the queries need `KeyValue`; `remove` and `deleteOp`+need only `Key`. Reading a key as a value type you did not declare for it is+a type error, rather than a decoding failure at run time.++### Prefix queries++RocksDB keeps keys sorted by their bytes, so all keys that start with the same+bytes sit next to each other. A query with a short key seeks to the first key+at or after its encoding and reads until a key no longer starts with it.++cereal encodes unsigned integers (`Word32`, `Word64`) in big-endian order, so+those key parts sort in numeric order. Signed integers do not: negative numbers+sort after positive ones.++### Decoding errors++If a key or value in the database cannot be decoded, these functions call+`error`. That means the instance used to read disagrees with the one used to+write, or two key types share the same prefix.++## Usage guide++### Reading and writing++```haskell+insert db key value+mvalue <- retrieve db key+remove db key+```++### Batches++All the operations in a batch are applied atomically:++```haskell+writeBatch db [insertOp k1 v1, insertOp k2 v2, deleteOp k3]+```++### Prefix queries++| Function            | Returns                                      |+| ------------------- | -------------------------------------------- |+| `matchingAsList`    | every match, as a list                       |+| `firstMatching`     | the first match                              |+| `matching`          | every match, as a conduit, on your iterator  |++The conduit version reads lazily, so it suits prefixes with many matches:++```haskell+import Conduit+import Database.RocksDB (withIter)++total <- withIter db $ \it ->+  runConduit $ matching it (OrderKeyS 1) .| mapC (qty . snd) .| sumC+```++### Pagination++The `Skip` variants start inside the prefix at a given key, or the next one+after it:++```haskell+page2 <- take 20 <$> matchingSkipAsList db (OrderKeyS 1) (OrderKey 1 lastSeen)+```++### Column families++Each function has a `CF` variant that takes a `ColumnFamily`:++```haskell+withDBCF dir cfg [("orders", cfg)] $ \db -> do+  let [orders] = columnFamilies db+  insertCF db orders (OrderKey 1 100) (Order "apple" 3)+  matchingAsListCF db orders (OrderKeyS 1)+```++## API overview++| Default column family                  | Column family variant                      |+| -------------------------------------- | ------------------------------------------ |+| `retrieve`                             | `retrieveCF`                               |+| `insert`, `remove`                     | `insertCF`, `removeCF`                     |+| `insertOp`, `deleteOp`                 | `insertOpCF`, `deleteOpCF`                 |+| `matchingAsList`, `matchingSkipAsList` | `matchingAsListCF`, `matchingSkipAsListCF` |+| `firstMatching`, `firstMatchingSkip`   | `firstMatchingCF`, `firstMatchingSkipCF`   |+| `matching`, `matchingSkip`             | use an iterator from `withIterCF`          |++`writeBatch` takes operations for any column family. Everything is in+`Database.RocksDB.Query`.++## Designing keys++- Start every key type with a distinct tag byte when several types share a+  column family, so that their prefixes never overlap:++  ```haskell+  put (OrderKey c o) = putWord8 0x01 >> put c >> put o+  put (OrderKeyS c) = putWord8 0x01 >> put c+  ```++- Give the short key the same leading fields, in the same order, as the full+  key. `get` only needs to decode the full key.+- Use fixed-width, big-endian numbers (`Word32`, `Word64`) for parts you sort+  or range over. Variable-length encodings do not sort numerically.+- To read newest first, store an inverted value, such as `maxBound - time`.++## Development++```sh+git clone https://github.com/jprupp/rocksdb-query.git+cd rocksdb-query++stack build                        # build the library+stack test                         # run the tests+stack haddock --no-haddock-deps    # build the API documentation+```++With `stack --nix`, the `nix` section of `stack.yaml` provides RocksDB.++`package.yaml` is the source of the package description. Change it rather than+`rocksdb-query.cabal`, which [hpack](https://github.com/sol/hpack) generates.++The tests are in `test/Spec.hs` and use [hspec](https://hspec.github.io/).++### Contributing++Pull requests are welcome.++- Keep the style of the code around your change.+- Add tests for the behaviour you change.+- Describe the change in `CHANGELOG.md`, which follows+  [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).++## Related projects++- [rocksdb-haskell-jprupp](https://github.com/jprupp/rocksdb-haskell) is the+  RocksDB binding underneath.+- [haskoin-node](https://github.com/jprupp/haskoin-node) and+  [haskoin-store](https://github.com/jprupp/haskoin-store) store their headers+  and indices with this library.++## License++rocksdb-query is released under the+[MIT License](https://github.com/jprupp/rocksdb-query/blob/master/LICENSE).
rocksdb-query.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           rocksdb-query-version:        0.6.0+version:        0.6.1 synopsis:       RocksDB database querying library for Haskell description:    Please see the README on GitHub at <https://github.com/jprupp/rocksdb-query#readme> category:       Database
src/Database/RocksDB/Query.hs view
@@ -4,13 +4,82 @@  -- | -- Module      : Database.RocksDB.Query--- Copyright   : No rights reserved--- License     : UNLICENSE--- Maintainer  : xenog@protonmail.com+-- Copyright   : Haskoin Developers+-- License     : MIT+-- Maintainer  : jprupp@protonmail.ch -- Stability   : experimental -- Portability : POSIX -- -- Query functions to make interaction with RocksDB stores easier and safer.+--+-- = Purpose+--+-- RocksDB stores bytes. A program stores records: users, orders,+-- transactions. Between the two sits a lot of repetitive and error-prone code+-- that encodes keys, decodes values, and remembers which kind of value lives+-- under which kind of key. Getting that last part wrong is a silent bug: the+-- bytes decode into nonsense, or into the wrong record, long after they were+-- written.+--+-- This module removes that code and makes the mapping from key types to value+-- types something the compiler checks. It is deliberately small, and leaves+-- opening, tuning and closing the database to "Database.RocksDB".+--+-- = Design+--+-- * __Types describe the schema.__ A key type and the instance+--   @'KeyValue' Key Value@ together say what is stored where. Reading a key as+--   a type you did not declare for it does not compile.+--+-- * __Serialization is cereal.__ Its encodings of fixed-width unsigned+--   integers are big-endian, so numeric key parts sort numerically, which makes+--   range scans work. You keep full control over the bytes by writing the+--   'Serialize' instance yourself when the order matters.+--+-- * __Prefix queries are the main way to read sets.__ RocksDB has no+--   secondary indices or queries, but it keeps keys sorted. A schema whose keys+--   start with what you look records up by turns "all orders of a customer"+--   into one seek and a short scan.+--+-- * __Corrupt data is a bug, not an outcome.__ Decoding failures call 'error'+--   rather than returning 'Maybe' or 'Either'. If data written by your own+--   program cannot be read back, the program is wrong, and carrying on would+--   only spread the damage.+--+-- Keys and values are Haskell types with 'Serialize' instances from the+-- @cereal@ package. They are encoded with 'encode' before being written and+-- decoded with 'decode' after being read. The empty classes 'Key' and+-- 'KeyValue' tie key types to value types, so that the type checker prevents+-- reading a value of the wrong type for a key:+--+-- @+-- data UserKey = UserKey Word32 deriving (Generic)+-- instance 'Serialize' UserKey+-- instance 'Key' UserKey+-- instance 'KeyValue' UserKey User+--+-- getUser :: DB -> Word32 -> IO (Maybe User)+-- getUser db = 'retrieve' db . UserKey+-- @+--+-- = Prefix queries+--+-- Because RocksDB keeps keys sorted by their bytes, all keys that start with+-- the same bytes are stored next to each other. The 'matching' family of+-- functions takes a short key whose encoding is a prefix of the encoding of+-- full keys, and returns every entry whose key starts with that prefix. See+-- 'matching' for an example.+--+-- = Decoding errors+--+-- If a stored key or value cannot be decoded, these functions call 'error'.+-- That indicates a bug, such as a mismatch between the 'Serialize' instance+-- used to write and to read, or two key types sharing a prefix.+--+-- = Column families+--+-- Functions ending in @CF@ act on a given column family. The others act on the+-- default column family. module Database.RocksDB.Query where  import Conduit@@ -19,13 +88,25 @@ import Data.Serialize as S import Database.RocksDB as R --- | Class for types that are database keys.+-- | Class for types that are database keys. It has no methods; an instance+-- marks a type as allowed to be used as a key in 'remove', 'deleteOp' and+-- related functions.+--+-- Deletion needs to know only that a type is a key, not what it maps to, which+-- is why it is a separate class. class Key key --- | Class for types that are corresponding database key and value.+-- | Class for types that are corresponding database key and value. It has no+-- methods; an instance states that values of type @value@ are stored under keys+-- of type @key@. Functions that read or write values require it.+--+-- The class has no methods because its job is purely to be checked by the+-- compiler: it records a decision about your schema in a place where every read+-- and write must agree with it. class KeyValue key value --- | Read a value from the database, or 'Nothing' if not found.+-- | Read a value from the default column family, or 'Nothing' if not found.+-- Calls 'error' if the stored value cannot be decoded. retrieve ::   (KeyValue key value, Serialize key, Serialize value) =>   DB ->@@ -33,6 +114,7 @@   IO (Maybe value) retrieve db = retrieveCommon db Nothing +-- | Like 'retrieve', but read from the given column family. retrieveCF ::   (KeyValue key value, Serialize key, Serialize value) =>   DB ->@@ -41,7 +123,8 @@   IO (Maybe value) retrieveCF db cf = retrieveCommon db (Just cf) --- | Read a value from the database, or 'Nothing' if not found.+-- | Read a value from the default column family, or from a column family if+-- one is given. Returns 'Nothing' if not found. retrieveCommon ::   (KeyValue key value, Serialize key, Serialize value) =>   DB ->@@ -60,6 +143,9 @@       Just cf -> R.getCF db cf (encode key)       Nothing -> R.get db (encode key) +-- | Internal function to collect, from the current position of the iterator,+-- every entry whose key starts with the encoding of the given key. Stops at the+-- first entry that does not match. matchRecursiveList ::   (KeyValue key value, Serialize key, Serialize value) =>   key ->@@ -80,7 +166,8 @@             else return []     base_bytes = encode base --- | Internal function for recursively matching a key.+-- | Internal function for recursively matching a key. Like+-- 'matchRecursiveList', but streams the entries through a conduit. matchRecursive ::   ( KeyValue key value,     Serialize key,@@ -113,18 +200,28 @@ -- >   put (FullKey a b) = put a >> put b -- >   get = FullKey <$> get <*> get -- > instance KeyValue MyKey String--- > main = do--- >   db <- open "test-db" defaultOptions {createIfMissing = True}--- >   insert db (FullKey "hello" "world") "despite all my rage"--- >   Just record <- runResourceT . runConduit $--- >     matching db def (ShortKey "hello") .| headC--- >   print (record :: (MyKey, String))--- >   -- (Fullkey "hello" "world","despite all my rage")+-- > main =+-- >   withDB "test-db" def {createIfMissing = True} $ \db -> do+-- >     insert db (FullKey "hello" "world") "despite all my rage"+-- >     Just record <- withIter db $ \it ->+-- >       runConduit $ matching it (ShortKey "hello") .| headC+-- >     print (record :: (MyKey, String))+-- >     -- (FullKey "hello" "world","despite all my rage") -- -- In this example the @ShortKey@ is serialized to the prefix of the only -- element in the database, which is then returned. Since the 'get' function of -- the 'Serialize' instance for @MyKey@ only understands how to deserialize a -- @FullKey@, then that is what is returned.+--+-- The iterator is moved to the first key at or after the prefix, and the+-- conduit yields entries until it finds a key that does not start with the+-- prefix. The iterator must stay open while the conduit runs. See+-- 'matchingAsList' and 'firstMatching' for versions that manage the iterator+-- themselves.+--+-- The conduit version exists for sets too large to hold in memory, and for+-- consumers that stop early: only the entries that are actually consumed are read+-- from the database. matching ::   ( KeyValue key value,     Serialize key,@@ -139,6 +236,15 @@  -- | Like 'matching', but skip to the second key passed as argument, or after if -- there is no entry for the second key.+--+-- This is useful for pagination: pass the last key seen in the previous page+-- (or the key just after it) as the second argument. The second key should+-- start with the same prefix as the first; if it sorts before the prefix, no+-- entries are returned.+--+-- Starting from a key instead of an offset is what makes deep pagination cheap:+-- the database seeks straight to the page instead of reading and discarding+-- every entry before it. matchingSkip ::   ( KeyValue key value,     Serialize key,@@ -152,7 +258,8 @@   liftIO (iterSeek it (encode start))   matchRecursive base it --- | Insert a record into the database.+-- | Insert a record into the default column family, replacing any previous+-- value for the key. insert ::   (KeyValue key value, Serialize key, Serialize value) =>   DB ->@@ -161,7 +268,7 @@   IO () insert db key value = R.put db (encode key) (encode value) --- | Insert a record into the database.+-- | Like 'insert', but write to the given column family. insertCF ::   (KeyValue key value, Serialize key, Serialize value) =>   DB ->@@ -171,17 +278,19 @@   IO () insertCF db cf key value = R.putCF db cf (encode key) (encode value) --- | Delete a record from the database.+-- | Delete a record from the default column family. Deleting a key that does+-- not exist is not an error. remove :: (Key key, Serialize key) => DB -> key -> IO () remove db key = delete db (encode key) --- | Delete a record from the database.+-- | Like 'remove', but delete from the given column family. removeCF ::   (Key key, Serialize key) =>   DB -> ColumnFamily -> key -> IO () removeCF db cf key = deleteCF db cf (encode key) --- | Get the 'BatchOp' to insert a record in the database.+-- | Get the 'BatchOp' to insert a record in the default column family. Use with+-- 'writeBatch' to apply several changes atomically. insertOp ::   (KeyValue key value, Serialize key, Serialize value) =>   key ->@@ -189,7 +298,7 @@   BatchOp insertOp key value = R.Put (encode key) (encode value) --- | Get the 'BatchOp' to insert a record in the database.+-- | Like 'insertOp', but for the given column family. insertOpCF ::   (KeyValue key value, Serialize key, Serialize value) =>   ColumnFamily ->@@ -198,19 +307,25 @@   BatchOp insertOpCF cf key value = R.PutCF cf (encode key) (encode value) --- | Get the 'BatchOp' to delete a record from the database.+-- | Get the 'BatchOp' to delete a record from the default column family. Use+-- with 'writeBatch' to apply several changes atomically. deleteOp :: (Key key, Serialize key) => key -> BatchOp deleteOp key = Del (encode key) --- | Get the 'BatchOp' to delete a record from the database.+-- | Like 'deleteOp', but for the given column family. deleteOpCF :: (Key key, Serialize key) => ColumnFamily -> key -> BatchOp deleteOpCF cf key = DelCF cf (encode key) --- | Write a batch to the database.+-- | Write a batch to the database atomically. Synonym for 'write'.+--+-- @+-- 'writeBatch' db ['insertOp' k1 v1, 'deleteOp' k2]+-- @ writeBatch :: DB -> [BatchOp] -> IO () writeBatch = write --- | Like 'matching' but return the first element only.+-- | Like 'matching' but return the first element only, or 'Nothing' if no key+-- starts with the prefix. Opens and closes its own iterator. firstMatching ::   ( KeyValue key value,     Serialize key,@@ -222,7 +337,7 @@ firstMatching db base =   withIter db $ \it -> runConduit $ matching it base .| headC --- | Like 'matching' but return the first element only.+-- | Like 'firstMatching', but read from the given column family. firstMatchingCF ::   ( KeyValue key value,     Serialize key,@@ -235,7 +350,8 @@ firstMatchingCF db cf base =   withIterCF db cf $ \it -> runConduit $ matching it base .| headC --- | Like 'matchingSkip', but return the first element only.+-- | Like 'matchingSkip', but return the first element only. Opens and closes+-- its own iterator. firstMatchingSkip ::   ( KeyValue key value,     Serialize key,@@ -250,7 +366,7 @@     runConduit $       matchingSkip it base start .| headC --- | Like 'matchingSkip', but return the first element only.+-- | Like 'firstMatchingSkip', but read from the given column family. firstMatchingSkipCF ::   ( KeyValue key value,     Serialize key,@@ -266,7 +382,9 @@     runConduit $       matchingSkip it base start .| headC --- | Like 'matching' but return a list.+-- | Like 'matching' but return a list. Opens and closes its own iterator. All+-- matching entries are loaded into memory, so use 'matching' for prefixes that+-- may match many entries. matchingAsList ::   ( KeyValue key value,     Serialize key,@@ -280,7 +398,7 @@     iterSeek it (encode base)     matchRecursiveList base it --- | Like 'matching' but return a list.+-- | Like 'matchingAsList', but read from the given column family. matchingAsListCF ::   ( KeyValue key value,     Serialize key,@@ -295,7 +413,7 @@     iterSeek it (encode base)     matchRecursiveList base it --- | Like 'matchingSkip', but return a list.+-- | Like 'matchingSkip', but return a list. Opens and closes its own iterator. matchingSkipAsList ::   ( KeyValue key value,     Serialize key,@@ -310,7 +428,7 @@     iterSeek it (encode start)     matchRecursiveList base it --- | Like 'matchingSkip', but return a list.+-- | Like 'matchingSkipAsList', but read from the given column family. matchingSkipAsListCF ::   ( KeyValue key value,     Serialize key,