This is an automated email from the ASF dual-hosted git repository. xxyu pushed a commit to branch kylin5 in repository https://gitbox.apache.org/repos/asf/kylin.git
commit 04a956f3983c3873fe38dfa2f597f8cab7681afa Author: lixiang <447399...@qq.com> AuthorDate: Sun Apr 9 19:05:53 2023 +0800 KYLIN-5604 add open api for update agg group (#30273) --- .../common/exception/code/ErrorCodeServer.java | 7 + .../resources/kylin_error_msg_conf_cn.properties | 7 + .../resources/kylin_error_msg_conf_en.properties | 7 + .../kylin_error_suggestion_conf_cn.properties | 1 + .../kylin_error_suggestion_conf_en.properties | 1 + .../main/resources/kylin_errorcode_conf.properties | 7 + .../controller/open/OpenIndexPlanController.java | 74 ++++++++++ .../open/OpenIndexPlanControllerTest.java | 114 ++++++++++++++ .../request/OpenUpdateRuleBasedCuboidRequest.java | 70 +++++++++ .../kylin/rest/service/FusionIndexService.java | 141 +++++++++++++++++- .../kylin/rest/service/FusionIndexServiceTest.java | 164 ++++++++++++++++++++- 11 files changed, 584 insertions(+), 9 deletions(-) diff --git a/src/core-common/src/main/java/org/apache/kylin/common/exception/code/ErrorCodeServer.java b/src/core-common/src/main/java/org/apache/kylin/common/exception/code/ErrorCodeServer.java index 5e0df273eb..91a14bb8ee 100644 --- a/src/core-common/src/main/java/org/apache/kylin/common/exception/code/ErrorCodeServer.java +++ b/src/core-common/src/main/java/org/apache/kylin/common/exception/code/ErrorCodeServer.java @@ -115,6 +115,12 @@ public enum ErrorCodeServer implements ErrorCodeProducer { INDEX_PARAMETER_INVALID("KE-010012203"), SHARD_BY_COLUMN_NOT_IN_INDEX("KE-010012204"), OUT_OF_MAX_DIM_COMBINATION("KE-010012205"), + DIMENSION_NOT_IN_MODEL("KE-010012206"), + MEASURE_NOT_IN_MODEL("KE-010012207"), + MANDATORY_NOT_IN_DIMENSION("KE-010012208"), + HIERARCHY_NOT_IN_DIMENSION("KE-010012209"), + JOINT_NOT_IN_DIMENSION("KE-010012210"), + DIMENSION_ONLY_SET_ONCE("KE-010012211"), // 10043XX parameter check REQUEST_PARAMETER_EMPTY_OR_VALUE_EMPTY("KE-010043201"), @@ -138,6 +144,7 @@ public enum ErrorCodeServer implements ErrorCodeProducer { USERNAME_COMPANY_NAME_INVALID_VALUE("KE-010043219"), USER_GROUP_NOT_EXIST("KE-010043220"), REPEATED_PARAMETER("KE-010043221"), + INTEGER_POSITIVE_CHECK("KE-010043222"), // 100313xx async query ASYNC_QUERY_RESULT_NOT_FOUND("KE-010031301"), diff --git a/src/core-common/src/main/resources/kylin_error_msg_conf_cn.properties b/src/core-common/src/main/resources/kylin_error_msg_conf_cn.properties index 2e4a068fe0..ee75b68336 100644 --- a/src/core-common/src/main/resources/kylin_error_msg_conf_cn.properties +++ b/src/core-common/src/main/resources/kylin_error_msg_conf_cn.properties @@ -116,6 +116,12 @@ KE-010012202=因为存在相同的索引,无法新建该索引。请修改。 KE-010012203=参数 “%s” 仅支持 “%s”。 KE-010012204=ShardBy 列不在索引包含的列中,请修改后重试。 KE-010012205=聚合组生成的索引数超出系统允许的最大索引数(%s)。 +KE-010012206=聚合组包含的维度必须已经添加在模型中,请修改后重试。 +KE-010012207=聚合组包含的度量必须已经添加在模型中,请修改后重试。 +KE-010012208=必需维度必须在 'dimension' 参数中,请修改后重试。 +KE-010012209=层级维度必须在 'dimension' 参数中,请修改后重试。 +KE-010012210=联合维度必须在 'dimension' 参数中,请修改后重试。 +KE-010012211=任一维度只能在必需维度、层级维度或联合维度中设置一次,请修改后重试。 ## 10043XX parameter check KE-010043201=请求参数 “%s” 为空或值为空。请检查请求参数是否正确填写。 @@ -139,6 +145,7 @@ KE-010043218=无效请求参数值 “%s”。请输入正确的sort_by字段。 KE-010043219=请使用中文、英文、空格命名用户名和公司。 KE-010043220=找不到用户组 “%s”。请检查后重试。 KE-010043221=参数 “%s” 已存在。请检查后重试。 +KE-010043222=输入的参数值无效,参数值需为正整数。请检查后重试。 ## Streaming KE-010035202=使用解析器 “%s” 解析Topic “%s” 的消息时发生异常,请检查后重试。 diff --git a/src/core-common/src/main/resources/kylin_error_msg_conf_en.properties b/src/core-common/src/main/resources/kylin_error_msg_conf_en.properties index 187c23a6e4..511f9b6ee0 100644 --- a/src/core-common/src/main/resources/kylin_error_msg_conf_en.properties +++ b/src/core-common/src/main/resources/kylin_error_msg_conf_en.properties @@ -115,6 +115,12 @@ KE-010012202=Can't add this index, as the same index already exists. Please modi KE-010012203=The parameter "%s" only supports "%s". KE-010012204=The ShardBy column is not included in the index. Please fix and try again. KE-010012205=The number of indexes generated by the aggregate group exceeds the maximum number(%s) of indexes allowed by the system. +KE-010012206=The dimension contained in the aggregate group must have been added to the model, please modify and try again. +KE-010012207=The measure contained in the aggregate group must have been added to the model, please modify and try again. +KE-010012208=The mandatory dimension must be included in the 'dimension' parameter. Please modify it and try again. +KE-010012209=The hierarchy dimension must be included in the 'dimension' parameter. Please modify it and try again. +KE-010012210=The joint dimension must be included in the 'dimension' parameter. Please modify it and try again. +KE-010012211=Any dimension can only be set once in mandatory dimension, hierarchy dimension or joint dimension. Please modify and try again. ## 10043XX parameter check KE-010043201=Request parameter "%s" is empty or value is empty. Please check the request parameters. @@ -138,6 +144,7 @@ KE-010043218=Invalid request parameter value "%s". Please enter the correct sort KE-010043219=Please use characters or spaces for username and company. KE-010043220=Can't find user group "%s". Please check and try again. KE-010043221=The parameter “%s” already exists. Please check and try again. +KE-010043222=The entered parameter value is invalid. The parameter value must be a positive integer. Please check and try again. ## Streaming KE-010035202=An exception occurred while parsing the messages of Topic "%2$s" with parser "%1$s". Please check and try again. diff --git a/src/core-common/src/main/resources/kylin_error_suggestion_conf_cn.properties b/src/core-common/src/main/resources/kylin_error_suggestion_conf_cn.properties index 42dc91cf84..43af745ba9 100644 --- a/src/core-common/src/main/resources/kylin_error_suggestion_conf_cn.properties +++ b/src/core-common/src/main/resources/kylin_error_suggestion_conf_cn.properties @@ -132,6 +132,7 @@ KE-010043218= KE-010043219= KE-010043220= KE-010043221= +KE-010043222= ## Streaming KE-010035215= diff --git a/src/core-common/src/main/resources/kylin_error_suggestion_conf_en.properties b/src/core-common/src/main/resources/kylin_error_suggestion_conf_en.properties index 105f521360..6dc32307e2 100644 --- a/src/core-common/src/main/resources/kylin_error_suggestion_conf_en.properties +++ b/src/core-common/src/main/resources/kylin_error_suggestion_conf_en.properties @@ -132,6 +132,7 @@ KE-010043218= KE-010043219= KE-010043220= KE-010043221= +KE-010043222= ## Streaming diff --git a/src/core-common/src/main/resources/kylin_errorcode_conf.properties b/src/core-common/src/main/resources/kylin_errorcode_conf.properties index ae53e187e4..30f304c8e0 100644 --- a/src/core-common/src/main/resources/kylin_errorcode_conf.properties +++ b/src/core-common/src/main/resources/kylin_errorcode_conf.properties @@ -127,6 +127,12 @@ KE-010012202 KE-010012203 KE-010012204 KE-010012205 +KE-010012206 +KE-010012207 +KE-010012208 +KE-010012209 +KE-010012210 +KE-010012211 ## 10043XX parameter check KE-010043201 @@ -150,6 +156,7 @@ KE-010043218 KE-010043219 KE-010043220 KE-010043221 +KE-010043222 ## Streaming KE-010035202 diff --git a/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/open/OpenIndexPlanController.java b/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/open/OpenIndexPlanController.java new file mode 100644 index 0000000000..432528b511 --- /dev/null +++ b/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/open/OpenIndexPlanController.java @@ -0,0 +1,74 @@ +/* + * 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.kylin.rest.controller.open; + +import static org.apache.kylin.common.constant.HttpConstant.HTTP_VND_APACHE_KYLIN_V4_PUBLIC_JSON; +import static org.apache.kylin.common.exception.code.ErrorCodeServer.MODEL_NOT_EXIST; + +import org.apache.kylin.common.KylinConfig; +import org.apache.kylin.common.exception.KylinException; +import org.apache.kylin.metadata.model.NDataModel; +import org.apache.kylin.metadata.model.NDataModelManager; +import org.apache.kylin.rest.controller.NBasicController; +import org.apache.kylin.rest.controller.NIndexPlanController; +import org.apache.kylin.rest.request.OpenUpdateRuleBasedCuboidRequest; +import org.apache.kylin.rest.request.UpdateRuleBasedCuboidRequest; +import org.apache.kylin.rest.response.DiffRuleBasedIndexResponse; +import org.apache.kylin.rest.response.EnvelopeResponse; +import org.apache.kylin.rest.service.FusionIndexService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.val; + +@RestController +@RequestMapping(value = "/api/index_plans", produces = { HTTP_VND_APACHE_KYLIN_V4_PUBLIC_JSON }) +public class OpenIndexPlanController extends NBasicController { + + private static final String MODEL_ALIAS = "model"; + + @Autowired + @Qualifier("fusionIndexService") + private FusionIndexService fusionIndexService; + + @Autowired + NIndexPlanController indexPlanController; + + @PutMapping(value = "/agg_groups") + public EnvelopeResponse<DiffRuleBasedIndexResponse> updateRule( + @RequestBody OpenUpdateRuleBasedCuboidRequest request) { + String projectName = checkProjectName(request.getProject()); + checkRequiredArg(MODEL_ALIAS, request.getModelAlias()); + request.setProject(projectName); + val modelManager = NDataModelManager.getInstance(KylinConfig.getInstanceFromEnv(), projectName); + NDataModel model = modelManager.getDataModelDescByAlias(request.getModelAlias()); + if (model == null) { + throw new KylinException(MODEL_NOT_EXIST); + } + UpdateRuleBasedCuboidRequest internalRequest = fusionIndexService.convertOpenToInternal(request, model); + EnvelopeResponse<DiffRuleBasedIndexResponse> response = indexPlanController + .calculateDiffRuleBasedIndex(internalRequest); + indexPlanController.updateRule(internalRequest); + return response; + } +} diff --git a/src/metadata-server/src/test/java/org/apache/kylin/rest/controller/open/OpenIndexPlanControllerTest.java b/src/metadata-server/src/test/java/org/apache/kylin/rest/controller/open/OpenIndexPlanControllerTest.java new file mode 100644 index 0000000000..529a491c92 --- /dev/null +++ b/src/metadata-server/src/test/java/org/apache/kylin/rest/controller/open/OpenIndexPlanControllerTest.java @@ -0,0 +1,114 @@ +/* + * 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.kylin.rest.controller.open; + +import static org.apache.kylin.common.constant.HttpConstant.HTTP_VND_APACHE_KYLIN_V4_PUBLIC_JSON; + +import java.nio.charset.StandardCharsets; + +import org.apache.kylin.common.KylinConfig; +import org.apache.kylin.common.util.JsonUtil; +import org.apache.kylin.common.util.NLocalFileMetadataTestCase; +import org.apache.kylin.guava30.shaded.common.collect.Lists; +import org.apache.kylin.metadata.model.NDataModel; +import org.apache.kylin.metadata.model.NDataModelManager; +import org.apache.kylin.rest.constant.Constant; +import org.apache.kylin.rest.controller.NIndexPlanController; +import org.apache.kylin.rest.request.OpenUpdateRuleBasedCuboidRequest; +import org.apache.kylin.rest.request.UpdateRuleBasedCuboidRequest; +import org.apache.kylin.rest.service.FusionIndexService; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import lombok.val; + +public class OpenIndexPlanControllerTest extends NLocalFileMetadataTestCase { + + @InjectMocks + private final OpenIndexPlanController openIndexPlanController = Mockito.spy(new OpenIndexPlanController()); + + @Mock + private FusionIndexService fusionIndexService; + + @Mock + private NIndexPlanController nIndexPlanController; + + private MockMvc mockMvc; + + private final Authentication authentication = new TestingAuthenticationToken("ADMIN", "ADMIN", Constant.ROLE_ADMIN); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mockMvc = MockMvcBuilders.standaloneSetup(openIndexPlanController) + .defaultRequest(MockMvcRequestBuilders.get("/")) + .defaultResponseCharacterEncoding(StandardCharsets.UTF_8).build(); + SecurityContextHolder.getContext().setAuthentication(authentication); + createTestMetadata(); + } + + @After + public void tearDown() { + cleanupTestMetadata(); + } + + @Test + public void testCreateAggGroups() throws Exception { + val request = UpdateRuleBasedCuboidRequest.builder().project("default") + .modelId("89af4ee2-2cdb-4b07-b39e-4c29856309aa").aggregationGroups(Lists.newArrayList()).build(); + + OpenUpdateRuleBasedCuboidRequest openRequest = new OpenUpdateRuleBasedCuboidRequest(); + openRequest.setModelAlias("nmodel_basic"); + openRequest.setProject("default"); + val modelManager = NDataModelManager.getInstance(KylinConfig.getInstanceFromEnv(), request.getProject()); + NDataModel model = modelManager.getDataModelDescByAlias(openRequest.getModelAlias()); + + Mockito.when(fusionIndexService.convertOpenToInternal(openRequest, model)).thenReturn(request); + mockMvc.perform(MockMvcRequestBuilders.put("/api/index_plans/agg_groups") + .contentType(MediaType.APPLICATION_JSON).content(JsonUtil.writeValueAsString(openRequest)) + .accept(MediaType.parseMediaType(HTTP_VND_APACHE_KYLIN_V4_PUBLIC_JSON))) + .andExpect(MockMvcResultMatchers.status().isOk()); + Mockito.verify(openIndexPlanController).updateRule(openRequest); + } + + @Test + public void testCreateAggGroupsNotExistModel() throws Exception { + OpenUpdateRuleBasedCuboidRequest openRequest = new OpenUpdateRuleBasedCuboidRequest(); + openRequest.setModelAlias("model_not_exist"); + openRequest.setProject("default"); + mockMvc.perform(MockMvcRequestBuilders.put("/api/index_plans/agg_groups") + .contentType(MediaType.APPLICATION_JSON).content(JsonUtil.writeValueAsString(openRequest)) + .accept(MediaType.parseMediaType(HTTP_VND_APACHE_KYLIN_V4_PUBLIC_JSON))) + .andExpect(MockMvcResultMatchers.status().is5xxServerError()); + Mockito.verify(openIndexPlanController).updateRule(openRequest); + } +} diff --git a/src/modeling-service/src/main/java/org/apache/kylin/rest/request/OpenUpdateRuleBasedCuboidRequest.java b/src/modeling-service/src/main/java/org/apache/kylin/rest/request/OpenUpdateRuleBasedCuboidRequest.java new file mode 100644 index 0000000000..2424fab513 --- /dev/null +++ b/src/modeling-service/src/main/java/org/apache/kylin/rest/request/OpenUpdateRuleBasedCuboidRequest.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.kylin.rest.request; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class OpenUpdateRuleBasedCuboidRequest { + + @JsonProperty("project") + private String project; + + @JsonProperty("model") + private String modelAlias; + + @JsonProperty("aggregation_groups") + private List<OpenAggGroupRequest> aggregationGroups; + + @JsonProperty("global_dim_cap") + private Integer globalDimCap; + + @JsonProperty("restore_deleted_index") + private boolean restoreDeletedIndex; + + @Data + public static class OpenAggGroupRequest { + + @JsonProperty("dimensions") + private String[] dimensions; + + @JsonProperty("measures") + private String[] measures; + + @JsonProperty("mandatory_dims") + private String[] mandatoryDims; + + @JsonProperty("hierarchy_dims") + private String[][] hierarchyDims; + + @JsonProperty("joint_dims") + private String[][] jointDims; + + @JsonProperty("dim_cap") + private Integer dimCap; + } +} diff --git a/src/modeling-service/src/main/java/org/apache/kylin/rest/service/FusionIndexService.java b/src/modeling-service/src/main/java/org/apache/kylin/rest/service/FusionIndexService.java index fd3b99a283..71cece5de9 100644 --- a/src/modeling-service/src/main/java/org/apache/kylin/rest/service/FusionIndexService.java +++ b/src/modeling-service/src/main/java/org/apache/kylin/rest/service/FusionIndexService.java @@ -18,20 +18,35 @@ package org.apache.kylin.rest.service; +import static org.apache.kylin.common.exception.code.ErrorCodeServer.INTEGER_POSITIVE_CHECK; + import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; +import java.util.TreeSet; +import java.util.function.Function; import java.util.stream.Collectors; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.ArrayUtils; import org.apache.kylin.common.KylinConfig; import org.apache.kylin.common.exception.KylinException; import org.apache.kylin.common.exception.ServerErrorCode; +import org.apache.kylin.common.exception.code.ErrorCodeServer; import org.apache.kylin.common.msg.MsgPicker; import org.apache.kylin.common.util.JsonUtil; import org.apache.kylin.common.util.Pair; +import org.apache.kylin.cube.model.SelectRule; +import org.apache.kylin.guava30.shaded.common.base.Preconditions; +import org.apache.kylin.guava30.shaded.common.collect.Lists; +import org.apache.kylin.guava30.shaded.common.collect.Sets; import org.apache.kylin.job.constant.JobStatusEnum; import org.apache.kylin.job.execution.JobTypeEnum; import org.apache.kylin.metadata.cube.cuboid.NAggregationGroup; @@ -50,6 +65,7 @@ import org.apache.kylin.metadata.model.NDataModelManager; import org.apache.kylin.rest.aspect.Transaction; import org.apache.kylin.rest.request.AggShardByColumnsRequest; import org.apache.kylin.rest.request.CreateTableIndexRequest; +import org.apache.kylin.rest.request.OpenUpdateRuleBasedCuboidRequest; import org.apache.kylin.rest.request.UpdateRuleBasedCuboidRequest; import org.apache.kylin.rest.response.AggIndexResponse; import org.apache.kylin.rest.response.BuildIndexResponse; @@ -62,8 +78,6 @@ import org.apache.kylin.streaming.metadata.StreamingJobMeta; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.apache.kylin.guava30.shaded.common.collect.Lists; - import lombok.val; import lombok.extern.slf4j.Slf4j; @@ -73,6 +87,8 @@ public class FusionIndexService extends BasicService { private static final List<JobStatusEnum> runningStatus = Arrays.asList(JobStatusEnum.STARTING, JobStatusEnum.RUNNING, JobStatusEnum.STOPPING); + private static final String COUNT_ALL_MEASURE = "COUNT_ALL"; + @Autowired private IndexPlanService indexPlanService; @@ -423,6 +439,127 @@ public class FusionIndexService extends BasicService { batchIndexIds); } + public UpdateRuleBasedCuboidRequest convertOpenToInternal(OpenUpdateRuleBasedCuboidRequest request, + NDataModel model) { + checkParamPositive(request.getGlobalDimCap()); + val dimMap = model.getEffectiveDimensions().entrySet().stream().collect(Collectors + .toMap(e -> e.getValue().getAliasDotName(), Map.Entry::getKey, (u, v) -> v, LinkedHashMap::new)); + val meaMap = model.getEffectiveMeasures().entrySet().stream().collect( + Collectors.toMap(e -> e.getValue().getName(), Map.Entry::getKey, (u, v) -> v, LinkedHashMap::new)); + + List<NAggregationGroup> newAdded = request.getAggregationGroups().stream().map(aggGroup -> { + NAggregationGroup group = new NAggregationGroup(); + Preconditions.checkNotNull(aggGroup.getDimensions(), "dimension should not null"); + checkParamPositive(aggGroup.getDimCap()); + val selectedDimMap = extractIds(aggGroup.getDimensions(), dimMap, AggGroupParams.DIMENSION); + group.setIncludes(selectedDimMap.values().toArray(new Integer[0])); + String[] measures = extractMeasures(aggGroup.getMeasures()); + val selectedMeaMap = extractIds(measures, meaMap, AggGroupParams.MEASURE); + group.setMeasures(selectedMeaMap.values().toArray(new Integer[0])); + SelectRule selectRule = new SelectRule(); + val mandatoryDimMap = extractIds(aggGroup.getMandatoryDims(), selectedDimMap, AggGroupParams.MANDATORY); + Set<String> allDims = new HashSet<>(mandatoryDimMap.keySet()); + selectRule.setMandatoryDims(mandatoryDimMap.values().toArray(new Integer[0])); + selectRule.setHierarchyDims(extractJointOrHierarchyIds(aggGroup.getHierarchyDims(), selectedDimMap, allDims, + AggGroupParams.HIERARCHY)); + selectRule.setJointDims( + extractJointOrHierarchyIds(aggGroup.getJointDims(), selectedDimMap, allDims, AggGroupParams.JOINT)); + selectRule.setDimCap(aggGroup.getDimCap() != null ? aggGroup.getDimCap() : request.getGlobalDimCap()); + group.setSelectRule(selectRule); + return group; + }).collect(Collectors.toList()); + + RuleBasedIndex ruleBasedIndex = getRule(request.getProject(), model.getUuid()); + List<NAggregationGroup> groups = ruleBasedIndex.getAggregationGroups(); + groups.addAll(newAdded); + + UpdateRuleBasedCuboidRequest result = new UpdateRuleBasedCuboidRequest(); + result.setModelId(model.getUuid()); + result.setProject(request.getProject()); + result.setLoadData(false); + result.setRestoreDeletedIndex(request.isRestoreDeletedIndex()); + result.setAggregationGroups(groups); + return result; + } + + private String[] extractMeasures(String[] measures) { + if (ArrayUtils.isEmpty(measures)) { + return new String[] { COUNT_ALL_MEASURE }; + } else { + List<String> list = Arrays.stream(measures).filter(m -> !m.equals(COUNT_ALL_MEASURE)) + .collect(Collectors.toList()); + list.add(0, COUNT_ALL_MEASURE); + return list.toArray(new String[0]); + } + } + + private void checkParamPositive(Integer dimCap) { + if (dimCap != null && dimCap <= 0) { + throw new KylinException(INTEGER_POSITIVE_CHECK); + } + } + + private Map<String, Integer> extractIds(String[] dimOrMeaNames, Map<String, Integer> nameIdMap, + AggGroupParams aggGroupParams) { + if (dimOrMeaNames == null || dimOrMeaNames.length == 0) { + return new HashMap<>(); + } + Set<String> set = Arrays.stream(dimOrMeaNames) + .map(str -> aggGroupParams == AggGroupParams.MEASURE ? str : StringUtils.upperCase(str, Locale.ROOT)) + .collect(Collectors.toCollection(TreeSet::new)); + if (set.size() < dimOrMeaNames.length) { + throw new IllegalStateException( + "Dimension or measure in agg group must not contain duplication: " + Arrays.asList(dimOrMeaNames)); + } + + Map<String, Integer> upperCaseMap = nameIdMap.entrySet().stream() + .collect(Collectors.toMap(entry -> aggGroupParams == AggGroupParams.MEASURE ? entry.getKey() + : StringUtils.upperCase(entry.getKey(), Locale.ROOT), Map.Entry::getValue)); + if (!upperCaseMap.keySet().containsAll(set)) { + switch (aggGroupParams) { + case DIMENSION: + throw new KylinException(ErrorCodeServer.DIMENSION_NOT_IN_MODEL); + case MEASURE: + throw new KylinException(ErrorCodeServer.MEASURE_NOT_IN_MODEL); + case MANDATORY: + throw new KylinException(ErrorCodeServer.MANDATORY_NOT_IN_DIMENSION); + case HIERARCHY: + throw new KylinException(ErrorCodeServer.HIERARCHY_NOT_IN_DIMENSION); + case JOINT: + throw new KylinException(ErrorCodeServer.JOINT_NOT_IN_DIMENSION); + default: + throw new IllegalStateException("this should not happen"); + } + } + return set.stream() + .collect(Collectors.toMap(Function.identity(), upperCaseMap::get, (v1, v2) -> v1, LinkedHashMap::new)); + } + + private Integer[][] extractJointOrHierarchyIds(String[][] origins, Map<String, Integer> selectedDimMap, Set<String> allDims, + AggGroupParams aggGroupParams) { + if (origins == null || origins.length == 0) { + return new Integer[0][]; + } + Integer[][] result = new Integer[origins.length][]; + for (int i = 0; i < origins.length; i++) { + if (ArrayUtils.isEmpty(origins[i])) { + continue; + } + Map<String, Integer> tmp = extractIds(origins[i], selectedDimMap, aggGroupParams); + if (Sets.intersection(tmp.keySet(), allDims).isEmpty()) { + allDims.addAll(tmp.keySet()); + result[i] = tmp.values().toArray(new Integer[0]); + } else { + throw new KylinException(ErrorCodeServer.DIMENSION_ONLY_SET_ONCE); + } + } + return result; + } + + enum AggGroupParams { + DIMENSION, MEASURE, MANDATORY, HIERARCHY, JOINT + } + private String getBatchModel(String project, String modelId) { FusionModel fusionModel = getManager(FusionModelManager.class, project).getFusionModel(modelId); return fusionModel.getBatchModel().getId(); diff --git a/src/modeling-service/src/test/java/org/apache/kylin/rest/service/FusionIndexServiceTest.java b/src/modeling-service/src/test/java/org/apache/kylin/rest/service/FusionIndexServiceTest.java index 1f78db3f14..4684c0fcdb 100644 --- a/src/modeling-service/src/test/java/org/apache/kylin/rest/service/FusionIndexServiceTest.java +++ b/src/modeling-service/src/test/java/org/apache/kylin/rest/service/FusionIndexServiceTest.java @@ -25,13 +25,10 @@ import java.util.Collections; import org.apache.kylin.common.KylinConfig; import org.apache.kylin.common.exception.KylinException; import org.apache.kylin.common.exception.ServerErrorCode; +import org.apache.kylin.common.exception.code.ErrorCodeServer; import org.apache.kylin.common.util.RandomUtil; import org.apache.kylin.cube.model.SelectRule; -import org.apache.kylin.metadata.model.SegmentRange; -import org.apache.kylin.rest.constant.Constant; -import org.apache.kylin.rest.response.AggIndexResponse; -import org.apache.kylin.rest.util.AclEvaluate; -import org.apache.kylin.rest.util.AclUtil; +import org.apache.kylin.guava30.shaded.common.collect.Lists; import org.apache.kylin.metadata.cube.cuboid.NAggregationGroup; import org.apache.kylin.metadata.cube.model.IndexEntity; import org.apache.kylin.metadata.cube.model.IndexEntity.Range; @@ -40,9 +37,17 @@ import org.apache.kylin.metadata.cube.model.LayoutEntity; import org.apache.kylin.metadata.cube.model.NDataflow; import org.apache.kylin.metadata.cube.model.NDataflowManager; import org.apache.kylin.metadata.cube.model.NIndexPlanManager; +import org.apache.kylin.metadata.model.NDataModel; +import org.apache.kylin.metadata.model.NDataModelManager; +import org.apache.kylin.metadata.model.SegmentRange; +import org.apache.kylin.rest.constant.Constant; import org.apache.kylin.rest.request.CreateTableIndexRequest; +import org.apache.kylin.rest.request.OpenUpdateRuleBasedCuboidRequest; import org.apache.kylin.rest.request.UpdateRuleBasedCuboidRequest; +import org.apache.kylin.rest.response.AggIndexResponse; import org.apache.kylin.rest.response.FusionRuleDataResult; +import org.apache.kylin.rest.util.AclEvaluate; +import org.apache.kylin.rest.util.AclUtil; import org.hamcrest.CoreMatchers; import org.junit.After; import org.junit.Assert; @@ -57,8 +62,6 @@ import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.util.ReflectionTestUtils; -import org.apache.kylin.guava30.shaded.common.collect.Lists; - import lombok.val; import lombok.var; @@ -860,4 +863,151 @@ public class FusionIndexServiceTest extends SourceTestCase { "data_size", true, Lists.newArrayList()); Assert.assertEquals(64, indexResponses2.size()); } + + @Test + public void testNormalConvertOpenToInternal() { + OpenUpdateRuleBasedCuboidRequest request = new OpenUpdateRuleBasedCuboidRequest(); + request.setProject("default"); + request.setModelAlias("test_bank"); + val modelManager = NDataModelManager.getInstance(KylinConfig.getInstanceFromEnv(), request.getProject()); + NDataModel model = modelManager.getDataModelDescByAlias(request.getModelAlias()); + val aggGroup = new OpenUpdateRuleBasedCuboidRequest.OpenAggGroupRequest(); + request.setAggregationGroups(Collections.singletonList(aggGroup)); + + { + aggGroup.setDimensions(new String[]{"TEST_BANK_INCOME.NAME", "TEST_BANK_INCOME.DT"}); + aggGroup.setMeasures(new String[]{"SUM_INCOME"}); + String[][] jointDims = new String[][]{{"TEST_BANK_INCOME.DT"}, {}}; + aggGroup.setJointDims(jointDims); + UpdateRuleBasedCuboidRequest internal = fusionIndexService.convertOpenToInternal(request, model); + Assert.assertEquals(2, internal.getAggregationGroups().size()); + } + + { + aggGroup.setDimensions(new String[]{"TEST_BANK_INCOME.NAME", "TEST_BANK_INCOME.DT"}); + aggGroup.setMeasures(null); + String[][] hierarchyDims = { { "TEST_BANK_INCOME.NAME" } }; + aggGroup.setHierarchyDims(hierarchyDims); + String[][] jointDims = new String[][]{{"TEST_BANK_INCOME.DT"}, {}}; + aggGroup.setJointDims(jointDims); + UpdateRuleBasedCuboidRequest internal = fusionIndexService.convertOpenToInternal(request, model); + Assert.assertEquals(2, internal.getAggregationGroups().size()); + } + + { + aggGroup.setDimensions(new String[]{"TEST_BANK_INCOME.NAME", "TEST_BANK_INCOME.DT"}); + aggGroup.setMeasures(new String[]{"SUM_INCOME"}); + aggGroup.setHierarchyDims(null); + aggGroup.setJointDims(new String[][]{}); + aggGroup.setMandatoryDims(new String[] {"TEST_BANK_INCOME.NAME"}); + UpdateRuleBasedCuboidRequest internal = fusionIndexService.convertOpenToInternal(request, model); + Assert.assertEquals(2, internal.getAggregationGroups().size()); + } + } + + @Test + public void testIllegalConvertOpenToInternal() { + OpenUpdateRuleBasedCuboidRequest request = new OpenUpdateRuleBasedCuboidRequest(); + request.setProject("default"); + request.setModelAlias("test_bank"); + val modelManager = NDataModelManager.getInstance(KylinConfig.getInstanceFromEnv(), request.getProject()); + NDataModel model = modelManager.getDataModelDescByAlias(request.getModelAlias()); + val aggGroup = new OpenUpdateRuleBasedCuboidRequest.OpenAggGroupRequest(); + request.setAggregationGroups(Collections.singletonList(aggGroup)); + + request.setGlobalDimCap(-1); + try { + fusionIndexService.convertOpenToInternal(request, model); + Assert.fail(); + } catch (KylinException e) { + Assert.assertEquals(ErrorCodeServer.INTEGER_POSITIVE_CHECK.getMsg(), e.getMessage()); + } + + checkDimAndMeas(aggGroup, request, model); + + checkSelectRuleDims(aggGroup, request, model); + } + + private void checkDimAndMeas(OpenUpdateRuleBasedCuboidRequest.OpenAggGroupRequest aggGroup, + OpenUpdateRuleBasedCuboidRequest request, NDataModel model) { + request.setGlobalDimCap(null); + aggGroup.setDimensions(null); + try { + fusionIndexService.convertOpenToInternal(request, model); + Assert.fail(); + } catch (NullPointerException e) { + Assert.assertEquals("dimension should not null", e.getMessage()); + } + + aggGroup.setDimCap(1); + aggGroup.setDimensions(new String[] { "TEST_BANK_INCOME.NAME", "TEST_BANK_INCOME.NAME" }); + try { + fusionIndexService.convertOpenToInternal(request, model); + Assert.fail(); + } catch (IllegalStateException e) { + Assert.assertTrue( + e.getMessage().startsWith("Dimension or measure in agg group must not contain duplication")); + } + + aggGroup.setDimensions(new String[] { "TEST_KYLIN_FACT.LSTG_SITE_ID1", "TEST_KYLIN_FACT.LSTG_SITE_ID" }); + try { + fusionIndexService.convertOpenToInternal(request, model); + Assert.fail(); + } catch (KylinException e) { + Assert.assertEquals(ErrorCodeServer.DIMENSION_NOT_IN_MODEL.getMsg(), e.getMessage()); + } + + aggGroup.setDimensions(new String[] { "TEST_BANK_INCOME.NAME", "TEST_BANK_INCOME.DT" }); + aggGroup.setMeasures(new String[] { "TRANS_CNT1" }); + try { + fusionIndexService.convertOpenToInternal(request, model); + Assert.fail(); + } catch (KylinException e) { + Assert.assertEquals(ErrorCodeServer.MEASURE_NOT_IN_MODEL.getMsg(), e.getMessage()); + } + } + + private void checkSelectRuleDims(OpenUpdateRuleBasedCuboidRequest.OpenAggGroupRequest aggGroup, + OpenUpdateRuleBasedCuboidRequest request, NDataModel model) { + aggGroup.setMeasures(new String[] { "SUM_INCOME" }); + aggGroup.setMandatoryDims(new String[] { "TEST_KYLIN_FACT.LSTG_FORMAT_NAME" }); + try { + fusionIndexService.convertOpenToInternal(request, model); + Assert.fail(); + } catch (KylinException e) { + Assert.assertEquals(ErrorCodeServer.MANDATORY_NOT_IN_DIMENSION.getMsg(), e.getMessage()); + } + + aggGroup.setMandatoryDims(new String[] {}); + String[][] hierarchyDims = { { "TEST_KYLIN_FACT.LSTG_FORMAT_NAME" } }; + aggGroup.setHierarchyDims(hierarchyDims); + try { + fusionIndexService.convertOpenToInternal(request, model); + Assert.fail(); + } catch (KylinException e) { + Assert.assertEquals(ErrorCodeServer.HIERARCHY_NOT_IN_DIMENSION.getMsg(), e.getMessage()); + } + + aggGroup.setMeasures(null); + aggGroup.setHierarchyDims(null); + String[][] jointDims = { { "TEST_KYLIN_FACT.LSTG_FORMAT_NAME" }, {} }; + aggGroup.setJointDims(jointDims); + try { + fusionIndexService.convertOpenToInternal(request, model); + Assert.fail(); + } catch (KylinException e) { + Assert.assertEquals(ErrorCodeServer.JOINT_NOT_IN_DIMENSION.getMsg(), e.getMessage()); + } + + hierarchyDims = new String[][] { { "TEST_BANK_INCOME.NAME" } }; + jointDims = new String[][] { { "TEST_BANK_INCOME.NAME" } }; + aggGroup.setHierarchyDims(hierarchyDims); + aggGroup.setJointDims(jointDims); + try { + fusionIndexService.convertOpenToInternal(request, model); + Assert.fail(); + } catch (KylinException e) { + Assert.assertEquals(ErrorCodeServer.DIMENSION_ONLY_SET_ONCE.getMsg(), e.getMessage()); + } + } }