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));
+ }
+}