Carry DCB tags through a bulk event import in hstore mode - #5267
Carry DCB tags through a bulk event import in hstore mode#5267erdtsieck wants to merge 2 commits into
Conversation
BulkInsertEventsAsync writes events with a single COPY into mt_events over a fixed column list, and the DCB tags were not in it - nor did the bulk path write anything to the per-type tag tables. So every event that arrived through a bulk import landed untagged, and a tag query saw nothing of it. For a store whose history came in through a migration that is most of the history, and it fails the wrong way: the answer looks like "no events" rather than like an error. In hstore mode the tags are a column on mt_events itself, so they can travel in the same COPY: one more column at the end of the list - after the optional metadata columns, so those keep their positions - and one more write per row. The value comes from EventTagOperations.BuildHstore, the same rule the append path applies, which is why BuildHstore is internal now rather than private; a second implementation would be a second thing to keep in step. IEvent.Tags is null rather than empty when nothing was tagged, so the write is guarded on that and emits NULL. The second test covers it: a COPY row that skips a column instead of writing null goes out of step with its column list, which would corrupt every later column rather than merely lose a tag. TagTables mode is deliberately untouched: that needs a second COPY per registered tag type keyed on the seq_id, which interacts with the sequence blocks the events COPY draws from.
Three things on top of the contributor's two commits. The XML doc on BuildHstore had two stacked <summary> blocks; the second is now a <remarks> and says what the shared method actually owes its callers, which since #5265 includes the duplicate-tag-type guard. #5265 landed after this PR was written and the two meet in BuildHstore. Because that method is deliberately shared -- a second implementation of the rule would be a second thing to keep in step -- the bulk path inherits the refusal of two tags of one type in HStore mode. That is the right outcome and it now has a test, rather than being an undocumented consequence of a merge. The docs gain the two real limits of the hstore support. One is that refusal. The other is that the bulk path does not bump mt_dcb_tag_version, so a concurrent FetchForWritingByTags has nothing to fail its consistency check against. That is consistent with an API that creates new streams, has no optimistic concurrency and is meant for loading a history -- but it was worth stating, since the whole argument for this PR is that a silently untagged event is the failure a consistency boundary must not have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Merged to master — your two commits cherry-picked with authorship intact (7d13c67, 2bceb30), plus 6591033 for the review follow-ups. Closing since GitHub will not do it for a cherry-picked merge. Taking it as-is on the substance. Appending Verified red-then-green rather than taken on trust: with What changed on topThe XML doc. Two stacked #5265 landed while this was open, and the two meet in The DCB version bump. I raised this in review and settled on documenting rather than implementing. The bulk path does not bump
Thanks for going straight to a PR on this one — it was small, additive, behind an existing mode flag, and the reasoning in the description made it quick to review. |
|
Landed at 6591033. |
The problem
BulkInsertEventsAsyncwrites events with a singleCOPY ... mt_events(<fixed column list>) FROM STDIN BINARY, and the DCB tags are not in that list — nor does the bulk path write anything to the per-typemt_event_tag_*tables. So every event that arrives through a bulk import lands untagged.That is documented today in
docs/events/bulk-appending.mdas a known trade-off ("No event tags"), so this is a limitation being lifted rather than a regression. The reason it seemed worth lifting: for a DCB user it fails in the quiet direction. An untagged event is not an error toQueryByTagsAsyncor to a boundary aggregate — it is simply absent from the answer. A store whose history was loaded by a migration then gets a consistency boundary that silently excludes most of its events, and nothing points at the import.The change
In
DcbStorageMode.HStorethe tags live in a column onmt_eventsitself, so they can travel in the same COPY:tagsis appended tobuildEventColumns()— last, after the optional metadata columns, so those keep the positions they had;writeEventRowwrites the value fromEventTagOperations.BuildHstore, the same rule the append path applies. That is whyBuildHstoreisinternalnow rather thanprivate: a second implementation of the rule would be a second thing to keep in step;IEvent.Tagsis null rather than empty when nothing was tagged, so the write is guarded on that and emitsNULL.No extra round trip and no second pass — one more column and one more write per row.
DcbStorageMode.TagTablesis deliberately untouched. That would need a second COPY per registered tag type keyed on theseq_id, which interacts with the sequence blocks the events COPY draws from, and it felt like a separate change. The docs now state which mode carries tags through a bulk import and which does not.Tests
hstore_dcb_tags_survive_a_bulk_import:QueryByTagsAsyncon either registered tag type;That second test earned itself immediately: it is what caught the null
Tags, and it guards something worse than a lost tag — a COPY row that skips a column instead of writing null goes out of step with its column list, which shifts every later column.Verified against the existing suites as well: 156 passed / 0 failed across
BulkEventAppendTests,BulkEventStreamAppendTestsand everyDcbtest (net10.0).Process note
CONTRIBUTING.mdasks to discuss first on Discord or in an issue. I went straight to a PR because the change is small, additive and behind an existing mode flag — happy to move the discussion to an issue if you would rather triage it that way, or to close this if you would prefer the TagTables half solved in the same go.