Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-198 · Case file with fixtures · Case 170 of 694 · Runtime evidence

Why BTreeMap::append Overwrites Conflicting Values

BTreeMap::append moves every entry out of the other map and uses an other-map-wins value policy for duplicate keys, while retaining self's equal key object. Use Entry or an explicit merge loop when conflicts need preservation, rejection, or combination.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all targets with alloc
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
append is a move-and-merge operation with a defined other-map-wins conflict rule, not a union that preserves values already in self.
First discriminating check
Put one duplicate key in both maps, append the other map, then inspect the conflict value and confirm that the other map is empty.

The word “append” sounds harmless when both maps contain different keys. With a duplicate key, it needs a conflict policy, and Rust has already chosen one.

The failing program has a current configuration where mode is safe and an incoming map where mode is fast. After current.append(&mut incoming), the value in current is fast. The incoming map is empty.

Values from other win

BTreeMap::append moves all elements from other into self, leaving other empty. If both maps contain a key, the value from self is overwritten by the value from other.

That gives the operation two effects:

unique other key     -> moved into self
conflicting key      -> other value replaces self value
all other entries    -> removed from other

It is a destructive merge with a fixed last-input-wins style policy. It is not a set union that preserves the destination, and it does not report conflicts.

The map type cannot infer whether safe or fast is more authoritative. Its rule is structural and consistent; application policy remains my responsibility.

Use Entry to preserve existing values

The repaired program consumes the incoming map in a loop and calls BTreeMap::entry:

for (key, value) in incoming {
    current.entry(key).or_insert(value);
}

Now existing values win and new keys are inserted. Because value already came out of the incoming map, eager evaluation of or_insert does not create extra work in this example. If construction were expensive and conditional, or_insert_with would be the relevant choice.

For rejection, I match the entry and return a conflict error. For combination, I modify the occupied value with a domain-specific function. Writing the loop makes policy reviewable.

The original equal key in self is retained

There is another subtle rule in the documentation: on a conflict, the value is overwritten but the key object already stored in self is not. Two keys can compare equal without being identical in every field.

For example, a key type may order only by an ID while carrying a display label that does not participate in Ord. Appending an equal incoming key updates the value but retains the old key representation.

This matches the general insert behaviour documented for ordered maps and resembles the HashMap::insert case elsewhere in the Atlas. If key metadata must be refreshed, I should not hide mutable attributes inside identity keys. I put them in the value, or remove and reinsert deliberately.

Map equality is about the key's Ord contract, not every byte stored in the key object.

other becoming empty is part of ownership transfer

append takes &mut other, not ownership by value, but it moves the entries out and leaves a valid empty map. Existing references into either map cannot survive the mutable borrow.

This is useful when I want to reuse other's allocation or keep the variable in scope. It can also surprise code that expects to inspect rejected or overwritten inputs afterward.

If audit data matters, I collect conflicts before merging or have the merge loop return them. Once overwritten values are dropped and other is empty, the original input is no longer available.

The fixture asserts emptiness separately from the conflict result. A test of only the destination would not capture the full ownership transition.

Configuration layering needs named direction

In configuration systems, “append environment into defaults” and “append defaults into environment” produce opposite winners. Generic variable names such as a and b make this easy to reverse.

I use names like defaults, file_config, environment, and cli_overrides, then write a merge helper whose name states precedence. The order of layers becomes part of the product contract.

I also distinguish absence from a value explicitly set to empty. A map overwrite cannot by itself express deletion, inheritance, or “use default.” Those states may require an enum value rather than a missing key convention.

Conflict policies deserve independent tests

The smallest useful fixture contains one unique key on each side and one duplicate. It asserts:

  1. unique destination data survives;
  2. unique incoming data arrives;
  3. the duplicate follows the chosen policy;
  4. the incoming map has the expected final ownership state;
  5. equal-but-not-identical keys follow the intended representative rule when that matters.

Testing only disjoint maps never executes the important branch. Large realistic maps can also hide which layer supplied the final value.

Do not infer database merge semantics

The same vocabulary appears in SQL, distributed state, and version control, but BTreeMap::append does not provide transactions, causal ordering, or conflict records. It is an in-memory collection operation under one mutable owner.

When values carry versions or timestamps, I compare those fields explicitly. “Other wins” is correct only if the caller has already established that other is the authoritative later layer.

The core principle is that every merge has a conflict policy even when the method signature does not accept a closure. BTreeMap::append chooses the other map's value and empties that map. When my domain needs another rule, I make the rule code rather than hoping “append” means preserve.