Omnigraph

Merging Branches

A merge integrates one branch into another and publishes the target change

A merge integrates one branch into another and publishes the target change atomically across the whole graph.

omnigraph branch merge review/2026-04-25 --into main \
  --store s3://bucket/graph.omni

The source is positional. --into defaults to main.

The same merge as a GQ statement, sent to POST /mutate with no request target, name, or parameters:

branch merge "review/2026-04-25" into main

into defaults to main; a name with /, -, or . is quoted, a bare identifier such as main is not. The answer is a ChangeOutput with outcome.kind = "merged" and outcome.merge one of already_up_to_date, fast_forward, or merged; after a fast_forward or merged result, commit identifies that merge's own publication, even if another writer has since advanced the target. The statement has no --delete-branch composition: follow it with branch delete <source>. A merge statement takes no commit precondition -- POST /mutate/if-graph-commit and --if-commit refuse one beside it -- and no front offers a conditional merge today.

A merge preserves changes already integrated into a branch when you later merge that branch back. For example, after merging a new edge from main into review, editing only a node on review and merging it into main preserves that edge. A merge whose source and target tables reached the same version count on different branches fast-forwards; earlier releases refused it with a table version … already exists error.

Outcomes

  • Already up to date: the target already contains the source changes.
  • Fast-forward: the target still has the merge base's schema and table versions, so it takes the source's state. The check reads state, not history: a target merged again from a branch it fast-forwarded before, with no write in between, fast-forwards again. One exception: when a fast-forward into main carried a table whose changes cancel out on the branch, or a table on a fork left by a branch created before storage format v11, main ends on its own version of that table, so a later merge from that branch can report Merged. The merged data is the same.
  • Merged: both branches changed, so OmniGraph performs a three-way, entity-level merge and creates a commit with two parents.

branch merge --json, the HTTP merge route and the GQ merge statement all return commit with the exact commit id, manifest version, parents, branch, actor and creation timestamp (Unix microseconds). Fast-forward returns the new target commit; its merged parent identifies the source head. An already-up-to-date merge publishes nothing and returns commit: null.

If a remote response is lost or incomplete, the merge may have committed. The CLI does not resubmit it automatically. Inspect the intended target changes and relevant history before deciding what to do next; a later head alone cannot identify the missing merge receipt.

When only the source changed a table since the merge base, a merge into a named branch can adopt that exact table snapshot without copying its rows. This also works when the target already has its own table history. Tables changed on both sides still use the normal three-way merge and constraint checks.

The target can continue writing after adoption. Other live branches keep their current table snapshots through those target writes and cleanup; creating a replacement branch is unnecessary. Cleanup retains the underlying histories they still need. Avoiding a table copy does not eliminate the reads needed to validate a merge.

The source branch is unchanged by the merge. Use --delete-branch for the normal review-branch lifecycle:

omnigraph branch merge review/2026-04-25 --into main --delete-branch \
  --store s3://bucket/graph.omni

Deletion happens after a successful merge and has its own authorization check. If deletion is denied or the source still has descendants, the merge remains successful and durable. The CLI exits 0 and warns in human output; HTTP and CLI JSON preserve commit, report branch_deleted: false and include the structured branch_delete_error_details (error, plus typed code and details where available). Successful deletion reports branch_deleted: true; without --delete-branch, both deletion fields are absent. v0.12 removes the old branch_delete_error string. Inspect and retry deletion separately; do not replay a successful merge.

Deleting a branch is irreversible and may make branch-only commits unavailable. It also releases old history for a later cleanup.

Conflicts

When both sides changed the same logical data incompatibly, the merge returns a structured conflict list and publishes nothing.

KindMeaning
divergent_insertBoth branches inserted the same id.
divergent_updateBoth branches updated the same entity differently.
delete_vs_updateOne branch deleted an entity the other updated.
orphan_edgeThe result would contain an edge whose endpoint was deleted.
unique_violationThe result would violate uniqueness.
cardinality_violationThe result would violate edge cardinality.
value_constraint_violationThe result would violate an enum, range, or other value constraint.

A merge classifies both sides against their merge base, the nearest commit both branches descend from, counting a merged branch as an ancestor. Two branches that each merged the same third branch therefore share that branch's commit as their base, and an entity only one of them changed after that import merges cleanly. The record of such a commit lives in the branch it was merged from, and the merge can read it from a live branch or retained retired history. Cleanup preserves the exact selected base and the retired histories needed to resolve it. Recreating the logical branch name cannot substitute the replacement branch's same-numbered snapshot.

Each conflict identifies the affected type and, when applicable, entity id. The HTTP server returns conflicts with status 409, the same answer for POST /branches/merge and for a branch merge statement on POST /mutate. Reconcile the data on one or both branches, then run the merge again.

Edges inserted on both sides

Whether the same edge added on both branches converges depends on the edge type's declared identity:

  • No declaration: edge ids are generated, so each side's insert is its own row and the merge keeps both. This is the documented multiset default; parallel edges are legitimate data.
  • @unique(@src, @dst): each branch's write succeeds on its own, and the merge reports unique_violation. Releases before edge keys provide this guard as @unique(src, dst); from 0.11 it is spelled @unique(@src, @dst).
  • @key(@src, @dst): both sides derive the same id, so identical inserts converge to one row with no conflict. If the sides disagree on a non-key property, the merge reports divergent_insert on the edge type with the derived id as the entity id (for @key(@src, @dst) a JSON array such as ["Alice","Bob"]; the elements are the endpoint node ids, so they are generated ids when the endpoint type declares no key). Reconcile it like any divergent insert: align the property on one branch (insert the same key again with the agreed values; the insert upserts), then merge again.

Merge classification mode

The merge_lineage session setting (request scope; set merge_lineage = off; before the statement, --set merge_lineage=off, or the request's settings field; process default OMNIGRAPH_MERGE_LINEAGE) selects how a branch merge finds what changed. on (the release default) discovers candidates from Lance version metadata — fragment lists and deletion files — and compares candidate rows. Known deleted row positions use bounded direct reads. Candidate filtering may still scan data when an applicable index is absent; a fail-closed precondition gate falls back to the full three-way scan whenever any assumption cannot be proven (Blob-bearing schema, differing schemas or storage paths across the pins, version pins not matching the manifest entries, missing stable identifiers or the exact-id primary-key contract, or a candidate set past its byte budget). The table version a side pins need not descend from the one the merge base pins, so merging a branch back after an earlier merge still qualifies. off forces the full three-way scan everywhere — the operational fallback if merge results are ever in question. verify runs both, compares their decisions entity by entity, publishes the scan's result, and fails the merge loudly on any divergence (the debug-build default, used for validation; it costs both paths). A merge that succeeds produces the same result in every mode; only cost differs. An unrecognized value is refused where it arrives: in the environment it refuses startup, in a request or a file it refuses that request.

Later writes create fresh table storage when needed and leave unused former storage for explicit cleanup. They do not reclaim a previous table history as part of preparing the write or merge.

After a large merge

Indexes do not define merge correctness. Newly merged entities remain queryable even when index coverage has not caught up, but some searches may scan them. Run omnigraph optimize after a large merge to restore efficient layout and index coverage.

A merge that writes rows is also bounded by the Blob payload it copies; see Blob limits.

See Branches, Commits, and History for the complete branch workflow.

On this page