-
-
Notifications
You must be signed in to change notification settings - Fork 96
Add SQLite event store implementation #502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
9b504a9
Add Eventuous.Sqlite project scaffolding with schema, connection fact…
alexeyzimarev 3ee5b7a
Add SqliteStore event store implementation
alexeyzimarev f8ca0ab
Add SchemaInitializer, DI registration, subscriptions, checkpoint sto…
alexeyzimarev 792f125
Add Eventuous.Sqlite to solution file
alexeyzimarev 4f90f5c
Add SQLite test project with store, subscription, and registration tests
alexeyzimarev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // Copyright (C) Eventuous HQ OÜ. All rights reserved | ||
| // Licensed under the Apache License, Version 2.0. | ||
|
|
||
| namespace Eventuous.Sqlite; | ||
|
|
||
| delegate Task<SqliteConnection> GetSqliteConnection(CancellationToken cancellationToken); | ||
|
|
||
| public static class ConnectionFactory { | ||
| public static async Task<SqliteConnection> GetConnection(string connectionString, CancellationToken cancellationToken) { | ||
| var connection = new SqliteConnection(connectionString); | ||
| await connection.OpenAsync(cancellationToken).NoContext(); | ||
|
|
||
| await using var walCmd = connection.CreateCommand(); | ||
| walCmd.CommandText = "PRAGMA journal_mode=WAL"; | ||
| await walCmd.ExecuteNonQueryAsync(cancellationToken).NoContext(); | ||
|
|
||
| return connection; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <ItemGroup> | ||
| <ProjectReference Include="$(CoreRoot)\Eventuous.Subscriptions\Eventuous.Subscriptions.csproj"/> | ||
| <ProjectReference Include="$(CoreRoot)\Eventuous.Persistence\Eventuous.Persistence.csproj"/> | ||
| <ProjectReference Include="$(SrcRoot)\Relational\src\Eventuous.Sql.Base\Eventuous.Sql.Base.csproj"/> | ||
| <ProjectReference Include="$(CoreRoot)\Eventuous.Producers\Eventuous.Producers.csproj"/> | ||
| </ItemGroup> | ||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.Data.Sqlite"/> | ||
| <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions"/> | ||
| <Using Include="Microsoft.Data.Sqlite"/> | ||
| <Using Include="Eventuous.Tools"/> | ||
| </ItemGroup> | ||
| <ItemGroup> | ||
| <EmbeddedResource Include="Scripts\1_Schema.sql"/> | ||
| </ItemGroup> | ||
| <ItemGroup> | ||
| <Compile Include="$(CoreRoot)\Eventuous.Shared\Tools\TaskExtensions.cs"> | ||
| <Link>Tools\TaskExtensions.cs</Link> | ||
| </Compile> | ||
| <Compile Include="$(CoreRoot)\Eventuous.Shared\Tools\Ensure.cs"> | ||
| <Link>Tools\Ensure.cs</Link> | ||
| </Compile> | ||
| </ItemGroup> | ||
| <ItemGroup> | ||
| <InternalsVisibleTo Include="Eventuous.Tests.Sqlite"/> | ||
| </ItemGroup> | ||
| </Project> |
88 changes: 88 additions & 0 deletions
88
src/Sqlite/src/Eventuous.Sqlite/Extensions/RegistrationExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| // Copyright (C) Eventuous HQ OÜ. All rights reserved | ||
| // Licensed under the Apache License, Version 2.0. | ||
|
|
||
| using Eventuous.Sqlite; | ||
| using Eventuous.Sqlite.Projections; | ||
| using Eventuous.Sqlite.Subscriptions; | ||
| using Microsoft.Extensions.Configuration; | ||
| using Microsoft.Extensions.DependencyInjection.Extensions; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Extensions.Options; | ||
|
|
||
| // ReSharper disable UnusedMethodReturnValue.Global | ||
| // ReSharper disable once CheckNamespace | ||
| namespace Microsoft.Extensions.DependencyInjection; | ||
|
|
||
| public static class ServiceCollectionExtensions { | ||
| /// <param name="services">Service collection</param> | ||
| extension(IServiceCollection services) { | ||
| /// <summary> | ||
| /// Adds SQLite event store and the necessary schema to the DI container. | ||
| /// </summary> | ||
| /// <param name="connectionString">Connection string</param> | ||
| /// <param name="schema">Schema name</param> | ||
| /// <param name="initializeDatabase">Set to true if you want the schema to be created on startup</param> | ||
| /// <returns></returns> | ||
| public IServiceCollection AddEventuousSqlite( | ||
| string connectionString, | ||
| string schema = Schema.DefaultSchema, | ||
| bool initializeDatabase = false | ||
| ) { | ||
| var options = new SqliteStoreOptions { | ||
| Schema = Ensure.NotEmptyString(schema), | ||
| ConnectionString = Ensure.NotEmptyString(connectionString), | ||
| InitializeDatabase = initializeDatabase | ||
| }; | ||
| services.AddSingleton(options); | ||
| services.AddSingleton<SqliteStore>(); | ||
| services.AddHostedService<SchemaInitializer>(); | ||
| services.TryAddSingleton(new SqliteConnectionOptions(connectionString, schema)); | ||
|
|
||
| return services; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds SQLite event store and the necessary schema to the DI container using the configuration. | ||
| /// </summary> | ||
| /// <param name="config">Configuration section for SQLite options</param> | ||
| /// <returns></returns> | ||
| public IServiceCollection AddEventuousSqlite(IConfiguration config) { | ||
| services.Configure<SqliteStoreOptions>(config); | ||
| services.AddSingleton<SqliteStoreOptions>(sp => sp.GetRequiredService<IOptions<SqliteStoreOptions>>().Value); | ||
| services.AddSingleton<SqliteStore>(); | ||
| services.AddHostedService<SchemaInitializer>(); | ||
|
|
||
| services.TryAddSingleton( | ||
| sp => { | ||
| var storeOptions = sp.GetRequiredService<IOptions<SqliteStoreOptions>>().Value; | ||
|
|
||
| return new SqliteConnectionOptions(Ensure.NotEmptyString(storeOptions.ConnectionString), storeOptions.Schema); | ||
| } | ||
| ); | ||
|
|
||
| return services; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Registers the SQLite-based checkpoint store using the details provided when registering | ||
| /// SQLite connection factory. | ||
| /// </summary> | ||
| /// <returns></returns> | ||
| public IServiceCollection AddSqliteCheckpointStore() | ||
| => services.AddCheckpointStore<SqliteCheckpointStore>( | ||
| sp => { | ||
| var loggerFactory = sp.GetService<ILoggerFactory>(); | ||
| var connectionOptions = sp.GetService<SqliteConnectionOptions>(); | ||
| var checkpointStoreOptions = sp.GetService<SqliteCheckpointStoreOptions>(); | ||
|
|
||
| var schema = connectionOptions?.Schema is not null and not Schema.DefaultSchema | ||
| && checkpointStoreOptions?.Schema is null or Schema.DefaultSchema | ||
| ? connectionOptions.Schema | ||
| : checkpointStoreOptions?.Schema ?? Schema.DefaultSchema; | ||
| var connectionString = checkpointStoreOptions?.ConnectionString ?? connectionOptions?.ConnectionString; | ||
|
|
||
| return new(Ensure.NotNull(connectionString), schema, loggerFactory); | ||
| } | ||
| ); | ||
| } | ||
| } | ||
25 changes: 25 additions & 0 deletions
25
src/Sqlite/src/Eventuous.Sqlite/Extensions/SqliteExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| // Copyright (C) Eventuous HQ OÜ. All rights reserved | ||
| // Licensed under the Apache License, Version 2.0. | ||
|
|
||
| namespace Eventuous.Sqlite.Extensions; | ||
|
|
||
| static class SqliteExtensions { | ||
| extension(SqliteCommand command) { | ||
| internal SqliteCommand Add(string parameterName, object? value) { | ||
| command.Parameters.AddWithValue(parameterName, value ?? DBNull.Value); | ||
|
|
||
| return command; | ||
| } | ||
| } | ||
|
|
||
| extension(SqliteConnection connection) { | ||
| internal SqliteCommand GetTextCommand(string sql, SqliteTransaction? transaction = null) { | ||
| var cmd = connection.CreateCommand(); | ||
| cmd.CommandType = System.Data.CommandType.Text; | ||
| cmd.CommandText = sql; | ||
| if (transaction != null) cmd.Transaction = transaction; | ||
|
|
||
| return cmd; | ||
| } | ||
alexeyzimarev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
6 changes: 6 additions & 0 deletions
6
src/Sqlite/src/Eventuous.Sqlite/Projections/SqliteConnectionOptions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| // Copyright (C) Eventuous HQ OÜ. All rights reserved | ||
| // Licensed under the Apache License, Version 2.0. | ||
|
|
||
| namespace Eventuous.Sqlite.Projections; | ||
|
|
||
| public record SqliteConnectionOptions(string ConnectionString, string Schema); |
55 changes: 55 additions & 0 deletions
55
src/Sqlite/src/Eventuous.Sqlite/Projections/SqliteProjector.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| // Copyright (C) Eventuous HQ OÜ. All rights reserved | ||
| // Licensed under the Apache License, Version 2.0. | ||
|
|
||
| using Eventuous.Subscriptions.Context; | ||
| using EventHandler = Eventuous.Subscriptions.EventHandler; | ||
|
|
||
| namespace Eventuous.Sqlite.Projections; | ||
|
|
||
| /// <summary> | ||
| /// Base class for projectors that store read models in SQLite. | ||
| /// </summary> | ||
| public abstract class SqliteProjector(SqliteConnectionOptions options, ITypeMapper? mapper = null) : EventHandler(mapper) { | ||
| readonly string _connectionString = Ensure.NotEmptyString(options.ConnectionString); | ||
|
|
||
| /// <summary> | ||
| /// Define how an event is converted to a SQLite command to update the read model using event data. | ||
| /// </summary> | ||
| /// <param name="handler">Function to synchronously create a SQLite command from the event context.</param> | ||
| /// <typeparam name="T"></typeparam> | ||
| protected void On<T>(ProjectToSqlite<T> handler) where T : class { | ||
| base.On<T>(async ctx => await Handle(ctx, GetCommand).NoContext()); | ||
|
|
||
| return; | ||
|
|
||
| ValueTask<SqliteCommand> GetCommand(SqliteConnection connection, MessageConsumeContext<T> context) => new(handler(connection, context)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Define how an event is converted to a SQLite command to update the read model using event data. | ||
| /// </summary> | ||
| /// <param name="handler">Function to asynchronously create a SQLite command from the event context.</param> | ||
| /// <typeparam name="T"></typeparam> | ||
| protected void On<T>(ProjectToSqliteAsync<T> handler) where T : class | ||
| => base.On<T>(async ctx => await Handle(ctx, handler).NoContext()); | ||
|
|
||
| async Task Handle<T>(MessageConsumeContext<T> context, ProjectToSqliteAsync<T> handler) where T : class { | ||
| await using var connection = await ConnectionFactory.GetConnection(_connectionString, context.CancellationToken); | ||
|
|
||
| var cmd = await handler(connection, context).ConfigureAwait(false); | ||
| await cmd.ExecuteNonQueryAsync(context.CancellationToken).ConfigureAwait(false); | ||
| } | ||
|
|
||
| protected static SqliteCommand Project(SqliteConnection connection, string commandText, params SqliteParameter[] parameters) { | ||
| var cmd = connection.CreateCommand(); | ||
| cmd.CommandText = commandText; | ||
| cmd.Parameters.AddRange(parameters); | ||
| cmd.CommandType = System.Data.CommandType.Text; | ||
|
|
||
| return cmd; | ||
| } | ||
| } | ||
|
|
||
| public delegate SqliteCommand ProjectToSqlite<T>(SqliteConnection connection, MessageConsumeContext<T> consumeContext) where T : class; | ||
|
|
||
| public delegate ValueTask<SqliteCommand> ProjectToSqliteAsync<T>(SqliteConnection connection, MessageConsumeContext<T> consumeContext) where T : class; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| // Copyright (C) Eventuous HQ OÜ. All rights reserved | ||
| // Licensed under the Apache License, Version 2.0. | ||
|
|
||
| using System.Reflection; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Eventuous.Sqlite; | ||
|
|
||
| public class Schema(string schema = Schema.DefaultSchema) { | ||
| public const string DefaultSchema = "eventuous"; | ||
|
|
||
| public readonly string StreamsTable = $"{schema}_streams"; | ||
| public readonly string MessagesTable = $"{schema}_messages"; | ||
| public readonly string CheckpointsTable = $"{schema}_checkpoints"; | ||
|
|
||
| public readonly string StreamExists = $"SELECT EXISTS(SELECT 1 FROM {schema}_streams WHERE stream_name = @name)"; | ||
| public readonly string GetCheckpointSql = $"SELECT position FROM {schema}_checkpoints WHERE id = @checkpointId"; | ||
| public readonly string AddCheckpointSql = $"INSERT INTO {schema}_checkpoints (id) VALUES (@checkpointId)"; | ||
| public readonly string UpdateCheckpointSql = $"UPDATE {schema}_checkpoints SET position = @position WHERE id = @checkpointId"; | ||
alexeyzimarev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| static readonly Assembly Assembly = typeof(Schema).Assembly; | ||
|
|
||
| public string SchemaName => schema; | ||
|
|
||
| [PublicAPI] | ||
| public async Task CreateSchema(string connectionString, ILogger<Schema>? log, CancellationToken cancellationToken) { | ||
| log?.LogInformation("Creating schema {Schema}", schema); | ||
|
|
||
| var names = Assembly.GetManifestResourceNames() | ||
| .Where(x => x.EndsWith(".sql")) | ||
| .OrderBy(x => x); | ||
|
|
||
| await using var connection = await ConnectionFactory.GetConnection(connectionString, cancellationToken).NoContext(); | ||
| await using var transaction = (SqliteTransaction)await connection.BeginTransactionAsync(cancellationToken).NoContext(); | ||
|
|
||
| try { | ||
| foreach (var name in names) { | ||
| log?.LogInformation("Executing {Script}", name); | ||
| await using var stream = Assembly.GetManifestResourceStream(name); | ||
| using var reader = new StreamReader(stream!); | ||
| var script = await reader.ReadToEndAsync(cancellationToken).NoContext(); | ||
| var cmdScript = script.Replace("__schema__", schema); | ||
|
|
||
| await using var cmd = new SqliteCommand(cmdScript, connection, transaction); | ||
| await cmd.ExecuteNonQueryAsync(cancellationToken).NoContext(); | ||
| } | ||
|
|
||
| await transaction.CommitAsync(cancellationToken).NoContext(); | ||
| } catch (Exception e) { | ||
| log?.LogCritical(e, "Unable to initialize the database schema"); | ||
| await transaction.RollbackAsync(cancellationToken); | ||
|
|
||
| throw; | ||
| } | ||
|
|
||
| log?.LogInformation("Database schema initialized"); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.