This is an automated email from the ASF dual-hosted git repository.
roryqi 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 e51be54441 [#11833] feat(core): Add JDBC-backed paginated list/count
APIs for users and groups (#11882)
e51be54441 is described below
commit e51be54441cb72b7556d810f463b042fa0909775
Author: MaSai <[email protected]>
AuthorDate: Fri Aug 7 16:02:33 2026 +0800
[#11833] feat(core): Add JDBC-backed paginated list/count APIs for users
and groups (#11882)
### What changes were proposed in this pull request?
Adds JDBC-backed pagination for users and groups at metalake scope.
- Add `PagedResult<T>` in `core` (internal) with `totalCount()` and
`items()`
- Add dispatcher APIs:
- `PagedResult<User> listUsers(metalake, offset, limit)`
- `long countUsers(metalake)`
- `PagedResult<Group> listGroups(metalake, offset, limit)`
- `long countGroups(metalake)`
- Implement pagination via `UserMetaService` / `GroupMetaService` with
`LIMIT`/`OFFSET` SQL ordered by `user_id` / `group_id ASC`, including
role joins
- Missing metalake throws `NoSuchMetalakeException` (consistent with
existing list APIs)
- Wire through `AccessControlManager`, `UserGroupManager`, hook
dispatcher, and `AccessControlEventDispatcher` with listener events for
paged list and count (pre/success/failure), following the same pattern
as existing list APIs
- Keep existing full-list APIs unchanged; no pagination on
`listUserNames` / `listGroupNames`; no REST exposure
Fix: #11833
### Why are the changes needed?
Large metalakes need efficient paginated list and count at the
dispatcher layer without loading entire user/group populations into
memory.
### Does this PR introduce _any_ user-facing change?
1. New internal dispatcher APIs for paginated list and count (not
exposed via REST in this PR).
2. New listener event types / `OperationType` values for paged list and
count operations.
### How was this patch tested?
- `./gradlew spotlessApply`
- `./gradlew :core:test --tests
"org.apache.gravitino.authorization.TestAccessControlManager.testUserPagination"
--tests
"org.apache.gravitino.authorization.TestAccessControlManager.testGroupPagination"
-PskipITs`
- `./gradlew :core:test --tests
"org.apache.gravitino.storage.relational.service.TestUserMetaService.testUserPagination"
--tests
"org.apache.gravitino.storage.relational.service.TestGroupMetaService.testGroupPagination"
-PskipITs`
- `./gradlew :core:test --tests
"org.apache.gravitino.authorization.TestAccessControlManager" -PskipITs`
- `./gradlew :core:test --tests
"org.apache.gravitino.audit.v2.TestCompatibilityUtils" -PskipITs`
---------
Co-authored-by: Cursor <[email protected]>
---
.../gravitino/audit/v2/CompatibilityUtils.java | 4 +
.../authorization/AccessControlDispatcher.java | 41 +++++++
.../authorization/AccessControlManager.java | 33 +++++
.../gravitino/authorization/PagedResult.java | 58 +++++++++
.../gravitino/authorization/UserGroupManager.java | 30 +++++
.../hook/AccessControlHookDispatcher.java | 22 ++++
.../listener/AccessControlEventDispatcher.java | 87 +++++++++++++
.../listener/api/event/CountGroupsEvent.java | 55 +++++++++
.../api/event/CountGroupsFailureEvent.java | 43 +++++++
.../listener/api/event/CountGroupsPreEvent.java | 42 +++++++
.../listener/api/event/CountUsersEvent.java | 55 +++++++++
.../listener/api/event/CountUsersFailureEvent.java | 43 +++++++
.../listener/api/event/CountUsersPreEvent.java | 42 +++++++
.../listener/api/event/ListGroupsPagedEvent.java | 88 +++++++++++++
.../api/event/ListGroupsPagedFailureEvent.java | 69 +++++++++++
.../api/event/ListGroupsPagedPreEvent.java | 67 ++++++++++
.../listener/api/event/ListUsersPagedEvent.java | 88 +++++++++++++
.../api/event/ListUsersPagedFailureEvent.java | 69 +++++++++++
.../listener/api/event/ListUsersPagedPreEvent.java | 67 ++++++++++
.../listener/api/event/OperationType.java | 4 +
.../storage/relational/mapper/GroupMetaMapper.java | 13 ++
.../mapper/GroupMetaSQLProviderFactory.java | 11 ++
.../storage/relational/mapper/UserMetaMapper.java | 11 ++
.../mapper/UserMetaSQLProviderFactory.java | 11 ++
.../provider/base/GroupMetaBaseSQLProvider.java | 49 ++++++++
.../provider/base/UserMetaBaseSQLProvider.java | 49 ++++++++
.../mapper/provider/h2/GroupMetaH2Provider.java | 73 ++++++-----
.../mapper/provider/h2/UserMetaH2Provider.java | 45 ++++++-
.../postgresql/GroupMetaPostgreSQLProvider.java | 41 +++++++
.../postgresql/UserMetaPostgreSQLProvider.java | 41 +++++++
.../relational/service/GroupMetaService.java | 39 ++++++
.../relational/service/UserMetaService.java | 39 ++++++
.../gravitino/audit/v2/TestCompatibilityUtils.java | 4 +
.../authorization/TestAccessControlManager.java | 62 ++++++++++
.../listener/api/event/TestGroupEvent.java | 89 ++++++++++++++
.../listener/api/event/TestUserEvent.java | 90 ++++++++++++++
.../provider/h2/TestGroupMetaH2Provider.java | 7 +-
.../relational/service/TestGroupMetaService.java | 131 ++++++++++++++++++++
.../relational/service/TestUserMetaService.java | 136 +++++++++++++++++++++
39 files changed, 1915 insertions(+), 33 deletions(-)
diff --git
a/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
b/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
index 5814e5ecbc..725bb5dba6 100644
--- a/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
+++ b/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
@@ -129,6 +129,8 @@ public class CompatibilityUtils {
.put(OperationType.ENABLE_USER, Operation.ENABLE_USER)
.put(OperationType.DISABLE_USER, Operation.DISABLE_USER)
.put(OperationType.LIST_USERS, Operation.LIST_USERS)
+ .put(OperationType.LIST_USERS_PAGED, Operation.LIST_USERS)
+ .put(OperationType.COUNT_USERS, Operation.LIST_USERS)
.put(OperationType.LIST_USER_NAMES, Operation.LIST_USER_NAMES)
.put(OperationType.GRANT_USER_ROLES, Operation.GRANT_USER_ROLES)
.put(OperationType.REVOKE_USER_ROLES, Operation.REVOKE_USER_ROLES)
@@ -141,6 +143,8 @@ public class CompatibilityUtils {
.put(OperationType.REMOVE_GROUP_BY_ID, Operation.REMOVE_GROUP_BY_ID)
.put(OperationType.ALTER_GROUP, Operation.ALTER_GROUP)
.put(OperationType.LIST_GROUPS, Operation.LIST_GROUPS)
+ .put(OperationType.LIST_GROUPS_PAGED, Operation.LIST_GROUPS)
+ .put(OperationType.COUNT_GROUPS, Operation.LIST_GROUPS)
.put(OperationType.LIST_GROUP_NAMES, Operation.LIST_GROUP_NAMES)
.put(OperationType.GRANT_GROUP_ROLES, Operation.GRANT_GROUP_ROLES)
.put(OperationType.REVOKE_GROUP_ROLES, Operation.REVOKE_GROUP_ROLES)
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
index 5f0a1bd462..25b48b1067 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
@@ -170,6 +170,27 @@ public interface AccessControlDispatcher {
*/
User[] listUsers(String metalake) throws NoSuchMetalakeException;
+ /**
+ * Lists users with pagination.
+ *
+ * @param metalake The Metalake of the User.
+ * @param offset The number of users to skip.
+ * @param limit The maximum number of users to return.
+ * @return The paginated User result.
+ * @throws NoSuchMetalakeException If the Metalake with the given name does
not exist.
+ */
+ PagedResult<User> listUsers(String metalake, int offset, int limit)
+ throws NoSuchMetalakeException;
+
+ /**
+ * Counts users in a metalake.
+ *
+ * @param metalake The Metalake of the User.
+ * @return The total number of users.
+ * @throws NoSuchMetalakeException If the Metalake with the given name does
not exist.
+ */
+ long countUsers(String metalake) throws NoSuchMetalakeException;
+
/**
* Lists the usernames.
*
@@ -312,6 +333,26 @@ public interface AccessControlDispatcher {
*/
Group[] listGroups(String metalake);
+ /**
+ * Lists groups with pagination.
+ *
+ * @param metalake The Metalake of the Group.
+ * @param offset The number of groups to skip.
+ * @param limit The maximum number of groups to return.
+ * @return The paginated Group result.
+ * @throws NoSuchMetalakeException If the Metalake with the given name does
not exist.
+ */
+ PagedResult<Group> listGroups(String metalake, int offset, int limit);
+
+ /**
+ * Counts groups in a metalake.
+ *
+ * @param metalake The Metalake of the Group.
+ * @return The total number of groups.
+ * @throws NoSuchMetalakeException If the Metalake with the given name does
not exist.
+ */
+ long countGroups(String metalake);
+
/**
* List group names
*
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
index 36cd4443b9..91dc0d1781 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
@@ -157,6 +157,23 @@ public class AccessControlManager implements
AccessControlDispatcher {
() -> userGroupManager.listUsers(metalake));
}
+ @Override
+ public PagedResult<User> listUsers(String metalake, int offset, int limit)
+ throws NoSuchMetalakeException {
+ return TreeLockUtils.doWithTreeLock(
+
NameIdentifier.of(AuthorizationUtils.ofUserNamespace(metalake).levels()),
+ LockType.READ,
+ () -> userGroupManager.listUsers(metalake, offset, limit));
+ }
+
+ @Override
+ public long countUsers(String metalake) throws NoSuchMetalakeException {
+ return TreeLockUtils.doWithTreeLock(
+
NameIdentifier.of(AuthorizationUtils.ofUserNamespace(metalake).levels()),
+ LockType.READ,
+ () -> userGroupManager.countUsers(metalake));
+ }
+
@Override
public Group addGroup(String metalake, String group)
throws GroupAlreadyExistsException, NoSuchMetalakeException {
@@ -244,6 +261,22 @@ public class AccessControlManager implements
AccessControlDispatcher {
() -> userGroupManager.listGroups(metalake));
}
+ @Override
+ public PagedResult<Group> listGroups(String metalake, int offset, int limit)
{
+ return TreeLockUtils.doWithTreeLock(
+
NameIdentifier.of(AuthorizationUtils.ofGroupNamespace(metalake).levels()),
+ LockType.READ,
+ () -> userGroupManager.listGroups(metalake, offset, limit));
+ }
+
+ @Override
+ public long countGroups(String metalake) {
+ return TreeLockUtils.doWithTreeLock(
+
NameIdentifier.of(AuthorizationUtils.ofGroupNamespace(metalake).levels()),
+ LockType.READ,
+ () -> userGroupManager.countGroups(metalake));
+ }
+
@Override
public String[] listGroupNames(String metalake) throws
NoSuchMetalakeException {
return TreeLockUtils.doWithTreeLock(
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/PagedResult.java
b/core/src/main/java/org/apache/gravitino/authorization/PagedResult.java
new file mode 100644
index 0000000000..1d525eef71
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/authorization/PagedResult.java
@@ -0,0 +1,58 @@
+/*
+ * 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.authorization;
+
+import java.util.Collections;
+import java.util.List;
+
+/** A paginated query result containing the total count and a page of items. */
+public final class PagedResult<T> {
+
+ private final long totalCount;
+ private final List<T> items;
+
+ /**
+ * Creates a paginated result.
+ *
+ * @param totalCount The total number of matching items.
+ * @param items The items in the current page.
+ */
+ public PagedResult(long totalCount, List<T> items) {
+ this.totalCount = totalCount;
+ this.items = items != null ? items : Collections.emptyList();
+ }
+
+ /**
+ * Returns the total number of matching items.
+ *
+ * @return The total count.
+ */
+ public long totalCount() {
+ return totalCount;
+ }
+
+ /**
+ * Returns the items in the current page.
+ *
+ * @return The page items.
+ */
+ public List<T> items() {
+ return items;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java
b/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java
index d6dc2b0636..be0d56879e 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java
@@ -27,6 +27,7 @@ import org.apache.gravitino.Entity;
import org.apache.gravitino.Entity.EntityType;
import org.apache.gravitino.EntityAlreadyExistsException;
import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.exceptions.GroupAlreadyExistsException;
import org.apache.gravitino.exceptions.NoSuchEntityException;
@@ -37,7 +38,10 @@ import
org.apache.gravitino.exceptions.UserAlreadyExistsException;
import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.GroupEntity;
import org.apache.gravitino.meta.UserEntity;
+import org.apache.gravitino.metalake.MetalakeManager;
import org.apache.gravitino.storage.IdGenerator;
+import org.apache.gravitino.storage.relational.service.GroupMetaService;
+import org.apache.gravitino.storage.relational.service.UserMetaService;
import org.apache.gravitino.utils.PrincipalUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -121,6 +125,19 @@ class UserGroupManager {
return listUsersInternal(metalake, true /* allFields */);
}
+ long countUsers(String metalake) {
+ MetalakeManager.checkMetalake(NameIdentifier.of(metalake), store);
+ return UserMetaService.getInstance().countUsersByMetalake(metalake);
+ }
+
+ PagedResult<User> listUsers(String metalake, int offset, int limit) {
+ MetalakeManager.checkMetalake(NameIdentifier.of(metalake), store);
+ PagedResult<UserEntity> result =
+ UserMetaService.getInstance().listUsersByMetalakePaginated(metalake,
offset, limit);
+ return new PagedResult<>(
+ result.totalCount(), Arrays.asList(result.items().toArray(new
User[0])));
+ }
+
Group addGroup(String metalake, String group) throws
GroupAlreadyExistsException {
try {
GroupEntity groupEntity =
@@ -178,6 +195,19 @@ class UserGroupManager {
return listGroupInternal(metalake, true);
}
+ long countGroups(String metalake) {
+ MetalakeManager.checkMetalake(NameIdentifier.of(metalake), store);
+ return GroupMetaService.getInstance().countGroupsByMetalake(metalake);
+ }
+
+ PagedResult<Group> listGroups(String metalake, int offset, int limit) {
+ MetalakeManager.checkMetalake(NameIdentifier.of(metalake), store);
+ PagedResult<GroupEntity> result =
+ GroupMetaService.getInstance().listGroupsByMetalakePaginated(metalake,
offset, limit);
+ return new PagedResult<>(
+ result.totalCount(), Arrays.asList(result.items().toArray(new
Group[0])));
+ }
+
String[] listGroupNames(String metalake) {
return Arrays.stream(listGroupInternal(metalake, false))
.map(Group::name)
diff --git
a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
index 92c6f47f1b..3d856a61f8 100644
---
a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
@@ -31,6 +31,7 @@ import org.apache.gravitino.authorization.Group;
import org.apache.gravitino.authorization.GroupChange;
import org.apache.gravitino.authorization.Owner;
import org.apache.gravitino.authorization.OwnerDispatcher;
+import org.apache.gravitino.authorization.PagedResult;
import org.apache.gravitino.authorization.Privilege;
import org.apache.gravitino.authorization.Role;
import org.apache.gravitino.authorization.SecurableObject;
@@ -122,6 +123,17 @@ public class AccessControlHookDispatcher implements
AccessControlDispatcher {
return dispatcher.listUsers(metalake);
}
+ @Override
+ public PagedResult<User> listUsers(String metalake, int offset, int limit)
+ throws NoSuchMetalakeException {
+ return dispatcher.listUsers(metalake, offset, limit);
+ }
+
+ @Override
+ public long countUsers(String metalake) throws NoSuchMetalakeException {
+ return dispatcher.countUsers(metalake);
+ }
+
@Override
public String[] listUserNames(String metalake) throws
NoSuchMetalakeException {
return dispatcher.listUserNames(metalake);
@@ -184,6 +196,16 @@ public class AccessControlHookDispatcher implements
AccessControlDispatcher {
return dispatcher.listGroups(metalake);
}
+ @Override
+ public PagedResult<Group> listGroups(String metalake, int offset, int limit)
{
+ return dispatcher.listGroups(metalake, offset, limit);
+ }
+
+ @Override
+ public long countGroups(String metalake) {
+ return dispatcher.countGroups(metalake);
+ }
+
@Override
public String[] listGroupNames(String metalake) throws
NoSuchMetalakeException {
return dispatcher.listGroupNames(metalake);
diff --git
a/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
b/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
index db94a7e989..a0e2517b1a 100644
---
a/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
@@ -26,6 +26,7 @@ import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.authorization.AccessControlDispatcher;
import org.apache.gravitino.authorization.Group;
import org.apache.gravitino.authorization.GroupChange;
+import org.apache.gravitino.authorization.PagedResult;
import org.apache.gravitino.authorization.Privilege;
import org.apache.gravitino.authorization.Role;
import org.apache.gravitino.authorization.SecurableObject;
@@ -52,6 +53,12 @@ import
org.apache.gravitino.listener.api.event.AlterGroupPreEvent;
import org.apache.gravitino.listener.api.event.AlterUserEvent;
import org.apache.gravitino.listener.api.event.AlterUserFailureEvent;
import org.apache.gravitino.listener.api.event.AlterUserPreEvent;
+import org.apache.gravitino.listener.api.event.CountGroupsEvent;
+import org.apache.gravitino.listener.api.event.CountGroupsFailureEvent;
+import org.apache.gravitino.listener.api.event.CountGroupsPreEvent;
+import org.apache.gravitino.listener.api.event.CountUsersEvent;
+import org.apache.gravitino.listener.api.event.CountUsersFailureEvent;
+import org.apache.gravitino.listener.api.event.CountUsersPreEvent;
import org.apache.gravitino.listener.api.event.CreateRoleEvent;
import org.apache.gravitino.listener.api.event.CreateRoleFailureEvent;
import org.apache.gravitino.listener.api.event.CreateRolePreEvent;
@@ -93,6 +100,9 @@ import
org.apache.gravitino.listener.api.event.ListGroupNamesFailureEvent;
import org.apache.gravitino.listener.api.event.ListGroupNamesPreEvent;
import org.apache.gravitino.listener.api.event.ListGroupsEvent;
import org.apache.gravitino.listener.api.event.ListGroupsFailureEvent;
+import org.apache.gravitino.listener.api.event.ListGroupsPagedEvent;
+import org.apache.gravitino.listener.api.event.ListGroupsPagedFailureEvent;
+import org.apache.gravitino.listener.api.event.ListGroupsPagedPreEvent;
import org.apache.gravitino.listener.api.event.ListGroupsPreEvent;
import org.apache.gravitino.listener.api.event.ListRoleNamesEvent;
import org.apache.gravitino.listener.api.event.ListRoleNamesFailureEvent;
@@ -102,6 +112,9 @@ import
org.apache.gravitino.listener.api.event.ListUserNamesFailureEvent;
import org.apache.gravitino.listener.api.event.ListUserNamesPreEvent;
import org.apache.gravitino.listener.api.event.ListUsersEvent;
import org.apache.gravitino.listener.api.event.ListUsersFailureEvent;
+import org.apache.gravitino.listener.api.event.ListUsersPagedEvent;
+import org.apache.gravitino.listener.api.event.ListUsersPagedFailureEvent;
+import org.apache.gravitino.listener.api.event.ListUsersPagedPreEvent;
import org.apache.gravitino.listener.api.event.ListUsersPreEvent;
import org.apache.gravitino.listener.api.event.OverridePrivilegesEvent;
import org.apache.gravitino.listener.api.event.OverridePrivilegesFailureEvent;
@@ -342,6 +355,43 @@ public class AccessControlEventDispatcher implements
AccessControlDispatcher {
}
}
+ /** {@inheritDoc} */
+ @Override
+ public PagedResult<User> listUsers(String metalake, int offset, int limit)
+ throws NoSuchMetalakeException {
+ String initiator = PrincipalUtils.getCurrentUserName();
+
+ eventBus.dispatchEvent(new ListUsersPagedPreEvent(initiator, metalake,
offset, limit));
+ try {
+ PagedResult<User> users = dispatcher.listUsers(metalake, offset, limit);
+ eventBus.dispatchEvent(
+ new ListUsersPagedEvent(
+ initiator, metalake, offset, limit, users.items().size(),
users.totalCount()));
+
+ return users;
+ } catch (Exception e) {
+ eventBus.dispatchEvent(new ListUsersPagedFailureEvent(initiator,
metalake, e, offset, limit));
+ throw e;
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long countUsers(String metalake) throws NoSuchMetalakeException {
+ String initiator = PrincipalUtils.getCurrentUserName();
+
+ eventBus.dispatchEvent(new CountUsersPreEvent(initiator, metalake));
+ try {
+ long count = dispatcher.countUsers(metalake);
+ eventBus.dispatchEvent(new CountUsersEvent(initiator, metalake, count));
+
+ return count;
+ } catch (Exception e) {
+ eventBus.dispatchEvent(new CountUsersFailureEvent(initiator, metalake,
e));
+ throw e;
+ }
+ }
+
/** {@inheritDoc} */
@Override
public String[] listUserNames(String metalake) throws
NoSuchMetalakeException {
@@ -544,6 +594,43 @@ public class AccessControlEventDispatcher implements
AccessControlDispatcher {
}
}
+ /** {@inheritDoc} */
+ @Override
+ public PagedResult<Group> listGroups(String metalake, int offset, int limit)
{
+ String initiator = PrincipalUtils.getCurrentUserName();
+
+ eventBus.dispatchEvent(new ListGroupsPagedPreEvent(initiator, metalake,
offset, limit));
+ try {
+ PagedResult<Group> groups = dispatcher.listGroups(metalake, offset,
limit);
+ eventBus.dispatchEvent(
+ new ListGroupsPagedEvent(
+ initiator, metalake, offset, limit, groups.items().size(),
groups.totalCount()));
+
+ return groups;
+ } catch (Exception e) {
+ eventBus.dispatchEvent(
+ new ListGroupsPagedFailureEvent(initiator, metalake, e, offset,
limit));
+ throw e;
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long countGroups(String metalake) {
+ String initiator = PrincipalUtils.getCurrentUserName();
+
+ eventBus.dispatchEvent(new CountGroupsPreEvent(initiator, metalake));
+ try {
+ long count = dispatcher.countGroups(metalake);
+ eventBus.dispatchEvent(new CountGroupsEvent(initiator, metalake, count));
+
+ return count;
+ } catch (Exception e) {
+ eventBus.dispatchEvent(new CountGroupsFailureEvent(initiator, metalake,
e));
+ throw e;
+ }
+ }
+
/** {@inheritDoc} */
@Override
public String[] listGroupNames(String metalake) {
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/CountGroupsEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/CountGroupsEvent.java
new file mode 100644
index 0000000000..c29712df64
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/CountGroupsEvent.java
@@ -0,0 +1,55 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered after successfully counting groups in a
metalake. */
+@DeveloperApi
+public class CountGroupsEvent extends GroupEvent {
+
+ private final long count;
+
+ /**
+ * Creates a new {@link CountGroupsEvent}.
+ *
+ * @param initiator the user who initiated the request.
+ * @param metalake the metalake name.
+ * @param count the total number of groups.
+ */
+ public CountGroupsEvent(String initiator, String metalake, long count) {
+ super(initiator, NameIdentifierUtil.ofMetalake(metalake));
+ this.count = count;
+ }
+
+ /**
+ * Returns the total number of groups.
+ *
+ * @return the group count.
+ */
+ public long count() {
+ return count;
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.COUNT_GROUPS;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/CountGroupsFailureEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/CountGroupsFailureEvent.java
new file mode 100644
index 0000000000..a8d31d849d
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/CountGroupsFailureEvent.java
@@ -0,0 +1,43 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered when counting groups fails. */
+@DeveloperApi
+public class CountGroupsFailureEvent extends GroupFailureEvent {
+
+ /**
+ * Creates a new {@link CountGroupsFailureEvent}.
+ *
+ * @param initiator the user who initiated the operation.
+ * @param metalake the metalake name.
+ * @param exception the exception encountered.
+ */
+ public CountGroupsFailureEvent(String initiator, String metalake, Exception
exception) {
+ super(initiator, NameIdentifierUtil.ofMetalake(metalake), exception);
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.COUNT_GROUPS;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/CountGroupsPreEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/CountGroupsPreEvent.java
new file mode 100644
index 0000000000..888e0d6c20
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/CountGroupsPreEvent.java
@@ -0,0 +1,42 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered before counting groups in a metalake. */
+@DeveloperApi
+public class CountGroupsPreEvent extends GroupPreEvent {
+
+ /**
+ * Creates a new {@link CountGroupsPreEvent}.
+ *
+ * @param initiator the user who initiated the request.
+ * @param metalake the metalake name.
+ */
+ public CountGroupsPreEvent(String initiator, String metalake) {
+ super(initiator, NameIdentifierUtil.ofMetalake(metalake));
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.COUNT_GROUPS;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/CountUsersEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/CountUsersEvent.java
new file mode 100644
index 0000000000..f63f2cd097
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/CountUsersEvent.java
@@ -0,0 +1,55 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/** Represents an event triggered after successfully counting users in a
metalake. */
+@DeveloperApi
+public class CountUsersEvent extends UserEvent {
+
+ private final long count;
+
+ /**
+ * Creates a new {@link CountUsersEvent}.
+ *
+ * @param initiator the user who initiated the request.
+ * @param metalake the metalake name.
+ * @param count the total number of users.
+ */
+ public CountUsersEvent(String initiator, String metalake, long count) {
+ super(initiator, NameIdentifier.of(metalake));
+ this.count = count;
+ }
+
+ /**
+ * Returns the total number of users.
+ *
+ * @return the user count.
+ */
+ public long count() {
+ return count;
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.COUNT_USERS;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/CountUsersFailureEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/CountUsersFailureEvent.java
new file mode 100644
index 0000000000..6737415a71
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/CountUsersFailureEvent.java
@@ -0,0 +1,43 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered when counting users fails. */
+@DeveloperApi
+public class CountUsersFailureEvent extends UserFailureEvent {
+
+ /**
+ * Creates a new {@link CountUsersFailureEvent}.
+ *
+ * @param user the user who initiated the operation.
+ * @param metalake the metalake name.
+ * @param exception the exception encountered.
+ */
+ public CountUsersFailureEvent(String user, String metalake, Exception
exception) {
+ super(user, NameIdentifierUtil.ofMetalake(metalake), exception);
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.COUNT_USERS;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/CountUsersPreEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/CountUsersPreEvent.java
new file mode 100644
index 0000000000..e32e66a814
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/CountUsersPreEvent.java
@@ -0,0 +1,42 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/** Represents an event triggered before counting users in a metalake. */
+@DeveloperApi
+public class CountUsersPreEvent extends UserPreEvent {
+
+ /**
+ * Creates a new {@link CountUsersPreEvent}.
+ *
+ * @param initiator the user who initiated the request.
+ * @param metalake the metalake name.
+ */
+ public CountUsersPreEvent(String initiator, String metalake) {
+ super(initiator, NameIdentifier.of(metalake));
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.COUNT_USERS;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/ListGroupsPagedEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/ListGroupsPagedEvent.java
new file mode 100644
index 0000000000..36f9b71b41
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/ListGroupsPagedEvent.java
@@ -0,0 +1,88 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered after successfully listing groups with
pagination. */
+@DeveloperApi
+public class ListGroupsPagedEvent extends GroupEvent implements ListEvent {
+
+ private final int offset;
+ private final int limit;
+ private final int pageSize;
+ private final long totalCount;
+
+ /**
+ * Creates a new {@link ListGroupsPagedEvent}.
+ *
+ * @param initiator the user who initiated the request.
+ * @param metalake the metalake name.
+ * @param offset the number of groups skipped.
+ * @param limit the requested page size limit.
+ * @param pageSize the number of groups returned in this page.
+ * @param totalCount the total number of groups in the metalake.
+ */
+ public ListGroupsPagedEvent(
+ String initiator, String metalake, int offset, int limit, int pageSize,
long totalCount) {
+ super(initiator, NameIdentifierUtil.ofMetalake(metalake));
+ this.offset = offset;
+ this.limit = limit;
+ this.pageSize = pageSize;
+ this.totalCount = totalCount;
+ }
+
+ /**
+ * Returns the pagination offset.
+ *
+ * @return the offset.
+ */
+ public int offset() {
+ return offset;
+ }
+
+ /**
+ * Returns the pagination limit.
+ *
+ * @return the limit.
+ */
+ public int limit() {
+ return limit;
+ }
+
+ /**
+ * Returns the total number of groups in the metalake.
+ *
+ * @return the total count.
+ */
+ public long totalCount() {
+ return totalCount;
+ }
+
+ @Override
+ public int resultCount() {
+ return pageSize;
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.LIST_GROUPS_PAGED;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/ListGroupsPagedFailureEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/ListGroupsPagedFailureEvent.java
new file mode 100644
index 0000000000..71b3ed5e19
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/ListGroupsPagedFailureEvent.java
@@ -0,0 +1,69 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered when a paginated group list operation fails.
*/
+@DeveloperApi
+public class ListGroupsPagedFailureEvent extends GroupFailureEvent {
+
+ private final int offset;
+ private final int limit;
+
+ /**
+ * Creates a new {@link ListGroupsPagedFailureEvent}.
+ *
+ * @param initiator the user who initiated the operation.
+ * @param metalake the metalake name.
+ * @param exception the exception encountered.
+ * @param offset the pagination offset.
+ * @param limit the pagination limit.
+ */
+ public ListGroupsPagedFailureEvent(
+ String initiator, String metalake, Exception exception, int offset, int
limit) {
+ super(initiator, NameIdentifierUtil.ofMetalake(metalake), exception);
+ this.offset = offset;
+ this.limit = limit;
+ }
+
+ /**
+ * Returns the pagination offset.
+ *
+ * @return the offset.
+ */
+ public int offset() {
+ return offset;
+ }
+
+ /**
+ * Returns the pagination limit.
+ *
+ * @return the limit.
+ */
+ public int limit() {
+ return limit;
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.LIST_GROUPS_PAGED;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/ListGroupsPagedPreEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/ListGroupsPagedPreEvent.java
new file mode 100644
index 0000000000..20acd6cc8a
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/ListGroupsPagedPreEvent.java
@@ -0,0 +1,67 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered before listing groups with pagination. */
+@DeveloperApi
+public class ListGroupsPagedPreEvent extends GroupPreEvent {
+
+ private final int offset;
+ private final int limit;
+
+ /**
+ * Creates a new {@link ListGroupsPagedPreEvent}.
+ *
+ * @param initiator the user who initiated the request.
+ * @param metalake the metalake name.
+ * @param offset the number of groups to skip.
+ * @param limit the maximum number of groups to return.
+ */
+ public ListGroupsPagedPreEvent(String initiator, String metalake, int
offset, int limit) {
+ super(initiator, NameIdentifierUtil.ofMetalake(metalake));
+ this.offset = offset;
+ this.limit = limit;
+ }
+
+ /**
+ * Returns the pagination offset.
+ *
+ * @return the offset.
+ */
+ public int offset() {
+ return offset;
+ }
+
+ /**
+ * Returns the pagination limit.
+ *
+ * @return the limit.
+ */
+ public int limit() {
+ return limit;
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.LIST_GROUPS_PAGED;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/ListUsersPagedEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/ListUsersPagedEvent.java
new file mode 100644
index 0000000000..3e62eebac2
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/ListUsersPagedEvent.java
@@ -0,0 +1,88 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/** Represents an event triggered after successfully listing users with
pagination. */
+@DeveloperApi
+public class ListUsersPagedEvent extends UserEvent implements ListEvent {
+
+ private final int offset;
+ private final int limit;
+ private final int pageSize;
+ private final long totalCount;
+
+ /**
+ * Creates a new {@link ListUsersPagedEvent}.
+ *
+ * @param initiator the user who initiated the request.
+ * @param metalake the metalake name.
+ * @param offset the number of users skipped.
+ * @param limit the requested page size limit.
+ * @param pageSize the number of users returned in this page.
+ * @param totalCount the total number of users in the metalake.
+ */
+ public ListUsersPagedEvent(
+ String initiator, String metalake, int offset, int limit, int pageSize,
long totalCount) {
+ super(initiator, NameIdentifier.of(metalake));
+ this.offset = offset;
+ this.limit = limit;
+ this.pageSize = pageSize;
+ this.totalCount = totalCount;
+ }
+
+ /**
+ * Returns the pagination offset.
+ *
+ * @return the offset.
+ */
+ public int offset() {
+ return offset;
+ }
+
+ /**
+ * Returns the pagination limit.
+ *
+ * @return the limit.
+ */
+ public int limit() {
+ return limit;
+ }
+
+ /**
+ * Returns the total number of users in the metalake.
+ *
+ * @return the total count.
+ */
+ public long totalCount() {
+ return totalCount;
+ }
+
+ @Override
+ public int resultCount() {
+ return pageSize;
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.LIST_USERS_PAGED;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/ListUsersPagedFailureEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/ListUsersPagedFailureEvent.java
new file mode 100644
index 0000000000..9aa46f5584
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/ListUsersPagedFailureEvent.java
@@ -0,0 +1,69 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered when a paginated user list operation fails.
*/
+@DeveloperApi
+public class ListUsersPagedFailureEvent extends UserFailureEvent {
+
+ private final int offset;
+ private final int limit;
+
+ /**
+ * Creates a new {@link ListUsersPagedFailureEvent}.
+ *
+ * @param user the user who initiated the operation.
+ * @param metalake the metalake name.
+ * @param exception the exception encountered.
+ * @param offset the pagination offset.
+ * @param limit the pagination limit.
+ */
+ public ListUsersPagedFailureEvent(
+ String user, String metalake, Exception exception, int offset, int
limit) {
+ super(user, NameIdentifierUtil.ofMetalake(metalake), exception);
+ this.offset = offset;
+ this.limit = limit;
+ }
+
+ /**
+ * Returns the pagination offset.
+ *
+ * @return the offset.
+ */
+ public int offset() {
+ return offset;
+ }
+
+ /**
+ * Returns the pagination limit.
+ *
+ * @return the limit.
+ */
+ public int limit() {
+ return limit;
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.LIST_USERS_PAGED;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/ListUsersPagedPreEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/ListUsersPagedPreEvent.java
new file mode 100644
index 0000000000..4527e422cd
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/ListUsersPagedPreEvent.java
@@ -0,0 +1,67 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/** Represents an event triggered before listing users with pagination. */
+@DeveloperApi
+public class ListUsersPagedPreEvent extends UserPreEvent {
+
+ private final int offset;
+ private final int limit;
+
+ /**
+ * Creates a new {@link ListUsersPagedPreEvent}.
+ *
+ * @param initiator the user who initiated the request.
+ * @param metalake the metalake name.
+ * @param offset the number of users to skip.
+ * @param limit the maximum number of users to return.
+ */
+ public ListUsersPagedPreEvent(String initiator, String metalake, int offset,
int limit) {
+ super(initiator, NameIdentifier.of(metalake));
+ this.offset = offset;
+ this.limit = limit;
+ }
+
+ /**
+ * Returns the pagination offset.
+ *
+ * @return the offset.
+ */
+ public int offset() {
+ return offset;
+ }
+
+ /**
+ * Returns the pagination limit.
+ *
+ * @return the limit.
+ */
+ public int limit() {
+ return limit;
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.LIST_USERS_PAGED;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
index c86c58e1b0..5350c18562 100644
---
a/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
@@ -137,6 +137,8 @@ public enum OperationType {
ENABLE_USER,
DISABLE_USER,
LIST_USERS,
+ LIST_USERS_PAGED,
+ COUNT_USERS,
LIST_USER_NAMES,
GRANT_USER_ROLES,
REVOKE_USER_ROLES,
@@ -151,6 +153,8 @@ public enum OperationType {
REMOVE_GROUP_BY_ID,
ALTER_GROUP,
LIST_GROUPS,
+ LIST_GROUPS_PAGED,
+ COUNT_GROUPS,
LIST_GROUP_NAMES,
GRANT_GROUP_ROLES,
REVOKE_GROUP_ROLES,
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java
index b74116fb24..8c30152d25 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java
@@ -67,6 +67,19 @@ public interface GroupMetaMapper {
method = "listExtendedGroupPOsByMetalakeId")
List<ExtendedGroupPO> listExtendedGroupPOsByMetalakeId(@Param("metalakeId")
Long metalakeId);
+ @SelectProvider(
+ type = GroupMetaSQLProviderFactory.class,
+ method = "countGroupMetasByMetalakeName")
+ Long countGroupMetasByMetalakeName(@Param("metalakeName") String
metalakeName);
+
+ @SelectProvider(
+ type = GroupMetaSQLProviderFactory.class,
+ method = "listExtendedGroupPOsByMetalakeNamePaginated")
+ List<ExtendedGroupPO> listExtendedGroupPOsByMetalakeNamePaginated(
+ @Param("metalakeName") String metalakeName,
+ @Param("offset") int offset,
+ @Param("limit") int limit);
+
@InsertProvider(type = GroupMetaSQLProviderFactory.class, method =
"insertGroupMeta")
void insertGroupMeta(@Param("groupMeta") GroupPO groupPO);
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java
index 0981d4b401..9fa4eece50 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java
@@ -97,6 +97,17 @@ public class GroupMetaSQLProviderFactory {
return getProvider().listExtendedGroupPOsByMetalakeId(metalakeId);
}
+ public static String countGroupMetasByMetalakeName(@Param("metalakeName")
String metalakeName) {
+ return getProvider().countGroupMetasByMetalakeName(metalakeName);
+ }
+
+ public static String listExtendedGroupPOsByMetalakeNamePaginated(
+ @Param("metalakeName") String metalakeName,
+ @Param("offset") int offset,
+ @Param("limit") int limit) {
+ return
getProvider().listExtendedGroupPOsByMetalakeNamePaginated(metalakeName, offset,
limit);
+ }
+
public static String deleteGroupMetasByLegacyTimeline(
@Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit)
{
return getProvider().deleteGroupMetasByLegacyTimeline(legacyTimeline,
limit);
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
index 45bcb63c5d..ff503326bb 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
@@ -65,6 +65,17 @@ public interface UserMetaMapper {
method = "listExtendedUserPOsByMetalakeId")
List<ExtendedUserPO> listExtendedUserPOsByMetalakeId(@Param("metalakeId")
Long metalakeId);
+ @SelectProvider(type = UserMetaSQLProviderFactory.class, method =
"countUserMetasByMetalakeName")
+ Long countUserMetasByMetalakeName(@Param("metalakeName") String
metalakeName);
+
+ @SelectProvider(
+ type = UserMetaSQLProviderFactory.class,
+ method = "listExtendedUserPOsByMetalakeNamePaginated")
+ List<ExtendedUserPO> listExtendedUserPOsByMetalakeNamePaginated(
+ @Param("metalakeName") String metalakeName,
+ @Param("offset") int offset,
+ @Param("limit") int limit);
+
@InsertProvider(
type = UserMetaSQLProviderFactory.class,
method = "insertUserMetaOnDuplicateKeyUpdate")
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
index 9d668dd2f3..57b4afa3cb 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
@@ -94,6 +94,17 @@ public class UserMetaSQLProviderFactory {
return getProvider().listExtendedUserPOsByMetalakeId(metalakeId);
}
+ public static String countUserMetasByMetalakeName(@Param("metalakeName")
String metalakeName) {
+ return getProvider().countUserMetasByMetalakeName(metalakeName);
+ }
+
+ public static String listExtendedUserPOsByMetalakeNamePaginated(
+ @Param("metalakeName") String metalakeName,
+ @Param("offset") int offset,
+ @Param("limit") int limit) {
+ return
getProvider().listExtendedUserPOsByMetalakeNamePaginated(metalakeName, offset,
limit);
+ }
+
public static String deleteUserMetasByLegacyTimeline(
@Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit)
{
return getProvider().deleteUserMetasByLegacyTimeline(legacyTimeline,
limit);
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java
index fa9085a429..852e48cc42 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java
@@ -76,6 +76,55 @@ public class GroupMetaBaseSQLProvider {
+ " GROUP BY gt.group_id";
}
+ public String countGroupMetasByMetalakeName(@Param("metalakeName") String
metalakeName) {
+ return "SELECT COUNT(*) FROM "
+ + GROUP_TABLE_NAME
+ + " gt JOIN "
+ + MetalakeMetaMapper.TABLE_NAME
+ + " mt ON gt.metalake_id = mt.metalake_id"
+ + " WHERE mt.metalake_name = #{metalakeName}"
+ + " AND gt.deleted_at = 0 AND mt.deleted_at = 0";
+ }
+
+ public String listExtendedGroupPOsByMetalakeNamePaginated(
+ @Param("metalakeName") String metalakeName,
+ @Param("offset") int offset,
+ @Param("limit") int limit) {
+ return "SELECT gt.group_id as groupId, gt.group_name as groupName,"
+ + " gt.metalake_id as metalakeId,"
+ + " gt.external_id as externalId,"
+ + " gt.audit_info as auditInfo,"
+ + " gt.current_version as currentVersion, gt.last_version as
lastVersion,"
+ + " gt.deleted_at as deletedAt,"
+ + " JSON_ARRAYAGG(rot.role_name) as roleNames,"
+ + " JSON_ARRAYAGG(rot.role_id) as roleIds"
+ + " FROM ("
+ + " SELECT gt.group_id FROM "
+ + GROUP_TABLE_NAME
+ + " gt JOIN "
+ + MetalakeMetaMapper.TABLE_NAME
+ + " mt ON gt.metalake_id = mt.metalake_id"
+ + " WHERE mt.metalake_name = #{metalakeName}"
+ + " AND gt.deleted_at = 0 AND mt.deleted_at = 0"
+ + " ORDER BY gt.group_id ASC LIMIT #{limit} OFFSET #{offset}"
+ + " ) paginated"
+ + " JOIN "
+ + GROUP_TABLE_NAME
+ + " gt ON gt.group_id = paginated.group_id"
+ + " LEFT OUTER JOIN ("
+ + " SELECT * FROM "
+ + GROUP_ROLE_RELATION_TABLE_NAME
+ + " WHERE deleted_at = 0)"
+ + " AS rt ON rt.group_id = gt.group_id"
+ + " LEFT OUTER JOIN ("
+ + " SELECT * FROM "
+ + ROLE_TABLE_NAME
+ + " WHERE deleted_at = 0)"
+ + " AS rot ON rot.role_id = rt.role_id"
+ + " GROUP BY gt.group_id"
+ + " ORDER BY gt.group_id ASC";
+ }
+
public String selectGroupMetaByMetalakeIdAndName(
@Param("metalakeId") Long metalakeId, @Param("groupName") String name) {
return "SELECT group_id as groupId, group_name as groupName,"
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/UserMetaBaseSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/UserMetaBaseSQLProvider.java
index c0647d3855..387c5676e0 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/UserMetaBaseSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/UserMetaBaseSQLProvider.java
@@ -247,6 +247,55 @@ public class UserMetaBaseSQLProvider {
+ " GROUP BY ut.user_id";
}
+ public String countUserMetasByMetalakeName(@Param("metalakeName") String
metalakeName) {
+ return "SELECT COUNT(*) FROM "
+ + USER_TABLE_NAME
+ + " ut JOIN "
+ + MetalakeMetaMapper.TABLE_NAME
+ + " mt ON ut.metalake_id = mt.metalake_id"
+ + " WHERE mt.metalake_name = #{metalakeName}"
+ + " AND ut.deleted_at = 0 AND mt.deleted_at = 0";
+ }
+
+ public String listExtendedUserPOsByMetalakeNamePaginated(
+ @Param("metalakeName") String metalakeName,
+ @Param("offset") int offset,
+ @Param("limit") int limit) {
+ return "SELECT ut.user_id as userId, ut.user_name as userName,"
+ + " ut.metalake_id as metalakeId,"
+ + " ut.external_id as externalId, ut.enabled as enabled,"
+ + " ut.audit_info as auditInfo,"
+ + " ut.current_version as currentVersion, ut.last_version as
lastVersion,"
+ + " ut.deleted_at as deletedAt,"
+ + " JSON_ARRAYAGG(rot.role_name) as roleNames,"
+ + " JSON_ARRAYAGG(rot.role_id) as roleIds"
+ + " FROM ("
+ + " SELECT ut.user_id FROM "
+ + USER_TABLE_NAME
+ + " ut JOIN "
+ + MetalakeMetaMapper.TABLE_NAME
+ + " mt ON ut.metalake_id = mt.metalake_id"
+ + " WHERE mt.metalake_name = #{metalakeName}"
+ + " AND ut.deleted_at = 0 AND mt.deleted_at = 0"
+ + " ORDER BY ut.user_id ASC LIMIT #{limit} OFFSET #{offset}"
+ + " ) paginated"
+ + " JOIN "
+ + USER_TABLE_NAME
+ + " ut ON ut.user_id = paginated.user_id"
+ + " LEFT OUTER JOIN ("
+ + " SELECT * FROM "
+ + USER_ROLE_RELATION_TABLE_NAME
+ + " WHERE deleted_at = 0)"
+ + " AS rt ON rt.user_id = ut.user_id"
+ + " LEFT OUTER JOIN ("
+ + " SELECT * FROM "
+ + ROLE_TABLE_NAME
+ + " WHERE deleted_at = 0)"
+ + " AS rot ON rot.role_id = rt.role_id"
+ + " GROUP BY ut.user_id"
+ + " ORDER BY ut.user_id ASC";
+ }
+
public String deleteUserMetasByLegacyTimeline(
@Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit)
{
return "DELETE FROM "
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/GroupMetaH2Provider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/GroupMetaH2Provider.java
index 6f0920de42..950a8a0125 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/GroupMetaH2Provider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/GroupMetaH2Provider.java
@@ -23,6 +23,7 @@ import static
org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.GROU
import static
org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.ROLE_TABLE_NAME;
import java.util.List;
+import org.apache.gravitino.storage.relational.mapper.MetalakeMetaMapper;
import
org.apache.gravitino.storage.relational.mapper.provider.base.GroupMetaBaseSQLProvider;
import org.apache.ibatis.annotations.Param;
@@ -35,20 +36,8 @@ public class GroupMetaH2Provider extends
GroupMetaBaseSQLProvider {
+ " gt.audit_info as auditInfo,"
+ " gt.current_version as currentVersion, gt.last_version as
lastVersion,"
+ " gt.deleted_at as deletedAt,"
- + " '[' || COALESCE(GROUP_CONCAT( "
- + " CASE "
- + " WHEN rot.role_name IS NOT NULL AND rot.role_name <> '' "
- + " THEN '\"' || rot.role_name || '\"' "
- + " ELSE NULL "
- + " END "
- + " ), '') || ']' as roleNames, "
- + " '[' || COALESCE(GROUP_CONCAT( "
- + " CASE "
- + " WHEN rot.role_id IS NOT NULL "
- + " THEN '\"' || rot.role_id || '\"' "
- + " ELSE NULL "
- + " END "
- + " ), '') || ']' as roleIds "
+ + " JSON_ARRAYAGG(rot.role_name) as roleNames,"
+ + " JSON_ARRAYAGG(rot.role_id) as roleIds"
+ " FROM "
+ GROUP_TABLE_NAME
+ " gt LEFT OUTER JOIN ("
@@ -67,6 +56,46 @@ public class GroupMetaH2Provider extends
GroupMetaBaseSQLProvider {
+ " GROUP BY gt.group_id";
}
+ @Override
+ public String listExtendedGroupPOsByMetalakeNamePaginated(
+ @Param("metalakeName") String metalakeName,
+ @Param("offset") int offset,
+ @Param("limit") int limit) {
+ return "SELECT gt.group_id as groupId, gt.group_name as groupName,"
+ + " gt.metalake_id as metalakeId,"
+ + " gt.external_id as externalId,"
+ + " gt.audit_info as auditInfo,"
+ + " gt.current_version as currentVersion, gt.last_version as
lastVersion,"
+ + " gt.deleted_at as deletedAt,"
+ + " JSON_ARRAYAGG(rot.role_name) as roleNames,"
+ + " JSON_ARRAYAGG(rot.role_id) as roleIds"
+ + " FROM ("
+ + " SELECT gt.group_id FROM "
+ + GROUP_TABLE_NAME
+ + " gt JOIN "
+ + MetalakeMetaMapper.TABLE_NAME
+ + " mt ON gt.metalake_id = mt.metalake_id"
+ + " WHERE mt.metalake_name = #{metalakeName}"
+ + " AND gt.deleted_at = 0 AND mt.deleted_at = 0"
+ + " ORDER BY gt.group_id ASC LIMIT #{limit} OFFSET #{offset}"
+ + " ) paginated"
+ + " JOIN "
+ + GROUP_TABLE_NAME
+ + " gt ON gt.group_id = paginated.group_id"
+ + " LEFT OUTER JOIN ("
+ + " SELECT * FROM "
+ + GROUP_ROLE_RELATION_TABLE_NAME
+ + " WHERE deleted_at = 0)"
+ + " AS rt ON rt.group_id = gt.group_id"
+ + " LEFT OUTER JOIN ("
+ + " SELECT * FROM "
+ + ROLE_TABLE_NAME
+ + " WHERE deleted_at = 0)"
+ + " AS rot ON rot.role_id = rt.role_id"
+ + " GROUP BY gt.group_id"
+ + " ORDER BY gt.group_id ASC";
+ }
+
@Override
public String listExtendedGroupPOsByMetalakeIdAndNames(
@Param("metalakeId") Long metalakeId, @Param("groupNames") List<String>
groupNames) {
@@ -77,20 +106,8 @@ public class GroupMetaH2Provider extends
GroupMetaBaseSQLProvider {
+ " gt.audit_info as auditInfo,"
+ " gt.current_version as currentVersion, gt.last_version as
lastVersion,"
+ " gt.deleted_at as deletedAt,"
- + " '[' || COALESCE(GROUP_CONCAT( "
- + " CASE "
- + " WHEN rot.role_name IS NOT NULL AND rot.role_name <>
'' "
- + " THEN '\"' || rot.role_name || '\"' "
- + " ELSE NULL "
- + " END "
- + " ), '') || ']' as roleNames, "
- + " '[' || COALESCE(GROUP_CONCAT( "
- + " CASE "
- + " WHEN rot.role_id IS NOT NULL "
- + " THEN '\"' || rot.role_id || '\"' "
- + " ELSE NULL "
- + " END "
- + " ), '') || ']' as roleIds "
+ + " JSON_ARRAYAGG(rot.role_name) as roleNames,"
+ + " JSON_ARRAYAGG(rot.role_id) as roleIds"
+ " FROM "
+ GROUP_TABLE_NAME
+ " gt LEFT OUTER JOIN ("
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/UserMetaH2Provider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/UserMetaH2Provider.java
index 894babf2db..4f85335ab4 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/UserMetaH2Provider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/UserMetaH2Provider.java
@@ -22,6 +22,7 @@ import static
org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.ROLE
import static
org.apache.gravitino.storage.relational.mapper.UserMetaMapper.USER_ROLE_RELATION_TABLE_NAME;
import static
org.apache.gravitino.storage.relational.mapper.UserRoleRelMapper.USER_TABLE_NAME;
+import org.apache.gravitino.storage.relational.mapper.MetalakeMetaMapper;
import
org.apache.gravitino.storage.relational.mapper.provider.base.UserMetaBaseSQLProvider;
import org.apache.ibatis.annotations.Param;
@@ -34,8 +35,8 @@ public class UserMetaH2Provider extends
UserMetaBaseSQLProvider {
+ " ut.audit_info as auditInfo,"
+ " ut.current_version as currentVersion, ut.last_version as
lastVersion,"
+ " ut.deleted_at as deletedAt,"
- + " '[' || GROUP_CONCAT('\"' || rot.role_name || '\"') || ']' as
roleNames,"
- + " '[' || GROUP_CONCAT('\"' || rot.role_id || '\"') || ']' as roleIds"
+ + " JSON_ARRAYAGG(rot.role_name) as roleNames,"
+ + " JSON_ARRAYAGG(rot.role_id) as roleIds"
+ " FROM "
+ USER_TABLE_NAME
+ " ut LEFT OUTER JOIN ("
@@ -53,4 +54,44 @@ public class UserMetaH2Provider extends
UserMetaBaseSQLProvider {
+ " ut.metalake_id = #{metalakeId}"
+ " GROUP BY ut.user_id";
}
+
+ @Override
+ public String listExtendedUserPOsByMetalakeNamePaginated(
+ @Param("metalakeName") String metalakeName,
+ @Param("offset") int offset,
+ @Param("limit") int limit) {
+ return "SELECT ut.user_id as userId, ut.user_name as userName,"
+ + " ut.metalake_id as metalakeId,"
+ + " ut.external_id as externalId, ut.enabled as enabled,"
+ + " ut.audit_info as auditInfo,"
+ + " ut.current_version as currentVersion, ut.last_version as
lastVersion,"
+ + " ut.deleted_at as deletedAt,"
+ + " JSON_ARRAYAGG(rot.role_name) as roleNames,"
+ + " JSON_ARRAYAGG(rot.role_id) as roleIds"
+ + " FROM ("
+ + " SELECT ut.user_id FROM "
+ + USER_TABLE_NAME
+ + " ut JOIN "
+ + MetalakeMetaMapper.TABLE_NAME
+ + " mt ON ut.metalake_id = mt.metalake_id"
+ + " WHERE mt.metalake_name = #{metalakeName}"
+ + " AND ut.deleted_at = 0 AND mt.deleted_at = 0"
+ + " ORDER BY ut.user_id ASC LIMIT #{limit} OFFSET #{offset}"
+ + " ) paginated"
+ + " JOIN "
+ + USER_TABLE_NAME
+ + " ut ON ut.user_id = paginated.user_id"
+ + " LEFT OUTER JOIN ("
+ + " SELECT * FROM "
+ + USER_ROLE_RELATION_TABLE_NAME
+ + " WHERE deleted_at = 0)"
+ + " AS rt ON rt.user_id = ut.user_id"
+ + " LEFT OUTER JOIN ("
+ + " SELECT * FROM "
+ + ROLE_TABLE_NAME
+ + " WHERE deleted_at = 0)"
+ + " AS rot ON rot.role_id = rt.role_id"
+ + " GROUP BY ut.user_id"
+ + " ORDER BY ut.user_id ASC";
+ }
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/GroupMetaPostgreSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/GroupMetaPostgreSQLProvider.java
index 4f617f98b5..450e26cef2 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/GroupMetaPostgreSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/GroupMetaPostgreSQLProvider.java
@@ -23,6 +23,7 @@ import static
org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.GROU
import static
org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.ROLE_TABLE_NAME;
import java.util.List;
+import org.apache.gravitino.storage.relational.mapper.MetalakeMetaMapper;
import
org.apache.gravitino.storage.relational.mapper.provider.base.GroupMetaBaseSQLProvider;
import org.apache.gravitino.storage.relational.po.GroupPO;
import org.apache.ibatis.annotations.Param;
@@ -99,6 +100,46 @@ public class GroupMetaPostgreSQLProvider extends
GroupMetaBaseSQLProvider {
+ " GROUP BY gt.group_id";
}
+ @Override
+ public String listExtendedGroupPOsByMetalakeNamePaginated(
+ @Param("metalakeName") String metalakeName,
+ @Param("offset") int offset,
+ @Param("limit") int limit) {
+ return "SELECT gt.group_id as groupId, gt.group_name as groupName,"
+ + " gt.metalake_id as metalakeId,"
+ + " gt.external_id as externalId,"
+ + " gt.audit_info as auditInfo,"
+ + " gt.current_version as currentVersion, gt.last_version as
lastVersion,"
+ + " gt.deleted_at as deletedAt,"
+ + " JSON_AGG(rot.role_name) as roleNames,"
+ + " JSON_AGG(rot.role_id) as roleIds"
+ + " FROM ("
+ + " SELECT gt.group_id FROM "
+ + GROUP_TABLE_NAME
+ + " gt JOIN "
+ + MetalakeMetaMapper.TABLE_NAME
+ + " mt ON gt.metalake_id = mt.metalake_id"
+ + " WHERE mt.metalake_name = #{metalakeName}"
+ + " AND gt.deleted_at = 0 AND mt.deleted_at = 0"
+ + " ORDER BY gt.group_id ASC LIMIT #{limit} OFFSET #{offset}"
+ + " ) paginated"
+ + " JOIN "
+ + GROUP_TABLE_NAME
+ + " gt ON gt.group_id = paginated.group_id"
+ + " LEFT OUTER JOIN ("
+ + " SELECT * FROM "
+ + GROUP_ROLE_RELATION_TABLE_NAME
+ + " WHERE deleted_at = 0)"
+ + " AS rt ON rt.group_id = gt.group_id"
+ + " LEFT OUTER JOIN ("
+ + " SELECT * FROM "
+ + ROLE_TABLE_NAME
+ + " WHERE deleted_at = 0)"
+ + " AS rot ON rot.role_id = rt.role_id"
+ + " GROUP BY gt.group_id"
+ + " ORDER BY gt.group_id ASC";
+ }
+
@Override
public String listExtendedGroupPOsByMetalakeIdAndNames(
@Param("metalakeId") Long metalakeId, @Param("groupNames") List<String>
groupNames) {
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/UserMetaPostgreSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/UserMetaPostgreSQLProvider.java
index 2305535b1d..a454efa6c6 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/UserMetaPostgreSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/UserMetaPostgreSQLProvider.java
@@ -22,6 +22,7 @@ import static
org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.ROLE
import static
org.apache.gravitino.storage.relational.mapper.UserMetaMapper.USER_ROLE_RELATION_TABLE_NAME;
import static
org.apache.gravitino.storage.relational.mapper.UserRoleRelMapper.USER_TABLE_NAME;
+import org.apache.gravitino.storage.relational.mapper.MetalakeMetaMapper;
import
org.apache.gravitino.storage.relational.mapper.provider.base.UserMetaBaseSQLProvider;
import org.apache.gravitino.storage.relational.po.UserPO;
import org.apache.ibatis.annotations.Param;
@@ -100,6 +101,46 @@ public class UserMetaPostgreSQLProvider extends
UserMetaBaseSQLProvider {
+ " GROUP BY ut.user_id";
}
+ @Override
+ public String listExtendedUserPOsByMetalakeNamePaginated(
+ @Param("metalakeName") String metalakeName,
+ @Param("offset") int offset,
+ @Param("limit") int limit) {
+ return "SELECT ut.user_id as userId, ut.user_name as userName,"
+ + " ut.metalake_id as metalakeId,"
+ + " ut.external_id as externalId, ut.enabled as enabled,"
+ + " ut.audit_info as auditInfo,"
+ + " ut.current_version as currentVersion, ut.last_version as
lastVersion,"
+ + " ut.deleted_at as deletedAt,"
+ + " JSON_AGG(rot.role_name) as roleNames,"
+ + " JSON_AGG(rot.role_id) as roleIds"
+ + " FROM ("
+ + " SELECT ut.user_id FROM "
+ + USER_TABLE_NAME
+ + " ut JOIN "
+ + MetalakeMetaMapper.TABLE_NAME
+ + " mt ON ut.metalake_id = mt.metalake_id"
+ + " WHERE mt.metalake_name = #{metalakeName}"
+ + " AND ut.deleted_at = 0 AND mt.deleted_at = 0"
+ + " ORDER BY ut.user_id ASC LIMIT #{limit} OFFSET #{offset}"
+ + " ) paginated"
+ + " JOIN "
+ + USER_TABLE_NAME
+ + " ut ON ut.user_id = paginated.user_id"
+ + " LEFT OUTER JOIN ("
+ + " SELECT * FROM "
+ + USER_ROLE_RELATION_TABLE_NAME
+ + " WHERE deleted_at = 0)"
+ + " AS rt ON rt.user_id = ut.user_id"
+ + " LEFT OUTER JOIN ("
+ + " SELECT * FROM "
+ + ROLE_TABLE_NAME
+ + " WHERE deleted_at = 0)"
+ + " AS rot ON rot.role_id = rt.role_id"
+ + " GROUP BY ut.user_id"
+ + " ORDER BY ut.user_id ASC";
+ }
+
@Override
public String deleteUserMetasByLegacyTimeline(
@Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit)
{
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java
index 50b2003862..1229fc0c49 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java
@@ -36,6 +36,7 @@ import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.authorization.PagedResult;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.meta.GroupEntity;
import org.apache.gravitino.meta.RoleEntity;
@@ -484,4 +485,42 @@ public class GroupMetaService {
groupId, Entity.EntityType.GROUP.name())));
return true;
}
+
+ @Monitored(
+ metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
+ baseMetricName = "countGroupsByMetalake")
+ public long countGroupsByMetalake(String metalakeName) {
+ Long count =
+ SessionUtils.getWithoutCommit(
+ GroupMetaMapper.class, mapper ->
mapper.countGroupMetasByMetalakeName(metalakeName));
+ return count == null ? 0L : count;
+ }
+
+ @Monitored(
+ metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
+ baseMetricName = "listGroupsByMetalakePaginated")
+ public PagedResult<GroupEntity> listGroupsByMetalakePaginated(
+ String metalakeName, int offset, int limit) {
+ Preconditions.checkArgument(offset >= 0, "offset must be >= 0");
+ Preconditions.checkArgument(limit >= 0, "limit must be >= 0");
+
+ long totalCount = countGroupsByMetalake(metalakeName);
+ if (limit == 0 || offset >= totalCount) {
+ return new PagedResult<>(totalCount, Collections.emptyList());
+ }
+
+ List<ExtendedGroupPO> groupPOs =
+ SessionUtils.getWithoutCommit(
+ GroupMetaMapper.class,
+ mapper ->
+
mapper.listExtendedGroupPOsByMetalakeNamePaginated(metalakeName, offset,
limit));
+ List<GroupEntity> groups =
+ groupPOs.stream()
+ .map(
+ po ->
+ POConverters.fromExtendedGroupPO(
+ po, AuthorizationUtils.ofGroupNamespace(metalakeName)))
+ .collect(Collectors.toList());
+ return new PagedResult<>(totalCount, groups);
+ }
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
index 0fbdab8ca2..3eacd9850f 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
@@ -36,6 +36,7 @@ import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.authorization.PagedResult;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.meta.RoleEntity;
import org.apache.gravitino.meta.UserEntity;
@@ -472,4 +473,42 @@ public class UserMetaService {
userId, Entity.EntityType.USER.name())));
return true;
}
+
+ @Monitored(
+ metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
+ baseMetricName = "countUsersByMetalake")
+ public long countUsersByMetalake(String metalakeName) {
+ Long count =
+ SessionUtils.getWithoutCommit(
+ UserMetaMapper.class, mapper ->
mapper.countUserMetasByMetalakeName(metalakeName));
+ return count == null ? 0L : count;
+ }
+
+ @Monitored(
+ metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
+ baseMetricName = "listUsersByMetalakePaginated")
+ public PagedResult<UserEntity> listUsersByMetalakePaginated(
+ String metalakeName, int offset, int limit) {
+ Preconditions.checkArgument(offset >= 0, "offset must be >= 0");
+ Preconditions.checkArgument(limit >= 0, "limit must be >= 0");
+
+ long totalCount = countUsersByMetalake(metalakeName);
+ if (limit == 0 || offset >= totalCount) {
+ return new PagedResult<>(totalCount, Collections.emptyList());
+ }
+
+ List<ExtendedUserPO> userPOs =
+ SessionUtils.getWithoutCommit(
+ UserMetaMapper.class,
+ mapper ->
+
mapper.listExtendedUserPOsByMetalakeNamePaginated(metalakeName, offset, limit));
+ List<UserEntity> users =
+ userPOs.stream()
+ .map(
+ po ->
+ POConverters.fromExtendedUserPO(
+ po, AuthorizationUtils.ofUserNamespace(metalakeName)))
+ .collect(Collectors.toList());
+ return new PagedResult<>(totalCount, users);
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
b/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
index 9e00a0b04e..94840fed0f 100644
---
a/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
+++
b/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
@@ -134,6 +134,8 @@ public class TestCompatibilityUtils {
{OperationType.ENABLE_USER, Operation.ENABLE_USER},
{OperationType.DISABLE_USER, Operation.DISABLE_USER},
{OperationType.LIST_USERS, Operation.LIST_USERS},
+ {OperationType.LIST_USERS_PAGED, Operation.LIST_USERS},
+ {OperationType.COUNT_USERS, Operation.LIST_USERS},
{OperationType.LIST_USER_NAMES, Operation.LIST_USER_NAMES},
{OperationType.GRANT_USER_ROLES, Operation.GRANT_USER_ROLES},
{OperationType.REVOKE_USER_ROLES, Operation.REVOKE_USER_ROLES},
@@ -146,6 +148,8 @@ public class TestCompatibilityUtils {
{OperationType.REMOVE_GROUP_BY_ID, Operation.REMOVE_GROUP_BY_ID},
{OperationType.ALTER_GROUP, Operation.ALTER_GROUP},
{OperationType.LIST_GROUPS, Operation.LIST_GROUPS},
+ {OperationType.LIST_GROUPS_PAGED, Operation.LIST_GROUPS},
+ {OperationType.COUNT_GROUPS, Operation.LIST_GROUPS},
{OperationType.LIST_GROUP_NAMES, Operation.LIST_GROUP_NAMES},
{OperationType.GRANT_GROUP_ROLES, Operation.GRANT_GROUP_ROLES},
{OperationType.REVOKE_GROUP_ROLES, Operation.REVOKE_GROUP_ROLES},
diff --git
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
index 182a184afd..fbc54b2903 100644
---
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
+++
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
@@ -70,6 +70,7 @@ import org.apache.gravitino.connector.BaseCatalog;
import org.apache.gravitino.connector.authorization.AuthorizationPlugin;
import org.apache.gravitino.exceptions.GroupAlreadyExistsException;
import org.apache.gravitino.exceptions.NoSuchGroupException;
+import org.apache.gravitino.exceptions.NoSuchMetalakeException;
import org.apache.gravitino.exceptions.NoSuchRoleException;
import org.apache.gravitino.exceptions.NoSuchUserException;
import org.apache.gravitino.exceptions.RoleAlreadyExistsException;
@@ -622,6 +623,67 @@ public class TestAccessControlManager {
accessControlManager.removeGroup(METALAKE, group);
}
+ @Test
+ public void testUserPagination() {
+ long beforeCount = accessControlManager.countUsers(METALAKE);
+ for (int i = 0; i < 5; i++) {
+ accessControlManager.addUser(METALAKE, "page_user_" + i);
+ }
+
+ Assertions.assertEquals(beforeCount + 5,
accessControlManager.countUsers(METALAKE));
+
+ PagedResult<User> page = accessControlManager.listUsers(METALAKE, (int)
beforeCount, 2);
+ Assertions.assertEquals(beforeCount + 5, page.totalCount());
+ Assertions.assertEquals(2, page.items().size());
+
+ // Repeated call with the same offset/limit must be stable.
+ PagedResult<User> pageAgain = accessControlManager.listUsers(METALAKE,
(int) beforeCount, 2);
+ Assertions.assertEquals(page.items().get(0).name(),
pageAgain.items().get(0).name());
+ Assertions.assertEquals(page.items().get(1).name(),
pageAgain.items().get(1).name());
+
+ PagedResult<User> lastPage =
+ accessControlManager.listUsers(METALAKE, (int) beforeCount + 4, 10);
+ Assertions.assertEquals(beforeCount + 5, lastPage.totalCount());
+ Assertions.assertEquals(1, lastPage.items().size());
+
+ for (int i = 0; i < 5; i++) {
+ accessControlManager.removeUser(METALAKE, "page_user_" + i);
+ }
+
+ Assertions.assertThrows(
+ NoSuchMetalakeException.class, () ->
accessControlManager.countUsers("no_such_metalake"));
+ Assertions.assertThrows(
+ NoSuchMetalakeException.class,
+ () -> accessControlManager.listUsers("no_such_metalake", 0, 10));
+ }
+
+ @Test
+ public void testGroupPagination() {
+ long beforeCount = accessControlManager.countGroups(METALAKE);
+ for (int i = 0; i < 3; i++) {
+ accessControlManager.addGroup(METALAKE, "page_group_" + i);
+ }
+ Assertions.assertEquals(beforeCount + 3,
accessControlManager.countGroups(METALAKE));
+
+ PagedResult<Group> page = accessControlManager.listGroups(METALAKE, (int)
beforeCount, 2);
+ Assertions.assertEquals(beforeCount + 3, page.totalCount());
+ Assertions.assertEquals(2, page.items().size());
+
+ PagedResult<Group> pageAgain = accessControlManager.listGroups(METALAKE,
(int) beforeCount, 2);
+ Assertions.assertEquals(page.items().get(0).name(),
pageAgain.items().get(0).name());
+ Assertions.assertEquals(page.items().get(1).name(),
pageAgain.items().get(1).name());
+
+ for (int i = 0; i < 3; i++) {
+ accessControlManager.removeGroup(METALAKE, "page_group_" + i);
+ }
+
+ Assertions.assertThrows(
+ NoSuchMetalakeException.class, () ->
accessControlManager.countGroups("no_such_metalake"));
+ Assertions.assertThrows(
+ NoSuchMetalakeException.class,
+ () -> accessControlManager.listGroups("no_such_metalake", 0, 10));
+ }
+
private void createCatalogRole(String role) {
accessControlManager.createRole(
METALAKE,
diff --git
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestGroupEvent.java
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestGroupEvent.java
index ab4e0ff146..5bd7a70662 100644
---
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestGroupEvent.java
+++
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestGroupEvent.java
@@ -25,6 +25,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -34,6 +35,7 @@ import
org.apache.gravitino.authorization.AccessControlDispatcher;
import org.apache.gravitino.authorization.AuthorizationUtils;
import org.apache.gravitino.authorization.Group;
import org.apache.gravitino.authorization.GroupChange;
+import org.apache.gravitino.authorization.PagedResult;
import org.apache.gravitino.exceptions.GravitinoRuntimeException;
import org.apache.gravitino.exceptions.NoSuchGroupException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
@@ -264,6 +266,90 @@ public class TestGroupEvent {
Assertions.assertEquals(identifier, listGroupsFailureEvent.identifier());
}
+ @Test
+ void testListGroupsPagedPreEvent() {
+ dispatcher.listGroups(METALAKE, 0, 10);
+
+ PreEvent preEvent = dummyEventListener.popPreEvent();
+ Assertions.assertEquals(ListGroupsPagedPreEvent.class,
preEvent.getClass());
+ Assertions.assertEquals(OperationStatus.UNPROCESSED,
preEvent.operationStatus());
+ Assertions.assertEquals(OperationType.LIST_GROUPS_PAGED,
preEvent.operationType());
+
+ ListGroupsPagedPreEvent pagedPreEvent = (ListGroupsPagedPreEvent) preEvent;
+ Assertions.assertEquals(NameIdentifier.of(METALAKE),
pagedPreEvent.identifier());
+ Assertions.assertEquals(0, pagedPreEvent.offset());
+ Assertions.assertEquals(10, pagedPreEvent.limit());
+ }
+
+ @Test
+ void testListGroupsPagedEvent() {
+ dispatcher.listGroups(METALAKE, 0, 10);
+
+ Event event = dummyEventListener.popPostEvent();
+ Assertions.assertEquals(ListGroupsPagedEvent.class, event.getClass());
+ Assertions.assertEquals(OperationStatus.SUCCESS, event.operationStatus());
+ Assertions.assertEquals(OperationType.LIST_GROUPS_PAGED,
event.operationType());
+
+ ListGroupsPagedEvent pagedEvent = (ListGroupsPagedEvent) event;
+ Assertions.assertEquals(NameIdentifier.of(METALAKE),
pagedEvent.identifier());
+ Assertions.assertEquals(0, pagedEvent.offset());
+ Assertions.assertEquals(10, pagedEvent.limit());
+ Assertions.assertEquals(2, pagedEvent.resultCount());
+ Assertions.assertEquals(2L, pagedEvent.totalCount());
+ }
+
+ @Test
+ void testListGroupsPagedFailureEvent() {
+ Assertions.assertThrows(
+ GravitinoRuntimeException.class, () ->
failureDispatcher.listGroups(METALAKE, 1, 5));
+
+ Event event = dummyEventListener.popPostEvent();
+ Assertions.assertEquals(ListGroupsPagedFailureEvent.class,
event.getClass());
+ Assertions.assertEquals(OperationStatus.FAILURE, event.operationStatus());
+ Assertions.assertEquals(OperationType.LIST_GROUPS_PAGED,
event.operationType());
+
+ ListGroupsPagedFailureEvent failureEvent = (ListGroupsPagedFailureEvent)
event;
+ Assertions.assertEquals(NameIdentifier.of(METALAKE),
failureEvent.identifier());
+ Assertions.assertEquals(1, failureEvent.offset());
+ Assertions.assertEquals(5, failureEvent.limit());
+ }
+
+ @Test
+ void testCountGroupsPreEvent() {
+ dispatcher.countGroups(METALAKE);
+
+ PreEvent preEvent = dummyEventListener.popPreEvent();
+ Assertions.assertEquals(CountGroupsPreEvent.class, preEvent.getClass());
+ Assertions.assertEquals(OperationStatus.UNPROCESSED,
preEvent.operationStatus());
+ Assertions.assertEquals(OperationType.COUNT_GROUPS,
preEvent.operationType());
+ Assertions.assertEquals(NameIdentifier.of(METALAKE),
preEvent.identifier());
+ }
+
+ @Test
+ void testCountGroupsEvent() {
+ dispatcher.countGroups(METALAKE);
+
+ Event event = dummyEventListener.popPostEvent();
+ Assertions.assertEquals(CountGroupsEvent.class, event.getClass());
+ Assertions.assertEquals(OperationStatus.SUCCESS, event.operationStatus());
+ Assertions.assertEquals(OperationType.COUNT_GROUPS, event.operationType());
+
+ CountGroupsEvent countEvent = (CountGroupsEvent) event;
+ Assertions.assertEquals(NameIdentifier.of(METALAKE),
countEvent.identifier());
+ Assertions.assertEquals(2L, countEvent.count());
+ }
+
+ @Test
+ void testCountGroupsFailureEvent() {
+ Assertions.assertThrows(
+ GravitinoRuntimeException.class, () ->
failureDispatcher.countGroups(METALAKE));
+
+ Event event = dummyEventListener.popPostEvent();
+ Assertions.assertEquals(CountGroupsFailureEvent.class, event.getClass());
+ Assertions.assertEquals(OperationStatus.FAILURE, event.operationStatus());
+ Assertions.assertEquals(OperationType.COUNT_GROUPS, event.operationType());
+ }
+
@Test
void testListGroupNamesPreEvent() {
dispatcher.listGroupNames(METALAKE);
@@ -691,6 +777,9 @@ public class TestGroupEvent {
when(dispatcher.removeGroupById(METALAKE, GROUP_ID)).thenReturn(true);
when(dispatcher.listGroups(METALAKE)).thenReturn(new Group[] {group,
otherGroup});
+ when(dispatcher.listGroups(eq(METALAKE), eq(0), eq(10)))
+ .thenReturn(new PagedResult<>(2, Arrays.asList(group, otherGroup)));
+ when(dispatcher.countGroups(METALAKE)).thenReturn(2L);
when(dispatcher.listGroupNames(METALAKE)).thenReturn(new String[]
{groupName, otherGroupName});
when(dispatcher.getGroup(METALAKE, groupName)).thenReturn(group);
diff --git
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestUserEvent.java
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestUserEvent.java
index ef10a28cea..b38b0bc453 100644
---
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestUserEvent.java
+++
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestUserEvent.java
@@ -25,11 +25,13 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.authorization.PagedResult;
import org.apache.gravitino.authorization.User;
import org.apache.gravitino.authorization.UserChange;
import org.apache.gravitino.exceptions.GravitinoRuntimeException;
@@ -267,6 +269,91 @@ public class TestUserEvent {
NameIdentifier.of(INEXIST_METALAKE),
listUsersFailureEvent.identifier());
}
+ @Test
+ void testListUsersPagedPreEvent() {
+ dispatcher.listUsers(METALAKE, 0, 10);
+
+ PreEvent preEvent = dummyEventListener.popPreEvent();
+ Assertions.assertEquals(ListUsersPagedPreEvent.class, preEvent.getClass());
+ Assertions.assertEquals(OperationStatus.UNPROCESSED,
preEvent.operationStatus());
+ Assertions.assertEquals(OperationType.LIST_USERS_PAGED,
preEvent.operationType());
+
+ ListUsersPagedPreEvent pagedPreEvent = (ListUsersPagedPreEvent) preEvent;
+ Assertions.assertEquals(NameIdentifier.of(METALAKE),
pagedPreEvent.identifier());
+ Assertions.assertEquals(0, pagedPreEvent.offset());
+ Assertions.assertEquals(10, pagedPreEvent.limit());
+ }
+
+ @Test
+ void testListUsersPagedEvent() {
+ dispatcher.listUsers(METALAKE, 0, 10);
+
+ Event event = dummyEventListener.popPostEvent();
+ Assertions.assertEquals(ListUsersPagedEvent.class, event.getClass());
+ Assertions.assertEquals(OperationStatus.SUCCESS, event.operationStatus());
+ Assertions.assertEquals(OperationType.LIST_USERS_PAGED,
event.operationType());
+
+ ListUsersPagedEvent pagedEvent = (ListUsersPagedEvent) event;
+ Assertions.assertEquals(NameIdentifier.of(METALAKE),
pagedEvent.identifier());
+ Assertions.assertEquals(0, pagedEvent.offset());
+ Assertions.assertEquals(10, pagedEvent.limit());
+ Assertions.assertEquals(2, pagedEvent.resultCount());
+ Assertions.assertEquals(2L, pagedEvent.totalCount());
+ }
+
+ @Test
+ void testListUsersPagedFailureEvent() {
+ Assertions.assertThrowsExactly(
+ GravitinoRuntimeException.class, () ->
failureDispatcher.listUsers(METALAKE, 1, 5));
+
+ Event event = dummyEventListener.popPostEvent();
+ Assertions.assertEquals(ListUsersPagedFailureEvent.class,
event.getClass());
+ Assertions.assertEquals(OperationStatus.FAILURE, event.operationStatus());
+ Assertions.assertEquals(OperationType.LIST_USERS_PAGED,
event.operationType());
+
+ ListUsersPagedFailureEvent failureEvent = (ListUsersPagedFailureEvent)
event;
+ Assertions.assertEquals(NameIdentifier.of(METALAKE),
failureEvent.identifier());
+ Assertions.assertEquals(1, failureEvent.offset());
+ Assertions.assertEquals(5, failureEvent.limit());
+ }
+
+ @Test
+ void testCountUsersPreEvent() {
+ dispatcher.countUsers(METALAKE);
+
+ PreEvent preEvent = dummyEventListener.popPreEvent();
+ Assertions.assertEquals(CountUsersPreEvent.class, preEvent.getClass());
+ Assertions.assertEquals(OperationStatus.UNPROCESSED,
preEvent.operationStatus());
+ Assertions.assertEquals(OperationType.COUNT_USERS,
preEvent.operationType());
+ Assertions.assertEquals(NameIdentifier.of(METALAKE),
preEvent.identifier());
+ }
+
+ @Test
+ void testCountUsersEvent() {
+ dispatcher.countUsers(METALAKE);
+
+ Event event = dummyEventListener.popPostEvent();
+ Assertions.assertEquals(CountUsersEvent.class, event.getClass());
+ Assertions.assertEquals(OperationStatus.SUCCESS, event.operationStatus());
+ Assertions.assertEquals(OperationType.COUNT_USERS, event.operationType());
+
+ CountUsersEvent countEvent = (CountUsersEvent) event;
+ Assertions.assertEquals(NameIdentifier.of(METALAKE),
countEvent.identifier());
+ Assertions.assertEquals(2L, countEvent.count());
+ }
+
+ @Test
+ void testCountUsersFailureEvent() {
+ Assertions.assertThrowsExactly(
+ GravitinoRuntimeException.class, () ->
failureDispatcher.countUsers(INEXIST_METALAKE));
+
+ Event event = dummyEventListener.popPostEvent();
+ Assertions.assertEquals(CountUsersFailureEvent.class, event.getClass());
+ Assertions.assertEquals(OperationStatus.FAILURE, event.operationStatus());
+ Assertions.assertEquals(OperationType.COUNT_USERS, event.operationType());
+ Assertions.assertEquals(NameIdentifier.of(INEXIST_METALAKE),
event.identifier());
+ }
+
@Test
void testListUserNamesPreEvent() {
dispatcher.listUserNames(METALAKE);
@@ -628,6 +715,9 @@ public class TestUserEvent {
when(dispatcher.removeUserByExternalId(METALAKE,
USER_EXT_ID)).thenReturn(true);
when(dispatcher.listUsers(METALAKE)).thenReturn(new User[] {user,
otherUser});
+ when(dispatcher.listUsers(eq(METALAKE), eq(0), eq(10)))
+ .thenReturn(new PagedResult<>(2, Arrays.asList(user, otherUser)));
+ when(dispatcher.countUsers(METALAKE)).thenReturn(2L);
when(dispatcher.listUserNames(METALAKE)).thenReturn(new String[]
{userName, otherUserName});
when(dispatcher.getUser(METALAKE, userName)).thenReturn(user);
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/h2/TestGroupMetaH2Provider.java
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/h2/TestGroupMetaH2Provider.java
index dba49e9372..06da33b3e6 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/h2/TestGroupMetaH2Provider.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/h2/TestGroupMetaH2Provider.java
@@ -103,7 +103,7 @@ class TestGroupMetaH2Provider {
try (ResultSet rs = statement.executeQuery(sql)) {
rs.next();
assertEquals("[\"role1\",\"role2\"]",
rs.getString(COLUMN_LABEL_ROLE_NAMES));
- assertEquals("[\"1\",\"2\"]", rs.getString(COLUMN_LABEL_ROLE_IDS));
+ assertEquals("[1,2]", rs.getString(COLUMN_LABEL_ROLE_IDS));
}
}
@@ -125,8 +125,9 @@ class TestGroupMetaH2Provider {
.replace(QUERY_PARAM_METALAKE_ID, "3");
try (ResultSet rs = statement.executeQuery(sql)) {
rs.next();
- assertEquals("[\"role3\"]", rs.getString(COLUMN_LABEL_ROLE_NAMES));
- assertEquals("[\"3\",\"4\",\"5\"]", rs.getString(COLUMN_LABEL_ROLE_IDS));
+ // JSON_ARRAYAGG keeps empty role names and omits SQL NULLs; numeric
role ids are unquoted.
+ assertEquals("[\"role3\",\"\"]", rs.getString(COLUMN_LABEL_ROLE_NAMES));
+ assertEquals("[3,4,5]", rs.getString(COLUMN_LABEL_ROLE_IDS));
}
}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestGroupMetaService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestGroupMetaService.java
index 801e8dadb5..e14f91a4c3 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestGroupMetaService.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestGroupMetaService.java
@@ -36,11 +36,13 @@ import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
+import java.util.stream.Collectors;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityAlreadyExistsException;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.authorization.PagedResult;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.BaseMetalake;
@@ -1100,6 +1102,135 @@ class TestGroupMetaService extends TestJDBCBackend {
assertThrowsExt(NoSuchEntityException.class, () ->
svc.deleteGroup(group.nameIdentifier()));
}
+ @TestTemplate
+ void testGroupPagination() throws IOException {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+ createAndInsertMakeLake(metalakeName);
+ createAndInsertCatalog(metalakeName, catalogName);
+
+ GroupMetaService svc = GroupMetaService.getInstance();
+ RoleMetaService roleMetaService = RoleMetaService.getInstance();
+ RoleEntity role1 =
+ createRoleEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofRoleNamespace(metalakeName),
+ "page_group_role",
+ auditInfo,
+ catalogName);
+ roleMetaService.insertRole(role1, false);
+
+ GroupEntity g1 =
+ createGroupEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofGroupNamespace(metalakeName),
+ "page_g1",
+ auditInfo);
+ GroupEntity g2 =
+ createGroupEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofGroupNamespace(metalakeName),
+ "page_g2",
+ auditInfo,
+ Lists.newArrayList(role1.name()),
+ Lists.newArrayList(role1.id()));
+ GroupEntity g3 =
+ createGroupEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofGroupNamespace(metalakeName),
+ "page_g3",
+ auditInfo);
+ GroupEntity g4 =
+ createGroupEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofGroupNamespace(metalakeName),
+ "page_g4",
+ auditInfo);
+ svc.insertGroup(g1, false);
+ svc.insertGroup(g2, false);
+ svc.insertGroup(g3, false);
+ svc.insertGroup(g4, false);
+
+ List<GroupEntity> ordered =
+ Lists.newArrayList(g1, g2, g3, g4).stream()
+ .sorted(Comparator.comparing(GroupEntity::id))
+ .collect(Collectors.toList());
+
+ Assertions.assertEquals(4, svc.countGroupsByMetalake(metalakeName));
+
+ // offset=1, limit=2 exercises JDBC OFFSET and stable ORDER BY group_id
ASC.
+ PagedResult<GroupEntity> page =
svc.listGroupsByMetalakePaginated(metalakeName, 1, 2);
+ Assertions.assertEquals(4, page.totalCount());
+ Assertions.assertEquals(2, page.items().size());
+ Assertions.assertEquals(ordered.get(1).name(), page.items().get(0).name());
+ Assertions.assertEquals(ordered.get(2).name(), page.items().get(1).name());
+ Assertions.assertEquals(ordered.get(1).id(), page.items().get(0).id());
+ Assertions.assertEquals(ordered.get(2).id(), page.items().get(1).id());
+
+ GroupEntity withRole =
+ ordered.stream().filter(g ->
"page_g2".equals(g.name())).findFirst().orElseThrow();
+ int roleGroupOffset = ordered.indexOf(withRole);
+ PagedResult<GroupEntity> rolePage =
+ svc.listGroupsByMetalakePaginated(metalakeName, roleGroupOffset, 1);
+ Assertions.assertEquals(1, rolePage.items().size());
+ Assertions.assertEquals(
+ Sets.newHashSet("page_group_role"),
Sets.newHashSet(rolePage.items().get(0).roleNames()));
+
+ PagedResult<GroupEntity> pageAgain =
svc.listGroupsByMetalakePaginated(metalakeName, 1, 2);
+ Assertions.assertEquals(page.items().get(0).name(),
pageAgain.items().get(0).name());
+ Assertions.assertEquals(page.items().get(1).name(),
pageAgain.items().get(1).name());
+
+ Assertions.assertTrue(svc.listGroupsByMetalakePaginated(metalakeName, 0,
0).items().isEmpty());
+ Assertions.assertTrue(
+ svc.listGroupsByMetalakePaginated(metalakeName, 10,
10).items().isEmpty());
+ }
+
+ @TestTemplate
+ void testGroupPaginationWithSpecialRoleNames() throws IOException {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+ createAndInsertMakeLake(metalakeName);
+ createAndInsertCatalog(metalakeName, catalogName);
+
+ GroupMetaService svc = GroupMetaService.getInstance();
+ RoleMetaService roleMetaService = RoleMetaService.getInstance();
+ String quotedRole = "role\"quoted";
+ String backslashRole = "back\\slash";
+ RoleEntity roleQuoted =
+ createRoleEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofRoleNamespace(metalakeName),
+ quotedRole,
+ auditInfo,
+ catalogName);
+ RoleEntity roleBackslash =
+ createRoleEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofRoleNamespace(metalakeName),
+ backslashRole,
+ auditInfo,
+ catalogName);
+ roleMetaService.insertRole(roleQuoted, false);
+ roleMetaService.insertRole(roleBackslash, false);
+
+ GroupEntity group =
+ createGroupEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofGroupNamespace(metalakeName),
+ "special_role_group",
+ auditInfo,
+ Lists.newArrayList(quotedRole, backslashRole),
+ Lists.newArrayList(roleQuoted.id(), roleBackslash.id()));
+ svc.insertGroup(group, false);
+
+ PagedResult<GroupEntity> page =
svc.listGroupsByMetalakePaginated(metalakeName, 0, 10);
+ Assertions.assertEquals(1, page.totalCount());
+ Assertions.assertEquals(1, page.items().size());
+ Assertions.assertEquals(
+ Sets.newHashSet(quotedRole, backslashRole),
+ Sets.newHashSet(page.items().get(0).roleNames()));
+ }
+
private NameIdentifier groupExtIdent(String externalId) {
return AuthorizationUtils.ofGroupExternalId(metalakeName, externalId);
}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
index f6370317a9..bb377e7c38 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
@@ -43,6 +43,7 @@ import org.apache.gravitino.EntityAlreadyExistsException;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.authorization.PagedResult;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.BaseMetalake;
@@ -1262,6 +1263,141 @@ class TestUserMetaService extends TestJDBCBackend {
assertTrue(none.isEmpty());
}
+ @TestTemplate
+ void testUserPagination() throws IOException {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+ createAndInsertMakeLake(metalakeName);
+ CatalogEntity catalog =
+ createCatalog(
+ RandomIdGenerator.INSTANCE.nextId(), Namespace.of(metalakeName),
"catalog", auditInfo);
+ backend.insert(catalog, false);
+
+ UserMetaService svc = UserMetaService.getInstance();
+ RoleMetaService roleMetaService = RoleMetaService.getInstance();
+ RoleEntity role1 =
+ createRoleEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofRoleNamespace(metalakeName),
+ "page_role",
+ auditInfo,
+ "catalog");
+ roleMetaService.insertRole(role1, false);
+
+ UserEntity u1 =
+ createUserEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofUserNamespace(metalakeName),
+ "page_u1",
+ auditInfo);
+ UserEntity u2 =
+ createUserEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofUserNamespace(metalakeName),
+ "page_u2",
+ auditInfo,
+ Lists.newArrayList(role1.name()),
+ Lists.newArrayList(role1.id()));
+ UserEntity u3 =
+ createUserEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofUserNamespace(metalakeName),
+ "page_u3",
+ auditInfo);
+ UserEntity u4 =
+ createUserEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofUserNamespace(metalakeName),
+ "page_u4",
+ auditInfo);
+ svc.insertUser(u1, false);
+ svc.insertUser(u2, false);
+ svc.insertUser(u3, false);
+ svc.insertUser(u4, false);
+
+ List<UserEntity> ordered =
+ Lists.newArrayList(u1, u2, u3, u4).stream()
+ .sorted(Comparator.comparing(UserEntity::id))
+ .collect(Collectors.toList());
+
+ Assertions.assertEquals(4, svc.countUsersByMetalake(metalakeName));
+
+ // offset=1, limit=2 exercises JDBC OFFSET and stable ORDER BY user_id ASC.
+ PagedResult<UserEntity> page =
svc.listUsersByMetalakePaginated(metalakeName, 1, 2);
+ Assertions.assertEquals(4, page.totalCount());
+ Assertions.assertEquals(2, page.items().size());
+ Assertions.assertEquals(ordered.get(1).name(), page.items().get(0).name());
+ Assertions.assertEquals(ordered.get(2).name(), page.items().get(1).name());
+ Assertions.assertEquals(ordered.get(1).id(), page.items().get(0).id());
+ Assertions.assertEquals(ordered.get(2).id(), page.items().get(1).id());
+
+ UserEntity withRole =
+ ordered.stream().filter(u ->
"page_u2".equals(u.name())).findFirst().orElseThrow();
+ int roleUserOffset = ordered.indexOf(withRole);
+ PagedResult<UserEntity> rolePage =
+ svc.listUsersByMetalakePaginated(metalakeName, roleUserOffset, 1);
+ Assertions.assertEquals(1, rolePage.items().size());
+ Assertions.assertEquals(
+ Sets.newHashSet("page_role"),
Sets.newHashSet(rolePage.items().get(0).roleNames()));
+
+ PagedResult<UserEntity> pageAgain =
svc.listUsersByMetalakePaginated(metalakeName, 1, 2);
+ Assertions.assertEquals(
+
page.items().stream().map(UserEntity::name).collect(Collectors.toList()),
+
pageAgain.items().stream().map(UserEntity::name).collect(Collectors.toList()));
+
+ Assertions.assertTrue(svc.listUsersByMetalakePaginated(metalakeName, 0,
0).items().isEmpty());
+ Assertions.assertTrue(svc.listUsersByMetalakePaginated(metalakeName, 10,
10).items().isEmpty());
+ }
+
+ @TestTemplate
+ void testUserPaginationWithSpecialRoleNames() throws IOException {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+ createAndInsertMakeLake(metalakeName);
+ CatalogEntity catalog =
+ createCatalog(
+ RandomIdGenerator.INSTANCE.nextId(), Namespace.of(metalakeName),
"catalog", auditInfo);
+ backend.insert(catalog, false);
+
+ UserMetaService svc = UserMetaService.getInstance();
+ RoleMetaService roleMetaService = RoleMetaService.getInstance();
+ String quotedRole = "role\"quoted";
+ String backslashRole = "back\\slash";
+ RoleEntity roleQuoted =
+ createRoleEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofRoleNamespace(metalakeName),
+ quotedRole,
+ auditInfo,
+ "catalog");
+ RoleEntity roleBackslash =
+ createRoleEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofRoleNamespace(metalakeName),
+ backslashRole,
+ auditInfo,
+ "catalog");
+ roleMetaService.insertRole(roleQuoted, false);
+ roleMetaService.insertRole(roleBackslash, false);
+
+ UserEntity user =
+ createUserEntity(
+ RandomIdGenerator.INSTANCE.nextId(),
+ AuthorizationUtils.ofUserNamespace(metalakeName),
+ "special_role_user",
+ auditInfo,
+ Lists.newArrayList(quotedRole, backslashRole),
+ Lists.newArrayList(roleQuoted.id(), roleBackslash.id()));
+ svc.insertUser(user, false);
+
+ PagedResult<UserEntity> page =
svc.listUsersByMetalakePaginated(metalakeName, 0, 10);
+ Assertions.assertEquals(1, page.totalCount());
+ Assertions.assertEquals(1, page.items().size());
+ Assertions.assertEquals(
+ Sets.newHashSet(quotedRole, backslashRole),
+ Sets.newHashSet(page.items().get(0).roleNames()));
+ }
+
@TestTemplate
void testUserExtId() throws IOException {
UserMetaService svc = userMetaService();