packages feed

cqrs-sqlite3 0.8.0 → 0.9.0

raw patch · 4 files changed

+145/−111 lines, 4 filesdep +cqrs-sqlite3dep +cqrs-testdep +cqrs-typesdep −cerealdep −cqrsdep ~conduitdep ~direct-sqlitePVP ok

version bump matches the API change (PVP)

Dependencies added: cqrs-sqlite3, cqrs-test, cqrs-types, hspec, pool-conduit, text, transformers

Dependencies removed: cereal, cqrs

Dependency ranges changed: conduit, direct-sqlite

API changes (from Hackage documentation)

- Data.CQRS.EventStore.Backend.Sqlite3: openSqliteEventStore :: String -> IO EventStoreBackend
+ Data.CQRS.EventStore.Backend.Sqlite3: createBackendPool :: Int -> Text -> IO (Pool SQLite3EventStoreBackend)
+ Data.CQRS.EventStore.Backend.Sqlite3: data SQLite3EventStoreBackend
+ Data.CQRS.EventStore.Backend.Sqlite3: instance EventStoreBackend SQLite3EventStoreBackend
- Data.CQRS.EventStore.Backend.Sqlite3Utils: execSql :: Database -> String -> [SQLData] -> IO ()
+ Data.CQRS.EventStore.Backend.Sqlite3Utils: execSql :: Database -> Text -> [SQLData] -> IO ()
- Data.CQRS.EventStore.Backend.Sqlite3Utils: sourceQuery :: Database -> String -> [SQLData] -> Source IO [SQLData]
+ Data.CQRS.EventStore.Backend.Sqlite3Utils: sourceQuery :: Database -> Text -> [SQLData] -> Source (ResourceT IO) [SQLData]

Files

cqrs-sqlite3.cabal view
@@ -1,24 +1,51 @@ Name:                cqrs-sqlite3-Version:             0.8.0+Version:             0.9.0 Synopsis:            SQLite3 backend for the cqrs package. Description:         SQLite3 backend for the cqrs package. License:             MIT License-file:        LICENSE Category:            Data-Cabal-version:       >=1.6.0.1+Cabal-version:       >=1.10 Build-type:          Simple Author:              Bardur Arantsson Maintainer:          Bardur Arantsson <bardur@scientician.net>  Library-  build-depends:   base == 4.*-                 , bytestring >= 0.9.0.1-                 , cereal >= 0.3.3 && < 0.4-                 , conduit >= 0.1 && < 0.2-                 , cqrs >= 0.8.0 && < 0.9-                 , direct-sqlite >= 1.1 && < 1.2-  extensions:      ScopedTypeVariables-  ghc-options:     -Wall-  hs-source-dirs:  src-  exposed-modules: Data.CQRS.EventStore.Backend.Sqlite3-                   Data.CQRS.EventStore.Backend.Sqlite3Utils+  build-depends:      base == 4.*+                    , bytestring >= 0.9.0.1+                    , conduit >= 1.0 && < 2+                    , cqrs-types >= 0.9.0 && < 0.10.0+                    , pool-conduit >= 0.1 && < 0.2+                    , direct-sqlite >= 2.2 && < 2.3+                    , text >= 0.11 && < 0.12+                    , transformers >= 0.2 && <0.4+  Default-Language:   Haskell2010+  Default-Extensions: OverloadedStrings+                      ScopedTypeVariables+                      TypeFamilies+  ghc-options:        -Wall+  hs-source-dirs:     src+  exposed-modules:    Data.CQRS.EventStore.Backend.Sqlite3+                      Data.CQRS.EventStore.Backend.Sqlite3Utils++Test-Suite cqrs-sqlite3-tests+  Type:                exitcode-stdio-1.0+  Hs-source-dirs:      src-test+  Main-is:             Main.hs+  Build-depends:       base == 4.*+                     , bytestring >= 0.9.0.1+                     , conduit >= 0.5 && < 0.6+                     , cqrs-test >= 0.9 && < 0.10.0+                     , pool-conduit >= 0.1 && < 0.2+                     , direct-sqlite >= 2.2 && < 2.3+                     , text >= 0.11 && < 0.12+                     , transformers >= 0.2.2 && < 0.4+                     -- Self-dependency+                     , cqrs-sqlite3+                     -- Test framework+                     , hspec >= 1.3 && < 2.0+  Ghc-options:         -Wall+  Default-language:    Haskell2010+  Default-extensions:  OverloadedStrings+                       ScopedTypeVariables+                       TypeFamilies
+ src-test/Main.hs view
@@ -0,0 +1,17 @@+module Main where++import Data.CQRS.Test.EventStore.BackendTest (testBackend)+import Data.CQRS.EventStore.Backend.Sqlite3+import Test.Hspec (hspec, Spec)++sqlite3BackendTest :: Spec+sqlite3BackendTest = do+  testBackend mkPool setup+  where+    mkPool = createBackendPool 1 ":memory:"+    setup = return ()++main :: IO ()+main = do+  hspec $ do+    sqlite3BackendTest
src/Data/CQRS/EventStore/Backend/Sqlite3.hs view
@@ -1,27 +1,29 @@ {-| Implementation of an SQLite3-based event store. -} module Data.CQRS.EventStore.Backend.Sqlite3-       ( openSqliteEventStore+       ( SQLite3EventStoreBackend+       , createBackendPool        ) where -import           Control.Monad (when, forM_, liftM)+import           Control.Monad (when, forM_) import           Data.ByteString (ByteString)-import           Data.Conduit (Source, ($=), ($$), runResourceT)+import           Data.Conduit (ResourceT, Source, ($=), ($$), runResourceT) import qualified Data.Conduit.List as CL+import           Data.Conduit.Pool (Pool, createPool) import           Data.CQRS.EventStore.Backend (EventStoreBackend(..), RawEvent, RawSnapshot(..)) import           Data.CQRS.EventStore.Backend.Sqlite3Utils (withTransaction, execSql, sourceQuery) import           Data.CQRS.GUID (GUID)+import qualified Data.CQRS.GUID as G import           Data.CQRS.PersistedEvent (PersistedEvent(..))-import           Data.CQRS.Serialize (decode', encode)+import           Data.Text (Text) import qualified Database.SQLite3 as SQL import           Database.SQLite3 (Database, SQLData(..))-import           Prelude hiding (catch)  -- Convenience class for converting values to SQLData. class ToSQLData a where   toSQLData :: a -> SQLData  instance ToSQLData GUID where-  toSQLData = SQLBlob . encode+  toSQLData = SQLBlob . G.toByteString  instance ToSQLData Int where   toSQLData = SQLInteger . fromIntegral@@ -30,39 +32,30 @@   toSQLData = SQLBlob  -- SQL-createEventsSql :: String-createEventsSql = "CREATE TABLE IF NOT EXISTS events ( guid BLOB , ev_data BLOB , version INTEGER , gversion INTEGER, PRIMARY KEY (guid, version) );"+createEventsSql :: Text+createEventsSql = "CREATE TABLE IF NOT EXISTS events ( guid BLOB , ev_data BLOB , version INTEGER , PRIMARY KEY (guid, version) );" -selectEventsSql :: String-selectEventsSql = "SELECT version, gversion, ev_data FROM events WHERE guid = ? AND version > ? ORDER BY version ASC;"+selectEventsSql :: Text+selectEventsSql = "SELECT version, ev_data FROM events WHERE guid = ? AND version >= ? ORDER BY version ASC;" -enumerateAllEventsSql :: String-enumerateAllEventsSql = "SELECT gversion, guid, version, ev_data FROM events WHERE gversion >= ? ORDER BY gversion ASC;"+enumerateAllEventsSql :: Text+enumerateAllEventsSql = "SELECT guid, version, ev_data FROM events ORDER BY version ASC;" -insertEventSql :: String-insertEventSql = "INSERT INTO events ( guid, version, ev_data, gversion ) VALUES (?, ?, ?, ?);"+insertEventSql :: Text+insertEventSql = "INSERT INTO events ( guid, version, ev_data ) VALUES (?, ?, ?);" -createAggregateVersionsSql :: String+createAggregateVersionsSql :: Text createAggregateVersionsSql = "CREATE TABLE IF NOT EXISTS versions ( guid BLOB PRIMARY KEY , version INTEGER );" -getCurrentVersionSql :: String+getCurrentVersionSql :: Text getCurrentVersionSql = "SELECT version FROM versions WHERE guid = ?;" -getLatestVersionSql :: String-getLatestVersionSql = "SELECT COALESCE(MAX(gversion), 0) FROM events;"--updateCurrentVersionSql :: String+updateCurrentVersionSql :: Text updateCurrentVersionSql = "INSERT OR REPLACE INTO versions ( guid, version ) VALUES (?,?);" -createSnapshotSql :: String+createSnapshotSql :: Text createSnapshotSql = "CREATE TABLE IF NOT EXISTS snapshots ( guid BLOB PRIMARY KEY , data BLOB , version INTEGER );" -writeSnapshotSql :: String-writeSnapshotSql = "INSERT OR REPLACE INTO snapshots ( guid , data, version ) VALUES ( ?, ?, ? );"--selectSnapshotSql :: String-selectSnapshotSql = "SELECT data, version FROM snapshots WHERE guid = ?;"- badQueryResultMsg :: [String] -> [SQLData] -> String badQueryResultMsg params columns = concat ["Invalid query result shape. Params: ", show params, ". Result columns: ", show columns] @@ -96,30 +89,25 @@       [ toSQLData guid       , toSQLData $ peSequenceNumber e       , toSQLData $ peEvent e-      , toSQLData $ peGlobalVer e       ] -retrieveEvents :: Database -> GUID -> Int -> Source IO RawEvent+retrieveEvents :: Database -> GUID -> Int -> Source (ResourceT IO) RawEvent retrieveEvents database guid v0 = do   -- Unpack the columns into tuples.-  let unpackColumns [SQLInteger version, SQLInteger gversion, SQLBlob eventData] = PersistedEvent guid eventData (fromIntegral version) (fromIntegral gversion)+  let unpackColumns [SQLInteger version, SQLBlob eventData] = PersistedEvent guid eventData (fromIntegral version)       unpackColumns columns = error $ badQueryResultMsg [show guid, show v0] columns   -- Find events with version numbers.-  fmap unpackColumns $ sourceQuery database selectEventsSql [toSQLData guid, toSQLData v0]+  sourceQuery database selectEventsSql [toSQLData guid, toSQLData v0] $= CL.map unpackColumns -enumerateAllEvents :: Database -> Int -> Source IO RawEvent-enumerateAllEvents database minVersion = do-  sourceQuery database enumerateAllEventsSql [toSQLData minVersion] $= CL.map-    (\columns -> do-        case columns of-          [ SQLInteger gv, SQLBlob g, SQLInteger v, SQLBlob ed ] ->-            PersistedEvent (decode' g) ed (fromIntegral v) (fromIntegral gv)-          _ ->-            error $ badQueryResultMsg [show minVersion] columns)+enumerateAllEvents :: Database -> Source (ResourceT IO) RawEvent+enumerateAllEvents database = do+  let unpackColumns [ SQLBlob g, SQLInteger v, SQLBlob ed ] = PersistedEvent (G.fromByteString g) ed (fromIntegral v)+      unpackColumns columns = error $ badQueryResultMsg [] columns+  sourceQuery database enumerateAllEventsSql [] $= CL.map unpackColumns  writeSnapshot :: Database -> GUID -> RawSnapshot -> IO () writeSnapshot database guid (RawSnapshot v d) = do-  execSql database writeSnapshotSql+  execSql database "INSERT OR REPLACE INTO snapshots ( guid , data, version ) VALUES ( ?, ?, ? );"     [ toSQLData guid     , toSQLData d     , toSQLData v@@ -131,34 +119,42 @@   let unpackColumns [SQLBlob d, SQLInteger v] = Just $ RawSnapshot (fromIntegral v) d       unpackColumns columns                   = error $ badQueryResultMsg [show guid] columns   -- Run the query.-  runResourceT $ (sourceQuery database selectSnapshotSql [toSQLData guid] $= (CL.map unpackColumns)) $$ CL.fold const Nothing+  runResourceT $ (sourceQuery database selectSnapshotSql [toSQLData guid] $= (CL.map unpackColumns)) $$ CL.fold (\_ x -> x) Nothing -getLatestVersion :: Database -> IO Int-getLatestVersion database = do-  -- Unpack columns from result.-  let unpackColumns :: [SQLData] -> Int-      unpackColumns [SQLInteger v] = fromIntegral v-      unpackColumns columns        = error $ badQueryResultMsg [] columns-  -- Run the query.-  liftM head $ runResourceT $ (sourceQuery database getLatestVersionSql [] $= (CL.map unpackColumns)) $$ CL.consume+  where+    selectSnapshotSql = "SELECT data, version FROM snapshots WHERE guid = ?;" --- | Open an SQLite3-based event store using the named SQLite database file.--- The database file is created if it does not exist.-openSqliteEventStore :: String -> IO EventStoreBackend-openSqliteEventStore databaseFileName = do-  -- Create the database.-  database <- SQL.open databaseFileName-  -- Set up tables.-  execSql database createEventsSql []-  execSql database createAggregateVersionsSql []-  execSql database createSnapshotSql []-  -- Return event store.-  return $ EventStoreBackend { esbStoreEvents = storeEvents database-                             , esbRetrieveEvents = retrieveEvents database-                             , esbEnumerateAllEvents = enumerateAllEvents database-                             , esbWriteSnapshot = writeSnapshot database-                             , esbGetLatestSnapshot = getLatestSnapshot database-                             , esbGetLatestVersion = getLatestVersion database-                             , esbWithTransaction = withTransaction database-                             , esbCloseEventStoreBackend = SQL.close database-                             }+-- | SQLite3 event store backend type alias.+newtype SQLite3EventStoreBackend = ESB Database++-- | Instance of EventStoreBackend for SQLite3EventStoreBackend+instance EventStoreBackend SQLite3EventStoreBackend where+    esbStoreEvents (ESB database) =+        storeEvents database+    esbRetrieveEvents (ESB database) =+        retrieveEvents database+    esbEnumerateAllEvents (ESB database) =+        enumerateAllEvents database+    esbWriteSnapshot (ESB database) =+        writeSnapshot database+    esbGetLatestSnapshot (ESB database) =+        getLatestSnapshot database+    esbWithTransaction (ESB database) =+        withTransaction database++-- | Create a pool of SQLite3-based event store backends.+createBackendPool :: Int -> Text -> IO (Pool SQLite3EventStoreBackend)+createBackendPool n databaseFileName = do+  createPool open close 1 1 n+  where+    open = do+      -- Create the database.+      database <- SQL.open databaseFileName+      -- Set up tables if necessary.+      execSql database createEventsSql []+      execSql database createAggregateVersionsSql []+      execSql database createSnapshotSql []+      -- Return event store.+      return $ ESB database+    close (ESB db) = do+      SQL.close db
src/Data/CQRS/EventStore/Backend/Sqlite3Utils.hs view
@@ -6,13 +6,11 @@        ) where  import           Control.Exception (catch, bracket, onException, SomeException)-import           Control.Monad (liftM, when)-import           Data.Conduit (Source)-import qualified Data.Conduit as C-import           Data.IORef (newIORef, readIORef, writeIORef)+import           Control.Monad.IO.Class (liftIO)+import           Data.Conduit (ResourceT, Source, bracketP, yield)+import           Data.Text (Text) import qualified Database.SQLite3 as SQL import           Database.SQLite3 (Database, Statement, SQLData(..), StepResult(..))-import           Prelude hiding (catch)  beginTransaction :: Database -> IO () beginTransaction database = execSql database "BEGIN TRANSACTION;" []@@ -23,42 +21,38 @@ rollbackTransaction :: Database -> IO () rollbackTransaction database = execSql database "ROLLBACK TRANSACTION;" [] -withSqlStatement :: Database -> String -> [SQLData] -> (Statement -> IO a) -> IO a+withSqlStatement :: Database -> Text -> [SQLData] -> (Statement -> IO a) -> IO a withSqlStatement database sql parameters action =   bracket (SQL.prepare database sql) SQL.finalize $ \statement -> do     SQL.bind statement parameters     action statement  -- | Execute an SQL statement for which no result is expected.-execSql :: Database -> String -> [SQLData] -> IO ()+execSql :: Database -> Text -> [SQLData] -> IO () execSql database sql parameters =   withSqlStatement database sql parameters $ \stmt -> do     _ <- SQL.step stmt     return () -data State = Unbound-           | Bound-             deriving (Eq)+data State = Unbound | Bound+           deriving (Eq) -sourceQuery :: Database -> String -> [SQLData] -> Source IO [SQLData]+sourceQuery :: Database -> Text -> [SQLData] -> Source (ResourceT IO) [SQLData] sourceQuery database sql parameters =-  C.sourceIO-  (do-      stateRef <- newIORef Unbound-      stmt <- SQL.prepare database sql-      return (stateRef, stmt))-  (\(_,stmt) -> SQL.finalize stmt)-  (\(stateRef,stmt) -> do-      -- Bind parameters if necessary.-      state <- readIORef stateRef-      when (state == Unbound) $ do-        SQL.bind stmt parameters-        writeIORef stateRef $ Bound-      -- Fetch results.-      nextResult <- SQL.step stmt-      case nextResult of-        Done -> return C.Closed-        Row -> liftM C.Open $ SQL.columns stmt)+    bracketP (SQL.prepare database sql)+             (SQL.finalize)+             run+    where+      run stmt = do+        liftIO $ SQL.bind stmt parameters+        loop stmt+      loop stmt = do+        nextResult <- liftIO $ SQL.step stmt+        case nextResult of+          Done -> return ()+          Row -> do+            (liftIO $ SQL.columns stmt) >>= yield+            loop stmt  -- | Execute an IO action with an active transaction. withTransaction :: Database -> IO a -> IO a