Skip to content

fix: carry instant temporal updates and destroys into later versions - #916

Open
hoanle0126 wants to merge 1 commit into
ash-project:mainfrom
hoanle0126:fix/temporal-instant-write-carry
Open

hoanle0126 wants to merge 1 commit into
ash-project:mainfrom
hoanle0126:fix/temporal-instant-write-carry

Conversation

@hoanle0126

@hoanle0126 hoanle0126 commented Nov 3, 2026 •

Copy link
Copy Markdown

Closes #915
Companion to ash-project/ash#3107

Summary

  • Widens the concurrency gate (__keys, __cand, __locked, __conflict) for instant-as_of writes in AshPostgres.Temporal to lock all slices overlapping [as_of, ∞) in deterministic (<pk>, lower(valid_at)) order.
  • Computes per-attribute carry stopping boundaries via __after and __stops using lag(...) window functions, carrying each changed attribute forward in __carry across contiguous later slices until that attribute was changed by a later write or a validity gap occurs (and restamping recorded_at on carried slices).
  • Extends the same instant carry CTEs to matched upserts in upsert_group/9.
  • For instant destroys, deletes contiguous later slices in __carry up to the first validity gap while leaving range-as_of updates and destroys unchanged.
  • Adds test coverage in test/temporal_test.exs for instant updates, non-atomic updates, bulk atomic updates, matched upserts, x -> y -> x transitions, independent multi-column stops, atomic expression evaluation, validity gaps, future-scheduled versions, recorded_at restamping, and contiguous instant destroys.

@matt-beanland

matt-beanland commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

@hoanle0126 thanks for this. The carry follows ash-project/ash#3104's rule on every history I generated against it, atomic and non-atomic, and the changes to the range path leave every range write I test unchanged.

I'd like to hold it until the fix for ash-project/ash#3104 settles in core. Carrying a value forward can produce a version that would be refused if it were written directly. Here, an identity breach is refused by the exclusion constraint, with "has already been taken", but an update that's valid at t still succeeds when its carry breaks a validation in a later version. I've set out on ash-project/ash#3104 what I think fixing that needs. Carry validity belongs to core, for every data layer, rather than to a data layer's constraints: core works out the carry, and validates each carried version before anything is written. If it lands that way, the carry would reach AshPostgres as writes over ranges, and most of the carry SQL here wouldn't be needed.

Two parts of this PR also differ from ash-project/ash#3107, and I've raised both on ash-project/ash#3104 so the rule says which:

@hoanle0126

Copy link
Copy Markdown
Author

Thanks @matt-beanland! Holding this until #3104 settles in core makes total sense, especially if core plans the carry as range writes once the range-write defects are addressed.

I've also updated ash-project/ash#3107 (d017ee2) to align with this PR on restamping recorded_at for carried versions and excluding non-writable generated? / update_default attributes. Happy to revisit or pare this down once the core changes land!

This branch has not been deployed

No deployments
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.

Temporal: an update or destroy as of an instant doesn't carry into later versions

2 participants