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 aee3290eb0 [#7271] feat(server): Add table statistics REST API (#7829)
aee3290eb0 is described below
commit aee3290eb05dcf74a8bd8cd0c3b2a4d6759f2ddf
Author: roryqi <[email protected]>
AuthorDate: Tue Aug 19 10:50:49 2025 +0800
[#7271] feat(server): Add table statistics REST API (#7829)
### What changes were proposed in this pull request?
Add table statistics REST API
### Why are the changes needed?
Fix: #7271
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
Add UT.
---
.../java/org/apache/gravitino/stats/Statistic.java | 5 +-
.../dto/requests/StatisticsDropRequest.java | 64 ++++
.../dto/requests/StatisticsUpdateRequest.java | 75 ++++
.../dto/responses/StatisticListResponse.java | 61 +++
.../apache/gravitino/dto/stats/StatisticDTO.java | 186 ++++++++++
.../apache/gravitino/dto/util/DTOConverters.java | 26 ++
.../gravitino/dto/stats/TestStatisticDTO.java | 182 +++++++++
.../apache/gravitino/stats/StatisticManager.java | 16 +-
.../apache/gravitino/server/GravitinoServer.java | 2 +
.../server/web/rest/ExceptionHandlers.java | 34 ++
.../gravitino/server/web/rest/OperationType.java | 3 +-
.../server/web/rest/StatisticOperations.java | 203 ++++++++++
.../server/web/rest/TestStatisticOperations.java | 409 +++++++++++++++++++++
13 files changed, 1260 insertions(+), 6 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/stats/Statistic.java
b/api/src/main/java/org/apache/gravitino/stats/Statistic.java
index ad10cb5b4c..e54c82a0cb 100644
--- a/api/src/main/java/org/apache/gravitino/stats/Statistic.java
+++ b/api/src/main/java/org/apache/gravitino/stats/Statistic.java
@@ -19,6 +19,7 @@
package org.apache.gravitino.stats;
import java.util.Optional;
+import org.apache.gravitino.Auditable;
import org.apache.gravitino.annotation.Evolving;
/**
@@ -27,10 +28,10 @@ import org.apache.gravitino.annotation.Evolving;
* statistics, fileset statistics, etc.
*/
@Evolving
-public interface Statistic {
+public interface Statistic extends Auditable {
/** The prefix for custom statistics. Custom statistics are user-defined
statistics. */
- String CUSTOM_PREFIX = "custom.";
+ String CUSTOM_PREFIX = "custom-";
/**
* Get the name of the statistic.
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/StatisticsDropRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/StatisticsDropRequest.java
new file mode 100644
index 0000000000..7cbacbc5fb
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/StatisticsDropRequest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.dto.requests;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.extern.jackson.Jacksonized;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.rest.RESTRequest;
+
+/** Represents a request to drop statistics for specified names. */
+@Getter
+@EqualsAndHashCode
+@ToString
+@Builder
+@Jacksonized
+public class StatisticsDropRequest implements RESTRequest {
+ @JsonProperty("names")
+ String[] names;
+
+ /**
+ * Creates a new StatisticsDropRequest with the specified names.
+ *
+ * @param names The names of the statistics to drop.
+ */
+ public StatisticsDropRequest(String[] names) {
+ this.names = names;
+ }
+
+ /** Default constructor for deserialization. */
+ public StatisticsDropRequest() {
+ this(null);
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ names != null && names.length > 0, "\"names\" must not be null or
empty");
+ for (String name : names) {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(name), "Each name must be a non-empty
string");
+ }
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/StatisticsUpdateRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/StatisticsUpdateRequest.java
new file mode 100644
index 0000000000..6cb94f71af
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/StatisticsUpdateRequest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.dto.requests;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.google.common.base.Preconditions;
+import java.util.Map;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.extern.jackson.Jacksonized;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.json.JsonUtils;
+import org.apache.gravitino.rest.RESTRequest;
+import org.apache.gravitino.stats.StatisticValue;
+
+/** Represents a request to update statistics. */
+@Getter
+@EqualsAndHashCode
+@ToString
+@Builder
+@Jacksonized
+public class StatisticsUpdateRequest implements RESTRequest {
+
+ @JsonProperty("updates")
+ @JsonSerialize(contentUsing = JsonUtils.StatisticValueSerializer.class)
+ @JsonDeserialize(contentUsing = JsonUtils.StatisticValueDeserializer.class)
+ Map<String, StatisticValue<?>> updates;
+
+ /**
+ * Creates a new StatisticsUpdateRequest with the specified updates.
+ *
+ * @param updates The statistics to update.
+ */
+ public StatisticsUpdateRequest(Map<String, StatisticValue<?>> updates) {
+ this.updates = updates;
+ }
+
+ /** Default constructor for deserialization. */
+ public StatisticsUpdateRequest() {
+ this(null);
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(
+ updates != null && !updates.isEmpty(), "\"updates\" must not be null
or empty");
+ updates.forEach(
+ (name, value) -> {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(name), "statistic \"name\" must not be
null or empty");
+ Preconditions.checkArgument(
+ value != null, "statistic \"value\" for '%s' must not be null",
name);
+ });
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/responses/StatisticListResponse.java
b/common/src/main/java/org/apache/gravitino/dto/responses/StatisticListResponse.java
new file mode 100644
index 0000000000..dcdf1050c1
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/responses/StatisticListResponse.java
@@ -0,0 +1,61 @@
+/*
+ * 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.dto.responses;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.gravitino.dto.stats.StatisticDTO;
+
+/** Represents a response containing a list of statistics. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class StatisticListResponse extends BaseResponse {
+
+ @JsonProperty("statistics")
+ private StatisticDTO[] statistics;
+
+ /**
+ * Constructor for StatisticsListResponse.
+ *
+ * @param statistics Array of StatisticDTO objects representing the
statistics.
+ */
+ public StatisticListResponse(StatisticDTO[] statistics) {
+ super(0);
+ this.statistics = statistics;
+ }
+
+ /** Default constructor for StatisticsListResponse (used by Jackson
deserializer). */
+ public StatisticListResponse() {
+ this(null);
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ Preconditions.checkArgument(statistics != null, "\"statistics\" must not
be null");
+
+ for (StatisticDTO statistic : statistics) {
+ Preconditions.checkArgument(statistic != null, "\"statistic\" must not
be null");
+ statistic.validate();
+ }
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/stats/StatisticDTO.java
b/common/src/main/java/org/apache/gravitino/dto/stats/StatisticDTO.java
new file mode 100644
index 0000000000..8742428329
--- /dev/null
+++ b/common/src/main/java/org/apache/gravitino/dto/stats/StatisticDTO.java
@@ -0,0 +1,186 @@
+/*
+ * 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.dto.stats;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.google.common.base.Preconditions;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.dto.AuditDTO;
+import org.apache.gravitino.json.JsonUtils;
+import org.apache.gravitino.stats.Statistic;
+import org.apache.gravitino.stats.StatisticValue;
+
+/** Data Transfer Object (DTO) for representing a statistic. */
+@EqualsAndHashCode
+@ToString
+public class StatisticDTO implements Statistic {
+
+ @JsonProperty("name")
+ private String name;
+
+ @Nullable
+ @JsonInclude
+ @JsonProperty("value")
+ @JsonSerialize(using = JsonUtils.StatisticValueSerializer.class)
+ @JsonDeserialize(using = JsonUtils.StatisticValueDeserializer.class)
+ private StatisticValue<?> value;
+
+ @JsonProperty("reserved")
+ private boolean reserved;
+
+ @JsonProperty("modifiable")
+ private boolean modifiable;
+
+ @JsonProperty("audit")
+ private AuditDTO audit;
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public Optional<StatisticValue<?>> value() {
+ return Optional.ofNullable(value);
+ }
+
+ @Override
+ public boolean reserved() {
+ return reserved;
+ }
+
+ @Override
+ public boolean modifiable() {
+ return modifiable;
+ }
+
+ /**
+ * Validates the StatisticDTO.
+ *
+ * @throws IllegalArgumentException if any of the required fields are
invalid.
+ */
+ public void validate() {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(name), "\"name\" is required and cannot be
empty");
+
+ Preconditions.checkArgument(
+ audit != null, "\"audit\" information is required and cannot be null");
+ }
+
+ @Override
+ public AuditDTO auditInfo() {
+ return audit;
+ }
+
+ /**
+ * Creates a new builder for constructing instances of {@link StatisticDTO}.
+ *
+ * @return a new instance of {@link Builder}
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Builder class for constructing instances of {@link StatisticDTO}. */
+ public static class Builder {
+ private String name;
+ private StatisticValue<?> value;
+ private boolean reserved;
+ private boolean modifiable;
+ private AuditDTO audit;
+
+ /**
+ * Sets the name of the statistic.
+ *
+ * @param name the name of the statistic
+ * @return the builder instance
+ */
+ public Builder withName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Sets the value of the statistic.
+ *
+ * @param value the value of the statistic
+ * @return the builder instance
+ */
+ public Builder withValue(Optional<StatisticValue<?>> value) {
+ value.ifPresent(v -> this.value = v);
+ return this;
+ }
+
+ /**
+ * Sets whether the statistic is reserved.
+ *
+ * @param reserved true if the statistic is reserved, false otherwise
+ * @return the builder instance
+ */
+ public Builder withReserved(boolean reserved) {
+ this.reserved = reserved;
+ return this;
+ }
+
+ /**
+ * Sets whether the statistic is modifiable.
+ *
+ * @param modifiable true if the statistic is modifiable, false otherwise
+ * @return the builder instance
+ */
+ public Builder withModifiable(boolean modifiable) {
+ this.modifiable = modifiable;
+ return this;
+ }
+
+ /**
+ * Sets the audit information for the statistic.
+ *
+ * @param audit the audit information
+ * @return the builder instance
+ */
+ public Builder withAudit(AuditDTO audit) {
+ this.audit = audit;
+ return this;
+ }
+
+ /**
+ * Builds an instance of {@link StatisticDTO} with the provided values.
+ *
+ * @return a new instance of {@link StatisticDTO}
+ */
+ public StatisticDTO build() {
+ StatisticDTO statisticDTO = new StatisticDTO();
+ statisticDTO.name = this.name;
+ statisticDTO.value = this.value;
+ statisticDTO.reserved = this.reserved;
+ statisticDTO.modifiable = this.modifiable;
+ statisticDTO.audit = this.audit;
+ statisticDTO.validate();
+ return statisticDTO;
+ }
+ }
+}
diff --git
a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
index 9ef4026431..fac66a4e99 100644
--- a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
+++ b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
@@ -83,6 +83,7 @@ import
org.apache.gravitino.dto.rel.partitions.IdentityPartitionDTO;
import org.apache.gravitino.dto.rel.partitions.ListPartitionDTO;
import org.apache.gravitino.dto.rel.partitions.PartitionDTO;
import org.apache.gravitino.dto.rel.partitions.RangePartitionDTO;
+import org.apache.gravitino.dto.stats.StatisticDTO;
import org.apache.gravitino.dto.tag.MetadataObjectDTO;
import org.apache.gravitino.dto.tag.TagDTO;
import org.apache.gravitino.file.FileInfo;
@@ -117,6 +118,7 @@ import org.apache.gravitino.rel.partitions.Partition;
import org.apache.gravitino.rel.partitions.Partitions;
import org.apache.gravitino.rel.partitions.RangePartition;
import org.apache.gravitino.rel.types.Types;
+import org.apache.gravitino.stats.Statistic;
import org.apache.gravitino.tag.Tag;
/** Utility class for converting between DTOs and domain objects. */
@@ -841,6 +843,30 @@ public class DTOConverters {
return
Arrays.stream(groups).map(DTOConverters::toDTO).toArray(GroupDTO[]::new);
}
+ /**
+ * Converts an array of statistics to an array of StatisticDTOs.
+ *
+ * @param statistics The statistics to be converted.
+ * @return The array of StatisticDTOs.
+ */
+ public static StatisticDTO[] toDTOs(Statistic[] statistics) {
+ if (ArrayUtils.isEmpty(statistics)) {
+ return new StatisticDTO[0];
+ }
+
+ return Arrays.stream(statistics)
+ .map(
+ statistic ->
+ StatisticDTO.builder()
+ .withName(statistic.name())
+ .withValue(statistic.value())
+ .withModifiable(statistic.modifiable())
+ .withReserved(statistic.reserved())
+ .withAudit(toDTO(statistic.auditInfo()))
+ .build())
+ .toArray(StatisticDTO[]::new);
+ }
+
/**
* Converts a DistributionDTO to a Distribution.
*
diff --git
a/common/src/test/java/org/apache/gravitino/dto/stats/TestStatisticDTO.java
b/common/src/test/java/org/apache/gravitino/dto/stats/TestStatisticDTO.java
new file mode 100644
index 0000000000..dbc940e020
--- /dev/null
+++ b/common/src/test/java/org/apache/gravitino/dto/stats/TestStatisticDTO.java
@@ -0,0 +1,182 @@
+/*
+ * 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.dto.stats;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import java.time.Instant;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.gravitino.dto.AuditDTO;
+import org.apache.gravitino.json.JsonUtils;
+import org.apache.gravitino.stats.StatisticValue;
+import org.apache.gravitino.stats.StatisticValues;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestStatisticDTO {
+ @Test
+ public void testStatisticSerDe() throws JsonProcessingException {
+ // case 1: StatisticDTO with long value
+ StatisticDTO statisticDTO =
+ StatisticDTO.builder()
+ .withName("statistic_test")
+ .withValue(Optional.of(StatisticValues.longValue(100L)))
+ .withReserved(false)
+ .withModifiable(false)
+ .withAudit(
+ AuditDTO.builder()
+ .withCreator("test_user")
+ .withCreateTime(Instant.now())
+ .withLastModifier("test_user")
+ .withLastModifiedTime(Instant.now())
+ .build())
+ .build();
+
+ String serJson = JsonUtils.objectMapper().writeValueAsString(statisticDTO);
+ StatisticDTO deserStatisticDTO =
+ JsonUtils.objectMapper().readValue(serJson, StatisticDTO.class);
+
+ Assertions.assertEquals(statisticDTO, deserStatisticDTO);
+
+ // case 2: StatisticDTO with string value
+ statisticDTO =
+ StatisticDTO.builder()
+ .withName("statistic_test")
+ .withValue(Optional.of(StatisticValues.stringValue("test_value")))
+ .withReserved(true)
+ .withModifiable(true)
+ .withAudit(
+ AuditDTO.builder()
+ .withCreator("test_user")
+ .withCreateTime(Instant.now())
+ .withLastModifier("test_user")
+ .withLastModifiedTime(Instant.now())
+ .build())
+ .build();
+ serJson = JsonUtils.objectMapper().writeValueAsString(statisticDTO);
+ deserStatisticDTO = JsonUtils.objectMapper().readValue(serJson,
StatisticDTO.class);
+ Assertions.assertEquals(statisticDTO, deserStatisticDTO);
+
+ // case 3: StatisticDTO with null value
+ statisticDTO =
+ StatisticDTO.builder()
+ .withName("statistic_test")
+ .withReserved(false)
+ .withModifiable(true)
+ .withAudit(
+ AuditDTO.builder()
+ .withCreator("test_user")
+ .withCreateTime(Instant.now())
+ .withLastModifier("test_user")
+ .withLastModifiedTime(Instant.now())
+ .build())
+ .build();
+ serJson = JsonUtils.objectMapper().writeValueAsString(statisticDTO);
+ Assertions.assertTrue(serJson.contains("null"));
+ deserStatisticDTO = JsonUtils.objectMapper().readValue(serJson,
StatisticDTO.class);
+ Assertions.assertEquals(statisticDTO, deserStatisticDTO);
+
+ // case 4: StatisticDTO with list value
+ statisticDTO =
+ StatisticDTO.builder()
+ .withName("statistic_test")
+ .withValue(
+ Optional.of(
+ StatisticValues.listValue(
+ Lists.newArrayList(
+ StatisticValues.stringValue("value1"),
+ StatisticValues.stringValue("value2")))))
+ .withReserved(false)
+ .withModifiable(true)
+ .withAudit(
+ AuditDTO.builder()
+ .withCreator("test_user")
+ .withCreateTime(Instant.now())
+ .withLastModifier("test_user")
+ .withLastModifiedTime(Instant.now())
+ .build())
+ .build();
+ serJson = JsonUtils.objectMapper().writeValueAsString(statisticDTO);
+ deserStatisticDTO = JsonUtils.objectMapper().readValue(serJson,
StatisticDTO.class);
+ Assertions.assertEquals(statisticDTO, deserStatisticDTO);
+
+ // case 5: StatisticDTO with double value
+ statisticDTO =
+ StatisticDTO.builder()
+ .withName("statistic_test")
+ .withValue(Optional.of(StatisticValues.doubleValue(99.99)))
+ .withReserved(true)
+ .withModifiable(false)
+ .withAudit(
+ AuditDTO.builder()
+ .withCreator("test_user")
+ .withCreateTime(Instant.now())
+ .withLastModifier("test_user")
+ .withLastModifiedTime(Instant.now())
+ .build())
+ .build();
+ serJson = JsonUtils.objectMapper().writeValueAsString(statisticDTO);
+ deserStatisticDTO = JsonUtils.objectMapper().readValue(serJson,
StatisticDTO.class);
+ Assertions.assertEquals(statisticDTO, deserStatisticDTO);
+
+ // case 6: StatisticDTO with boolean value
+ statisticDTO =
+ StatisticDTO.builder()
+ .withName("statistic_test")
+ .withValue(Optional.of(StatisticValues.booleanValue(true)))
+ .withReserved(false)
+ .withModifiable(true)
+ .withAudit(
+ AuditDTO.builder()
+ .withCreator("test_user")
+ .withCreateTime(Instant.now())
+ .withLastModifier("test_user")
+ .withLastModifiedTime(Instant.now())
+ .build())
+ .build();
+ serJson = JsonUtils.objectMapper().writeValueAsString(statisticDTO);
+ deserStatisticDTO = JsonUtils.objectMapper().readValue(serJson,
StatisticDTO.class);
+ Assertions.assertEquals(statisticDTO, deserStatisticDTO);
+
+ // case 7: StatisticDTO with complex object value
+ Map<String, StatisticValue<?>> map = Maps.newHashMap();
+ map.put("key1", StatisticValues.stringValue("value1"));
+ map.put("key2", StatisticValues.longValue(200L));
+ StatisticDTO complexStatisticDTO =
+ StatisticDTO.builder()
+ .withName("complex_statistic")
+ .withValue(Optional.of(StatisticValues.objectValue(map)))
+ .withReserved(false)
+ .withModifiable(true)
+ .withAudit(
+ AuditDTO.builder()
+ .withCreator("test_user")
+ .withCreateTime(Instant.now())
+ .withLastModifier("test_user")
+ .withLastModifiedTime(Instant.now())
+ .build())
+ .build();
+ serJson = JsonUtils.objectMapper().writeValueAsString(complexStatisticDTO);
+ StatisticDTO deserComplexStatisticDTO =
+ JsonUtils.objectMapper().readValue(serJson, StatisticDTO.class);
+ Assertions.assertEquals(complexStatisticDTO, deserComplexStatisticDTO);
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/stats/StatisticManager.java
b/core/src/main/java/org/apache/gravitino/stats/StatisticManager.java
index c1fc9bc892..b62527dca6 100644
--- a/core/src/main/java/org/apache/gravitino/stats/StatisticManager.java
+++ b/core/src/main/java/org/apache/gravitino/stats/StatisticManager.java
@@ -18,6 +18,7 @@
*/
package org.apache.gravitino.stats;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.time.Instant;
@@ -26,6 +27,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.Pair;
+import org.apache.gravitino.Audit;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.MetadataObject;
@@ -73,7 +75,7 @@ public class StatisticManager {
entity -> {
String name = entity.name();
StatisticValue<?> value = entity.value();
- return new CustomStatistic(name, value);
+ return new CustomStatistic(name, value,
entity.auditInfo());
})
.collect(Collectors.toList()));
} catch (NoSuchEntityException nse) {
@@ -180,14 +182,17 @@ public class StatisticManager {
}
}
- private static class CustomStatistic implements Statistic {
+ @VisibleForTesting
+ public static class CustomStatistic implements Statistic {
private final String name;
private final StatisticValue<?> value;
+ private final Audit auditInfo;
- CustomStatistic(String name, StatisticValue<?> value) {
+ public CustomStatistic(String name, StatisticValue<?> value, Audit
auditInfo) {
this.name = name;
this.value = value;
+ this.auditInfo = auditInfo;
}
@Override
@@ -209,5 +214,10 @@ public class StatisticManager {
public boolean modifiable() {
return true;
}
+
+ @Override
+ public Audit auditInfo() {
+ return auditInfo;
+ }
}
}
diff --git
a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
index 3063a12278..48e22e0490 100644
--- a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
+++ b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
@@ -56,6 +56,7 @@ import
org.apache.gravitino.server.web.mapper.JsonMappingExceptionMapper;
import org.apache.gravitino.server.web.mapper.JsonParseExceptionMapper;
import org.apache.gravitino.server.web.mapper.JsonProcessingExceptionMapper;
import org.apache.gravitino.server.web.ui.WebUIFilter;
+import org.apache.gravitino.stats.StatisticManager;
import org.apache.gravitino.tag.TagDispatcher;
import org.glassfish.hk2.api.InterceptionService;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
@@ -147,6 +148,7 @@ public class GravitinoServer extends ResourceConfig {
bind(gravitinoEnv.modelDispatcher()).to(ModelDispatcher.class).ranked(1);
bind(lineageService).to(LineageDispatcher.class).ranked(1);
bind(gravitinoEnv.jobOperationDispatcher()).to(JobOperationDispatcher.class).ranked(1);
+
bind(gravitinoEnv.statisticManager()).to(StatisticManager.class).ranked(1);
}
});
register(JsonProcessingExceptionMapper.class);
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
index 2e933c10c9..7b5ed17336 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
@@ -188,6 +188,11 @@ public class ExceptionHandlers {
return RolePermissionOperationHandler.INSTANCE.handle(type, name, parent,
e);
}
+ public static Response handleStatisticException(
+ OperationType type, String name, String parent, Exception e) {
+ return StatisticExceptionHandler.INSTANCE.handle(type, name, parent, e);
+ }
+
private static class PartitionExceptionHandler extends BaseExceptionHandler {
private static final ExceptionHandler INSTANCE = new
PartitionExceptionHandler();
@@ -906,6 +911,35 @@ public class ExceptionHandlers {
}
}
+ private static class StatisticExceptionHandler extends BaseExceptionHandler {
+
+ private static final ExceptionHandler INSTANCE = new
StatisticExceptionHandler();
+
+ private static String getStatisticErrorMsg(
+ String name, String operation, String parent, String reason) {
+ return String.format(
+ "Failed to operate statistic(s)%s operation [%s] on parent [%s],
reason [%s]",
+ name, operation, parent, reason);
+ }
+
+ @Override
+ public Response handle(OperationType op, String name, String object,
Exception e) {
+ String formatted = StringUtil.isBlank(name) ? "" : " [" + name + "]";
+ String errorMsg = getStatisticErrorMsg(formatted, op.name(), object,
getErrorMsg(e));
+ LOG.warn(errorMsg, e);
+
+ if (e instanceof IllegalArgumentException) {
+ return Utils.illegalArguments(errorMsg, e);
+
+ } else if (e instanceof NotFoundException) {
+ return Utils.notFound(errorMsg, e);
+
+ } else {
+ return super.handle(op, name, object, e);
+ }
+ }
+ }
+
@VisibleForTesting
static class BaseExceptionHandler extends ExceptionHandler {
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/OperationType.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/OperationType.java
index 7b9025eacb..04f8e26de7 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/OperationType.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/OperationType.java
@@ -39,5 +39,6 @@ public enum OperationType {
LIST_VERSIONS, // An operation to list versions of a model
LINK, // An operation to link a version to a model
RUN, // An operation to run a job
- CANCEL // An operation to cancel a job
+ CANCEL, // An operation to cancel a job
+ UPDATE
}
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/StatisticOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/StatisticOperations.java
new file mode 100644
index 0000000000..2ddcfc2e50
--- /dev/null
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/StatisticOperations.java
@@ -0,0 +1,203 @@
+/*
+ * 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.server.web.rest;
+
+import com.codahale.metrics.annotation.ResponseMetered;
+import com.codahale.metrics.annotation.Timed;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.dto.requests.StatisticsDropRequest;
+import org.apache.gravitino.dto.requests.StatisticsUpdateRequest;
+import org.apache.gravitino.dto.responses.BaseResponse;
+import org.apache.gravitino.dto.responses.DropResponse;
+import org.apache.gravitino.dto.responses.StatisticListResponse;
+import org.apache.gravitino.dto.util.DTOConverters;
+import org.apache.gravitino.exceptions.IllegalStatisticNameException;
+import org.apache.gravitino.metrics.MetricNames;
+import org.apache.gravitino.server.web.Utils;
+import org.apache.gravitino.stats.Statistic;
+import org.apache.gravitino.stats.StatisticManager;
+import org.apache.gravitino.stats.StatisticValue;
+import org.apache.gravitino.utils.MetadataObjectUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Path("/metalakes/{metalake}/objects/{type}/{fullName}/statistics")
+@Consumes(MediaType.APPLICATION_JSON)
+@Produces(MediaType.APPLICATION_JSON)
+public class StatisticOperations {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(StatisticOperations.class);
+
+ @Context private HttpServletRequest httpRequest;
+
+ private final StatisticManager statisticManager;
+
+ @Inject
+ public StatisticOperations(StatisticManager statisticManager) {
+ this.statisticManager = statisticManager;
+ }
+
+ @GET
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "list-stats." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
+ @ResponseMetered(name = "list-stats", absolute = true)
+ public Response listStatistics(
+ @PathParam("metalake") String metalake,
+ @PathParam("type") String type,
+ @PathParam("fullName") String fullName) {
+ LOG.info(
+ "Received list statistics request for object full name: {} type: {} in
the metalake {}",
+ fullName,
+ type,
+ metalake);
+ try {
+
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ MetadataObject object =
+ MetadataObjects.parse(
+ fullName,
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+ if (object.type() != MetadataObject.Type.TABLE) {
+ throw new UnsupportedOperationException(
+ "Listing statistics is only supported for tables now.");
+ }
+
+ MetadataObjectUtil.checkMetadataObject(metalake, object);
+
+ List<Statistic> statistics =
statisticManager.listStatistics(metalake, object);
+ return Utils.ok(
+ new StatisticListResponse(
+ DTOConverters.toDTOs(statistics.toArray(new
Statistic[0]))));
+ });
+ } catch (Exception e) {
+ return ExceptionHandlers.handleStatisticException(OperationType.LIST,
fullName, metalake, e);
+ }
+ }
+
+ @PUT
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "update-stats." + MetricNames.HTTP_PROCESS_DURATION, absolute
= true)
+ @ResponseMetered(name = "update-stats", absolute = true)
+ public Response updateStatistics(
+ @PathParam("metalake") String metalake,
+ @PathParam("type") String type,
+ @PathParam("fullName") String fullName,
+ StatisticsUpdateRequest request) {
+ try {
+ LOG.info(
+ "Received update statistics request for object full name: {} type:
{} in the metalake {}",
+ fullName,
+ type,
+ metalake);
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ request.validate();
+ MetadataObject object =
+ MetadataObjects.parse(
+ fullName,
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+ if (object.type() != MetadataObject.Type.TABLE) {
+ throw new UnsupportedOperationException(
+ "Update statistics is only supported for tables now.");
+ }
+
+ Map<String, StatisticValue<?>> statisticMaps = Maps.newHashMap();
+ for (Map.Entry<String, StatisticValue<?>> entry :
request.getUpdates().entrySet()) {
+ // Current we only support custom statistics
+ if (!entry.getKey().startsWith(Statistic.CUSTOM_PREFIX)) {
+ throw new IllegalStatisticNameException(
+ "Statistic name must start with %s , but got: %s",
+ Statistic.CUSTOM_PREFIX, entry.getKey());
+ }
+
+ statisticMaps.put(entry.getKey(), entry.getValue());
+ }
+
+ MetadataObjectUtil.checkMetadataObject(metalake, object);
+
+ statisticManager.updateStatistics(metalake, object, statisticMaps);
+ return Utils.ok(new BaseResponse(0));
+ });
+ } catch (Exception e) {
+ return ExceptionHandlers.handleStatisticException(
+ OperationType.UPDATE,
StringUtils.join(request.getUpdates().keySet(), ","), fullName, e);
+ }
+ }
+
+ @POST
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "drop-stats." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
+ @ResponseMetered(name = "drop-stats", absolute = true)
+ public Response dropStatistics(
+ @PathParam("metalake") String metalake,
+ @PathParam("type") String type,
+ @PathParam("fullName") String fullName,
+ StatisticsDropRequest request) {
+ try {
+ LOG.info(
+ "Received drop statistics request for object full name: {} type: {}
in the metalake {}",
+ fullName,
+ type,
+ metalake);
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ request.validate();
+
+ MetadataObject object =
+ MetadataObjects.parse(
+ fullName,
MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT)));
+ if (object.type() != MetadataObject.Type.TABLE) {
+ throw new UnsupportedOperationException(
+ "Dropping statistics is only supported for tables now.");
+ }
+
+ MetadataObjectUtil.checkMetadataObject(metalake, object);
+
+ boolean dropped =
+ statisticManager.dropStatistics(
+ metalake, object, Lists.newArrayList(request.getNames()));
+ return Utils.ok(new DropResponse(dropped));
+ });
+ } catch (Exception e) {
+ return ExceptionHandlers.handleStatisticException(
+ OperationType.DROP, StringUtils.join(request.getNames(), ","),
fullName, e);
+ }
+ }
+}
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestStatisticOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestStatisticOperations.java
new file mode 100644
index 0000000000..1a55b10282
--- /dev/null
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestStatisticOperations.java
@@ -0,0 +1,409 @@
+/*
+ * 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.server.web.rest;
+
+import static javax.ws.rs.client.Entity.entity;
+import static org.apache.gravitino.Configs.TREE_LOCK_CLEAN_INTERVAL;
+import static org.apache.gravitino.Configs.TREE_LOCK_MAX_NODE_IN_MEMORY;
+import static org.apache.gravitino.Configs.TREE_LOCK_MIN_NODE_IN_MEMORY;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.catalog.TableDispatcher;
+import org.apache.gravitino.dto.requests.StatisticsDropRequest;
+import org.apache.gravitino.dto.requests.StatisticsUpdateRequest;
+import org.apache.gravitino.dto.responses.BaseResponse;
+import org.apache.gravitino.dto.responses.DropResponse;
+import org.apache.gravitino.dto.responses.ErrorConstants;
+import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.dto.responses.StatisticListResponse;
+import org.apache.gravitino.dto.stats.StatisticDTO;
+import org.apache.gravitino.dto.util.DTOConverters;
+import org.apache.gravitino.exceptions.NoSuchMetadataObjectException;
+import org.apache.gravitino.lock.LockManager;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.stats.Statistic;
+import org.apache.gravitino.stats.StatisticManager;
+import org.apache.gravitino.stats.StatisticValue;
+import org.apache.gravitino.stats.StatisticValues;
+import org.glassfish.jersey.internal.inject.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.TestProperties;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class TestStatisticOperations extends JerseyTest {
+
+ private static class MockServletRequestFactory extends
ServletRequestFactoryBase {
+ @Override
+ public HttpServletRequest get() {
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ when(request.getRemoteUser()).thenReturn(null);
+ return request;
+ }
+ }
+
+ private static TableDispatcher tableDispatcher = mock(TableDispatcher.class);
+ private StatisticManager manager = mock(StatisticManager.class);
+
+ private final String metalake = "metalake1";
+
+ private final String catalog = "catalog1";
+
+ private final String schema = "schema1";
+
+ private final String table = "table1";
+
+ @BeforeAll
+ public static void setup() throws IllegalAccessException {
+ Config config = mock(Config.class);
+ Mockito.doReturn(100000L).when(config).get(TREE_LOCK_MAX_NODE_IN_MEMORY);
+ Mockito.doReturn(1000L).when(config).get(TREE_LOCK_MIN_NODE_IN_MEMORY);
+ Mockito.doReturn(36000L).when(config).get(TREE_LOCK_CLEAN_INTERVAL);
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "lockManager", new
LockManager(config), true);
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "tableDispatcher",
tableDispatcher, true);
+ }
+
+ @Override
+ protected Application configure() {
+ try {
+ forceSet(
+ TestProperties.CONTAINER_PORT,
String.valueOf(RESTUtils.findAvailablePort(2000, 3000)));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ ResourceConfig resourceConfig = new ResourceConfig();
+ resourceConfig.register(StatisticOperations.class);
+ resourceConfig.register(
+ new AbstractBinder() {
+ @Override
+ protected void configure() {
+ bind(manager).to(StatisticManager.class).ranked(2);
+
bindFactory(MockServletRequestFactory.class).to(HttpServletRequest.class);
+ }
+ });
+
+ return resourceConfig;
+ }
+
+ @Test
+ public void testListTableStatistics() {
+
+ AuditInfo auditInfo =
+ AuditInfo.builder()
+ .withCreateTime(Instant.now())
+ .withCreator("test")
+ .withLastModifiedTime(Instant.now())
+ .withLastModifier("test")
+ .build();
+
+ Statistic stat1 =
+ new StatisticManager.CustomStatistic(
+ "test", StatisticValues.stringValue("test"), auditInfo);
+
+ Statistic stat2 =
+ new StatisticManager.CustomStatistic("test2",
StatisticValues.longValue(1L), auditInfo);
+
+ when(manager.listStatistics(any(),
any())).thenReturn(Lists.newArrayList(stat1, stat2));
+ when(tableDispatcher.tableExists(any())).thenReturn(true);
+
+ MetadataObject tableObject =
+ MetadataObjects.parse(
+ String.format("%s.%s.%s", catalog, schema, table),
MetadataObject.Type.TABLE);
+ Response resp =
+ target(
+ "/metalakes/"
+ + metalake
+ + "/objects/"
+ + tableObject.type()
+ + "/"
+ + tableObject.fullName()
+ + "/statistics")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
+
+ StatisticListResponse listResp =
resp.readEntity(StatisticListResponse.class);
+ listResp.validate();
+ Assertions.assertEquals(0, listResp.getCode());
+
+ StatisticDTO[] statisticDTOS = listResp.getStatistics();
+ Assertions.assertEquals(2, statisticDTOS.length);
+ Assertions.assertEquals(stat1.name(), statisticDTOS[0].name());
+ Assertions.assertEquals(DTOConverters.toDTO(auditInfo),
statisticDTOS[0].auditInfo());
+ Assertions.assertEquals(stat1.value().get(),
statisticDTOS[0].value().get());
+ Assertions.assertEquals(stat2.name(), statisticDTOS[1].name());
+ Assertions.assertEquals(stat2.value().get(),
statisticDTOS[1].value().get());
+ Assertions.assertEquals(DTOConverters.toDTO(auditInfo),
statisticDTOS[1].auditInfo());
+
+ // Test throw NoSuchMetadataObjectException
+ when(tableDispatcher.tableExists(any())).thenReturn(false);
+
+ Response resp1 =
+ target(
+ "/metalakes/"
+ + metalake
+ + "/objects/"
+ + tableObject.type()
+ + "/"
+ + tableObject.fullName()
+ + "/statistics")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
resp1.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp1.getMediaType());
+
+ ErrorResponse errorResp = resp1.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResp.getCode());
+ Assertions.assertEquals(
+ NoSuchMetadataObjectException.class.getSimpleName(),
errorResp.getType());
+
+ // Test throw RuntimeException
+ when(tableDispatcher.tableExists(any())).thenReturn(true);
+ doThrow(new RuntimeException("mock
error")).when(manager).listStatistics(any(), any());
+ Response resp2 =
+ target(
+ "/metalakes/"
+ + metalake
+ + "/objects/"
+ + tableObject.type()
+ + "/"
+ + tableObject.fullName()
+ + "/statistics")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .get();
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp2.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp2.getMediaType());
+
+ ErrorResponse errorResp2 = resp2.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp2.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp2.getType());
+ }
+
+ @Test
+ public void testUpdateTableStatistics() {
+ Map<String, StatisticValue<?>> statsMap = Maps.newHashMap();
+ statsMap.put(Statistic.CUSTOM_PREFIX + "test1",
StatisticValues.stringValue("test"));
+ statsMap.put(Statistic.CUSTOM_PREFIX + "test2",
StatisticValues.longValue(1L));
+
+ StatisticsUpdateRequest req = new StatisticsUpdateRequest(statsMap);
+ MetadataObject tableObject =
+ MetadataObjects.parse(
+ String.format("%s.%s.%s", catalog, schema, table),
MetadataObject.Type.TABLE);
+
+ when(tableDispatcher.tableExists(any())).thenReturn(true);
+
+ Response resp =
+ target(
+ "/metalakes/"
+ + metalake
+ + "/objects/"
+ + tableObject.type()
+ + "/"
+ + tableObject.fullName()
+ + "/statistics")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .put(entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
+
+ BaseResponse updateResp = resp.readEntity(BaseResponse.class);
+ Assertions.assertEquals(0, updateResp.getCode());
+
+ // Test throw NoSuchMetadataObjectException
+ when(tableDispatcher.tableExists(any())).thenReturn(false);
+ Response resp1 =
+ target(
+ "/metalakes/"
+ + metalake
+ + "/objects/"
+ + tableObject.type()
+ + "/"
+ + tableObject.fullName()
+ + "/statistics")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .put(entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
resp1.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp1.getMediaType());
+
+ ErrorResponse errorResp = resp1.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResp.getCode());
+ Assertions.assertEquals(
+ NoSuchMetadataObjectException.class.getSimpleName(),
errorResp.getType());
+
+ // Test throw RuntimeException
+ when(tableDispatcher.tableExists(any())).thenReturn(true);
+ doThrow(new RuntimeException("mock
error")).when(manager).updateStatistics(any(), any(), any());
+ Response resp2 =
+ target(
+ "/metalakes/"
+ + metalake
+ + "/objects/"
+ + tableObject.type()
+ + "/"
+ + tableObject.fullName()
+ + "/statistics")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .put(entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp2.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp2.getMediaType());
+
+ ErrorResponse errorResp2 = resp2.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp2.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp2.getType());
+
+ // Test throw IllegalStatisticNameException
+ statsMap.put("test1", StatisticValues.longValue(1L));
+
+ req = new StatisticsUpdateRequest(statsMap);
+ Response resp3 =
+ target(
+ "/metalakes/"
+ + metalake
+ + "/objects/"
+ + tableObject.type()
+ + "/"
+ + tableObject.fullName()
+ + "/statistics")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .put(entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp3.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp3.getMediaType());
+
+ ErrorResponse errorResp3 = resp3.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp3.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp3.getType());
+ }
+
+ @Test
+ public void testDropTableStatistics() {
+ StatisticsDropRequest req = new StatisticsDropRequest(new String[]
{"test1", "test2"});
+ when(manager.dropStatistics(any(), any(), any())).thenReturn(true);
+ when(tableDispatcher.tableExists(any())).thenReturn(true);
+ MetadataObject tableObject =
+ MetadataObjects.parse(
+ String.format("%s.%s.%s", catalog, schema, table),
MetadataObject.Type.TABLE);
+
+ Response resp =
+ target(
+ "/metalakes/"
+ + metalake
+ + "/objects/"
+ + tableObject.type()
+ + "/"
+ + tableObject.fullName()
+ + "/statistics")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
+
+ DropResponse dropResp = resp.readEntity(DropResponse.class);
+ Assertions.assertEquals(0, dropResp.getCode());
+ Assertions.assertTrue(dropResp.dropped());
+
+ // Test throw NoSuchMetadataObjectException
+ when(tableDispatcher.tableExists(any())).thenReturn(false);
+ Response resp1 =
+ target(
+ "/metalakes/"
+ + metalake
+ + "/objects/"
+ + tableObject.type()
+ + "/"
+ + tableObject.fullName()
+ + "/statistics")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
resp1.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp1.getMediaType());
+
+ ErrorResponse errorResp = resp1.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResp.getCode());
+ Assertions.assertEquals(
+ NoSuchMetadataObjectException.class.getSimpleName(),
errorResp.getType());
+
+ // Test throw RuntimeException
+ when(tableDispatcher.tableExists(any())).thenReturn(true);
+ doThrow(new RuntimeException("mock
error")).when(manager).dropStatistics(any(), any(), any());
+ Response resp2 =
+ target(
+ "/metalakes/"
+ + metalake
+ + "/objects/"
+ + tableObject.type()
+ + "/"
+ + tableObject.fullName()
+ + "/statistics")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .post(entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp2.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp2.getMediaType());
+
+ ErrorResponse errorResp2 = resp2.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResp2.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp2.getType());
+ }
+}