From dbc51a826e70c622fc96332009540aef31002ff0 Mon Sep 17 00:00:00 2001 From: Edmund Noble Date: Tue, 7 May 2024 10:11:49 -0400 Subject: [PATCH] feature: support for reflecting db contents into another db This lets us for example reflect all of the things we read from one PactDb (like chainweb's) into another (like a pure one) for later use (for example in a unit test). Change-Id: I476b90b2074ad2b6cd4de0b376931108181e638d --- docs/en/pact-functions.md | 2 +- pact.cabal | 1 + src/Pact/MockDb.hs | 4 +- src/Pact/Persist.hs | 38 ++++- src/Pact/Persist/Pure.hs | 30 +++- src/Pact/Persist/Taped.hs | 222 +++++++++++++++++++++++++++ src/Pact/PersistPactDb.hs | 6 +- src/Pact/PersistPactDb/Regression.hs | 72 ++++----- src/Pact/Types/Persistence.hs | 2 +- src/Pact/Types/Runtime.hs | 2 +- tests/PersistSpec.hs | 98 +++++++++++- 11 files changed, 421 insertions(+), 56 deletions(-) create mode 100644 src/Pact/Persist/Taped.hs diff --git a/docs/en/pact-functions.md b/docs/en/pact-functions.md index 9dac53523..4cc9f47b1 100644 --- a/docs/en/pact-functions.md +++ b/docs/en/pact-functions.md @@ -461,7 +461,7 @@ Return ID if called during current pact execution, failing if not. Obtain current pact build version. ```lisp pact> (pact-version) -"4.11" +"4.12" ``` Top level only: this function will fail if used in module code. diff --git a/pact.cabal b/pact.cabal index 84b61c365..b533f22e3 100644 --- a/pact.cabal +++ b/pact.cabal @@ -128,6 +128,7 @@ library Pact.Persist.MockPersist Pact.Persist.Pure Pact.Persist.SQLite + Pact.Persist.Taped Pact.PersistPactDb Pact.PersistPactDb.Regression Pact.Repl diff --git a/src/Pact/MockDb.hs b/src/Pact/MockDb.hs index c8c24912f..3d6c2a022 100644 --- a/src/Pact/MockDb.hs +++ b/src/Pact/MockDb.hs @@ -25,8 +25,8 @@ newtype MockTxIds = instance Default MockTxIds where def = MockTxIds (\_t _i -> rc []) newtype MockGetUserTableInfo = - MockGetUserTableInfo (TableName -> Method () ModuleName) -instance Default MockGetUserTableInfo where def = MockGetUserTableInfo (\_t -> rc "") + MockGetUserTableInfo (TableName -> Method () (Maybe ModuleName)) +instance Default MockGetUserTableInfo where def = MockGetUserTableInfo (\_t -> rc (Just "")) newtype MockCommitTx = MockCommitTx (Method () [TxLogJson]) diff --git a/src/Pact/Persist.hs b/src/Pact/Persist.hs index 66316924d..c29e1ec5f 100644 --- a/src/Pact/Persist.hs +++ b/src/Pact/Persist.hs @@ -4,6 +4,10 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE ScopedTypeVariables #-} module Pact.Persist (Persist, @@ -25,6 +29,7 @@ import Data.String import Data.Hashable import Data.Text (Text) import Data.Typeable (Typeable) +import GHC.Generics import Pact.Types.Pretty import Pact.Types.Runtime @@ -32,21 +37,48 @@ import Pact.Types.Runtime type Persist s a = s -> IO (s,a) newtype DataKey = DataKey Text - deriving (Eq,Ord,IsString,AsString,Hashable,Pretty) + deriving (Eq,Generic,Ord) + deriving newtype (FromJSONKey, ToJSONKey, IsString, AsString, Hashable, Pretty) instance Show DataKey where show (DataKey k) = show k newtype TxKey = TxKey Integer - deriving (Eq,Ord,Num,Enum,Real,Integral,Hashable,Pretty) + deriving stock (Eq,Generic, Ord) + deriving newtype (Num, Enum, Real, Integral, Hashable, Pretty, FromJSONKey, ToJSONKey) instance Show TxKey where show (TxKey k) = show k type DataTable = Table DataKey type TxTable = Table TxKey newtype TableId = TableId Text - deriving (Eq,Show,Ord,IsString,AsString,Hashable, Pretty) + deriving stock (Eq,Generic,Show,Ord) + deriving newtype (IsString,AsString,Hashable,Pretty) data Table k where DataTable :: !TableId -> DataTable TxTable :: !TableId -> TxTable +instance ToJSON DataTable where + toJSON (DataTable (TableId tid)) = object + [ "ttype" .= ("data" :: Text) + , "tid" .= tid + ] +instance ToJSONKey DataTable +instance ToJSON TxTable where + toJSON (TxTable (TableId tid)) = object + [ "ttype" .= ("tx" :: Text) + , "tid" .= tid + ] +instance ToJSONKey TxTable +instance FromJSON DataTable where + parseJSON = withObject "DataTable" $ \o -> do + "data" :: Text <- o .: "ttype" + tid <- o .: "tid" + return $ DataTable (TableId tid) +instance FromJSONKey DataTable +instance FromJSON TxTable where + parseJSON = withObject "TxTable" $ \o -> do + "tx" :: Text <- o .: "ttype" + tid <- o .: "tid" + return $ TxTable (TableId tid) +instance FromJSONKey TxTable tableId :: Table k -> TableId tableId (DataTable t) = t diff --git a/src/Pact/Persist/Pure.hs b/src/Pact/Persist/Pure.hs index c866a95d3..b134f010c 100644 --- a/src/Pact/Persist/Pure.hs +++ b/src/Pact/Persist/Pure.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} @@ -7,6 +8,11 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} module Pact.Persist.Pure ( @@ -26,28 +32,45 @@ import Control.Monad.Reader () import Control.Monad.State import Data.Default import Data.Typeable +import GHC.Generics import Pact.Types.Persistence import Pact.Persist hiding (compileQuery) import Pact.Types.Pretty import qualified Pact.JSON.Encode as J +import qualified Data.Text.Encoding as T newtype Tbl k = Tbl { _tbl :: M.Map k B.ByteString - } deriving (Show,Semigroup,Monoid) + } + deriving stock (Eq, Generic, Show) + deriving newtype (Semigroup, Monoid) makeLenses ''Tbl +instance (FromJSONKey k, Ord k) => FromJSON (Tbl k) where + parseJSON = fmap (Tbl . fmap T.encodeUtf8) . parseJSON +instance (ToJSONKey k, Ord k) => ToJSON (Tbl k) where + toJSON = toJSON . fmap T.decodeUtf8 . _tbl + newtype Tables k = Tables { _tbls :: M.Map (Table k) (Tbl k) - } deriving (Show,Semigroup,Monoid) + } + deriving stock (Eq, Generic, Show) + deriving newtype (Semigroup,Monoid) makeLenses ''Tables +deriving anyclass instance FromJSON (Tables DataKey) +deriving anyclass instance FromJSON (Tables TxKey) +deriving anyclass instance ToJSON (Tables DataKey) +deriving anyclass instance ToJSON (Tables TxKey) data Db = Db { _dataTables :: !(Tables DataKey), _txTables :: !(Tables TxKey) - } deriving (Show) + } + deriving stock (Eq, Generic, Show) + deriving anyclass (FromJSON, ToJSON) makeLenses ''Db instance Default Db where def = Db mempty mempty @@ -59,6 +82,7 @@ data PureDb = PureDb { _committed :: !Db, _temp :: !Db } + deriving stock (Eq, Show) makeLenses ''PureDb instance Default PureDb where def = PureDb def def diff --git a/src/Pact/Persist/Taped.hs b/src/Pact/Persist/Taped.hs new file mode 100644 index 000000000..b539df1f2 --- /dev/null +++ b/src/Pact/Persist/Taped.hs @@ -0,0 +1,222 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DeriveTraversable #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE InstanceSigs #-} + +-- | +-- Module : Pact.Persist.Taped +-- Copyright : (C) 2024 Kadena +-- License : BSD-style (see the file LICENSE) +-- Maintainer : Edmund Noble +-- +-- Tapes for recording and playing back database access. +-- +module Pact.Persist.Taped + ( WriteSet(..) + , reflectingDb + , ReflectingDbEnv(..) + , reflectingInputMirror + , reflectingOutputMirror + , reflectingSource + , reflectingWriteSet + ) where + +import Control.Concurrent.MVar +import Control.Lens + +import Data.Default +import Data.Map (Map) +import Data.Set (Set) +import qualified Data.Set as Set + +import Pact.Types.Term +import Pact.Types.Persistence +import Control.Monad.State +import Control.Monad +import qualified Data.Map as Map +import Pact.Types.RowData +import Pact.Types.Lang (Code(..)) +import Pact.Types.Namespace +import Data.Maybe (isNothing) + +data WriteSet + = WriteSet + { _userTableWrites :: Map TableName (Set RowKey) + , _keySetWrites :: Set KeySetName + , _moduleWrites :: Set ModuleName + , _namespaceWrites :: Set NamespaceName + , _defPactWrites :: Set PactId + } + +instance Semigroup WriteSet where + ws <> ws' = WriteSet + { _userTableWrites = Map.unionWith Set.union (_userTableWrites ws) (_userTableWrites ws') + , _keySetWrites = Set.union (_keySetWrites ws) (_keySetWrites ws') + , _moduleWrites = Set.union (_moduleWrites ws) (_moduleWrites ws') + , _namespaceWrites = Set.union (_namespaceWrites ws) (_namespaceWrites ws') + , _defPactWrites = Set.union (_defPactWrites ws) (_defPactWrites ws') + } +instance Monoid WriteSet where + mempty = WriteSet mempty mempty mempty mempty mempty + +data ReflectingDbEnv mi mo s = ReflectingDbEnv + { _reflectingWriteSet :: !WriteSet + , _reflectingInputMirror :: !mi + , _reflectingOutputMirror :: !mo + , _reflectingSource :: !s + } + +makeLenses ''ReflectingDbEnv + +-- Given inputMirrorDb, outputMirrorDb, and sourceDb, this function creates a PactDb which forwards its read and +-- write requests to sourceDb, and simultaneously populates inputMirrorDb with enough data to allow re-running +-- the same requests without the entire source db. It also populates outputMirrorDb with all of the writes made. +-- Together these allow making small, self-contained tests by: +-- 1. running Pact transactions on reflectingDb with larger source databases +-- 2. extracting the mirror databases in JSON format +-- 3. running the same Pact transactions on reflectingDb with the source set to the original input mirror database +-- 4. asserting that the final output mirror is equal to the original output mirror +-- +-- Note: createSchema operates on Persister, which is a layer lower than PactDb; +-- one must have valid schemas in all database values before using them with reflectingDb. +reflectingDb :: PactDb mi -> PactDb mo -> PactDb s -> PactDb (ReflectingDbEnv mi mo s) +reflectingDb inputMirrorDb outputMirrorDb sourceDb = PactDb + { _readRow = \d k -> modifyMVarStateT $ do + -- when read from, the reflecting db reflects the write from the source to the mirror + -- and returns the read value + mv <- wrapMethod reflectingSource (_readRow sourceDb d k) + createTableForDomainIfMissing reflectingInputMirror inputMirrorDb d + WriteSet{..} <- use reflectingWriteSet + wrapMethod reflectingInputMirror $ \var -> do + -- write to the mirror db only if this row wasn't already written to the source during this transaction + () <- case d of + UserTables tn + | Just tableWrites <- Map.lookup tn _userTableWrites + , Set.member k tableWrites -> return () + | otherwise -> forM_ mv $ \v -> + _writeRow inputMirrorDb Write d k v var + KeySets + | Set.member k _keySetWrites -> return () + | otherwise -> forM_ mv $ \v -> + _writeRow inputMirrorDb Write d k v var + Modules + | Set.member k _moduleWrites -> return () + | otherwise -> forM_ mv $ \v -> + _writeRow inputMirrorDb Write d k v var + Namespaces + | Set.member k _namespaceWrites -> return () + | otherwise -> forM_ mv $ \v -> + _writeRow inputMirrorDb Write d k v var + Pacts + | Set.member k _defPactWrites -> return () + | otherwise -> forM_ mv $ \v -> + _writeRow inputMirrorDb Write d k v var + return () + return mv + + , _writeRow = \wt d k v -> modifyMVarStateT $ do + wrapMethod reflectingSource (_writeRow sourceDb wt d k v) + createTableForDomainIfMissing reflectingOutputMirror outputMirrorDb d + -- we always use Write mode here, because the above write must have succeeded + wrapMethod reflectingOutputMirror (_writeRow outputMirrorDb Write d k v) + reflectingWriteSet <>= case d of + UserTables tn -> mempty { _userTableWrites = Map.singleton tn (Set.singleton k) } + KeySets -> mempty { _keySetWrites = Set.singleton k } + Modules -> mempty { _moduleWrites = Set.singleton k } + Namespaces -> mempty { _namespaceWrites = Set.singleton k } + Pacts -> mempty { _defPactWrites = Set.singleton k } + , _keys = \d -> modifyMVarStateT $ do + ks <- wrapMethod reflectingSource (_keys sourceDb d) + createTableForDomainIfMissing reflectingInputMirror inputMirrorDb d + -- we have no way of knowing the values at these keys, so we write fake values + () <- case d of + UserTables tn -> do + forM_ ks $ \k -> wrapMethod reflectingInputMirror (_writeRow inputMirrorDb Write (UserTables tn) k + RowData { _rdVersion = RDV1, _rdData = ObjectMap mempty }) + KeySets -> do + forM_ ks $ \k -> wrapMethod reflectingInputMirror (_writeRow inputMirrorDb Write KeySets k + KeySet { _ksKeys = mempty, _ksPredFun = Name $ BareName "fake" def }) + Modules -> do + forM_ ks $ \k -> + wrapMethod reflectingInputMirror + (_writeRow inputMirrorDb Write Modules k + (ModuleData (MDInterface (Interface "fake" (Code "") (Meta Nothing []) []) ) mempty mempty)) + Namespaces -> do + forM_ ks $ \k -> + wrapMethod reflectingInputMirror + (_writeRow inputMirrorDb Write Namespaces k + (Namespace (NamespaceName "fake") (GKeySetRef "fake") (GKeySetRef "fake"))) + Pacts -> do + forM_ ks $ \k -> + wrapMethod reflectingInputMirror (_writeRow inputMirrorDb Write Pacts k Nothing) + + return ks + + , _txids = \tn txid -> modifyMVarStateT $ wrapMethod reflectingSource (_txids sourceDb tn txid) + , _createUserTable = \tn mn -> modifyMVarStateT $ do + wrapMethod reflectingSource (_createUserTable sourceDb tn mn) + wrapMethod reflectingOutputMirror (_createUserTable outputMirrorDb tn mn) + + , _getUserTableInfo = \tn -> modifyMVarStateT $ wrapMethod reflectingSource (_getUserTableInfo sourceDb tn) + , _beginTx = \em -> modifyMVarStateT $ do + txid <- wrapMethod reflectingSource (_beginTx sourceDb em) + _ <- wrapMethod reflectingInputMirror (_beginTx inputMirrorDb em) + _ <- wrapMethod reflectingOutputMirror (_beginTx outputMirrorDb em) + return txid + , _commitTx = modifyMVarStateT $ do + logs <- wrapMethod reflectingSource (_commitTx sourceDb) + _ <- wrapMethod reflectingInputMirror (_commitTx inputMirrorDb) + _ <- wrapMethod reflectingOutputMirror (_commitTx outputMirrorDb) + -- reset write set outside of transaction + reflectingWriteSet .= mempty + return logs + , _rollbackTx = modifyMVarStateT $ do + wrapMethod reflectingSource (_rollbackTx sourceDb) + -- we commit to the mirror databases even on a rollback, to allow observing even failed txs + _ <- wrapMethod reflectingInputMirror (_commitTx inputMirrorDb) + _ <- wrapMethod reflectingOutputMirror (_commitTx outputMirrorDb) + -- reset write set outside of transaction + reflectingWriteSet .= mempty + , _getTxLog = \d txid -> modifyMVarStateT $ do + wrapMethod reflectingSource (_getTxLog sourceDb d txid) + } + where + wrapMethod :: Lens' s a -> (MVar a -> IO r) -> StateT s IO r + wrapMethod l m = do + v <- get + var <- liftIO $ newMVar (v ^. l) + r <- liftIO $ m var + v' <- liftIO $ takeMVar var + put (v & l .~ v') + return r + + modifyMVarStateT act var = do + modifyMVar var $ \v -> do + (a, v') <- runStateT act v + return (v', a) + + createTableForDomainIfMissing :: Lens' s a -> PactDb a -> Domain k v -> StateT s IO () + createTableForDomainIfMissing l db d = wrapMethod l $ \var -> + -- create a user table for this write if missing, do this even if the value + -- itself is Nothing, to avoid missing table errors + case d of + UserTables tn -> do + tableModule <- _getUserTableInfo db tn var + when (isNothing tableModule) $ + _createUserTable db tn "fake module name" var + _ -> return () diff --git a/src/Pact/PersistPactDb.hs b/src/Pact/PersistPactDb.hs index 64754bf8b..82a890687 100644 --- a/src/Pact/PersistPactDb.hs +++ b/src/Pact/PersistPactDb.hs @@ -319,12 +319,12 @@ record tt k v = modify' append (h:t) b = let !x = append t b in h : x {-# INLINE record #-} -getUserTableInfo' :: MVar (DbEnv p) -> TableName -> IO ModuleName +getUserTableInfo' :: MVar (DbEnv p) -> TableName -> IO (Maybe ModuleName) getUserTableInfo' e tn = runMVState e $ do r <- doPersist $ \p -> readValue p (DataTable userTableInfo) (DataKey $ asString tn) case r of - (Just (UserTableInfo mn)) -> return mn - Nothing -> throwDbError $ "getUserTableInfo: no such table: " <> pretty tn + (Just (UserTableInfo mn)) -> return (Just mn) + Nothing -> return Nothing {-# INLINE getUserTableInfo' #-} diff --git a/src/Pact/PersistPactDb/Regression.hs b/src/Pact/PersistPactDb/Regression.hs index 91f3fe45d..4a5aa500c 100644 --- a/src/Pact/PersistPactDb/Regression.hs +++ b/src/Pact/PersistPactDb/Regression.hs @@ -51,40 +51,39 @@ nativeLookup (NativeDefName n) = case HM.lookup n nativeDefs of Just (Direct t) -> Just t _ -> Nothing -runRegression :: DbEnv p -> IO (MVar (DbEnv p)) -runRegression p = do - v <- newMVar p - createSchema v - (Just t1) <- begin v +runRegression :: PactDb p -> MVar p -> IO () +runRegression pdb v = do + (Just t1) <- _beginTx pdb Transactional v let user1 = "user1" usert = UserTables user1 toPV :: ToTerm a => a -> PactValue toPV = toPactValueLenient . toTerm' - createUserTable' v user1 "free.some-Module" + _createUserTable pdb user1 "free.some-module" v assertEquals' "output of commit 2" [ TxLogJson $ J.encodeJsonText $ TxLog "SYS_usertables" "user1" $ LegacyValue $ object [ "utModule" .= object - [ "name" .= String "some-Module" + [ "name" .= String "some-module" , "namespace" .= String "free" ] ] ] - (commit v) - void $ begin v - assertEquals' "user table info correct" "free.some-Module" $ _getUserTableInfo pactdb user1 v + (_commitTx pdb v) + void $ _beginTx pdb Transactional v + assertEquals' "user table info correct" (Just "free.some-module") $ _getUserTableInfo pdb user1 v + assertEquals' "user table info missing" Nothing $ _getUserTableInfo pdb "user2" v let row = RowData RDV0 $ ObjectMap $ M.fromList [("gah",pactValueToRowData $ PLiteral (LDecimal 123.454345))] - _writeRow pactdb Insert usert "key1" row v - assertEquals' "user insert" (Just row) (_readRow pactdb usert "key1" v) + _writeRow pdb Insert usert "key1" row v + assertEquals' "user insert" (Just row) (_readRow pdb usert "key1" v) let row' = RowData RDV1 $ ObjectMap $ fmap pactValueToRowData $ M.fromList [("gah",toPV False),("fh",toPV (1 :: Int))] - _writeRow pactdb Update usert "key1" row' v - assertEquals' "user update" (Just row') (_readRow pactdb usert "key1" v) + _writeRow pdb Update usert "key1" row' v + assertEquals' "user update" (Just row') (_readRow pdb usert "key1" v) let ks = mkKeySet [PublicKeyText "skdjhfskj"] "predfun" - _writeRow pactdb Write KeySets "ks1" ks v - assertEquals' "keyset write" (Just ks) $ _readRow pactdb KeySets "ks1" v + _writeRow pdb Write KeySets "ks1" ks v + assertEquals' "keyset write" (Just ks) $ _readRow pdb KeySets "ks1" v (modName,modRef,mod') <- loadModule - _writeRow pactdb Write Modules modName mod' v - assertEquals' "module write" (Just mod') $ _readRow pactdb Modules modName v + _writeRow pdb Write Modules modName mod' v + assertEquals' "module write" (Just mod') $ _readRow pdb Modules modName v assertEquals "module native repopulation" (Right modRef) $ traverse (traverse (fromPersistDirect nativeLookup)) mod' assertEquals' "result of commit 3" @@ -108,35 +107,28 @@ runRegression p = do } ] ) - (commit v) - void $ begin v - tids <- _txids pactdb user1 t1 v + (_commitTx pdb v) + void $ _beginTx pdb Transactional v + tids <- _txids pdb user1 t1 v assertEquals "user txids" [1] tids assertEquals' "user txlogs" [TxLog "USER_user1" "key1" row, TxLog "USER_user1" "key1" row'] $ - _getTxLog pactdb usert (head tids) v - _writeRow pactdb Insert usert "key2" row v - assertEquals' "user insert key2 pre-rollback" (Just row) (_readRow pactdb usert "key2" v) - assertEquals' "keys pre-rollback" ["key1","key2"] $ _keys pactdb (UserTables user1) v - _rollbackTx pactdb v - assertEquals' "rollback erases key2" Nothing $ _readRow pactdb usert "key2" v - assertEquals' "keys" ["key1"] $ _keys pactdb (UserTables user1) v + _getTxLog pdb usert (head tids) v + _writeRow pdb Insert usert "key2" row v + assertEquals' "user insert key2 pre-rollback" (Just row) (_readRow pdb usert "key2" v) + assertEquals' "keys pre-rollback" ["key1","key2"] $ _keys pdb (UserTables user1) v + _rollbackTx pdb v + assertEquals' "rollback erases key2" Nothing $ _readRow pdb usert "key2" v + assertEquals' "keys" ["key1"] $ _keys pdb (UserTables user1) v -- Reversed just to ensure inserts are not in order. for_ (reverse [2::Int .. 9]) $ \k -> - _writeRow pactdb Insert usert (RowKey $ "key" <> (pack $ show k)) row' v - assertEquals' "keys" [RowKey ("key" <> (pack $ show k)) | k <- [1 :: Int .. 9]] $ _keys pactdb (UserTables user1) v - return v + _writeRow pdb Insert usert (RowKey $ "key" <> (pack $ show k)) row' v + assertEquals' "keys" [RowKey ("key" <> (pack $ show k)) | k <- [1 :: Int .. 9]] $ _keys pdb (UserTables user1) v toTerm' :: ToTerm a => a -> Term Name toTerm' = toTerm -begin :: MVar (DbEnv p) -> IO (Maybe TxId) -begin = _beginTx pactdb Transactional - -commit :: MVar (DbEnv p) -> IO [TxLogJson] -commit = _commitTx pactdb - throwFail :: String -> IO a throwFail = throwIO . userError @@ -150,8 +142,10 @@ assertEquals' msg a b = assertEquals msg a =<< b regressPure :: Loggers -> IO (MVar (DbEnv PureDb)) regressPure l = do - let e = initDbEnv l persister initPureDb - runRegression e + v <- newMVar $ initDbEnv l persister initPureDb + createSchema v + runRegression pactdb v + return v _regress :: IO () diff --git a/src/Pact/Types/Persistence.hs b/src/Pact/Types/Persistence.hs index 3ef231ceb..af1db7812 100644 --- a/src/Pact/Types/Persistence.hs +++ b/src/Pact/Types/Persistence.hs @@ -384,7 +384,7 @@ data PactDb e = PactDb { -- | Create a user table. , _createUserTable :: !(TableName -> ModuleName -> Method e ()) -- | Get module, keyset for user table. - , _getUserTableInfo :: !(TableName -> Method e ModuleName) + , _getUserTableInfo :: !(TableName -> Method e (Maybe ModuleName)) -- | Initiate transactional state. Returns txid for 'Transactional' mode -- or Nothing for 'Local' mode. If state already initiated, rollback and throw error. , _beginTx :: !(ExecutionMode -> Method e (Maybe TxId)) diff --git a/src/Pact/Types/Runtime.hs b/src/Pact/Types/Runtime.hs index 72525cba6..6aaea3326 100644 --- a/src/Pact/Types/Runtime.hs +++ b/src/Pact/Types/Runtime.hs @@ -462,7 +462,7 @@ createUserTable :: Info -> TableName -> ModuleName -> Eval e () createUserTable i t m = method i $ \db -> _createUserTable db t m -- | Invoke _getUserTableInfo -getUserTableInfo :: Info -> TableName -> Eval e ModuleName +getUserTableInfo :: Info -> TableName -> Eval e (Maybe ModuleName) getUserTableInfo i t = method i $ \db -> _getUserTableInfo db t -- | Invoke _beginTx diff --git a/tests/PersistSpec.hs b/tests/PersistSpec.hs index f99ce56f0..94fbaebc6 100644 --- a/tests/PersistSpec.hs +++ b/tests/PersistSpec.hs @@ -3,26 +3,118 @@ module PersistSpec (spec) where import Test.Hspec +import Pact.PersistPactDb(createSchema, pactdb) import Pact.PersistPactDb.Regression import qualified Pact.Persist.SQLite as SQLite import System.Directory +import Control.Lens import Control.Monad import Pact.Types.Logger +import Pact.Types.Persistence +import Pact.Persist.Taped +import qualified Pact.Persist.Pure as Pure import Control.Concurrent +import qualified Data.Map as Map +import Pact.Types.RowData +import Pact.Types.Term(ObjectMap(..)) +import Pact.Types.Exp(Literal(..)) spec :: Spec spec = do it "regress Pure" (void $ regressPure neverLog) describe "regress SQLite" regressSQLite + describe "reflected pure db" reflectedTests regressSQLite :: Spec regressSQLite = it "SQLite successfully closes" $ do - let f = "deleteme.sqllite" + let f = "deleteme.sqlite" db <- do doesFileExist f >>= \b -> when b (removeFile f) - sl <- SQLite.initSQLite (SQLite.SQLiteConfig "deleteme.sqllite" []) neverLog - mv <- runRegression (initDbEnv neverLog SQLite.persister sl) + sl <- SQLite.initSQLite (SQLite.SQLiteConfig "deleteme.sqlite" []) neverLog + mv <- newMVar $ initDbEnv neverLog SQLite.persister sl + createSchema mv + runRegression pactdb mv _db <$> readMVar mv SQLite.closeSQLite db `shouldReturn` Right () removeFile f + +regressReflected :: IO () +regressReflected = do + -- create an empty reflecting db + reflectedV <- do + pureDbEnv <- initPureDbEnv + newMVar ReflectingDbEnv + { _reflectingWriteSet = mempty + , _reflectingInputMirror = pureDbEnv + , _reflectingOutputMirror = pureDbEnv + , _reflectingSource = pureDbEnv + } + -- do a bunch of things with the reflecting db, grab the resulting db state + reflectedOut <- do + runRegression (reflectingDb pactdb pactdb pactdb) reflectedV + readMVar reflectedV + -- do the same things with just a pure db, grab the resulting db state + pureOut <- do + v <- newMVar =<< initPureDbEnv + runRegression pactdb v + readMVar v + -- reflected source should have the same contents afterward as pure db + _db (_reflectingSource reflectedOut) `shouldBe` _db pureOut + +reflectedTests :: Spec +reflectedTests = do + it "should reflect reads to the mirrordb" reflectionTest + it "should regress the same as the read db" regressReflected + +initPureDbEnv :: IO (DbEnv Pure.PureDb) +initPureDbEnv = do + v <- newMVar (initDbEnv neverLog Pure.persister Pure.initPureDb) + createSchema v + readMVar v + +reflectionTest :: IO () +reflectionTest = do + sourceDb <- do + mv <- newMVar $ initDbEnv neverLog Pure.persister Pure.initPureDb + createSchema mv + -- prepare initial database + _ <- _beginTx pactdb Transactional mv + _createUserTable pactdb "mod.tbl" "mod" mv + _writeRow pactdb Insert (UserTables "mod.tbl") "key" (RowData RDV1 (ObjectMap $ Map.singleton "vkey" (RDLiteral $ LString "value"))) mv + _ <- _commitTx pactdb mv + readMVar mv + -- initialize, read and write reflecting database + (inputValue, reflectedOut) <- do + inputMirrorDb <- initPureDbEnv + outputMirrorDb <- initPureDbEnv + reflectedV <- newMVar ReflectingDbEnv + { _reflectingWriteSet = mempty + , _reflectingInputMirror = inputMirrorDb + , _reflectingOutputMirror = outputMirrorDb + , _reflectingSource = sourceDb + } + let rdb = reflectingDb pactdb pactdb pactdb + -- read a value from the source database, reflecting it into the input mirror database + inputValue <- _readRow rdb (UserTables "mod.tbl") "key" reflectedV + -- write a new value to the read database, which should not be reflected by further reads into the input mirror, + -- but into the output mirror instead + _writeRow rdb Write (UserTables "mod.tbl") "key2" + (RowData RDV1 (ObjectMap $ Map.singleton "vkey" (RDLiteral $ LString "new value"))) + reflectedV + _ <- _readRow rdb (UserTables "mod.tbl") "key2" reflectedV + reflectedOut <- readMVar reflectedV + return (inputValue, reflectedOut) + do + -- check that the input mirror has the read value + inputMirrorV <- newMVar $ view reflectingInputMirror reflectedOut + reflectedValue <- _readRow pactdb (UserTables "mod.tbl") "key" inputMirrorV + reflectedValue `shouldBe` inputValue + -- check that the input mirror does not have the written value + unreflectedValue <- _readRow pactdb (UserTables "mod.tbl") "key2" inputMirrorV + unreflectedValue `shouldBe` Nothing + do + -- check that the output mirror has the written value + outputMirrorV <- newMVar $ view reflectingOutputMirror reflectedOut + reflectedOutputValue <- _readRow pactdb (UserTables "mod.tbl") "key2" outputMirrorV + reflectedOutputValue `shouldBe` Just (RowData RDV1 (ObjectMap $ Map.singleton "vkey" $ RDLiteral (LString "new value")))