content: /*
   * 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.carbondata.core.scan.model;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  
  import org.apache.carbondata.common.logging.LogServiceFactory;
  import org.apache.carbondata.core.index.IndexFilter;
  import org.apache.carbondata.core.metadata.datatype.DataTypes;
  import org.apache.carbondata.core.metadata.schema.table.CarbonTable;
  import org.apache.carbondata.core.metadata.schema.table.column.CarbonDimension;
  import org.apache.carbondata.core.metadata.schema.table.column.CarbonMeasure;
  import org.apache.carbondata.core.util.DataTypeConverter;
  
  import org.apache.log4j.Logger;
  
  public class QueryModelBuilder {
  
    private CarbonTable table;
    private QueryProjection projection;
    private IndexFilter indexFilter;
    private DataTypeConverter dataTypeConverter;
    private boolean forcedDetailRawQuery;
    private boolean readPageByPage;
    private boolean convertToRangeFilter = true;
    /**
     * log information
     */
    private static final Logger LOGGER =
        LogServiceFactory.getLogService(QueryModelBuilder.class.getName());
  
    public QueryModelBuilder(CarbonTable table) {
      this.table = table;
    }
  
    public QueryModelBuilder projectColumns(String[] projectionColumns) {
      Objects.requireNonNull(projectionColumns);
      String factTableName = table.getTableName();
      QueryProjection projection = new QueryProjection();
  
      int i = 0;
      for (String projectionColumnName : projectionColumns) {
        CarbonDimension dimension = table.getDimensionByName(projectionColumnName);
        if (dimension != null) {
          CarbonDimension complexParentDimension = dimension.getComplexParentDimension();
          if (null != complexParentDimension && dimension.getDataType() == DataTypes.DATE) {
            if (!isAlreadyExists(complexParentDimension, projection.getDimensions())) {
              projection.addDimension(complexParentDimension, i);
              i++;
            }
          } else {
            projection.addDimension(dimension, i);
            i++;
          }
        } else {
          CarbonMeasure measure = table.getMeasureByName(projectionColumnName);
          if (measure == null) {
            throw new RuntimeException(
                projectionColumnName + " column not found in the table " + factTableName);
          }
          projection.addMeasure(measure, i);
          i++;
        }
      }
      projection = optimizeProjectionForComplexColumns(projection, projectionColumns, factTableName);
      List<String> projectionDimensionAndMeasures = new ArrayList<>();
      this.projection = projection;
      for (ProjectionDimension projectionDimension : projection.getDimensions()) {
        projectionDimensionAndMeasures.add(projectionDimension.getColumnName());
      }
      for (ProjectionMeasure projectionMeasure : projection.getMeasures()) {
        projectionDimensionAndMeasures.add(projectionMeasure.getColumnName());
      }
      LOGGER.info("Projection Columns: " + projectionDimensionAndMeasures);
      return this;
    }
  
    /**
     * For complex dimensions, check if the dimension already exists in the projection list or not
     *
     * @param dimension
     * @param projectionDimensions
     * @return
     */
    private boolean isAlreadyExists(CarbonDimension dimension,
        List<ProjectionDimension> projectionDimensions) {
      boolean exists = false;
      for (ProjectionDimension projectionDimension : projectionDimensions) {
        if (dimension.getColName().equals(projectionDimension.getColumnName())) {
          exists = true;
          break;
        }
      }
      return exists;
    }
  
    private QueryProjection optimizeProjectionForComplexColumns(QueryProjection projection,
        String[] projectionColumns, String factTableName) {
      // Get the List of Complex Column Projection.
      // The optimization techniques which can be applied are
      // A. Merging in Driver Side
      // B. Merging in the result Collector side.
      // Merging is driver side cases are
      // Driver merging will eliminate one of the CarbonDimension.
      // Executor merging will merge the column output in Result Collector.
      // In this routine we are going to do driver merging and leave executor merging.
      Map<Integer, List<Integer>> complexColumnMap = new HashMap<>();
      List<ProjectionDimension> carbonDimensions = projection.getDimensions();
      // Traverse and find out if the top most parent of projection column is already there
      List<CarbonDimension> projectionDimensionToBeMerged = new ArrayList<>();
      for (ProjectionDimension projectionDimension : carbonDimensions) {
        CarbonDimension complexParentDimension =
            projectionDimension.getDimension().getComplexParentDimension();
        if (null != complexParentDimension && isAlreadyExists(complexParentDimension,
            carbonDimensions)) {
          projectionDimensionToBeMerged.add(projectionDimension.getDimension());
        }
      }
  
      if (projectionDimensionToBeMerged.size() != 0) {
        projection =
            removeMergedDimensions(projectionDimensionToBeMerged, projectionColumns, factTableName);
        carbonDimensions = projection.getDimensions();
      }
  
      for (ProjectionDimension cols : carbonDimensions) {
        // get all the Projections with Parent Ordinal Set.
        if (null != cols.getDimension().getComplexParentDimension()) {
          if (complexColumnMap.get(cols.getDimension().getComplexParentDimension().getOrdinal())
              != null) {
            List<Integer> childColumns =
                complexColumnMap.get(cols.getDimension().getComplexParentDimension().getOrdinal());
            childColumns.add(cols.getDimension().getOrdinal());
            complexColumnMap
                .put(cols.getDimension().getComplexParentDimension().getOrdinal(), childColumns);
          } else {
            List<Integer> childColumns = new ArrayList<>();
            childColumns.add(cols.getDimension().getOrdinal());
            complexColumnMap
                .put(cols.getDimension().getComplexParentDimension().getOrdinal(), childColumns);
          }
        }
      }
  
      // Traverse the Map to Find any columns are parent.
      for (Map.Entry<Integer, List<Integer>> entry : complexColumnMap.entrySet()) {
        List<Integer> childOrdinals = entry.getValue();
        if (childOrdinals.size() > 1) {
          // In case of more that one child, have to check if the child columns are in the same path
          // and have a common parent.
          Collections.sort(childOrdinals);
          List<CarbonDimension> mergedDimensions = mergeChildColumns(childOrdinals);
          if (mergedDimensions.size() > 0) {
            projection = removeMergedDimensions(mergedDimensions, projectionColumns, factTableName);
          }
        }
      }
      return projection;
    }
  
    /**
     * Remove the dimensions from the projection list which are merged
     *
     * @param mergedDimensions
     * @param projectionColumns
     * @param factTableName
     * @return
     */
    private QueryProjection removeMergedDimensions(List<CarbonDimension> mergedDimensions,
        String[] projectionColumns, String factTableName) {
      QueryProjection queryProjection = new QueryProjection();
      int i = 0;
      for (String projectionColumnName : projectionColumns) {
        CarbonDimension dimension = table.getDimensionByName(projectionColumnName);
        if (dimension != null) {
          if (!mergedDimensions.contains(dimension)) {
            if (!isAlreadyExists(dimension, queryProjection.getDimensions())) {
              queryProjection.addDimension(dimension, i);
              i++;
            }
          }
        } else {
          CarbonMeasure measure = table.getMeasureByName(projectionColumnName);
          if (measure == null) {
            throw new RuntimeException(
                projectionColumnName + " column not found in the table " + factTableName);
          }
          queryProjection.addMeasure(measure, i);
          i++;
        }
      }
      return queryProjection;
    }
  
    private List<CarbonDimension> mergeChildColumns(List<Integer> childOrdinals) {
      // Check If children if they are in the path of not.
      List<CarbonDimension> mergedChild = new ArrayList<>();
      List<CarbonDimension> dimList = table.getVisibleDimensions();
      for (int i = 0; i < childOrdinals.size(); i++) {
        for (int j = i; j < childOrdinals.size(); j++) {
          CarbonDimension parentDimension = getDimensionBasedOnOrdinal(dimList, childOrdinals.get(i));
          CarbonDimension childDimension = getDimensionBasedOnOrdinal(dimList, childOrdinals.get(j));
          if (!mergedChild.contains(childOrdinals.get(j)) && checkChildrenInSamePath(parentDimension,
              childDimension)) {
            mergedChild.add(childDimension);
          }
        }
      }
      return mergedChild;
    }
  
    private boolean checkChildrenInSamePath(CarbonDimension parentDimension,
        CarbonDimension childDimension) {
      if (parentDimension.getColName().equals(childDimension.getColName())) {
        return false;
      }
      return checkForChildColumns(parentDimension, childDimension);
    }
  
    private boolean checkForChildColumns(CarbonDimension parentDimension,
        CarbonDimension childDimension) {
      boolean output = false;
      if (parentDimension.getOrdinal() == childDimension.getOrdinal()) {
        output = true;
      } else if (parentDimension.getNumberOfChild() > 0) {
        for (int i = 0; i < parentDimension.getNumberOfChild() && !output; i++) {
          output =
              checkForChildColumns(parentDimension.getListOfChildDimensions().get(i), childDimension);
        }
      } else {
        output = false;
      }
      return output;
    }
  
    private CarbonDimension getDimensionBasedOnOrdinal(List<CarbonDimension> dimList,
        Integer ordinal) {
      for (CarbonDimension dims : dimList) {
        if (dims.getOrdinal() == ordinal) {
          return dims;
        } else if (dims.getNumberOfChild() > 0) {
          CarbonDimension dimensionBasedOnOrdinal =
              getDimensionBasedOnOrdinal(dims.getListOfChildDimensions(), ordinal);
          if (null != dimensionBasedOnOrdinal) {
            return dimensionBasedOnOrdinal;
          }
        }
      }
      return null;
    }
  
    public QueryModelBuilder projectAllColumns() {
      QueryProjection projection = new QueryProjection();
      List<CarbonDimension> dimensions = table.getVisibleDimensions();
      for (int i = 0; i < dimensions.size(); i++) {
        projection.addDimension(dimensions.get(i), i);
      }
      List<CarbonMeasure> measures = table.getVisibleMeasures();
      for (int i = 0; i < measures.size(); i++) {
        projection.addMeasure(measures.get(i), i);
      }
      this.projection = projection;
      return this;
    }
  
    public QueryModelBuilder filterExpression(IndexFilter filterExpression) {
      this.indexFilter = filterExpression;
      return this;
    }
  
    public QueryModelBuilder dataConverter(DataTypeConverter dataTypeConverter) {
      this.dataTypeConverter = dataTypeConverter;
      return this;
    }
  
    public QueryModelBuilder enableForcedDetailRawQuery() {
      this.forcedDetailRawQuery = true;
      return this;
    }
  
    public QueryModelBuilder convertToRangeFilter(boolean convertToRangeFilter) {
      this.convertToRangeFilter = convertToRangeFilter;
      return this;
    }
  
    public boolean isConvertToRangeFilter() {
      return this.convertToRangeFilter;
    }
  
    public void enableReadPageByPage() {
      this.readPageByPage = true;
    }
  
    public QueryModel build() {
      QueryModel queryModel = QueryModel.newInstance(table);
      queryModel.setConverter(dataTypeConverter);
      queryModel.setForcedDetailRawQuery(forcedDetailRawQuery);
      queryModel.setReadPageByPage(readPageByPage);
      queryModel.setProjection(projection);
  
      if (table.isTransactionalTable() && !table.hasColumnDrift()) {
        // set the filter to the query model in order to filter blocklet before scan
        boolean[] isFilterDimensions = new boolean[table.getDimensionOrdinalMax()];
        boolean[] isFilterMeasures = new boolean[table.getAllMeasures().size()];
        queryModel.setIsFilterDimensions(isFilterDimensions);
        queryModel.setIsFilterMeasures(isFilterMeasures);
        // In case of Dictionary Include Range Column we do not optimize the range expression
        if (indexFilter != null) {
          if (isConvertToRangeFilter()) {
            indexFilter.processFilterExpression(isFilterDimensions, isFilterMeasures);
          } else {
            indexFilter.processFilterExpressionWithoutRange(isFilterDimensions, isFilterMeasures);
          }
        }
      }
      queryModel.setIndexFilter(indexFilter);
      return queryModel;
    }
  }
  