This is an automated email from the ASF dual-hosted git repository.

yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 1ab4fdb81a [#13421] fix(core): make entity hashCode consistent with 
unordered-collection equals (#13422)
1ab4fdb81a is described below

commit 1ab4fdb81a783bc75c7d61f722e7c5375cc5e231
Author: YangJie <[email protected]>
AuthorDate: Tue Sep 22 10:36:40 2026 -0400

    [#13421] fix(core): make entity hashCode consistent with 
unordered-collection equals (#13422)
    
    ### What changes were proposed in this pull request?
    
    Adds an order-independent, null-safe
    `CollectionUtils.unorderedHashCode`, right next to `isEqualCollection`,
    and uses it from the five entities whose `equals` compares a collection
    with bag semantics:
    
    - `GroupEntity`, `UserEntity`: `roleNames`, `roleIds` (also folds in
    `namespace`, which `equals` already compares)
    - `ModelVersionEntity`: `aliases`
    - `RoleEntity`: `securableObjects`
    - `TableEntity`: `columns` (also drops a duplicated `distribution`
    argument)
    
    ### Why are the changes needed?
    
    Each entity's `equals` compares the collection field order-independently
    via `CollectionUtils.isEqualCollection`, but `hashCode` hashed the
    `List` directly, so two equal entities that differ only in element order
    hashed differently, breaking the `Object.hashCode` contract for any
    hash-based collection. Sharing one helper keeps `equals` and `hashCode`
    consistent, and the helper is null-safe on both the collection and its
    elements.
    
    Fix: #13421
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    Added `TestCollectionUtils` covering `unorderedHashCode`
    order-independence, bag-equality consistency, and null-collection and
    null-element handling. Added per-entity regression tests
    (`TestGroupEntity`, `TestUserEntity`, `TestModelVersionEntity`,
    `TestRoleEntity`, `TestEntity`) pinning that two entities equal under an
    unordered collection share a hash code. Each fails against the pre-fix
    code.
---
 .../gravitino/authorization/SecurableObjects.java  |  9 ++-
 .../authorization/TestSecurableObjects.java        | 17 ++++++
 .../apache/gravitino/utils/CollectionUtils.java    | 20 +++++++
 .../gravitino/utils/TestCollectionUtils.java       | 62 +++++++++++++++++++
 .../org/apache/gravitino/meta/GroupEntity.java     |  7 ++-
 .../apache/gravitino/meta/ModelVersionEntity.java  |  5 +-
 .../java/org/apache/gravitino/meta/RoleEntity.java |  5 +-
 .../org/apache/gravitino/meta/TableEntity.java     | 28 +++++----
 .../java/org/apache/gravitino/meta/UserEntity.java |  7 ++-
 .../java/org/apache/gravitino/meta/TestEntity.java | 50 ++++++++++++++++
 .../org/apache/gravitino/meta/TestGroupEntity.java | 65 ++++++++++++++++++++
 .../gravitino/meta/TestModelVersionEntity.java     | 35 +++++++++++
 .../org/apache/gravitino/meta/TestRoleEntity.java  | 70 ++++++++++++++++++++++
 .../org/apache/gravitino/meta/TestUserEntity.java  | 64 ++++++++++++++++++++
 14 files changed, 425 insertions(+), 19 deletions(-)

diff --git 
a/api/src/main/java/org/apache/gravitino/authorization/SecurableObjects.java 
b/api/src/main/java/org/apache/gravitino/authorization/SecurableObjects.java
index dd69a87427..c25d258c95 100644
--- a/api/src/main/java/org/apache/gravitino/authorization/SecurableObjects.java
+++ b/api/src/main/java/org/apache/gravitino/authorization/SecurableObjects.java
@@ -221,8 +221,13 @@ public class SecurableObjects {
 
     @Override
     public int hashCode() {
-      int result = super.hashCode();
-      return Objects.hash(result, privileges);
+      // equals compares privileges as an unordered collection, so the hash 
must be
+      // order-independent too rather than relying on the stored list order.
+      int privilegesHash = 0;
+      for (Privilege privilege : privileges) {
+        privilegesHash += Objects.hashCode(privilege);
+      }
+      return Objects.hash(super.hashCode(), privilegesHash);
     }
 
     @Override
diff --git 
a/api/src/test/java/org/apache/gravitino/authorization/TestSecurableObjects.java
 
b/api/src/test/java/org/apache/gravitino/authorization/TestSecurableObjects.java
index f1c6cd3c0e..82fd8711db 100644
--- 
a/api/src/test/java/org/apache/gravitino/authorization/TestSecurableObjects.java
+++ 
b/api/src/test/java/org/apache/gravitino/authorization/TestSecurableObjects.java
@@ -575,4 +575,21 @@ public class TestSecurableObjects {
     Assertions.assertFalse(modifyFunction.canBindTo(MetadataObject.Type.ROLE));
     
Assertions.assertFalse(modifyFunction.canBindTo(MetadataObject.Type.COLUMN));
   }
+
+  @Test
+  public void testHashCodeConsistentWithUnorderedPrivileges() {
+    SecurableObject one =
+        SecurableObjects.ofCatalog(
+            "catalog",
+            Lists.newArrayList(Privileges.UseCatalog.allow(), 
Privileges.CreateSchema.allow()));
+    SecurableObject another =
+        SecurableObjects.ofCatalog(
+            "catalog",
+            Lists.newArrayList(Privileges.CreateSchema.allow(), 
Privileges.UseCatalog.allow()));
+
+    // equals compares privileges as an unordered collection, so hashCode must 
agree
+    // regardless of the order the privileges were supplied in.
+    Assertions.assertEquals(one, another);
+    Assertions.assertEquals(one.hashCode(), another.hashCode());
+  }
 }
diff --git 
a/common/src/main/java/org/apache/gravitino/utils/CollectionUtils.java 
b/common/src/main/java/org/apache/gravitino/utils/CollectionUtils.java
index 43ade6c119..66f7c36127 100644
--- a/common/src/main/java/org/apache/gravitino/utils/CollectionUtils.java
+++ b/common/src/main/java/org/apache/gravitino/utils/CollectionUtils.java
@@ -20,6 +20,7 @@
 package org.apache.gravitino.utils;
 
 import java.util.Collection;
+import java.util.Objects;
 
 /** Utility class for working with collection. */
 public class CollectionUtils {
@@ -41,4 +42,23 @@ public class CollectionUtils {
     }
     return 
org.apache.commons.collections4.CollectionUtils.isEqualCollection(c1, c2);
   }
+
+  /**
+   * Computes an order-independent hash code for a collection, so that two 
collections equal under
+   * {@link #isEqualCollection} produce the same value. The element hashes are 
summed, which is
+   * commutative, and null elements are treated as zero. A null collection 
hashes to zero.
+   *
+   * @param collection the collection to hash, may be null and may contain 
null elements
+   * @return an order-independent hash code consistent with {@link 
#isEqualCollection}
+   */
+  public static int unorderedHashCode(Collection<?> collection) {
+    if (collection == null) {
+      return 0;
+    }
+    int hash = 0;
+    for (Object element : collection) {
+      hash += Objects.hashCode(element);
+    }
+    return hash;
+  }
 }
diff --git 
a/common/src/test/java/org/apache/gravitino/utils/TestCollectionUtils.java 
b/common/src/test/java/org/apache/gravitino/utils/TestCollectionUtils.java
new file mode 100644
index 0000000000..2a7c382566
--- /dev/null
+++ b/common/src/test/java/org/apache/gravitino/utils/TestCollectionUtils.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.utils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link CollectionUtils}. */
+public class TestCollectionUtils {
+
+  @Test
+  void unorderedHashCodeIsOrderIndependent() {
+    assertEquals(
+        CollectionUtils.unorderedHashCode(Arrays.asList("a", "b", "c")),
+        CollectionUtils.unorderedHashCode(Arrays.asList("c", "b", "a")),
+        "Reordering the elements must not change the hash");
+  }
+
+  @Test
+  void unorderedHashCodeAgreesWithBagEquality() {
+    // isEqualCollection uses bag semantics: cardinality matters, order does 
not.
+    assertEquals(
+        CollectionUtils.unorderedHashCode(Arrays.asList(1, 2, 2)),
+        CollectionUtils.unorderedHashCode(Arrays.asList(2, 1, 2)),
+        "Bag-equal collections must hash the same");
+  }
+
+  @Test
+  void nullAndEmptyCollectionsHashToZero() {
+    assertEquals(0, CollectionUtils.unorderedHashCode(null));
+    assertEquals(0, 
CollectionUtils.unorderedHashCode(Collections.emptyList()));
+  }
+
+  @Test
+  void unorderedHashCodeToleratesNullElements() {
+    // A null element must be treated as zero rather than throwing, matching 
List.hashCode.
+    assertEquals(
+        CollectionUtils.unorderedHashCode(Arrays.asList("a", null)),
+        CollectionUtils.unorderedHashCode(Arrays.asList(null, "a")),
+        "Null elements must be hashed safely and order-independently");
+  }
+}
diff --git a/core/src/main/java/org/apache/gravitino/meta/GroupEntity.java 
b/core/src/main/java/org/apache/gravitino/meta/GroupEntity.java
index 6344661445..78db9b6d9c 100644
--- a/core/src/main/java/org/apache/gravitino/meta/GroupEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/GroupEntity.java
@@ -169,7 +169,12 @@ public class GroupEntity implements Group, Entity, 
Auditable, HasIdentifier {
 
   @Override
   public int hashCode() {
-    return Objects.hash(id, name, auditInfo, roleNames, roleIds);
+    // roleNames/roleIds are compared as unordered collections in equals, so 
their
+    // hash contribution must not depend on element order either.
+    int hash = Objects.hash(id, name, namespace, auditInfo);
+    hash = 31 * hash + CollectionUtils.unorderedHashCode(roleNames);
+    hash = 31 * hash + CollectionUtils.unorderedHashCode(roleIds);
+    return hash;
   }
 
   public static Builder builder() {
diff --git 
a/core/src/main/java/org/apache/gravitino/meta/ModelVersionEntity.java 
b/core/src/main/java/org/apache/gravitino/meta/ModelVersionEntity.java
index 96d577b449..4f4446581d 100644
--- a/core/src/main/java/org/apache/gravitino/meta/ModelVersionEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/ModelVersionEntity.java
@@ -170,7 +170,10 @@ public class ModelVersionEntity implements Entity, 
Auditable, HasIdentifier {
 
   @Override
   public int hashCode() {
-    return Objects.hash(modelIdent, version, comment, aliases, uris, 
properties, auditInfo);
+    // aliases are compared as an unordered collection in equals, so their hash
+    // contribution must not depend on element order either.
+    int hash = Objects.hash(modelIdent, version, comment, uris, properties, 
auditInfo);
+    return 31 * hash + CollectionUtils.unorderedHashCode(aliases);
   }
 
   public static Builder builder() {
diff --git a/core/src/main/java/org/apache/gravitino/meta/RoleEntity.java 
b/core/src/main/java/org/apache/gravitino/meta/RoleEntity.java
index 9ded34692f..dea3a98740 100644
--- a/core/src/main/java/org/apache/gravitino/meta/RoleEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/RoleEntity.java
@@ -151,7 +151,10 @@ public class RoleEntity implements Role, Entity, 
Auditable, HasIdentifier {
 
   @Override
   public int hashCode() {
-    return Objects.hash(id, name, properties, auditInfo, securableObjects, 
namespace);
+    // securableObjects are compared as an unordered collection in equals, so 
their
+    // hash contribution must not depend on element order either.
+    int hash = Objects.hash(id, name, properties, auditInfo, namespace);
+    return 31 * hash + CollectionUtils.unorderedHashCode(securableObjects);
   }
 
   /**
diff --git a/core/src/main/java/org/apache/gravitino/meta/TableEntity.java 
b/core/src/main/java/org/apache/gravitino/meta/TableEntity.java
index 8110f5a6ac..3995eec278 100644
--- a/core/src/main/java/org/apache/gravitino/meta/TableEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/TableEntity.java
@@ -200,19 +200,21 @@ public class TableEntity implements Entity, Auditable, 
HasIdentifier {
 
   @Override
   public int hashCode() {
-    return Objects.hashCode(
-        id,
-        name,
-        auditInfo,
-        columns,
-        namespace,
-        properties,
-        Arrays.hashCode(partitioning),
-        Arrays.hashCode(sortOrders),
-        distribution,
-        distribution,
-        Arrays.hashCode(indexes),
-        comment);
+    // columns are compared as an unordered collection in equals, so their hash
+    // contribution must not depend on element order either.
+    int hash =
+        Objects.hashCode(
+            id,
+            name,
+            auditInfo,
+            namespace,
+            properties,
+            Arrays.hashCode(partitioning),
+            Arrays.hashCode(sortOrders),
+            distribution,
+            Arrays.hashCode(indexes),
+            comment);
+    return 31 * hash + CollectionUtils.unorderedHashCode(columns);
   }
 
   public static class Builder {
diff --git a/core/src/main/java/org/apache/gravitino/meta/UserEntity.java 
b/core/src/main/java/org/apache/gravitino/meta/UserEntity.java
index da4094aa13..cfac5ac0cf 100644
--- a/core/src/main/java/org/apache/gravitino/meta/UserEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/UserEntity.java
@@ -172,7 +172,12 @@ public class UserEntity implements User, Entity, 
Auditable, HasIdentifier {
 
   @Override
   public int hashCode() {
-    return Objects.hash(id, name, auditInfo, roleNames, roleIds);
+    // roleNames/roleIds are compared as unordered collections in equals, so 
their
+    // hash contribution must not depend on element order either.
+    int hash = Objects.hash(id, name, namespace, auditInfo);
+    hash = 31 * hash + CollectionUtils.unorderedHashCode(roleNames);
+    hash = 31 * hash + CollectionUtils.unorderedHashCode(roleIds);
+    return hash;
   }
 
   public static Builder builder() {
diff --git a/core/src/test/java/org/apache/gravitino/meta/TestEntity.java 
b/core/src/test/java/org/apache/gravitino/meta/TestEntity.java
index 0bb16c0494..d3ff8b2bf0 100644
--- a/core/src/test/java/org/apache/gravitino/meta/TestEntity.java
+++ b/core/src/test/java/org/apache/gravitino/meta/TestEntity.java
@@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import java.time.Instant;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Map;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.Field;
@@ -39,6 +40,7 @@ import org.apache.gravitino.rel.expressions.sorts.SortOrder;
 import org.apache.gravitino.rel.expressions.sorts.SortOrders;
 import org.apache.gravitino.rel.indexes.Index;
 import org.apache.gravitino.rel.indexes.Indexes;
+import org.apache.gravitino.rel.types.Types;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -422,4 +424,52 @@ public class TestEntity {
 
     Assertions.assertEquals(table1.hashCode(), table2.hashCode(), "hashCode 
should be the same");
   }
+
+  @Test
+  public void testTableEntityHashCodeConsistentWithBagEquality() {
+    AuditInfo audit =
+        AuditInfo.builder()
+            .withCreator("a")
+            .withCreateTime(Instant.parse("2026-01-01T00:00:00Z"))
+            .build();
+    Types.IntegerType integerType = Types.IntegerType.get();
+    ColumnEntity c1 =
+        ColumnEntity.builder()
+            .withId(1L)
+            .withName("col1")
+            .withPosition(0)
+            .withDataType(integerType)
+            .withAuditInfo(audit)
+            .build();
+    ColumnEntity c2 =
+        ColumnEntity.builder()
+            .withId(2L)
+            .withName("col2")
+            .withPosition(1)
+            .withDataType(integerType)
+            .withAuditInfo(audit)
+            .build();
+    TableEntity table1 =
+        TableEntity.builder()
+            .withId(1L)
+            .withName("t")
+            .withNamespace(Namespace.of("catalog", "schema"))
+            .withColumns(Lists.newArrayList(c1, c2))
+            .withAuditInfo(audit)
+            .build();
+    TableEntity table2 =
+        TableEntity.builder()
+            .withId(1L)
+            .withName("t")
+            .withNamespace(Namespace.of("catalog", "schema"))
+            .withColumns(Lists.newArrayList(c2, c1))
+            .withAuditInfo(audit)
+            .build();
+
+    // equals() compares columns as an unordered collection, so equal tables
+    // must hash equally — otherwise HashSet/HashMap drop them.
+    Assertions.assertEquals(table1, table2);
+    Assertions.assertEquals(table1.hashCode(), table2.hashCode());
+    Assertions.assertTrue(new 
HashSet<>(Collections.singletonList(table1)).contains(table2));
+  }
 }
diff --git a/core/src/test/java/org/apache/gravitino/meta/TestGroupEntity.java 
b/core/src/test/java/org/apache/gravitino/meta/TestGroupEntity.java
new file mode 100644
index 0000000000..fd78a514fe
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/meta/TestGroupEntity.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.meta;
+
+import com.google.common.collect.Lists;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashSet;
+import org.apache.gravitino.Namespace;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link GroupEntity} equality and hashing. */
+public class TestGroupEntity {
+
+  @Test
+  public void testHashCodeConsistentWithBagEquality() {
+    // One shared AuditInfo: distinct Instant.now() per build would make 
equals flaky.
+    AuditInfo audit =
+        AuditInfo.builder()
+            .withCreator("a")
+            .withCreateTime(Instant.parse("2026-01-01T00:00:00Z"))
+            .build();
+    GroupEntity g1 =
+        GroupEntity.builder()
+            .withId(1L)
+            .withName("group")
+            .withNamespace(Namespace.of("metalake"))
+            .withAuditInfo(audit)
+            .withRoleNames(Lists.newArrayList("role1", "role2"))
+            .withRoleIds(Lists.newArrayList(1L, 2L))
+            .build();
+    GroupEntity g2 =
+        GroupEntity.builder()
+            .withId(1L)
+            .withName("group")
+            .withNamespace(Namespace.of("metalake"))
+            .withAuditInfo(audit)
+            .withRoleNames(Lists.newArrayList("role2", "role1"))
+            .withRoleIds(Lists.newArrayList(2L, 1L))
+            .build();
+
+    // equals() compares role names/ids as unordered collections, so equal 
objects
+    // must hash equally — otherwise HashSet/HashMap drop them.
+    Assertions.assertEquals(g1, g2);
+    Assertions.assertEquals(g1.hashCode(), g2.hashCode());
+    Assertions.assertTrue(new 
HashSet<>(Collections.singletonList(g1)).contains(g2));
+  }
+}
diff --git 
a/core/src/test/java/org/apache/gravitino/meta/TestModelVersionEntity.java 
b/core/src/test/java/org/apache/gravitino/meta/TestModelVersionEntity.java
index df6aca484e..4d19379f3c 100644
--- a/core/src/test/java/org/apache/gravitino/meta/TestModelVersionEntity.java
+++ b/core/src/test/java/org/apache/gravitino/meta/TestModelVersionEntity.java
@@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import java.time.Instant;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import org.apache.gravitino.NameIdentifier;
@@ -129,4 +130,38 @@ public class TestModelVersionEntity {
         IllegalArgumentException.class,
         () -> 
ModelVersionEntity.builder().withVersion(1).withUris(Collections.emptyMap()).build());
   }
+
+  @Test
+  public void testHashCodeConsistentWithBagEquality() {
+    AuditInfo audit =
+        AuditInfo.builder()
+            .withCreator("a")
+            .withCreateTime(Instant.parse("2026-01-01T00:00:00Z"))
+            .build();
+    Map<String, String> uris = ImmutableMap.of(ModelVersion.URI_NAME_UNKNOWN, 
"s3://b/m/1");
+    ModelVersionEntity v1 =
+        ModelVersionEntity.builder()
+            .withModelIdentifier(NameIdentifier.of("metalake", "catalog", 
"schema", "model"))
+            .withVersion(1)
+            .withComment("c")
+            .withAuditInfo(audit)
+            .withUris(uris)
+            .withAliases(Lists.newArrayList("alias1", "alias2"))
+            .build();
+    ModelVersionEntity v2 =
+        ModelVersionEntity.builder()
+            .withModelIdentifier(NameIdentifier.of("metalake", "catalog", 
"schema", "model"))
+            .withVersion(1)
+            .withComment("c")
+            .withAuditInfo(audit)
+            .withUris(uris)
+            .withAliases(Lists.newArrayList("alias2", "alias1"))
+            .build();
+
+    // equals() compares aliases as an unordered collection, so equal objects
+    // must hash equally — otherwise HashSet/HashMap drop them.
+    Assertions.assertEquals(v1, v2);
+    Assertions.assertEquals(v1.hashCode(), v2.hashCode());
+    Assertions.assertTrue(new 
HashSet<>(Collections.singletonList(v1)).contains(v2));
+  }
 }
diff --git a/core/src/test/java/org/apache/gravitino/meta/TestRoleEntity.java 
b/core/src/test/java/org/apache/gravitino/meta/TestRoleEntity.java
new file mode 100644
index 0000000000..d029968a4e
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/meta/TestRoleEntity.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.meta;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashSet;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.authorization.SecurableObject;
+import org.apache.gravitino.authorization.SecurableObjects;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link RoleEntity} equality and hashing. */
+public class TestRoleEntity {
+
+  @Test
+  public void testHashCodeConsistentWithBagEquality() {
+    AuditInfo audit =
+        AuditInfo.builder()
+            .withCreator("a")
+            .withCreateTime(Instant.parse("2026-01-01T00:00:00Z"))
+            .build();
+    SecurableObject table1 =
+        SecurableObjects.parse("catalog.db.table1", MetadataObject.Type.TABLE, 
ImmutableList.of());
+    SecurableObject table2 =
+        SecurableObjects.parse("catalog.db.table2", MetadataObject.Type.TABLE, 
ImmutableList.of());
+    RoleEntity r1 =
+        RoleEntity.builder()
+            .withId(1L)
+            .withName("role")
+            .withNamespace(Namespace.of("metalake"))
+            .withAuditInfo(audit)
+            .withSecurableObjects(Lists.newArrayList(table1, table2))
+            .build();
+    RoleEntity r2 =
+        RoleEntity.builder()
+            .withId(1L)
+            .withName("role")
+            .withNamespace(Namespace.of("metalake"))
+            .withAuditInfo(audit)
+            .withSecurableObjects(Lists.newArrayList(table2, table1))
+            .build();
+
+    // equals() compares securable objects as an unordered collection, so equal
+    // objects must hash equally — otherwise HashSet/HashMap drop them.
+    Assertions.assertEquals(r1, r2);
+    Assertions.assertEquals(r1.hashCode(), r2.hashCode());
+    Assertions.assertTrue(new 
HashSet<>(Collections.singletonList(r1)).contains(r2));
+  }
+}
diff --git a/core/src/test/java/org/apache/gravitino/meta/TestUserEntity.java 
b/core/src/test/java/org/apache/gravitino/meta/TestUserEntity.java
new file mode 100644
index 0000000000..a68fd4486e
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/meta/TestUserEntity.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.meta;
+
+import com.google.common.collect.Lists;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashSet;
+import org.apache.gravitino.Namespace;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link UserEntity} equality and hashing. */
+public class TestUserEntity {
+
+  @Test
+  public void testHashCodeConsistentWithBagEquality() {
+    AuditInfo audit =
+        AuditInfo.builder()
+            .withCreator("a")
+            .withCreateTime(Instant.parse("2026-01-01T00:00:00Z"))
+            .build();
+    UserEntity u1 =
+        UserEntity.builder()
+            .withId(1L)
+            .withName("user")
+            .withNamespace(Namespace.of("metalake"))
+            .withAuditInfo(audit)
+            .withRoleNames(Lists.newArrayList("role1", "role2"))
+            .withRoleIds(Lists.newArrayList(1L, 2L))
+            .build();
+    UserEntity u2 =
+        UserEntity.builder()
+            .withId(1L)
+            .withName("user")
+            .withNamespace(Namespace.of("metalake"))
+            .withAuditInfo(audit)
+            .withRoleNames(Lists.newArrayList("role2", "role1"))
+            .withRoleIds(Lists.newArrayList(2L, 1L))
+            .build();
+
+    // equals() compares role names/ids as unordered collections, so equal 
objects
+    // must hash equally — otherwise HashSet/HashMap drop them.
+    Assertions.assertEquals(u1, u2);
+    Assertions.assertEquals(u1.hashCode(), u2.hashCode());
+    Assertions.assertTrue(new 
HashSet<>(Collections.singletonList(u1)).contains(u2));
+  }
+}

Reply via email to