Skip to content

Akka.Persistence.Embedded: SQLite journal, snapshot store and read journal (AOT-compatible) - #8739

Merged
Aaronontheweb merged 13 commits into
feature/persistence-hosting-aot-registrationfrom
feature/persistence-embedded-sqlite
Oct 6, 2026
Merged

Aaronontheweb merged 13 commits into
feature/persistence-hosting-aot-registrationfrom
feature/persistence-embedded-sqlite

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Stacked on #8738 (Hosting-based plugin registration). Replaces #8733.

Adds Akka.Persistence.Embedded: a SQLite journal, snapshot store and read journal that read and write the tables Akka.Persistence.Sql 1.5.70 creates on SQLite by default, so a database file can move between the two plugins. It supports that one layout only. It uses plain ADO.NET on Microsoft.Data.Sqlite, has no linq2db or Sql.Common dependency, and publishes with Native AOT with zero warnings from its own code. Akka.Persistence.Embedded.Hosting is the way to use it.

Usage

builder.Services.AddAkka("my-system", akka => akka
    .WithEmbeddedPersistence("Data Source=app.db"));

That adds the journal, snapshot store and read journal as the default plugins, creates the tables, and needs no HOCON, no class setting and no WithFallback. It works with Akka.DynamicTypeLoading off (Native AOT). WithEmbeddedPersistence has the same overloads and parameter names as WithSqlPersistence (connection string, options objects, delegates), minus the linq2db ones. Event adapters and health checks go in journalBuilder and snapshotBuilder; call it again with another pluginIdentifier for a second database. The read journal comes with the journal: its settings are Query* properties of EmbeddedJournalOptions, so nothing registers twice. The options classes derive from #8738's JournalOptions<SqliteWriteJournal, SqliteReadJournalProvider> and SnapshotOptions<SqliteSnapshotStore>, so Hosting registers the plugin types. The read journal id is akka.persistence.query.journal.{identifier}.

Layout and settings

  • One layout: Akka.Persistence.Sql's default (tag table). Tags always go to the tags table, writer_uuid is always written, there is no journal_metadata table and no CSV tags column. Column names are fixed; table names are settings.
  • Delete keeps the highest row at or below the target as a tombstone (deleted = 1, empty message) and deletes lower rows and their tag rows. The highest sequence number is MAX(sequence_number) over all rows, deleted ones included. Queries and replay skip deleted rows, tag queries too.
  • Settings (HOCON key, Hosting option): connection-string, auto-initialize, table-name / JournalTableName, tag-table-name / TagTableName, buffer-size, batch-size, replay-batch-size, read-threads. Snapshot store: connection-string, auto-initialize, table-name / TableName. Both also read serializer / Serializer: a serialization binding wins, otherwise the named serializer replaces the System.Object fallback (as in Akka.Persistence.Sql). Read journal: write-plugin, max-buffer-size, refresh-interval, query-threads. Other keys are not read.
  • The startup check needs the tables, columns and the ordering rowid column, and ignores extra columns, so a table made from the Akka.Persistence.Sql docs DDL (with its nullable tags column) opens.

Changes

  • New package Akka.Persistence.Embedded (net10.0, IsAotCompatible), plugin ids akka.persistence.{journal,snapshot-store,query.journal}.embedded.
  • New package Akka.Persistence.Embedded.Hosting: EmbeddedJournalOptions (journal and read journal settings), EmbeddedSnapshotOptions and the three WithEmbeddedPersistence(...) overloads, health checks included.
  • Schema module builds the exact DDL text Akka.Persistence.Sql sends (byte-identical sqlite_master), creates missing tables, verifies columns, never alters.
  • Journal: one writer thread, BEGIN IMMEDIATE batches, tag rows, tombstone deletes.
  • Snapshot store: one worker thread, update-then-insert upsert, same four-way criteria dispatch.
  • Read journal: all eight query interfaces, FromEnd, keyset-paged ids. Each batch is bounded by MAX(ordering) read in the same transaction; there is no gap-tracking actor (SQLite has one writer).
  • AOT canary src/aot/Akka.Persistence.Embedded.AOT.App in the AotCanary CI job, built with Akka.Hosting: persist, snapshot, recover, delete, every query, an event adapter, health checks, custom table names, and the unregistered failure. Its warning gate covers the plugin, the Hosting package and Akka.Streams.

Testing

  • Full TCK (journal, serialization, perf, snapshot store and all query suites), plus plugin tests for schema, row format, batching, deletes (tombstone message is empty, highest sequence number survives, tag queries skip the tombstone), queries, custom table names end to end, the docs DDL, multi-page replay, pooling, provider path, forced connection reset and shutdown (208 passed, 2 skipped for documented reasons).
  • Compatibility fixture: a 56 KB SQLite file written by Akka.Persistence.Sql 1.5.70 with default settings (three persistence ids, tags, a snapshot, tombstones) is committed with its generator and README. A spec starts Embedded on a copy, checks the tables stay untouched and that replay, highest sequence numbers, tag and all-events queries (offsets included) and the snapshot match literals, then writes, deletes and restarts. A one-time reverse check (not in CI) had Akka.Persistence.Sql 1.5.70 read a file Embedded had written to, including Embedded's tombstones with an empty message, and it recovered and queried the same data.
  • Hosting tests (21): a Hosting-built system with Akka.DynamicTypeLoading off persists, snapshots, recovers and queries by tag through an event adapter; the same with it on; every overload, journal-only, snapshot-only, autoInitialize: false, custom table names, query settings, health checks, two journals via successive calls, two read journals.
  • AOT canary: PublishAot on linux-x64, 0 warnings from the plugin, its Hosting package and Akka.Streams, runs persist, recover, snapshot, delete and every query, ships libe_sqlite3.so.

Breaking changes

None. New packages.

Deliberate differences from Akka.Persistence.Sql: D1 tags with ; come back intact, D2 live PersistenceIds polls on an interval, D3 no gap-tracking actor, D4 snapshot timestamps are UTC, D5 delete pre-query runs in the delete transaction, D6 CurrentPersistenceIds is keyset-paged, D7 no blocking read journal constructor, D8 the tombstone row's message is emptied.

Deviations from the spec text: the read journal reads ahead one element (Buffer(1) after SelectMany), like Akka.Persistence.Sql does through SelectAsync(1, ...) and ConcatMany, so a finished query completes without extra demand; worker connections open with Pooling=False unless the connection string sets Pooling itself (logged once at Debug) instead of calling ClearPool at shutdown, so a reset gets a new handle and nothing else's pooled handles are dropped.

Dependencies and licenses: Microsoft.Data.Sqlite and .Core 10.0.12 (MIT), SQLitePCLRaw 2.1.12 (Apache-2.0), SQLite (public domain).

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Self-review: what each change and test is for. Inline comments have the detail.

What this PR does

  • Adds Akka.Persistence.Embedded: a SQLite journal, snapshot store and read journal on plain ADO.NET (Microsoft.Data.Sqlite). It aims to write the same tables and rows as Akka.Persistence.Sql 1.5.70.
  • Adds Akka.Persistence.Embedded.Hosting, the only way to register the plugin. It uses #8738's CreatePluginActorFactory and WithReadJournal, so nothing needs Akka.DynamicTypeLoading.
  • SqliteSchema builds the DDL text APS sends, creates missing tables and checks columns. It never alters a table.
  • RowCodec and JournalSql define the row format: storage classes, manifest never NULL, one created value per write call, one writer uuid per journal instance.
  • JournalWriter is one thread, one connection, BEGIN IMMEDIATE batches. SqliteWorkerPool gives reads and the snapshot store their own threads. SQLite's one-writer rule lives in one place.
  • The read journal has all eight query interfaces, FromEnd, and an opt-in gap tracker (JournalSequenceActor).
  • Tests: the TCK in five modes (thin wrappers), plugin specs for schema, row format, journal, snapshot, queries, lifecycle and settings, nine Hosting specs, and an AOT canary app in the AotCanary job.

Public API (Hosting)

  • WithEmbeddedPersistence(connectionString, ...) adds journal, snapshot store and read journal as the defaults. mode, tagWriteMode, pluginIdentifier and isDefaultPlugin work like WithSqlPersistence.
  • A second overload takes EmbeddedJournalOptions?, EmbeddedSnapshotOptions? and EmbeddedReadJournalOptions?.
  • WithEmbeddedJournal, WithEmbeddedSnapshotStore and WithEmbeddedReadJournal add one part each.
  • EmbeddedJournalOptions and EmbeddedSnapshotOptions derive from JournalOptions and SnapshotOptions and override CreatePluginActorFactory.
  • EmbeddedReadJournalOptions stands alone and exposes three settings: RefreshInterval, MaxBufferSize, JournalSequenceRetrievalEnabled.
  • Parameter order and names differ from WithSqlPersistence and from the spec's string overload (callbacks second, configureSnapshot last, readJournalOptions third in the options overload). See the inline comments.
  • Public API beyond the spec's Hosting section: EmbeddedReadJournalOptions, the three single-part methods, ReadThreads, and the constructor SqliteReadJournalProvider(system, config, pluginPath). The API approval files lock them.
  • No breaking change, so BREAKING_CHANGES_V1.6.md is untouched.

Deliberate differences from Akka.Persistence.Sql

  • D1: tags with ; come back whole. QuerySql aggregates with group_concat(tag, char(31)). Test: Should_return_tags_containing_semicolon_intact_When_TagTable.
  • D2: live PersistenceIds polls every refresh-interval. LiveIdsStep. Only the TCK PersistenceIdsSpec covers it, so nothing checks that it does not spin.
  • D3: batches are bounded by MAX(ordering) read in the same transaction (ReadMaxAndBatch). The tracker is opt-in. Tests: the stall spec and the gap spec.
  • D4: snapshot Timestamp is UTC. Should_return_utc_timestamp_When_loading.
  • D5: the delete pre-query runs inside the delete transaction (RunDelete). No test can show the race it removes.
  • D6: CurrentPersistenceIds is keyset-paged (ReadPersistenceIdsPage). Should_page_persistence_ids_When_more_than_max_buffer_size.
  • D7: no blocking constructor. Queries await EnsureInitializedAsync. Should_wait_for_journal_initialization_When_query_starts_first.
  • D8: tag-read-mode = auto. Should_resolve_tag_read_mode_from_write_mode_When_set_to_auto, plus the Both-mode query specs.
  • Also in the PR body: Buffer(1) before SelectMany, Pooling=False instead of ClearPool, schema errors that name the setting.

Risks to notice

  • One failed insert fails every request in its batch, including other persistence ids. APS does the same; a test pins it.
  • Pooling=False overrides a Pooling=True the user set, silently.
  • The Csv LIKE escape of ~ is unverified against linq2db. Our test only shows self-consistency.
  • DDL padding for custom names is our reading of linq2db, checked against hand-captured text.
  • JournalWriter has a static test seam and a hook in the production loop.
  • FindPluginPath matches config text, so the HOCON class path can fall back to the default id with no warning.
  • The write path can commit after core's 10 s circuit breaker reports failure, because Default Timeout is 30 s. The spec notes it; the code does not warn.
  • PostStop blocks a dispatcher thread for up to 5 s per thread group while it joins.
  • Windows and macOS Native AOT are not checked; the canary runs on linux-x64 only.

Overlap

  • Builds on #8738 (PluginActorFactory, WithReadJournal, persistence setups). It replaces #8733, whose plugin review still applies.
  • Same schema and settings names as Akka.Persistence.Sql, so a config copied across mostly works. table-mapping and Sql.Common keys fail on purpose.
  • The TCK wrappers cover behavior the plugin specs also touch (FromEnd, deletes, tags). The plugin specs add APS-specific numbers.
  • OptionsSpec and Should_write_the_options_into_config_... check some of the same HOCON.
  • Cross-compat with real APS files, docs and migration pages are PR 3.

Not covered

  • Any real Akka.Persistence.Sql database file, in either direction. Expected values in these tests come from capture notes, not from running APS.
  • Reads and writes with custom table and column names. Only DDL text and HOCON are tested.
  • Multi-page replay: no test sets a small replay-batch-size.
  • ordering that is not an INTEGER primary key, and auto-initialize = true over docs-DDL tables.
  • WithEmbeddedJournal, WithEmbeddedSnapshotStore, PersistenceMode.Journal, tagWriteMode: Csv and autoInitialize: false through Hosting.
  • That a later WithEmbeddedReadJournal call replaces the one WithEmbeddedPersistence added.
  • NULL serializer_id on a snapshot row, WAL mode, two processes on one file, and a crash mid-transaction.
  • Hosting specs run on the JIT with the switch off. Only the canary shows ILC is clean.

// ---- DDL text ------------------------------------------------------------------------------

/// <summary>J1, J2 and J3: the journal table plus its three indexes.</summary>
public static string JournalDdl(JournalSettings settings)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We build the CREATE text by hand, byte for byte, because a file that moves between this plugin and Akka.Persistence.Sql must keep the same sqlite_master text. ExpectedSchemas pins it for default names in five modes. The ;\r\n between statements never reaches sqlite_master, so no test can see it.

return sb.ToString();
}

private static void AppendCreateTable(StringBuilder sb, string table, List<Column> columns, int typePadding, string? constraint)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We pad columns with one rule (name width = longest + 1, type width = longest + 1, or + 2 for the journal). It matches linq2db for default names. For custom names it is our reading of linq2db, checked only against text captured by hand. A miss changes the sqlite_master text, not behaviour.

}

var (bytes, manifest, identifier) = SerializePayload(payload, _settings.DefaultSerializer);
var tagArray = tags.ToArray();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We write tags in the set's enumeration order, as APS does. String hashes are random per process, so ;red;blue; and ;blue;red; can both occur, and tag rows insert in that order. A row-level comparison with APS has to compare tags as sets.

var tagArray = tags.ToArray();
return new JournalRow
{
Created = representation.Timestamp == 0 ? batchTicks : representation.Timestamp,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We use the call's single tick value for created unless the representation already has a timestamp. Akka clears that timestamp before the journal sees an actor's write, so the second branch only runs for direct callers.

/// A plain Microsoft host with Akka.Hosting, no TestKit: the TestKit itself loads types by name, so it cannot run
/// with <c>Akka.DynamicTypeLoading</c> off. Flips the switch around the host's life.
/// </summary>
internal sealed class HostedSystem : IAsyncDisposable

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We use a plain Microsoft host and flip Akka.DynamicTypeLoading around its life, because the Akka TestKit loads types by name. The switch is process-wide, so these classes run in a non-parallel collection. This proves the registration path needs no type loading on the JIT. Only the AOT canary proves ILC.

.WithHealthCheck(),
configureSnapshot: snapshot => snapshot.WithHealthCheck());

[Fact(DisplayName = "Should_persist_recover_snapshot_and_query_by_tag_When_dynamic_type_loading_is_off")]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We persist, snapshot, recover and query by tag through a Hosting-built system with the switch off. The tag comes from an adapter registered with a factory, so it proves AddWriteEventAdapter(factory) and WithEmbeddedPersistence need no type names.

Comment thread src/aot/Akka.Persistence.Embedded.AOT.App/Program.cs Outdated
@Aaronontheweb
Aaronontheweb force-pushed the feature/persistence-embedded-sqlite branch 2 times, most recently from d81340c to d1734ae Compare October 3, 2026 16:20

@to11mtm to11mtm left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

... Would hit 'approve' but the comment I left outside of this review means I selected what I selected ;)

Edit: My comment outside of this review was incorrect after digging deeper. This looks good to me. Below is one question, one low hanging fruit that is easy to solve before merge, and possible future improvements.

/// would wait forever. Cost: one batch is read ahead of demand, and a stream that is cancelled may already have
/// issued one more query. Spec 9.9 does not list this operator; it is a deliberate deviation.
/// </remarks>
public static Source<TElem, NotUsed> Flatten<TElem>(this Source<IReadOnlyList<TElem>, NotUsed> batches)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Curious as to whether this exists in Persistence.Sql and I didn't notice, or if not why it's not needed there.

Comment thread src/contrib/persistence/Akka.Persistence.Embedded/Query/QuerySql.cs Outdated
transaction.Dispose();
}

foreach (var request in batch)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We could re-run the other requests alone after a constraint error.

FWIW, The general justification in Persistence.Sql (Inherited from pekko justification) is that 'if you are failing a write on one actor, you probably want everything to fail' since it could just be adding more questionable (may be related, may be unrelated) state. (The whole journal winds up shutting down when that happens, right?)

Comment on lines +417 to +424
foreach (var tag in row.Tags)
{
tagParameters["@ordering_id"].Value = ordering;
tagParameters["@tag"].Value = tag;
tagParameters["@sequence_nr"].Value = row.SequenceNr;
tagParameters["@persistence_id"].Value = row.PersistenceId;
_insertTag.ExecuteNonQuery();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fair. It's SQLite and p/invoke is probably improved enough we don't need to really batch for tags here.

Comment on lines +302 to +306
foreach (var request in batch)
{
foreach (var row in request.Rows)
InsertRow(row);
}

@to11mtm to11mtm Oct 3, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's probably fine, I still have to wonder whether for cases of non-tag-table if a lot of records going in would benefit from more proper batching or not due to p/invoke and marshalling of params or not.

(If nothing else, something for someone to explore in future. I get this is MVP-ish)

Edit: I am impressed by p/invoke improvements in .NET, don't think we need to worry here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good to know!

@Aaronontheweb
Aaronontheweb force-pushed the feature/persistence-embedded-sqlite branch 3 times, most recently from 6eb0b58 to 1b3ccb1 Compare October 4, 2026 21:06
@Aaronontheweb
Aaronontheweb force-pushed the feature/persistence-embedded-sqlite branch from 1b3ccb1 to 0a24fc9 Compare October 5, 2026 16:13

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm trimming this plugin down - we over-ported a ton of the compat bits from Akka.Persistence.Sql and IMHO that's not really necessary.

  • Stick with tag tables only
  • WriterUuid always enforced
  • Delete compatibility mode and metadata table

@Aaronontheweb
Aaronontheweb force-pushed the feature/persistence-embedded-sqlite branch 4 times, most recently from 0a8f43f to e3dc090 Compare October 6, 2026 13:21
…urnal (AOT-compatible)

A new SQLite-only plugin that writes the same tables and rows as Akka.Persistence.Sql 1.5.70 on
SQLite, so either plugin can open the other's database. Plain ADO.NET on Microsoft.Data.Sqlite,
no linq2db. Full persistence TCK in every tag mode, plus an AOT canary.
…osting

New package with EmbeddedJournalOptions, EmbeddedSnapshotOptions and
EmbeddedReadJournalOptions, and AkkaConfigurationBuilder.WithEmbeddedPersistence.
The options supply the plugin factories through Akka.Persistence.Hosting's
CreatePluginActorFactory, and the read journal registers with WithReadJournal, so
nothing needs a HOCON class and the plugin starts with Akka.DynamicTypeLoading off.

The plugin no longer exposes PersistenceSetup extensions. The AOT canary builds its
systems with Akka.Hosting and WithEmbeddedPersistence, drops StreamsRoots.xml and
gates Akka.Streams and the Hosting package at zero warnings.
…om names and paging, harden pooling and provider path

Hosting: WithEmbeddedPersistence now has the same overloads and parameter order as
WithSqlPersistence. The read journal's settings are properties of EmbeddedJournalOptions, so
customizing it needs no second registration. Removes WithEmbeddedJournal, WithEmbeddedSnapshotStore,
WithEmbeddedReadJournal and EmbeddedReadJournalOptions.

Plugin: keep an explicit Pooling setting and log once when forcing it off; the read journal provider
refuses to guess its plugin path; a CrashForTests message replaces the WriteFinished(null) trick.

Tests: custom table and column names end to end (Both with delete compatibility and Csv), multi-page
replay, Hosting surface, pooling, provider path, ordering primary key error, tighter gap timing.
…nnection strings

The static CreatedForTests seam matched the writer's connection string against Db.ConnectionString.
The writer gets the HOCON spelling (forward slashes), Db.ConnectionString has the OS spelling, so
the match missed on Windows and eight journal behavior tests plus the pooling test failed there.
The journal actor now answers GetWriterForTests with its writer, so no string is compared.
… struct

QuerySql builds every statement variant in its constructor (by tag or not, tag table or Csv, first or
next page), so a query only binds parameters. NumericRangeEntry is a readonly struct: it is only held
in an ImmutableList, so it needs no object per entry.
EmbeddedJournalOptions derives from JournalOptions<SqliteWriteJournal,
SqliteReadJournalProvider> and EmbeddedSnapshotOptions from
SnapshotOptions<SqliteSnapshotStore>, so WithJournal and WithSnapshot register
the plugin types. The PluginActorFactory overrides and the explicit WithReadJournal
call are gone, and the typed AddWriteEventAdapter<T> replaces the factory overload.

The read journal id follows the journal's identifier, as the base class defines it.
A default plugin under another identifier no longer also answers to
akka.persistence.query.journal.embedded, because the base registers one read journal.
… SQLite one

The plugin now writes and reads only the layout Akka.Persistence.Sql creates on
SQLite by default. The other options came from the obsolete Sql.Common plugins.

Removed:
- TagWriteMode (public), Csv and Both tag modes, tag-separator, tag-read-mode.
  Tags always go to the tag table; writer_uuid is always written.
- delete-compatibility-mode, the journal_metadata table and its SQL.
- use-writer-uuid-column, custom column names, schema-name, table-mapping,
  table-compatibility-mode, and the code that accepted, ignored or rejected
  Akka.Persistence.Sql keys. Unknown keys are not read.
- warn-on-auto-init-fail, the serializer fallback, the read journal's own
  connection-string, journal-sequence-retrieval and JournalSequenceActor.
- max-concurrent-queries, query-throttle-timeout and write-plugin-init-timeout
  are internal constants with the old defaults.
- The matching Hosting options and the tagStorageMode, deleteCompatibilityMode,
  useWriterUuidColumn and maxConcurrentQueries parameters.

Changed:
- Flat HOCON keys: table-name and tag-table-name on the journal, table-name on
  the snapshot store.
- The delete tombstone now gets an empty message. The highest sequence number
  still comes from MAX(sequence_number), and queries skip deleted rows.
- Tests: the Csv, DeleteCompat and NoWriterUuid suites and the column-name and
  schema-variant tests are gone. The TagTable TCK suites lost their prefix. New
  tests cover the tombstone and tag queries.
Flatten now buffers one element after SelectMany instead of one batch before it.
The next query runs once the current batch is drained, and a finished query
still completes without extra demand. This matches Akka.Persistence.Sql, which
gets the same read-ahead from SelectAsync(1, ...) and ConcatMany.

The doc comment now says the buffer is load-bearing and what it costs.
Akka.Persistence.Sql looks up the writer with
FindSerializerForType(type, serializer): a serialization binding wins,
otherwise the named serializer replaces the System.Object fallback.
Do the same for journal events and snapshots. Reads still use the stored
serializer id and manifest.

The reference HOCON gets `serializer = null` back, and the Hosting README
no longer says the option is ignored.
TestData/aps-compat holds a SQLite file that Akka.Persistence.Sql 1.5.70
wrote with its default settings: three persistence ids, string and record
events, tags in the tag table, one snapshot, and two tombstones. The
generator source and a README (versions, regeneration, reverse check) sit
next to it as .txt files so the test project does not compile them.

ApsCompatSpec copies the file, starts Embedded on it, and checks that
Embedded leaves the existing tables untouched, replays every id with the
right highest sequence number, answers the tag and all-events queries with
the same offsets, and loads the snapshot. Then it writes, deletes and
restarts, and reads old and new data back.
…_uuid non-null on write

The Embedded canary now links src/aot/Shared/LogWatchdogFilter.cs like the other canaries.
JournalRow.WriterUuid is required and non-null because every write sets it. RawJournalRow.WriterUuid
stays nullable: rows written by Akka.Persistence.Sql may lack it.
@Aaronontheweb
Aaronontheweb force-pushed the feature/persistence-embedded-sqlite branch from e3dc090 to dda6bdb Compare October 6, 2026 14:47
@Aaronontheweb
Aaronontheweb merged commit cbdec74 into dev Oct 6, 2026
17 checks passed
@Aaronontheweb
Aaronontheweb deleted the feature/persistence-embedded-sqlite branch October 6, 2026 16:06
@Aaronontheweb Aaronontheweb added the akka.net v1.6 Akka.NET v1.6-related issues label Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

akka.net v1.6 Akka.NET v1.6-related issues akka-persistence AOT Ahead-of-Time (AOT) Compilation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants