Skip to content

Two entities merged in March are one node in February #336

Description

@WaylandYang

0019 named this as the second cut and #317 landed the first: facts and derivations now rewind on the recording axis, but entities do not. merged_into says a merge happened, not when, so two entities merged in March are still one node at an as_of in February — and the facts that moved with the merge show up under a name that did not hold them yet.

The clock exists already: entity_merges carries created_at and reverted_at, which is the same shape record_axis::held_at reads on every other table.

Two halves

Which nodes exist at T. An entity is swallowed at T when a merge with source_id = e was in effect at T — created_at <= T AND (reverted_at IS NULL OR reverted_at > T). Reads currently ask merged_into IS NULL, which answers only for now. An entity created after T should also be absent (entities.created_at).

Which entity a fact belonged to at T. A merge rewrites rows in place — UPDATE facts SET subject_id = target WHERE subject_id = source — and records exactly which rows moved, in moved_subject_facts and moved_object_facts. So before that merge, those facts belonged to the source, and the row cannot say so on its own.

The set to undo is merges currently in effect (reverted_at IS NULL) whose created_at > T. A reverted merge is not in it: revert_merge already moved those rows back, so undoing them a second time would move them somewhere they never were. The symmetric case is the same arrays read the other way — a reverted merge whose window contains T means the facts were on the target then, and the move has to be applied rather than undone.

Chains resolve in one step: with A→B and then B→C, a fact that started on A appears in both merges' arrays, and the earliest not-yet-made merge that moved it names the entity that held it at T.

Already free

The facts a merge invalidates because they would become self-loops carry invalidated_at like any other retraction, so #317's predicate brings them back at an as_of before the merge with nothing further to do. Same for whatever reconcile_moved_facts closed on the way.

Out of scope for this cut

The target's own type and profile at T. entity_merges keeps target_type_before and target_profile_before, so it is answerable later; the node's label is unaffected, since a merge leaves the target's canonical_name alone.

Shape

The predicate belongs in record_axis beside the others, and the undo mapping belongs in one CTE that the node and edge queries join, not spread across each read — the same discipline 0019 asked for and for the same reason. Database-backed tests should assert both directions: two nodes before the merge with their own facts, one node after, and a reverted merge's window reading as merged from inside and separate from either side of it.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions