eventium-sqlite (empty) → 0.1.0
raw patch · 7 files changed
+442/−0 lines, 7 filesdep +HUnitdep +aesondep +base
Dependencies added: HUnit, aeson, base, bytestring, eventium-core, eventium-sql-common, eventium-test-helpers, hspec, mtl, persistent, persistent-sqlite, text, uuid
Files
- CHANGELOG.md +0/−0
- LICENSE.md +19/−0
- README.md +222/−0
- eventium-sqlite.cabal +78/−0
- src/Eventium/Store/Sqlite.hs +64/−0
- tests/Eventium/Store/SqliteSpec.hs +58/−0
- tests/Spec.hs +1/−0
+ CHANGELOG.md view
+ LICENSE.md view
@@ -0,0 +1,19 @@+Copyright (c) 2016-2017 David Reaver++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies+of the Software, and to permit persons to whom the Software is furnished to do+so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,222 @@+# Eventium SQLite++SQLite-based event store implementation for embedded and single-process applications.++## Overview++`eventium-sqlite` provides a lightweight, file-based event store implementation using SQLite. It's perfect for single-process applications, embedded systems, mobile apps, and scenarios where you want persistent storage without the complexity of a database server.++## Features++- ✅ **Zero Configuration** - No database server required+- ✅ **File-Based Storage** - Single database file for easy backup+- ✅ **ACID Transactions** - Full consistency guarantees from SQLite+- ✅ **Optimistic Concurrency** - Version-based conflict detection+- ✅ **Type-Safe Access** - Uses Persistent library+- ✅ **Cross-Platform** - Works on Linux, macOS, Windows+- ✅ **Embedded-Friendly** - Low resource footprint+- ✅ **Easy Deployment** - No separate database process++## When to Use SQLite++### ✅ Good Fit+- **Desktop Applications** - Local data storage+- **CLI Tools** - Persistent command-line applications+- **Mobile Apps** - Embedded event storage+- **Development/Testing** - Persistent data without server setup+- **Single-Process Systems** - No concurrent process access needed+- **Edge Computing** - Resource-constrained environments++### ⚠️ Consider Alternatives+- **Multi-Process Systems** - Use `eventium-postgresql` instead+- **High Write Concurrency** - PostgreSQL handles concurrent writes better+- **Distributed Systems** - Need a client-server database+- **Very Large Datasets** - PostgreSQL scales better for TB+ data++## Installation++Add to your `package.yaml`:++```yaml+dependencies:+ - eventium-core+ - eventium-sql-common+ - eventium-sqlite+ - persistent-sqlite # SQLite driver+```++## Usage++```haskell+import Eventium.Store.Sqlite+import Database.Persist.Sqlite++main :: IO ()+main = do+ -- Use file-based storage+ withSqlitePool "events.db" 1 $ \pool -> do+ -- Initialize schema+ flip runSqlPool pool $ do+ runMigration migrateAll+ + -- Create event store+ let store = makeSqliteEventStore pool+ + -- Use with command handlers+ result <- applyCommandHandler + (eventStoreWriter store)+ (eventStoreReader store)+ commandHandler+ aggregateId+ command+```++## Database Location++### File-Based Storage+```haskell+-- Relative path+withSqlitePool "events.db" 1 $ \pool -> ...++-- Absolute path+withSqlitePool "/var/lib/myapp/events.db" 1 $ \pool -> ...++-- User-specific location+home <- getHomeDirectory+let dbPath = home </> ".myapp" </> "events.db"+withSqlitePool dbPath 1 $ \pool -> ...+```++### In-Memory Storage (Testing)+```haskell+-- Temporary in-memory database+withSqlitePool ":memory:" 1 $ \pool -> ...+```++## Configuration++### Connection Pool+SQLite works best with a single connection per process:++```haskell+-- Recommended for SQLite+withSqlitePool "events.db" 1 $ \pool -> ...+```++### WAL Mode (Recommended)+Enable Write-Ahead Logging for better concurrent read performance:++```haskell+withSqlitePool "events.db" 1 $ \pool -> do+ flip runSqlPool pool $ do+ rawExecute "PRAGMA journal_mode=WAL;" []+ runMigration migrateAll+```++Benefits:+- Readers don't block writers+- Better performance for read-heavy workloads+- Safer concurrent access++## Performance++Typical SQLite event store performance:+- **Writes**: ~1000-3000 events/sec+- **Reads**: ~5000-20000 events/sec+- **Storage**: ~1KB per event (JSON serialized)++### Optimization Tips++1. **Use WAL Mode** - Better concurrent access+2. **Batch Writes** - Multiple events per transaction+3. **Index Strategy** - Default indexes cover common queries+4. **VACUUM Regularly** - Reclaim space from deleted data+5. **Synchronous Mode** - Balance durability vs speed++## Backup & Recovery++### Simple File Copy+```bash+# Stop application or ensure no writes+cp events.db events.db.backup++# Or use SQLite backup command+sqlite3 events.db ".backup events.db.backup"+```++### Continuous Backup+```bash+# With WAL mode, backup while app runs+sqlite3 events.db ".backup events.db.backup"+```++## Migration from In-Memory++```haskell+-- Development: in-memory+development :: IO ()+development = withSqlitePool ":memory:" 1 $ \pool -> ...++-- Production: file-based+production :: IO ()+production = withSqlitePool "events.db" 1 $ \pool -> ...+```++## Example: Complete CLI Application++```haskell+import Eventium.Store.Sqlite+import System.Directory (getAppUserDataDirectory)++main :: IO ()+main = do+ -- Store in application data directory+ dataDir <- getAppUserDataDirectory "myapp"+ createDirectoryIfMissing True dataDir+ + let dbPath = dataDir </> "events.db"+ + withSqlitePool dbPath 1 $ \pool -> do+ -- Initialize on first run+ flip runSqlPool pool $ do+ rawExecute "PRAGMA journal_mode=WAL;" []+ runMigration migrateAll+ + -- Run application+ runApp pool+```++## Tools++### SQLite CLI+```bash+# Open database+sqlite3 events.db++# Inspect schema+.schema++# Query events+SELECT * FROM events ORDER BY version DESC LIMIT 10;++# Check database size+.dbinfo+```++## Limitations++- **Single Writer** - Only one process should write at a time+- **File Locking** - May have issues on network filesystems+- **Database Size** - Practical limit around 100GB-1TB+- **Concurrent Writes** - Limited compared to PostgreSQL++## Documentation++- [Main README](../README.md) - Project overview+- [SQL Common](../eventium-sql-common/) - Shared SQL utilities+- [Design Documentation](../DESIGN.md) - Architecture details+- [Examples](../examples/) - Bank and cafe examples use SQLite++## License++MIT - see [LICENSE.md](LICENSE.md)
+ eventium-sqlite.cabal view
@@ -0,0 +1,78 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.37.0.+--+-- see: https://github.com/sol/hpack++name: eventium-sqlite+version: 0.1.0+synopsis: SQLite implementations for eventium+description: Eventium-sqlite provides a SQLite-based event store implementation for the Eventium event sourcing+ framework. It uses the Persistent library for type-safe database access and provides efficient event+ storage and retrieval with support for aggregate streams, event versioning, and optimistic concurrency+ control. This backend is ideal for single-process applications, embedded systems, and scenarios where+ a lightweight database is preferred.+category: Database,Eventsourcing,SQLite+stability: experimental+homepage: https://github.com/aleks-sidorenko/eventium#readme+bug-reports: https://github.com/aleks-sidorenko/eventium/issues+maintainer: Alexander Sidorenko+license: MIT+license-file: LICENSE.md+build-type: Simple+extra-source-files:+ CHANGELOG.md+ README.md++source-repository head+ type: git+ location: https://github.com/aleks-sidorenko/eventium++library+ exposed-modules:+ Eventium.Store.Sqlite+ other-modules:+ Paths_eventium_sqlite+ hs-source-dirs:+ src+ ghc-options: -Wall+ build-depends:+ aeson >=1.5 && <2.3+ , base >=4.9 && <5+ , bytestring >=0.10 && <0.13+ , eventium-core+ , eventium-sql-common+ , mtl >=2.2 && <2.4+ , persistent >=2.13 && <2.15+ , text >=1.2 && <2.2+ , uuid >=1.3 && <1.4+ default-language: Haskell2010++test-suite spec+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Eventium.Store.SqliteSpec+ Eventium.Store.Sqlite+ Paths_eventium_sqlite+ hs-source-dirs:+ tests+ src+ ghc-options: -Wall+ build-tool-depends:+ hspec-discover:hspec-discover+ build-depends:+ HUnit+ , aeson >=1.5 && <2.3+ , base >=4.9 && <5+ , bytestring >=0.10 && <0.13+ , eventium-core+ , eventium-sql-common+ , eventium-test-helpers+ , hspec+ , mtl >=2.2 && <2.4+ , persistent >=2.13 && <2.15+ , persistent-sqlite+ , text >=1.2 && <2.2+ , uuid >=1.3 && <1.4+ default-language: Haskell2010
+ src/Eventium/Store/Sqlite.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}++-- | Defines an Sqlite event store.+module Eventium.Store.Sqlite+ ( sqliteEventStoreWriter,+ initializeSqliteEventStore,+ module Eventium.Store.Class,+ module Eventium.Store.Sql,+ )+where++import Control.Monad.Reader+import Data.Text (Text)+import Database.Persist+import Database.Persist.Class (SafeToInsert)+import Database.Persist.Names (EntityNameDB (..), FieldNameDB (..))+import Database.Persist.Sql+import Eventium.Store.Class+import Eventium.Store.Sql++-- | An 'EventStoreWriter' that uses an SQLite database as a backend. Use+-- 'SqlEventStoreConfig' to configure this event store.+sqliteEventStoreWriter ::+ (MonadIO m, PersistEntity entity, PersistEntityBackend entity ~ SqlBackend, SafeToInsert entity) =>+ SqlEventStoreConfig entity serialized ->+ VersionedEventStoreWriter (SqlPersistT m) serialized+sqliteEventStoreWriter config = EventStoreWriter $ transactionalExpectedWriteHelper getLatestVersion storeEvents'+ where+ getLatestVersion = sqlMaxEventVersion config maxSqliteVersionSql+ storeEvents' = sqlStoreEvents config Nothing maxSqliteVersionSql++maxSqliteVersionSql :: FieldNameDB -> FieldNameDB -> FieldNameDB -> Text+maxSqliteVersionSql (FieldNameDB tableName) (FieldNameDB uuidFieldName) (FieldNameDB versionFieldName) =+ "SELECT IFNULL(MAX(" <> versionFieldName <> "), -1) FROM " <> tableName <> " WHERE " <> uuidFieldName <> " = ?"++-- | This functions runs the migrations required to create the events table and+-- also adds an index on the UUID column.+initializeSqliteEventStore ::+ (MonadIO m, PersistEntity entity, PersistEntityBackend entity ~ SqlBackend) =>+ SqlEventStoreConfig entity serialized ->+ ConnectionPool ->+ m ()+initializeSqliteEventStore SqlEventStoreConfig {..} pool = do+ -- Run migrations+ _ <- liftIO $ runSqlPool (runMigrationSilent migrateSqlEvent) pool++ -- Create index on uuid field so retrieval is very fast+ let tableName = unEntityNameDB $ tableDBName (sqlEventStoreConfigSequenceMakeEntity undefined undefined undefined)+ uuidFieldName = unFieldNameDB $ fieldDBName sqlEventStoreConfigSequenceNumberField+ indexSql =+ "CREATE INDEX IF NOT EXISTS "+ <> uuidFieldName+ <> "_index"+ <> " ON "+ <> tableName+ <> " ("+ <> uuidFieldName+ <> ")"+ liftIO $ flip runSqlPool pool $ rawExecute indexSql []++ return ()
+ tests/Eventium/Store/SqliteSpec.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE OverloadedStrings #-}++module Eventium.Store.SqliteSpec (spec) where++import Database.Persist.Sqlite+import Eventium.Store.Sqlite+import Eventium.TestHelpers+import Test.Hspec++spec :: Spec+spec = do+ describe "Sqlite event store" $ do+ eventStoreSpec sqliteStoreRunner+ globalStreamEventStoreSpec sqliteStoreGlobalRunner++ -- This is really a test for runEventStoreUsing and+ -- runGlobalStreamEventStoreUsing. This is just a good place to put it.+ describe "runEventStoreUsing and runGlobalStreamEventStoreUsing for the sqlite store" $ do+ eventStoreSpec sqliteIOStoreRunner+ globalStreamEventStoreSpec sqliteIOStoreGlobalRunner++makeStore :: IO (VersionedEventStoreWriter (SqlPersistT IO) CounterEvent, VersionedEventStoreReader (SqlPersistT IO) CounterEvent, ConnectionPool)+makeStore = do+ pool <- liftIO $ runNoLoggingT (createSqlitePool ":memory:" 1)+ let writer = serializedEventStoreWriter jsonStringSerializer $ sqliteEventStoreWriter defaultSqlEventStoreConfig+ reader = serializedVersionedEventStoreReader jsonStringSerializer $ sqlEventStoreReader defaultSqlEventStoreConfig+ initializeSqliteEventStore defaultSqlEventStoreConfig pool+ return (writer, reader, pool)++sqliteStoreRunner :: EventStoreRunner (SqlPersistT IO)+sqliteStoreRunner = EventStoreRunner $ \action -> do+ (writer, reader, pool) <- makeStore+ runSqlPool (action writer reader) pool++sqliteStoreGlobalRunner :: GlobalStreamEventStoreRunner (SqlPersistT IO)+sqliteStoreGlobalRunner = GlobalStreamEventStoreRunner $ \action -> do+ (writer, _, pool) <- makeStore+ let globalStore = serializedGlobalEventStoreReader jsonStringSerializer (sqlGlobalEventStoreReader defaultSqlEventStoreConfig)+ runSqlPool (action writer globalStore) pool++makeIOStore :: IO (VersionedEventStoreWriter IO CounterEvent, VersionedEventStoreReader IO CounterEvent, ConnectionPool)+makeIOStore = do+ (writer, reader, pool) <- makeStore+ let writer' = runEventStoreWriterUsing (`runSqlPool` pool) writer+ reader' = runEventStoreReaderUsing (`runSqlPool` pool) reader+ return (writer', reader', pool)++sqliteIOStoreRunner :: EventStoreRunner IO+sqliteIOStoreRunner = EventStoreRunner $ \action -> do+ (writer, reader, _) <- makeIOStore+ action writer reader++sqliteIOStoreGlobalRunner :: GlobalStreamEventStoreRunner IO+sqliteIOStoreGlobalRunner = GlobalStreamEventStoreRunner $ \action -> do+ (writer, _, pool) <- makeIOStore+ let globalStore = serializedGlobalEventStoreReader jsonStringSerializer (sqlGlobalEventStoreReader defaultSqlEventStoreConfig)+ globalStore' = runEventStoreReaderUsing (`runSqlPool` pool) globalStore+ action writer globalStore'
+ tests/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}