-
Notifications
You must be signed in to change notification settings - Fork 211
/
QueryStore.hs
77 lines (66 loc) · 2.61 KB
/
QueryStore.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
module Cardano.Wallet.DB.Store.QueryStore
( QueryStore (..)
, Query (..)
, queryStoreProperty
, untry
) where
import Prelude
import Control.Exception
( SomeException (..), throwIO )
import Control.Monad.IO.Class
( MonadIO (liftIO) )
import Data.DBVar
( Store (loadS) )
import Data.Delta
( Delta (Base) )
{-----------------------------------------------------------------------------
General QueryStore abstraction
------------------------------------------------------------------------------}
{- |
A 'QueryStore' is a storage facility for a Haskell value of type @a ~@'Base'@ da@.
Typical use cases are a file or a database on the hard disk.
In addition, 'QueryStore' also allows reading /parts/ of the data through 'queryS'
— often, it is more efficient to read part of the data from disk
rather than first load the entire data through 'loadDB' into memory,
and then filtering the parts of interest.
The parts of the data are expressed through a type constructor @read@
— typically implemented as a GADT representing different read operations.
We expect that there is a function @query :: read b -> World a -> b@ which
applies the operation to a plain value.
Then, 'queryS' must satisfy
> ∀ qs read. query read <$> (loadS . store) qs = queryS qs read
In other words, loading the value into memory and reading a part
is equivalent to reading it directly.
For notational simplicity, we make no attempt at codifying this expectation
in Haskell.
We stress that all these operations — especially 'updateS' and 'queryS' —
only exist in order to express control over the storage location
and in order to enable an efficient implementation.
Conceptually, a 'QueryStore' is very plain — it stores a single value of type
@a ~@'Base'@ da@, nothing more, nothing less.
(If you want to store multiple values, consider storing a 'Set' or 'Map'.)
-}
data QueryStore m qa da = QueryStore
{ store :: Store m da
, queryS :: forall b. qa b -> m b
}
class Query qa where
type family World qa
query :: qa b -> World qa -> b
queryStoreProperty
:: (Eq b, Query qa, MonadFail m, Base da ~ World qa)
=> QueryStore m qa da
-> qa b
-> m Bool
queryStoreProperty QueryStore{store, queryS} r = do
Right z <- loadS store
(query r z ==) <$> queryS r
-- | Helper function to retry the exception reported by 'loadS'.
untry :: MonadIO m => m (Either SomeException a) -> m a
untry action = action >>= liftIO . \case
Left (SomeException e) -> throwIO e
Right a -> pure a