Skip to content

[#13230] fix(api): defensively copy fieldNames and properties in IndexImpl - #13231

Open
LuciferYang wants to merge 2 commits into
apache:mainfrom
LuciferYang:fix/find-issues-06-index-defensive-copies
Open

LuciferYang wants to merge 2 commits into
apache:mainfrom
LuciferYang:fix/find-issues-06-index-defensive-copies

Conversation

@LuciferYang

@LuciferYang LuciferYang commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

IndexImpl now deep-copies fieldNames at construction and on fieldNames(), preserving null rows (row == null ? null : row.clone()), and stores properties as a null-tolerant unmodifiable copy (Collections.unmodifiableMap(new HashMap<>(properties)), matching the sibling TableChange.AddIndex). equals/hashCode semantics are unchanged.

Why are the changes needed?

The index stored and returned the caller's String[][] and Map by reference, so mutating the caller's arrays before or after Indexes.of changed the built index and drifted its equality. The defensive copy keeps a built index immutable while still accepting the inputs the old code accepted, including a null field-name row and null property values (which Arrays.deepEquals/deepHashCode and the map comparison handle).

Fix: #13230

Does this PR introduce any user-facing change?

No API change. A built index is now immutable: mutating the caller's arrays or map no longer changes the index. Inputs that were previously accepted (including a null field-name row or null property value) are still accepted. equals/hashCode semantics are unchanged.

How was this patch tested?

Added TestIndexes cases that pin: mutating the caller-supplied arrays and map does not change the built index; a null field-name row is accepted and compares correctly; and a null property value is accepted. They fail on the pre-fix tree and pass after the fix.

IndexImpl stored the caller-supplied String[][] fieldNames and Map
properties by reference and returned both directly, so the value-object
index stayed externally mutable after build: mutating the caller's
arrays (before or after Indexes.of) changed the built index and drifted
its equality.

Deep-copy fieldNames at construction and on fieldNames(), and snapshot
properties into an ImmutableMap. equals/hashCode semantics unchanged.

Repro TestIndexes failed against the unfixed tree; passes after the fix.
Found by find-issues sweep.
Copilot AI lite review requested due to automatic review settings September 16, 2026 12:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Null-handling compatibility regressions remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This pull request makes IndexImpl immutable through defensive copying and adds regression tests.

Changes:

  • Deep-copy field-name arrays.
  • Snapshot index properties.
  • Test mutation isolation.
File summaries
File Description
api/src/test/java/org/apache/gravitino/rel/indexes/TestIndexes.java Tests mutation isolation.
api/src/main/java/org/apache/gravitino/rel/indexes/Indexes.java Implements defensive copying.
Review details

Suppressed comments (2)

api/src/main/java/org/apache/gravitino/rel/indexes/Indexes.java:164

  • ImmutableMap.copyOf rejects null keys and values. Before this change, Indexes.of accepted a Map<String, String> containing null entries, and the API does not document a null-entry restriction; such inputs now fail with an NPE during construction despite the stated no-API-change behavior. Snapshot with an unmodifiable mutable-map copy (as TableChange.AddIndex does at api/src/main/java/org/apache/gravitino/rel/TableChange.java:790-793) or explicitly validate and document the new restriction.
      this.properties = properties == null ? ImmutableMap.of() : ImmutableMap.copyOf(properties);

api/src/main/java/org/apache/gravitino/rel/indexes/Indexes.java:190

  • The accessor repeats the same unconditional clone, so an index containing a null field-name row would still throw NullPointerException when read. Apply the same null-preserving copy here so fieldNames() does not introduce a new failure mode.
          : Arrays.stream(fieldNames).map(String[]::clone).toArray(String[][]::new);
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

this.fieldNames =
fieldNames == null
? null
: Arrays.stream(fieldNames).map(String[]::clone).toArray(String[][]::new);
The defensive-copy change regressed two inputs the pre-fix code accepted:
a null field-name row NPE'd in String[]::clone (constructor and
fieldNames()), and ImmutableMap.copyOf rejected null property values.
Preserve null rows with a null-checked clone and snapshot properties via
Collections.unmodifiableMap(new HashMap<>(...)), matching the sibling
TableChange.AddIndex contract while keeping the copies immutable.
@LuciferYang

Copy link
Copy Markdown
Contributor Author

Addressed in 206060b. The field-name deep copy now preserves null rows (row == null ? null : row.clone()) in both the constructor and fieldNames(), and properties is stored as a null-tolerant unmodifiableMap(new HashMap<>(...)) copy, matching the sibling TableChange.AddIndex. A null field-name row or a null property value no longer throws, and the built index stays externally immutable. Added regression tests for both.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug report] IndexImpl exposes its fieldNames array and properties map to external mutation

2 participants