Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions docs/docs/infra/sqlite/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
---
title: "SQLite"
description: "Supported SQLite infrastructure"
sidebar_position: 5
---

SQLite is a self-contained, serverless, zero-configuration SQL database engine. It is the most widely deployed database engine in the world. [source](https://www.sqlite.org/).

Eventuous supports SQLite as an event store for embedded and local applications (desktop, mobile, CLI tools) that need event sourcing without external database dependencies. It supports catch-up subscriptions to the global event log and to individual streams, as well as projections.

The SQLite implementation uses the `Microsoft.Data.Sqlite` provider and enables WAL (Write-Ahead Logging) mode by default for concurrent read access.

## Data model

Eventuous uses a single table to store events. The table name is `{schema}_messages`, where `{schema}` defaults to `eventuous`. In addition, another table called `{schema}_streams` is used to control the stream existence, and store the last event number for each stream. Events and metadata are stored as TEXT (JSON) columns. The table schema is as follows:

```sql
global_position INTEGER PRIMARY KEY AUTOINCREMENT,
message_id TEXT NOT NULL,
message_type TEXT NOT NULL,
stream_id INTEGER NOT NULL REFERENCES streams(stream_id),
stream_position INTEGER NOT NULL,
Comment on lines +18 to +22

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Sqlite fk references wrong table 🐞 Bug ✓ Correctness

The SQLite schema snippet references streams(stream_id) even though Eventuous uses schema-prefixed
table names (e.g. eventuous_streams). Users manually creating tables from the snippet would end up
with an invalid foreign key reference.
Agent Prompt
### Issue description
The SQLite schema example uses `REFERENCES streams(stream_id)` but the real schema uses `{schema}_streams` (e.g. `eventuous_streams`).

### Issue Context
This can lead users who provision the schema manually to create an invalid FK.

### Fix Focus Areas
- docs/docs/infra/sqlite/index.md[13-31]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

json_data TEXT NOT NULL,
json_metadata TEXT,
created TEXT NOT NULL
```

Since SQLite doesn't support SQL schema namespaces, Eventuous uses a table name prefix instead (e.g. `eventuous_streams`, `eventuous_messages`).

For subscriptions, Eventuous adds a table called `{schema}_checkpoints` that stores the last processed event number for each subscription.

## Event persistence

To register the SQLite event store, use the `AddEventuousSqlite` extension method. This registers the store, schema, and optionally initializes the database on startup:

```csharp title="Program.cs"
builder.Services.AddEventuousSqlite(
"Data Source=myapp.db",
schema: "eventuous",
initializeDatabase: true
);
builder.Services.AddEventStore<SqliteStore>();
```

You can also configure the store using `IConfiguration`:

```json title="appsettings.json"
{
"SqliteStore": {
"ConnectionString": "Data Source=myapp.db",
"Schema": "eventuous",
"InitializeDatabase": true
}
}
```

```csharp title="Program.cs"
builder.Services.AddEventuousSqlite(
builder.Configuration.GetSection("SqliteStore")
);
builder.Services.AddEventStore<SqliteStore>();
```

When that's done, Eventuous will persist aggregates in SQLite when you use the [command service](../../application/app-service).

## Subscriptions

Eventuous supports two types of subscriptions to SQLite: global and stream. The global subscription is a catch-up subscription that reads all events from the global event log. The stream subscription reads events from a specific stream only.

Both subscription types use continuous polling to check for new events.

### Registering subscriptions

Registering a global log subscription:

```csharp title="Program.cs"
builder.Services.AddSubscription<SqliteAllStreamSubscription, SqliteAllStreamSubscriptionOptions>(
"BookingsProjections",
builder => builder
.AddEventHandler<BookingStateProjection>()
.AddEventHandler<MyBookingsProjection>()
);
```

When you register a subscription to a single stream, you need to configure the subscription options to specify the stream name:

```csharp title="Program.cs"
builder.Services.AddSubscription<SqliteStreamSubscription, SqliteStreamSubscriptionOptions>(
"StreamSubscription",
builder => builder
.Configure(x => x.Stream = "my-stream")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Assign a StreamName value in stream subscription example

This example does not compile as written: SqliteStreamSubscriptionOptions.Stream is a StreamName, and StreamName does not define an implicit conversion from string, so x.Stream = "my-stream" produces a type-mismatch error for users following the docs. The snippet should construct a StreamName explicitly (or use a documented factory) before assignment.

Useful? React with 👍 / 👎.

.AddEventHandler<StreamSubscriptionHandler>()
);
Comment on lines +88 to +93

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Streamname set from string 🐞 Bug ✓ Correctness

The SQLite stream subscription example assigns a string to SqliteStreamSubscriptionOptions.Stream,
but that property is a StreamName and there is no implicit conversion from string. Copy/pasting
the snippet will not compile.
Agent Prompt
### Issue description
The docs example for `SqliteStreamSubscriptionOptions` sets `Stream` with a string literal, but the property type is `StreamName` and there is no implicit conversion from `string` to `StreamName`.

### Issue Context
This causes a compile-time error for anyone following the documentation.

### Fix Focus Areas
- docs/docs/infra/sqlite/index.md[85-94]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

```

### Checkpoint store

Catch-up subscriptions need a [checkpoint](../../subscriptions/checkpoint). You can register the SQLite checkpoint store, and it will be used for all subscriptions in the application:

```csharp title="Program.cs"
builder.Services.AddSqliteCheckpointStore();
```

The checkpoint store uses the same connection string and schema as the event store when registered via `AddEventuousSqlite`.

## Projections

You can use SQLite both as an event store and as a read model store. Eventuous provides a projector base class that allows you to emit SQL statements for events, and the projector will execute them.

Consider the following table schema for the query model:

```sql
CREATE TABLE IF NOT EXISTS bookings (
booking_id TEXT NOT NULL PRIMARY KEY,
checkin_date TEXT,
price REAL
);
```

You can project the `BookingImported` event to this table:

```csharp title="ImportingBookingsProjector.cs"
public class ImportingBookingsProjector : SqliteProjector {
public ImportingBookingsProjector(SqliteConnectionOptions connectionOptions)
: base(connectionOptions) {
const string insert = """
INSERT OR REPLACE INTO bookings
(booking_id, checkin_date, price)
VALUES (@booking_id, @checkin_date, @price)
""";

On<BookingEvents.BookingImported>(
(connection, ctx) =>
Project(
connection,
insert,
new SqliteParameter("@booking_id", ctx.Stream.GetId()),
new SqliteParameter("@checkin_date", ctx.Message.CheckIn.ToString("o")),
new SqliteParameter("@price", ctx.Message.Price)
)
);
}
}
```

You can then register the projector as a subscription handler:

```csharp title="Program.cs"
builder.Services.AddSubscription<SqliteAllStreamSubscription, SqliteAllStreamSubscriptionOptions>(
"ImportedBookingsProjections",
builder => builder
.UseCheckpointStore<SqliteCheckpointStore>()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid unusable checkpoint-store registration in SQLite sample

Using .UseCheckpointStore<SqliteCheckpointStore>() is not valid for the SQLite implementation because UseCheckpointStore<T>() relies on DI to construct T, while SqliteCheckpointStore only exposes constructors that need either raw string parameters or a SqliteCheckpointStoreOptions instance; this causes subscription resolution to fail at startup when readers copy this snippet. The sample should use the already-documented AddSqliteCheckpointStore() default registration or a factory-based UseCheckpointStore(...) that resolves a preconfigured store.

Useful? React with 👍 / 👎.

.AddEventHandler<ImportingBookingsProjector>()
);
```

Using `INSERT OR REPLACE` makes the projection idempotent, so reprocessing events after a failure won't cause errors.
3 changes: 2 additions & 1 deletion docs/docs/persistence/event-store.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ Eventuous has several implementations of the event store:
* [EventStoreDB](../infra/esdb)
* [PostgreSQL](../infra/postgres)
* [Microsoft SQL Server](../infra/mssql)
* [Elasticsearch](../infra/elastic)
* [SQLite](../infra/sqlite)
* [Elasticsearch](../infra/elastic)

If you use one of the implementations provided, you won't need to know about the event store abstraction. It is required though if you want to implement it for your preferred database.

Expand Down
5 changes: 3 additions & 2 deletions docs/docs/prologue/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Eventuous is a (relatively) lightweight library, which allows building productio
The base library has a set of abstractions, following Domain-Driven Design tactical patterns, like `Aggregate`.

Additional components include:
- [Aggregate persistence](../persistence) using [EventStoreDB](https://eventstore.com), PostgreSQL, and Microsoft SQL Server
- [Real-time subscriptions](../subscriptions) for EventStoreDB, PostgreSQL, Microsoft SQL Server, RabbitMQ, and Google PubSub
- [Aggregate persistence](../persistence) using [EventStoreDB](https://eventstore.com), PostgreSQL, Microsoft SQL Server, and SQLite
- [Real-time subscriptions](../subscriptions) for EventStoreDB, PostgreSQL, Microsoft SQL Server, SQLite, RabbitMQ, and Google PubSub
- [Command services](../application) and HTTP-based commands
- Extensive observability, including Open Telemetry support
- Integration with ASP.NET Core dependency injection, logging, and Web API
Expand Down Expand Up @@ -45,6 +45,7 @@ You can find all the NuGet packages by visiting the [Eventuous profile](https://
| `Eventuous.EventStore` | Support for [EventStoreDB](../infra/esdb) (event store, subscriptions, producers) |
| `Eventuous.Postgresql` | Support for [PostgreSQL](../infra/postgres) (event store, subscriptions, producers) |
| `Eventuous.SqlServer` | Support for [Microsoft SQL Server](../infra/mssql) (event store, subscriptions, producers) |
| `Eventuous.Sqlite` | Support for [SQLite](../infra/sqlite) (event store, subscriptions, projections) |
| `Eventuous.RabbitMq` | Support for RabbitMQ (subscriptions, producers) |
| `Eventuous.GooglePubSub` | Support for Google PubSub (subscriptions, producers) |
| `Eventuous.Kafka` | Support for Apache Kafka (producers) |
Expand Down
1 change: 1 addition & 0 deletions docs/docs/read-models/supported-projectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ Eventuous supports the following projection targets:
- [MongoDB projections](/docs/infra/mongodb)
- [PostgreSQL projections](/docs/infra/postgres/#projections)
- [Microsoft SQL Server projections](/docs/infra/mssql/#projections)
- [SQLite projections](/docs/infra/sqlite/#projections)

You can project to any other database using a custom projector, which can be built as a [custom event handler](/docs/subscriptions/eventhandler/#custom-handlers).
8 changes: 6 additions & 2 deletions docs/docs/subscriptions/checkpoint/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,15 @@ The Postgres checkpoint store will create and use the `checkpoint` table, and th

```sql
create table if not exists __schema__.checkpoints (
id varchar primary key,
position bigint null
id varchar primary key,
position bigint null
);
```

#### SQLite

The SQLite checkpoint store uses a `{schema}_checkpoints` table (default: `eventuous_checkpoints`), with the subscription id as the row key. The table is created automatically when `initializeDatabase: true` is set.

#### Other stores

In addition to that, Eventuous has two implementations in the core subscriptions package:
Expand Down