Skip to content

Commit

Permalink
Support Range and SortOrder in DB.readTxHistory
Browse files Browse the repository at this point in the history
I made DB.readTxHistory take and apply sortOrder and range filtering with implementations in Sqlite, MVar and Sqlite's StateMachine tests.

Squashed from:

Ensure readTxHistory tests pass with `Descending noFilter`
[WIP] Make tests concider sorting and filtering
Do actual Sqlite sorting and filtering
Apply suggestions from code review
Introduce defaultTxSortDirection
Temporary overflow fix to make StateMachine tests pass
Rename Filter to Range`
Fixup: more polish
  • Loading branch information
Anviking committed Jul 30, 2019
1 parent e58d7db commit 315fec4
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 43 deletions.
9 changes: 7 additions & 2 deletions lib/core/src/Cardano/Wallet.hs
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,12 @@ import Cardano.Wallet.Primitive.Types
, WalletPassphraseInfo (..)
, WalletState (..)
, computeUtxoStatistics
, defaultTxSortOrder
, log10
, slotDifference
, slotRatio
, slotStartTime
, wholeRange
)
import Cardano.Wallet.Transaction
( ErrMkStdTx (..), ErrValidateSelection, TransactionLayer (..) )
Expand Down Expand Up @@ -685,7 +687,8 @@ newWalletLayer tracer bp db nw tl = do
_listAddresses wid = do
(s, txs) <- DB.withLock db $ (,)
<$> (getState <$> _readWalletCheckpoint wid)
<*> liftIO (DB.readTxHistory db (PrimaryKey wid))
<*> liftIO (DB.readTxHistory db
(PrimaryKey wid) defaultTxSortOrder wholeRange)
let maybeIsOurs (TxOut a _) = if fst (isOurs a s)
then Just a
else Nothing
Expand Down Expand Up @@ -758,7 +761,9 @@ newWalletLayer tracer bp db nw tl = do
ErrStartTimeLaterThanEndTime start end
_ -> pure ()
let tip = currentTip w ^. #slotId
liftIO $ assemble tip <$> DB.readTxHistory db (PrimaryKey wid)
liftIO $ assemble tip
<$> DB.readTxHistory db (PrimaryKey wid)
defaultTxSortOrder wholeRange
where
-- This relies on DB.readTxHistory returning all necessary transactions
-- to assemble coin selection information for outgoing payments.
Expand Down
12 changes: 11 additions & 1 deletion lib/core/src/Cardano/Wallet/DB.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ import Cardano.Wallet.Primitive.AddressDerivation
import Cardano.Wallet.Primitive.Model
( Wallet )
import Cardano.Wallet.Primitive.Types
( DefineTx (..), Hash, TxMeta, WalletId, WalletMetadata )
( DefineTx (..)
, Hash
, Range (..)
, SlotId (..)
, SortOrder (..)
, TxMeta
, WalletId
, WalletMetadata
)
import Control.Monad.Trans.Except
( ExceptT, runExceptT )
import Data.Map.Strict
Expand Down Expand Up @@ -102,6 +110,8 @@ data DBLayer m s t = DBLayer

, readTxHistory
:: PrimaryKey WalletId
-> SortOrder
-> Range SlotId
-> m [(Hash "Tx", (Tx t, TxMeta))]
-- ^ Fetch the current transaction history of a known wallet, ordered by
-- descending slot number.
Expand Down
26 changes: 19 additions & 7 deletions lib/core/src/Cardano/Wallet/DB/MVar.hs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ import Cardano.Wallet.Primitive.AddressDerivation
import Cardano.Wallet.Primitive.Model
( Wallet )
import Cardano.Wallet.Primitive.Types
( Hash, Tx, TxMeta (slotId), WalletId, WalletMetadata )
( Hash
, SortOrder (..)
, Tx
, TxMeta (slotId)
, WalletId
, WalletMetadata
, isWithinRange
)
import Control.Concurrent.MVar
( MVar, modifyMVar, newMVar, readMVar, withMVar )
import Control.DeepSeq
Expand All @@ -37,11 +44,11 @@ import Control.Monad
import Control.Monad.Trans.Except
( ExceptT (..), runExceptT )
import Data.List
( sortOn )
( sortBy )
import Data.Map.Strict
( Map )
import Data.Ord
( Down (..) )
( Down (..), comparing )

import qualified Data.Map.Strict as Map

Expand Down Expand Up @@ -125,10 +132,15 @@ newDBLayer = do
Right $ Just $ Database cp meta (txs' <> txs) k
txs' `deepseq` alterMVar db alter key

, readTxHistory = \key -> let
sortedTxHistory = sortOn slot . Map.toList . txHistory
slot = Down . slotId . snd . snd
in maybe mempty sortedTxHistory . Map.lookup key <$> readMVar db
, readTxHistory = \key order range -> let
order' = case order of
Ascending -> comparing slot
Descending -> comparing $ Down . slot
result =
filter (isWithinRange range . slot)
. sortBy order' . Map.toList . txHistory
slot = slotId . snd . snd
in maybe mempty result . Map.lookup key <$> readMVar db

{-----------------------------------------------------------------------
Keystore
Expand Down
28 changes: 19 additions & 9 deletions lib/core/src/Cardano/Wallet/DB/Sqlite.hs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ import Data.List.Split
import Data.Map.Strict
( Map )
import Data.Maybe
( fromMaybe )
( catMaybes, fromMaybe )
import Data.Quantity
( Quantity (..) )
import Data.Text
Expand Down Expand Up @@ -137,8 +137,10 @@ import Database.Persist.Sql
, selectList
, updateWhere
, (<-.)
, (<=.)
, (=.)
, (==.)
, (>=.)
)
import Database.Persist.Sqlite
( SqlBackend, SqlPersistT, mkSqliteConnectionInfo, wrapConnectionInfo )
Expand Down Expand Up @@ -306,7 +308,8 @@ newDBLayer logConfig trace fp = do
selectLatestCheckpoint wid >>= \case
Just cp -> do
utxo <- selectUTxO cp
txs <- selectTxHistory @t wid [TxMetaStatus ==. W.Pending]
txs <- selectTxHistory @t wid
W.defaultTxSortOrder [TxMetaStatus ==. W.Pending]
s <- selectState (checkpointId cp)
pure (checkpointFromEntity @s @t cp utxo txs <$> s)
Nothing -> pure Nothing
Expand Down Expand Up @@ -347,9 +350,12 @@ newDBLayer logConfig trace fp = do
pure $ Right ()
Nothing -> pure $ Left $ ErrNoSuchWallet wid

, readTxHistory = \(PrimaryKey wid) ->
, readTxHistory = \(PrimaryKey wid) order range ->
runQuery $
selectTxHistory @t wid []
selectTxHistory @t wid order $ catMaybes
[ (TxMetaSlotId >=.) <$> W.rStart range
, (TxMetaSlotId <=.) <$> W.rEnd range
]

{-----------------------------------------------------------------------
Keystore
Expand Down Expand Up @@ -739,18 +745,22 @@ selectTxs txids = do
selectTxHistory
:: forall t. PersistTx t
=> W.WalletId
-> W.SortOrder
-> [Filter TxMeta]
-> SqlPersistT IO [(W.Hash "Tx", (W.Tx t, W.TxMeta))]
selectTxHistory wid conditions = do
-- Note: there are sorted indices on these columns.
-- The secondary sort by TxId is to make the ordering stable
-- so that testing with random data always works.
let sortOpt = [ Desc TxMetaSlotId, Asc TxMetaTxId ]
selectTxHistory wid order conditions = do
metas <- fmap entityVal <$> selectList
((TxMetaWalletId ==. wid) : conditions) sortOpt
let txids = map txMetaTxId metas
(ins, outs) <- selectTxs txids
pure $ txHistoryFromEntity @t metas ins outs
where
-- Note: there are sorted indices on these columns.
-- The secondary sort by TxId is to make the ordering stable
-- so that testing with random data always works.
sortOpt = case order of
W.Ascending -> [Asc TxMetaSlotId, Desc TxMetaTxId]
W.Descending -> [Desc TxMetaSlotId, Asc TxMetaTxId]

---------------------------------------------------------------------------
-- DB queries for address discovery state
Expand Down
37 changes: 37 additions & 0 deletions lib/core/src/Cardano/Wallet/Primitive/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ module Cardano.Wallet.Primitive.Types

-- * Querying
, SortOrder (..)
, Range (..)
, wholeRange
, isWithinRange
, defaultTxSortOrder

-- * Polymorphic
, Hash (..)
Expand Down Expand Up @@ -326,6 +330,39 @@ instance ToText SortOrder where
instance FromText SortOrder where
fromText = fromTextToBoundedEnum SnakeLowerCase

defaultTxSortOrder :: SortOrder
defaultTxSortOrder = Descending

-- |'Range a' is used to filter data with optional `>=` and `<=` bounds.
--
-- When both bounds are 'Just', it functions like the closed range
-- '[start, end]'. When both bounds are 'Nothing' it functions like
-- '(-∞,∞)', including everything (see 'wholeRange'.)
--
-- The full four interesting cases with the corresponding predicate in the third
-- column:
--
-- - [start, end] Range (Just start) (Just end) \x -> x >= start && x <= end
-- - [start,∞) Range (Just start) Nothing \x -> x >= start
-- - (-∞,end) Range Nothing (Just end) \x -> x <= end
-- - (-∞,∞) Range Nothing Nothing \_x -> True
data Range a = Range
{ rStart :: Maybe a
, rEnd :: Maybe a
} deriving (Eq, Show)

-- | The range that includes everything.
wholeRange :: Range a
wholeRange = Range Nothing Nothing

isWithinRange :: Ord a => Range a -> a -> Bool
isWithinRange (Range low high) x =
(maybe True (x >=) low) &&
(maybe True (x <=) high)

-- NOTE: We could imagine replacing 'Range (Just 3) Nothing' with something
-- more magic like: `(Including 3) ... NoBound`

{-------------------------------------------------------------------------------
Stake Pools
-------------------------------------------------------------------------------}
Expand Down
6 changes: 5 additions & 1 deletion lib/core/test/unit/Cardano/Wallet/DB/SqliteFileModeSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import Cardano.Wallet.Primitive.Types
, WalletName (..)
, WalletPassphraseInfo (..)
, WalletState (..)
, defaultTxSortOrder
, wholeRange
)
import Cardano.Wallet.Unsafe
( unsafeRunExceptT )
Expand Down Expand Up @@ -131,7 +133,9 @@ spec = do
unsafeRunExceptT $ createWallet db testWid testCp testMetadata
unsafeRunExceptT $ putTxHistory db testWid (Map.fromList testTxs)
destroyDBLayer ctx
testOpeningCleaning f (`readTxHistory` testWid) testTxs mempty
testOpeningCleaning f
(\db' -> readTxHistory db' testWid defaultTxSortOrder wholeRange)
testTxs mempty

it "put and read checkpoint" $ \f -> do
(ctx, db) <- newDBLayer' (Just f)
Expand Down
8 changes: 6 additions & 2 deletions lib/core/test/unit/Cardano/Wallet/DB/SqliteSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ import Cardano.Wallet.Primitive.Types
, WalletName (..)
, WalletPassphraseInfo (..)
, WalletState (..)
, defaultTxSortOrder
, wholeRange
)
import Cardano.Wallet.Unsafe
( unsafeRunExceptT )
Expand Down Expand Up @@ -169,15 +171,17 @@ simpleSpec = do
unsafeRunExceptT $ createWallet db testPk testCp testMetadata
runExceptT (putTxHistory db testPk (Map.fromList testTxs))
`shouldReturn` Right ()
readTxHistory db testPk `shouldReturn` testTxs
readTxHistory db testPk
defaultTxSortOrder wholeRange `shouldReturn` testTxs

it "put and read tx history - regression case" $ \db -> do
unsafeRunExceptT $ createWallet db testPk testCp testMetadata
unsafeRunExceptT $ createWallet db testPk1 testCp testMetadata
runExceptT (putTxHistory db testPk1 (Map.fromList testTxs))
`shouldReturn` Right ()
runExceptT (removeWallet db testPk) `shouldReturn` Right ()
readTxHistory db testPk1 `shouldReturn` testTxs
readTxHistory db testPk1
defaultTxSortOrder wholeRange `shouldReturn` testTxs

it "put and read checkpoint" $ \db -> do
unsafeRunExceptT $ createWallet db testPk testCp testMetadata
Expand Down
Loading

0 comments on commit 315fec4

Please sign in to comment.