Skip to content

DocumentIndexer.containsTuple uses shallow Arrays.equals, so an array-valued key churns remove+re-put on every update (the #5318 defect in the new #6934 path) #7109

Closed

Description

@ruispereira

Summary

The #6934 fix introduced a tuple-level delta for composite indexes with one collection modifier. Its containsTuple helper compares tuples with the shallow Arrays.equals, so a tuple element that is itself an array never matches - which is exactly the defect #5318 fixed for the scalar-only branch of the same method, with a comment naming the same example.

Not a correctness bug: the final index state is right. It is a spurious remove + re-put on every update of such a record.

Where (HEAD 38034d9c8)

engine/src/main/java/com/arcadedb/database/DocumentIndexer.java:537-542, added by the #6934 fix:

private static boolean containsTuple(final List<Object[]> tuples, final Object[] tuple) {
  for (final Object[] t : tuples)
    if (Arrays.equals(t, tuple))
      return true;
  return false;
}

Arrays.equals on two Object[] compares elements with Object.equals(). For an element that is itself an array that is identity, so two independently deserialized but identical arrays compare unequal.

The precedent is in the same file, :648-653, where #5318 established the opposite for the scalar-only path - and the comment names the very case:

// Use content-aware comparison: array-typed keys (e.g. the float[] of a vector index) do not override
// Object.equals(), so two independently deserialized-but-identical arrays would compare unequal and
// trigger a spurious remove()+put() on every record update (issue #5318). Objects.deepEquals falls back
// to Object.equals() for scalar keys and to element-wise Arrays.equals() for array keys.
if (!keyValuesAreModified && !Objects.deepEquals(newKeyValues[i], oldKeyValues[i]))

Impact

A composite index over an array-valued scalar plus a BY ITEM property - the shape the #6934 fix created this path for - churns index.remove(oldTuple, rid) followed by index.put(newTuple, rid) on every update of the record, even when neither key changed. That is index write amplification and extra WAL per update, on the path a user reaches by declaring exactly the kind of index #6934 made work.

Correctness is unaffected: remove-then-put lands on the same final state, and the removes precede the puts (:523-533), so no live entry is lost.

Suggested fix

Arrays.deepEquals(t, tuple) at :539. One word, and it makes the composite path agree with the scalar path that #5318 already settled.

Worth noting for whoever takes it: the two branches now use two different comparison policies for the same question, which is the drift that produced this. A shared helper would prevent the third path from picking a third policy.

Scope / confidence

High on the mechanism and the precedent - both quoted above from the same file. Filed as low severity because the outcome is redundant writes rather than a wrong answer.

Activity

  1. added this to the 26.9.1 milestone on Sep 3, 2026
  2. added
    in progressThe team is actively working on this issue
    on Sep 3, 2026
  3. self-assigned this
    on Sep 3, 2026
  4. added 2 commits that reference this issue on Sep 3, 2026
  5. removed
    in progressThe team is actively working on this issue
    on Sep 3, 2026
  6. lvca commented on Sep 3, 2026

    @lvca
    Member

    Fixed in #7117 (milestone 26.9.1).

    Updating a record indexed by a composite index that mixes a BY ITEM/BY KEY/BY VALUE property with an array-valued key (a BINARY property, for example) no longer removes and re-adds its index entries when the indexed values did not change. Every key comparison in the indexer now goes through one content-aware equality, so the same slip cannot come back through another update path.


    ❤️ Thanks for helping make ArcadeDB better! ArcadeDB is free and open source, sustained by its community. If it's useful to you or your company, please consider becoming a sponsor to keep development going.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions