MultiPartitionDesc.java

// 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.doris.analysis;

import org.apache.doris.analysis.TimestampArithmeticExpr.TimeUnit;
import org.apache.doris.catalog.DynamicPartitionProperty;
import org.apache.doris.catalog.ReplicaAllocation;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.util.DynamicPartitionUtil;
import org.apache.doris.common.util.TimeUtils;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.time.temporal.WeekFields;
import java.util.List;
import java.util.Map;

// to describe the key range multi partition's information in create table stmt
public class MultiPartitionDesc implements AllPartitionDesc {
    public static final String HOURS_FORMAT = "yyyyMMddHH";
    public static final String HOUR_FORMAT = "yyyy-MM-dd HH";
    public static final String DATES_FORMAT = "yyyyMMdd";
    public static final String DATE_FORMAT = "yyyy-MM-dd";
    public static final String MONTHS_FORMAT = "yyyyMM";
    public static final String MONTH_FORMAT = "yyyy-MM";
    public static final String YEAR_FORMAT = "yyyy";
    public static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";


    private LocalDateTime startTime;
    private LocalDateTime endTime;

    private String startString;
    private String endString;

    private DateTimeFormatter startDateTimeFormat;
    private DateTimeFormatter endDateTimeFormat;


    private Long interval;
    private final PartitionKeyDesc partitionKeyDesc;
    private TimeUnit timeUnitType;
    private final Map<String, String> properties;
    private final List<SinglePartitionDesc> singlePartitionDescList = Lists.newArrayList();

    private final ImmutableSet<TimeUnit> timeUnitTypeMultiPartition = ImmutableSet.of(
            TimeUnit.HOUR,
            TimeUnit.DAY,
            TimeUnit.WEEK,
            TimeUnit.MONTH,
            TimeUnit.YEAR
    );

    private final Integer maxAllowedLimit = Config.max_multi_partition_num;
    //multi_partition_name_prefix default: p_
    private final String partitionPrefix = Config.multi_partition_name_prefix;

    public MultiPartitionDesc(PartitionKeyDesc partitionKeyDesc,
            Map<String, String> properties) throws AnalysisException {
        this.partitionKeyDesc = partitionKeyDesc;
        this.properties = properties;
        this.intervalTrans();
        this.trans();
    }

    /**
     * for Nereids
     */
    public MultiPartitionDesc(PartitionKeyDesc partitionKeyDesc, ReplicaAllocation replicaAllocation,
            Map<String, String> properties) throws AnalysisException {
        this.partitionKeyDesc = partitionKeyDesc;
        this.properties = properties;
        this.intervalTrans();
        this.trans();
        for (SinglePartitionDesc desc : getSinglePartitionDescList()) {
            desc.setReplicaAlloc(replicaAllocation);
            desc.setAnalyzed(true);
        }
    }

    public List<SinglePartitionDesc> getSinglePartitionDescList() throws AnalysisException {
        if (singlePartitionDescList.size() == 0) {
            if (this.timeUnitType == null) {
                buildNumberMultiPartitionToSinglePartitionDesc();
            } else {
                buildTimeMultiPartitionToSinglePartitionDesc();
            }
        }
        return singlePartitionDescList;
    }


    private List<SinglePartitionDesc> buildNumberMultiPartitionToSinglePartitionDesc() throws AnalysisException {
        long countNum = 0;
        long beginNum;
        long endNum;
        String partitionPrefix = this.partitionPrefix;


        try {
            beginNum = Long.parseLong(startString);
            endNum = Long.parseLong(endString);
        } catch (NumberFormatException ex) {
            throw new AnalysisException("Batch build partition INTERVAL is number type "
                    + "but From or TO does not type match.");
        }
        if (beginNum >= endNum) {
            throw new AnalysisException("Batch build partition From value should less then TO value.");
        }

        while (beginNum < endNum) {
            String partitionName = partitionPrefix + beginNum;
            PartitionValue lowerPartitionValue = new PartitionValue(beginNum);
            beginNum += interval;
            Long currentEnd = Math.min(beginNum, endNum);
            PartitionValue upperPartitionValue = new PartitionValue(currentEnd);
            partitionName = partitionName + "_" + currentEnd;
            PartitionKeyDesc partitionKeyDesc = PartitionKeyDesc.createFixed(
                    Lists.newArrayList(lowerPartitionValue),
                    Lists.newArrayList(upperPartitionValue)
            );
            singlePartitionDescList.add(
                    new SinglePartitionDesc(
                            false,
                            partitionName,
                            partitionKeyDesc,
                            properties)
            );
            countNum++;
            if (countNum > maxAllowedLimit) {
                throw new AnalysisException("The number of Multi partitions too much, should not exceed:"
                        + maxAllowedLimit);
            }
        }
        return singlePartitionDescList;
    }

    private List<SinglePartitionDesc> buildTimeMultiPartitionToSinglePartitionDesc() throws AnalysisException {
        String partitionName;
        long countNum = 0;
        int startDayOfWeek = 1;
        int startDayOfMonth = 1;
        String partitionPrefix = this.partitionPrefix;
        LocalDateTime startTime = this.startTime;
        if (properties != null) {
            if (properties.containsKey(DynamicPartitionProperty.START_DAY_OF_WEEK)) {
                String dayOfWeekStr = properties.get(DynamicPartitionProperty.START_DAY_OF_WEEK);
                try {
                    DynamicPartitionUtil.checkStartDayOfWeek(dayOfWeekStr);
                } catch (DdlException e) {
                    throw new AnalysisException(e.getMessage());
                }
                startDayOfWeek = Integer.parseInt(dayOfWeekStr);
            }
            if (properties.containsKey(DynamicPartitionProperty.START_DAY_OF_MONTH)) {
                String dayOfMonthStr = properties.get(DynamicPartitionProperty.START_DAY_OF_MONTH);
                try {
                    DynamicPartitionUtil.checkStartDayOfMonth(dayOfMonthStr);
                } catch (DdlException e) {
                    throw new AnalysisException(e.getMessage());
                }
                startDayOfMonth = Integer.parseInt(dayOfMonthStr);
            }

            if (properties.containsKey(DynamicPartitionProperty.CREATE_HISTORY_PARTITION)) {
                properties.put(DynamicPartitionProperty.CREATE_HISTORY_PARTITION, "false");
            }
            if (properties.containsKey(DynamicPartitionProperty.PREFIX)) {
                partitionPrefix = properties.get(DynamicPartitionProperty.PREFIX);
                try {
                    DynamicPartitionUtil.checkPrefix(partitionPrefix);
                } catch (DdlException e) {
                    throw new AnalysisException(e.getMessage());
                }
            }
        }
        WeekFields weekFields = WeekFields.of(DayOfWeek.of(startDayOfWeek), 1);
        while (startTime.isBefore(this.endTime)) {
            PartitionValue lowerPartitionValue = new PartitionValue(
                    startTime.format(dateTypeFormat(partitionKeyDesc.getLowerValues().get(0).getStringValue()))
            );
            switch (this.timeUnitType) {
                case HOUR:
                    partitionName = partitionPrefix + startTime.format(DateTimeFormatter.ofPattern(HOURS_FORMAT));
                    startTime = startTime.plusHours(interval);
                    break;
                case DAY:
                    partitionName = partitionPrefix + startTime.format(DateTimeFormatter.ofPattern(DATES_FORMAT));
                    startTime = startTime.plusDays(interval);
                    break;
                case WEEK:
                    LocalDate localDate = LocalDate.of(startTime.getYear(), startTime.getMonthValue(),
                            startTime.getDayOfMonth());
                    int weekOfYear = localDate.get(weekFields.weekOfYear());
                    partitionName = String.format("%s%s_%02d", partitionPrefix,
                            startTime.format(DateTimeFormatter.ofPattern(YEAR_FORMAT)), weekOfYear);
                    startTime = startTime.with(ChronoField.DAY_OF_WEEK, startDayOfMonth);
                    startTime = startTime.plusWeeks(interval);
                    break;
                case MONTH:
                    partitionName = partitionPrefix + startTime.format(DateTimeFormatter.ofPattern(MONTHS_FORMAT));
                    startTime = startTime.withDayOfMonth(startDayOfMonth);
                    startTime = startTime.plusMonths(interval);
                    break;
                case YEAR:
                    partitionName = partitionPrefix + startTime.format(DateTimeFormatter.ofPattern(YEAR_FORMAT));
                    startTime = startTime.withDayOfYear(1);
                    startTime = startTime.plusYears(interval);
                    break;
                default:
                    throw new AnalysisException("Multi build partition does not support time interval type: "
                            + this.timeUnitType);
            }
            if (this.timeUnitType != TimeUnit.DAY && startTime.isAfter(this.endTime)) {
                startTime = this.endTime;
            }
            PartitionValue upperPartitionValue = new PartitionValue(
                    startTime.format(dateTypeFormat(partitionKeyDesc.getUpperValues().get(0).getStringValue()))
            );
            PartitionKeyDesc partitionKeyDesc = PartitionKeyDesc.createFixed(
                    Lists.newArrayList(lowerPartitionValue),
                    Lists.newArrayList(upperPartitionValue)
            );
            singlePartitionDescList.add(
                    new SinglePartitionDesc(
                            false,
                            partitionName,
                            partitionKeyDesc,
                            properties)
            );

            countNum++;
            if (countNum > maxAllowedLimit) {
                throw new AnalysisException("The number of Multi partitions too much, should not exceed:"
                        + maxAllowedLimit);
            }
        }
        return singlePartitionDescList;
    }

    private void trans() throws AnalysisException {

        if (partitionKeyDesc.getLowerValues().size() != 1 || partitionKeyDesc.getUpperValues().size() != 1) {
            throw new AnalysisException("partition column number in multi partition clause must be one "
                    + "but start column size is " + partitionKeyDesc.getLowerValues().size()
                    + ", end column size is " + partitionKeyDesc.getUpperValues().size() + ".");
        }

        this.startString = partitionKeyDesc.getLowerValues().get(0).getStringValue();
        this.endString = partitionKeyDesc.getUpperValues().get(0).getStringValue();

        if (this.timeUnitType == null) {
            if (this.startString.compareTo(this.endString) >= 0) {
                throw new AnalysisException("Multi build partition start number should less than end number.");
            }
        } else {
            try {
                this.startDateTimeFormat = dateFormat(this.timeUnitType, this.startString);
                this.endDateTimeFormat = dateFormat(this.timeUnitType, this.endString);
                this.startTime = TimeUtils.formatDateTimeAndFullZero(this.startString, startDateTimeFormat);
                this.endTime = TimeUtils.formatDateTimeAndFullZero(this.endString, endDateTimeFormat);
            } catch (Exception e) {
                throw new AnalysisException("Multi build partition start or end time style is illegal.");
            }

            if (!this.startTime.isBefore(this.endTime)) {
                throw new AnalysisException("Multi build partition start time should less than end time.");
            }
        }

    }


    private void intervalTrans() throws AnalysisException {
        this.interval = partitionKeyDesc.getTimeInterval();
        String timeType = partitionKeyDesc.getTimeType();

        if (timeType == null) {
            throw new AnalysisException("Unknown time interval type for Multi build partition.");
        }

        if (this.interval <= 0) {
            throw new AnalysisException("Multi partition time interval must be larger than zero.");
        }

        if (!timeType.equals("")) {
            try {
                this.timeUnitType = TimeUnit.valueOf(timeType.toUpperCase());
            } catch (Exception e) {
                throw new AnalysisException("Multi build partition got an unknown time interval type: "
                        + timeType);
            }
        }

        if (this.timeUnitType != null && !timeUnitTypeMultiPartition.contains(this.timeUnitType)) {
            throw new AnalysisException("Multi build partition does not support time interval type: "
                    + this.timeUnitType);
        }
    }

    private static DateTimeFormatter dateFormat(TimeUnit timeUnitType,
            String dateTimeStr) throws AnalysisException {
        DateTimeFormatter res;
        switch (timeUnitType) {
            case HOUR:
                if (dateTimeStr.length() == 10) {
                    res = DateTimeFormatter.ofPattern(HOURS_FORMAT);
                } else if (dateTimeStr.length() == 13) {
                    res = DateTimeFormatter.ofPattern(HOUR_FORMAT);
                } else if (dateTimeStr.length() == 19) {
                    res = DateTimeFormatter.ofPattern(DATETIME_FORMAT);
                } else {
                    throw new AnalysisException("can not probe datetime(hour) format:" + dateTimeStr);
                }
                break;
            case DAY: case WEEK:
                if (dateTimeStr.length() == 8) {
                    res = DateTimeFormatter.ofPattern(DATES_FORMAT);
                } else if (dateTimeStr.length() == 10) {
                    res = DateTimeFormatter.ofPattern(DATE_FORMAT);
                } else if (dateTimeStr.length() == 19) {
                    res = DateTimeFormatter.ofPattern(DATETIME_FORMAT);
                } else {
                    throw new AnalysisException("can not probe datetime(day or week) format:" + dateTimeStr);
                }
                break;
            case MONTH:
                if (dateTimeStr.length() == 6) {
                    res = DateTimeFormatter.ofPattern(MONTHS_FORMAT);
                } else if (dateTimeStr.length() == 7) {
                    res = DateTimeFormatter.ofPattern(MONTH_FORMAT);
                } else if (dateTimeStr.length() == 10) {
                    res = DateTimeFormatter.ofPattern(DATE_FORMAT);
                } else if (dateTimeStr.length() == 19) {
                    res = DateTimeFormatter.ofPattern(DATETIME_FORMAT);
                } else {
                    throw new AnalysisException("can not probe datetime(month) format:" + dateTimeStr);
                }
                break;
            case YEAR:
                if (dateTimeStr.length() == 4) {
                    res = DateTimeFormatter.ofPattern(YEAR_FORMAT);
                } else if (dateTimeStr.length() == 8) {
                    res = DateTimeFormatter.ofPattern(DATES_FORMAT);
                } else if (dateTimeStr.length() == 10) {
                    res = DateTimeFormatter.ofPattern(DATE_FORMAT);
                } else if (dateTimeStr.length() == 19) {
                    res = DateTimeFormatter.ofPattern(DATETIME_FORMAT);
                } else {
                    throw new AnalysisException("can not probe datetime(year) format:" + dateTimeStr);
                }
                break;
            default:
                throw new AnalysisException("Multi build partition does not support time interval type: "
                        + timeUnitType);
        }
        return res;
    }

    private DateTimeFormatter dateTypeFormat(String dateTimeStr) {
        String s = DATE_FORMAT;
        if (this.timeUnitType.equals(TimeUnit.HOUR) || dateTimeStr.length() == 19) {
            s = DATETIME_FORMAT;
        }
        return DateTimeFormatter.ofPattern(s);
    }

}