Skip to content

Move the tag rows when archiving a stream under archived-stream partitioning - #5679

Merged
jeremydmiller merged 1 commit into
masterfrom
fix/5675-archive-tagged-stream
Oct 15, 2026
Merged

jeremydmiller merged 1 commit into
masterfrom
fix/5675-archive-tagged-stream

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #5675.

A correction to the cause

The issue describes mt_archive_stream as running update mt_events set is_archived = TRUE and the FK rejecting that update. That is the non-partitioned path, and it is fine: seq_id does not change, so the plain seq_id foreign key is undisturbed.

Under UseArchivedStreamPartitioning the function does something else entirely — it inserts the rows into the archived partition and deletes the originals. So the statement being refused is the delete from mt_events … is_archived = FALSE, with the tag rows still pointing at the (seq_id, FALSE) rows it is removing:

23503: update or delete on table "mt_events_default" violates foreign key constraint
       "mt_event_tag_ledger_seq_id_is_archived_fkey1" on table "mt_event_tag_ledger"

Two things follow. ON UPDATE CASCADE (the issue's second fix option) would not have helped, because nothing updates the referenced key. And no schema migration is needed — only the function body changes, which Weasel regenerates on its own.

The fix

mt_archive_stream moves the tag rows with their events. The composite FK is not deferrable, so the statement order is the whole fix — every statement has to leave both sides of the key consistent by itself:

  1. archived event rows inserted first, so (seq_id, TRUE) exists to point at;
  2. then the archived tag rows, which reference it;
  3. then the original tag rows deleted, before the events they still reference;
  4. and only then the original event rows.

Details worth knowing:

  • Statements are emitted for every registered tag type, with an explicit column list — same reason mt_archive_stream generates a positional INSERT ... SELECT that breaks after a column-add migration #4619 made the event and stream inserts explicit: an ALTER TABLE ADD COLUMN on an existing tag table appends physically, and a positional insert would then load the wrong column.
  • Nothing is emitted in HStore mode, where tags live inline on mt_events.tags and travel with the row, so the function must not reference a tag table that does not exist.
  • The tenant predicate is written against an alias, because both the tag table and mt_events carry tenant_id under conjoined tenancy and an unqualified column in the subquery would bind to the inner table.
  • The non-partitioned path is untouched.

Tests

Bug_5675_archiving_a_tagged_stream, 6 tests:

archiving_a_tagged_stream_moves_its_tag_rows both rows land in the archived partition, none left behind, neighbouring stream's tag untouched
archiving_moves_every_registered_tag_table two tag types — one passing proves nothing about the loop
..._under_conjoined_tenancy_leaves_other_tenants_alone the same stream id and the same tag value in two tenants, archiving one
an_untagged_stream_beside_a_registered_tag_type_still_archives nothing may assume there is at least one tag row
archiving_a_tagged_stream_in_hstore_mode_still_works no tag tables exist at all
a_tag_query_still_sees_the_archived_events_which_is_a_separate_bug see below

A/B against the unfixed code: 4 of the 6 fail with exactly the reported 23503. The two that pass are the untagged and HStore cases, which should pass both ways. With the fix: 6/6.

Full EventSourcingTests on net10.0: 2540 passed, 27 skipped, 0 failed.

What this does NOT fix: #5677

The issue's "tests to add" asks that tag queries "behave as documented" for archived events afterwards. They do not behave like any other event query:

query archived event
QueryAllRawEvents() 0 — correctly excluded
QueryByTagsAsync 1 — still returned
EventsExistAsync true

I verified that is independent of this change: the same two answers come back with UseArchivedStreamPartitioning off, which the archive function change never touches, and in both storage modes. So it is pre-existing and filed separately as #5677 — it matters most for EventsExistAsync, which is what a DCB boundary assertion is built on.

The sixth test pins the current answers and names that issue, so the behaviour is written down rather than rediscovered. It should be inverted when #5677 is fixed.

Follow-up this unblocks

tombstoning_streams.tombstoning_deletes_tag_rows_in_a_partitioned_tag_table (from #5674) carries a comment saying it deliberately does not archive first because of this bug. It can archive now; I left it alone to keep this diff to the fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_01481eiEZg1Bv6pu4DrgPRg3

…am partitioning

With UseArchivedStreamPartitioning on and any DCB tag type registered, ArchiveStream on a stream with
tagged events came back as 23503. Each mt_event_tag_* table is itself partitioned by is_archived and
carries a composite foreign key (seq_id, is_archived) to mt_events, which is not deferrable.

The issue describes the cause as a rejected UPDATE, but under partitioning mt_archive_stream does not
update is_archived at all: it inserts the rows into the archived partition and DELETES the originals.
So it is the delete that is refused, with the tag rows still pointing at the (seq_id, FALSE) rows it
is removing. That also means ON UPDATE CASCADE would not have helped, and no schema migration is
needed.

mt_archive_stream now moves the tag rows with their events, and the statement ORDER is the whole fix,
because every statement has to leave both sides of that key consistent on its own:

  1. the archived event rows are inserted first, so (seq_id, TRUE) exists to point at;
  2. then the archived tag rows, which reference it;
  3. then the original tag rows are deleted, BEFORE the events they still reference;
  4. and only then the original event rows.

Statements are emitted for every registered tag type, with an explicit column list for the same
reason #4619 made the event and stream inserts explicit. Nothing is emitted in HStore mode, where
tags live inline on mt_events.tags and travel with the row, and the tenant predicate is written
against an alias because both the tag table and mt_events carry tenant_id under conjoined tenancy.

The non-partitioned path is untouched: it updates is_archived in place, seq_id does not change, and
the plain foreign key is undisturbed.

Six tests: the move itself, two tag tables, conjoined tenancy with the same stream id and tag value
in two tenants, an untagged stream beside a registered tag type, and HStore mode. Four of the six
fail on master with the reported 23503.

The sixth pins something this does NOT fix: tag queries return archived events, where every other
event query excludes them. Verified independent of this change -- the same answers come back with
partitioning off -- and filed as #5677.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01481eiEZg1Bv6pu4DrgPRg3
@jeremydmiller

Copy link
Copy Markdown
Member Author

Also ran TenantPartitionedEventsTests on net10.0, since the archive function is shared with per-tenant event partitioning: 253 passed, 2 skipped, 0 failed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant