PaimonDefaultValue.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.datasource.paimon;

import org.apache.doris.common.util.TimeUtils;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.literal.ArrayLiteral;
import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral;
import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
import org.apache.doris.nereids.trees.expressions.literal.DateV2Literal;
import org.apache.doris.nereids.trees.expressions.literal.DecimalV3Literal;
import org.apache.doris.nereids.trees.expressions.literal.DoubleLiteral;
import org.apache.doris.nereids.trees.expressions.literal.FloatLiteral;
import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
import org.apache.doris.nereids.trees.expressions.literal.Literal;
import org.apache.doris.nereids.trees.expressions.literal.MapLiteral;
import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
import org.apache.doris.nereids.trees.expressions.literal.SmallIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
import org.apache.doris.nereids.trees.expressions.literal.StructLiteral;
import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.VarBinaryLiteral;
import org.apache.doris.nereids.types.ArrayType;
import org.apache.doris.nereids.types.DataType;
import org.apache.doris.nereids.types.DateTimeV2Type;
import org.apache.doris.nereids.types.DecimalV3Type;
import org.apache.doris.nereids.types.MapType;
import org.apache.doris.nereids.types.StructType;
import org.apache.doris.nereids.types.VarBinaryType;

import com.google.common.annotations.VisibleForTesting;
import org.apache.paimon.data.BinaryString;
import org.apache.paimon.data.Decimal;
import org.apache.paimon.data.InternalArray;
import org.apache.paimon.data.InternalMap;
import org.apache.paimon.data.InternalRow;
import org.apache.paimon.data.LocalZoneTimestamp;
import org.apache.paimon.data.Timestamp;
import org.apache.paimon.types.DataField;
import org.apache.paimon.types.RowType;
import org.apache.paimon.utils.DefaultValueUtils;

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/** Converts a Paimon schema default into the equivalent Doris planner expression. */
public final class PaimonDefaultValue {
    private PaimonDefaultValue() {
    }

    public static Expression toDorisExpression(DataField field, DataType targetType) {
        if (field.defaultValue() == null) {
            return new NullLiteral(targetType);
        }
        try {
            Object value = DefaultValueUtils.convertDefaultValue(field.type(), field.defaultValue());
            return toDorisExpression(field.type(), value, targetType);
        } catch (Exception e) {
            throw new AnalysisException(String.format(
                    "Failed to convert Paimon default value %s for column '%s' of type %s",
                    field.defaultValue(), field.name(), field.type()), e);
        }
    }

    @VisibleForTesting
    static Expression toDorisExpression(
            org.apache.paimon.types.DataType paimonType, Object value, DataType targetType) {
        if (value == null) {
            return new NullLiteral(targetType);
        }
        switch (paimonType.getTypeRoot()) {
            case CHAR:
            case VARCHAR:
                return new StringLiteral(((BinaryString) value).toString());
            case BOOLEAN:
                return BooleanLiteral.of((Boolean) value);
            case BINARY:
            case VARBINARY:
                return targetType instanceof VarBinaryType
                        ? new VarBinaryLiteral(targetType, (byte[]) value)
                        : new StringLiteral(new String((byte[]) value, StandardCharsets.UTF_8));
            case DECIMAL:
                return new DecimalV3Literal(
                        (DecimalV3Type) targetType, ((Decimal) value).toBigDecimal());
            case TINYINT:
                return new TinyIntLiteral((Byte) value);
            case SMALLINT:
                return new SmallIntLiteral((Short) value);
            case INTEGER:
                return new IntegerLiteral((Integer) value);
            case BIGINT:
                return new BigIntLiteral((Long) value);
            case FLOAT:
                return new FloatLiteral((Float) value);
            case DOUBLE:
                return new DoubleLiteral((Double) value);
            case DATE:
                LocalDate date = LocalDate.ofEpochDay(((Integer) value).longValue());
                return new DateV2Literal(date.getYear(), date.getMonthValue(), date.getDayOfMonth());
            case TIMESTAMP_WITHOUT_TIME_ZONE:
                return timestampLiteral(((Timestamp) value).toLocalDateTime(), targetType);
            case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
                Instant instant = value instanceof LocalZoneTimestamp
                        ? ((LocalZoneTimestamp) value).toInstant()
                        : ((Timestamp) value).toInstant();
                return timestampLiteral(LocalDateTime.ofInstant(instant, TimeUtils.getDorisZoneId()), targetType);
            case ARRAY:
                return arrayLiteral((org.apache.paimon.types.ArrayType) paimonType,
                        (InternalArray) value, targetType);
            case MAP:
                return mapLiteral((org.apache.paimon.types.MapType) paimonType,
                        (InternalMap) value, targetType);
            case ROW:
                return rowLiteral((RowType) paimonType, (InternalRow) value, targetType);
            default:
                throw new AnalysisException("Unsupported Paimon write-default type: " + paimonType);
        }
    }

    private static Literal timestampLiteral(LocalDateTime value, DataType targetType) {
        DateTimeV2Type type = (DateTimeV2Type) targetType;
        return new DateTimeV2Literal(type, value.getYear(), value.getMonthValue(), value.getDayOfMonth(),
                value.getHour(), value.getMinute(), value.getSecond(), value.getNano() / 1_000);
    }

    private static Literal arrayLiteral(org.apache.paimon.types.ArrayType paimonType,
            InternalArray value, DataType targetType) {
        ArrayType arrayType = (ArrayType) targetType;
        org.apache.paimon.types.DataType elementType = paimonType.getElementType();
        InternalArray.ElementGetter getter = InternalArray.createElementGetter(elementType);
        List<Literal> elements = new ArrayList<>(value.size());
        for (int i = 0; i < value.size(); i++) {
            elements.add((Literal) toDorisExpression(
                    elementType, getter.getElementOrNull(value, i), arrayType.getItemType()));
        }
        return new ArrayLiteral(elements, targetType);
    }

    private static Literal mapLiteral(org.apache.paimon.types.MapType paimonType,
            InternalMap value, DataType targetType) {
        MapType mapType = (MapType) targetType;
        org.apache.paimon.types.DataType keyType = paimonType.getKeyType();
        org.apache.paimon.types.DataType valueType = paimonType.getValueType();
        InternalArray.ElementGetter keyGetter = InternalArray.createElementGetter(keyType);
        InternalArray.ElementGetter valueGetter = InternalArray.createElementGetter(valueType);
        Map<Literal, Literal> entries = new LinkedHashMap<>();
        for (int i = 0; i < value.size(); i++) {
            entries.put(
                    (Literal) toDorisExpression(
                            keyType, keyGetter.getElementOrNull(value.keyArray(), i), mapType.getKeyType()),
                    (Literal) toDorisExpression(
                            valueType, valueGetter.getElementOrNull(value.valueArray(), i), mapType.getValueType()));
        }
        return new MapLiteral(entries, targetType);
    }

    private static Literal rowLiteral(RowType paimonType, InternalRow value, DataType targetType) {
        StructType structType = (StructType) targetType;
        List<Literal> fields = new ArrayList<>(paimonType.getFieldCount());
        for (int i = 0; i < paimonType.getFieldCount(); i++) {
            org.apache.paimon.types.DataType fieldType = paimonType.getTypeAt(i);
            Object fieldValue = InternalRow.createFieldGetter(fieldType, i).getFieldOrNull(value);
            fields.add((Literal) toDorisExpression(
                    fieldType, fieldValue, structType.getFields().get(i).getDataType()));
        }
        return new StructLiteral(fields, targetType);
    }
}