Skip to content

fix(#3071): align Sqlite DLQ expiration column with the shared 'expires' name#3079

Merged
jeremydmiller merged 1 commit into
mainfrom
fix-3071-sqlite-dlq-expiration
Jun 11, 2026
Merged

fix(#3071): align Sqlite DLQ expiration column with the shared 'expires' name#3079
jeremydmiller merged 1 commit into
mainfrom
fix-3071-sqlite-dlq-expiration

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #3071.

Diagnosis

The Sqlite DeadLettersTable named the conditional expiration column keep_until (DatabaseConstants.KeepUntil) while every other RDBMS backend — Postgres, SqlServer, MySql, Oracle — names it expires (DatabaseConstants.Expires):

// Wolverine.Postgresql / SqlServer / MySql / Oracle Schema/DeadLettersTable.cs
AddColumn<DateTimeOffset>(DatabaseConstants.Expires).AllowNulls();

// Wolverine.Sqlite Schema/DeadLettersTable.cs (pre-fix)
if (durability.DeadLetterQueueExpirationEnabled)
{
    AddColumn(DatabaseConstants.KeepUntil, "TEXT");   // ← wrong constant
}

The shared DLQ insert path (Wolverine.RDBMS.DatabasePersistence.WriteDeadLetter) and the cleanup query (Wolverine.RDBMS.Durability.DeleteExpiredDeadLetterMessagesOperation) both target DatabaseConstants.Expires:

deadLetterFields += ", " + DatabaseConstants.Expires;
// "delete from {schema}.{DeadLetterTable} where {Expires} < @utcnow"

So a Sqlite host with DeadLetterQueueExpirationEnabled = true provisions a column the shared SQL never writes to and then hits SqliteException: no such column: expires on the periodic cleanup job (and on the next DLQ insert). The reporter calls out that this happens on a fresh database too — confirmed by the regression test.

The fix

Rename the column to expires so it matches the constant the shared SQL uses. One line in Wolverine.Sqlite/Schema/DeadLettersTable.cs. Aligns Sqlite with every other backend.

Regression coverage

SqliteTests/Bug_3071_sqlite_dlq_expiration_creates_expires_column.cs — mirrors the bug report verbatim:

  1. dlq_table_has_expires_column_when_expiration_is_enabled — boots a host with PersistMessagesWithSqlite + DeadLetterQueueExpirationEnabled = true + AddResourceSetupOnStartup, then asserts the wolverine_dead_letters table carries expires and not the legacy keep_until. Fails on pre-fix code with the column-name mismatch.
  2. dlq_table_has_no_expires_column_when_expiration_is_disabled — regression guard the other way: with expiration off, neither column appears. Locks in the gating so a careless future refactor can't reintroduce the bug from the other side.

Reflects column names via PRAGMA table_info directly rather than going through Weasel's FetchExistingAsync so the assertion shape is independent of any future Weasel column-name normalization choice.

All 142 SqliteTests green (1 pre-existing skip).

Why no migration

The bug never produced a working install — the cleanup job throws on every pass — so there's no existing fleet of Sqlite databases out there with a populated keep_until column to migrate. Anyone who flipped the flag on either ran into the exception immediately or never enabled the feature. Renaming the column is the right end-state and any in-flight database that did happen to provision the wrong-named empty column will simply re-provision the correct one on next resource-setup pass.

🤖 Generated with Claude Code

…es' name

The Sqlite DeadLettersTable named the conditional expiration column
'keep_until' (DatabaseConstants.KeepUntil) while every other RDBMS
backend (Postgres, SqlServer, MySql, Oracle) names it 'expires'
(DatabaseConstants.Expires). The shared DLQ insert path in
DatabasePersistence.WriteDeadLetter and the cleanup query in
DeleteExpiredDeadLetterMessagesOperation both reference 'expires', so
Sqlite hosts with DeadLetterQueueExpirationEnabled = true hit
'SqliteException: no such column: expires' both on the periodic cleanup
job and on the next DLQ insert.

Renaming the column to 'expires' (the constant the shared SQL uses)
closes the gap. The reporter notes the bug shows up on a fresh database
too — exactly what the test exercises.

Regression test mirrors the bug report verbatim: PersistMessagesWithSqlite
+ DeadLetterQueueExpirationEnabled=true + AddResourceSetupOnStartup,
then asserts the dead_letters table has an 'expires' column and not the
legacy 'keep_until'. A second case exercises the off-default to lock in
that 'expires' is gated correctly.

All 142 SqliteTests green (1 pre-existing skip).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sqlite: enabling dead letter expiration fails with 'no such column expires'

1 participant