TemporalRepository save semantics: stable-live-row + historical-copy #13
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
TemporalRepository<TDto>::save(...)currently implements close-then-insert semantics: when an entity is updated, the prior live row'svalid_untilis flipped from SENTINEL tonow(), then a new row is inserted with the updated values andvalid_until = SENTINEL. The PKid(version UUID) of the live row changes on every update.The downstream consumer (
fewo-webapp#459) accepted a stable-live-row + historical-copy design instead:This way the live row's PK never changes; FKs continue to resolve to the same row identity across updates; ON UPDATE CASCADE on every child propagates
valid_untilflips on delete.Scope
Switch
TemporalRepository<TDto>::save(...)to:entity_id. If absent, this is a fresh insert — setvalid_from=now,valid_until=SENTINEL, save, return.b. Setb.id = newId()(new PK),b.valid_until = now(). Saveb— that's the historical copy.valid_from = now(), leavevalid_until = SENTINEL. Save it (same PK as before).softDelete(...)is unchanged conceptually — setvalid_until = now()on the live row. With ON UPDATE CASCADE wired up by the consumer's schema, child FKs follow the change.findByEntityIdAt(...)andhistory(...)are read paths and don't change.Inner adapter contract impact
The inner
Repository<TDto>was previously expected to treatsaveas upsert keyed by(entity_id, valid_from). The new flow still issues two saves per update — one for the historical copy (new PK), one for the live row (existing PK). The keying remains(entity_id, valid_from)as long as we use the row PK as a tiebreaker; the doc onTemporalRepositorywill be updated to document this clearly.Acceptance
TemporalRepository<TDto>::save(dto)follows the new flow.test_repository_decorators.cppupdated to assert: live PK is stable across updates; historical row PK is fresh; live row'svalid_until == SENTINELafter every update.live.idafter first save, assert it matches after a second save.repo/TemporalRepository.hpprow.Out of scope
fewo-webapp#459). The decorator stays schema-agnostic; the inner adapter'ssaveis whatever the consumer's DbClient does.applyDecoratorMigrations/RESHAPE_STEPS. Those are still composite-unique on(entity_id, valid_until)either way.Effort
Small (~30–50 LOC + test updates).
Blocks
fewo-webapp#459PR 1 needs this to land first; it's pinned via authkit version bump.Agent Evaluation
Feasibility: High. The decorator already does close-then-insert in ~30 LOC of
save(). Switching to stable-live + historical-copy is a straightforward rewrite — same primitives (findByEntityId,save), different orchestration. Tests already exercise the temporal flow; updating them to assert stable PK is a minor addition.Impact: Unblocks
fewo-webapp#459PR 1 (and every per-table migration after it). Without this change, the schema migration cannot ship — close-then-insert on a composite-FK schema with stable child references is incoherent.Effort: Small (~30–50 LOC + test updates). Single decorator method, one new test, README tweak.
Recommendation: Accept.
Implementation plan
include/oatpp-authkit/repo/TemporalRepository.hpp— rewritesave(dto):entity_id, behave as today (allocate id if null, setvalid_from = now,valid_until = SENTINEL, save).id, setclone.valid_until = now(), save the clone (historical copy). Then update the live row in place — copy the new dto's mutable fields onto the existing live row, setvalid_from = now(), leavevalid_until = SENTINEL, save (sameid).test/test_repository_decorators.cpp— updatetest_save_closes_prior_version_and_inserts_newto assert: live row'sidis preserved across the second save; the new row'sidis the historical copy. Existing assertions onvalid_from/valid_untilstay.repo/TemporalRepository.hpprow to describe the new write semantics. Note ON UPDATE CASCADE on consumer-side child FKs as the propagation mechanism for delete.saveno longer reallocates the live PK).No decision needed
Single approach — implementation plan stands.
Evaluated #13 (Small) — recommend Accept; stable-live + historical-copy rewrite of TemporalRepository::save.
Implemented in commit
792e509—TemporalRepository<TDto>::savenow does stable-live + historical-copy. On update: clone live (via oatpp reflection throughpolymorphicDispatcher->getProperties()), assign freshid+valid_until=now, save → INSERT historical; then set dto'sid=live.id(preserve PK) +valid_from=now+valid_until=SENTINEL, save → UPDATE live in place by PK. Inner contract changes from "upsert keyed by (entity_id, valid_from)" to "upsert keyed by id".TemporalFieldTraitsgains anid()accessor;OATPP_AUTHKIT_REGISTER_TEMPORALgrows to 5 args. Tests updated to assert stable live PK and fresh historical PK across updates — all 10 ctest cases pass. Version bumped 0.7.0 → 0.8.0 (semantic break). README updated.Implemented #13 — stable-live + historical-copy semantics shipped in commit
792e509, tagged v0.8.0; all 10 tests pass including new stable-PK assertion.