ExprToStringValueVisitor.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.catalog.ScalarType;
import org.apache.doris.catalog.StructType;
import org.apache.doris.catalog.Type;
import org.apache.doris.common.FeConstants;
import org.apache.doris.common.FractionalFormat;
import org.apache.doris.foundation.format.FormatOptions;
import org.apache.doris.nereids.util.DateUtils;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;

/**
 * Visitor that generates string values for Expr instances, handling different
 * output modes (query, stream load) and complex type nesting.
 *
 * <p>This visitor extracts the logic previously in {@code getStringValueForQuery},
 * {@code getStringValueInComplexTypeForQuery}, and {@code getStringValueForStreamLoad}
 * from Expr subclasses, following the same visitor pattern as {@link ExprToSqlVisitor}.
 */
public class ExprToStringValueVisitor extends ExprVisitor<String, StringValueContext> {
    private static final Logger LOG = LogManager.getLogger(ExprToStringValueVisitor.class);

    public static final ExprToStringValueVisitor INSTANCE = new ExprToStringValueVisitor();

    @Override
    public String visit(Expr expr, StringValueContext ctx) {
        return expr.getStringValue();
    }

    @Override
    public String visitDateLiteral(DateLiteral expr, StringValueContext ctx) {
        String value;
        if (expr.getType().isTimeStampTz()) {
            try {
                ZoneId dorisZone = DateUtils.getTimeZone();
                String offset = dorisZone.getRules().getOffset(java.time.Instant.now()).toString();
                DateLiteral dateLiteral = new DateLiteral(expr.getStringValue(),
                        ScalarType.createDatetimeV2Type(((ScalarType) expr.getType()).getScalarScale()));
                value = dateLiteral.getStringValue() + offset;
            } catch (Exception e) {
                LOG.warn("generate timestamptz({})'s string value for query failed. ",
                        expr.getStringValue(), e);
                value = expr.getStringValue();
            }
        } else {
            value = expr.getStringValue();
        }
        if (ctx.isInComplexType()) {
            return wrapWithQuotes(value, ctx);
        }
        return value;
    }

    @Override
    public String visitFloatLiteral(FloatLiteral expr, StringValueContext ctx) {
        String value;
        if (expr.getType() == Type.TIMEV2) {
            String timeStr = expr.getStringValue();
            value = timeStr.substring(1, timeStr.length() - 1);
        } else {
            double dValue = expr.getValue();
            if (expr.getType() == Type.FLOAT) {
                Float fValue = (float) dValue;
                if (fValue.equals(Float.POSITIVE_INFINITY)) {
                    dValue = Double.POSITIVE_INFINITY;
                }
                if (fValue.equals(Float.NEGATIVE_INFINITY)) {
                    dValue = Double.NEGATIVE_INFINITY;
                }
            }
            value = FractionalFormat.getFormatStringValue(dValue,
                    expr.getType() == Type.DOUBLE ? 16 : 7,
                    expr.getType() == Type.DOUBLE ? "%.15E" : "%.6E");
        }
        if (ctx.isInComplexType() && expr.getType() == Type.TIMEV2) {
            return wrapWithQuotes(value, ctx);
        }
        return value;
    }

    @Override
    public String visitBoolLiteral(BoolLiteral expr, StringValueContext ctx) {
        FormatOptions options = ctx.getFormatOptions();
        if (options.level > 0) {
            return options.isBoolValueNum() ? expr.getStringValue() : (expr.getValue() ? "true" : "false");
        }
        return expr.getStringValue();
    }

    @Override
    public String visitNullLiteral(NullLiteral expr, StringValueContext ctx) {
        if (ctx.isInComplexType()) {
            return ctx.getFormatOptions().getNullFormat();
        }
        if (ctx.isForStreamLoad()) {
            return FeConstants.null_string;
        }
        return null;
    }

    @Override
    public String visitDecimalLiteral(DecimalLiteral expr, StringValueContext ctx) {
        return expr.getValue().toPlainString();
    }

    @Override
    public String visitArrayLiteral(ArrayLiteral expr, StringValueContext ctx) {
        FormatOptions options = ctx.getFormatOptions();
        List<String> list = new ArrayList<>(expr.getChildren().size());
        ++options.level;
        for (Expr child : expr.getChildren()) {
            list.add(child.accept(this, ctx.asComplexType()));
        }
        --options.level;
        return "[" + StringUtils.join(list, options.getCollectionDelim()) + "]";
    }

    @Override
    public String visitMapLiteral(MapLiteral expr, StringValueContext ctx) {
        FormatOptions options = ctx.getFormatOptions();
        List<Expr> children = expr.getChildren();
        List<String> list = new ArrayList<>(children.size());
        ++options.level;
        StringValueContext childCtx = ctx.asComplexType();
        for (int i = 0; i < children.size() && i + 1 < children.size(); i += 2) {
            if (children.get(i).getType().isComplexType()) {
                throw new UnsupportedOperationException(
                        "Unsupported key type for MAP: " + children.get(i).getType());
            }
            list.add(children.get(i).accept(this, childCtx)
                    + options.getMapKeyDelim()
                    + children.get(i + 1).accept(this, childCtx));
        }
        --options.level;
        return "{" + StringUtils.join(list, options.getCollectionDelim()) + "}";
    }

    @Override
    public String visitStructLiteral(StructLiteral expr, StringValueContext ctx) {
        FormatOptions options = ctx.getFormatOptions();
        List<Expr> children = expr.getChildren();
        List<String> list = new ArrayList<>(children.size());
        if (ctx.isForStreamLoad()) {
            for (int i = 0; i < children.size(); i++) {
                list.add(children.get(i).accept(this, ctx.asQueryComplexType()));
            }
        } else {
            ++options.level;
            StringValueContext childCtx = ctx.asComplexType();
            for (int i = 0; i < children.size(); i++) {
                list.add(options.getNestedStringWrapper()
                        + ((StructType) expr.getType()).getFields().get(i).getName()
                        + options.getNestedStringWrapper()
                        + options.getMapKeyDelim()
                        + children.get(i).accept(this, childCtx));
            }
            --options.level;
        }
        return "{" + StringUtils.join(list, options.getCollectionDelim()) + "}";
    }

    @Override
    public String visitCastExpr(CastExpr expr, StringValueContext ctx) {
        if (ctx.isInComplexType() || ctx.isForStreamLoad()) {
            return expr.getChildren().get(0).accept(this, ctx);
        }
        return expr.getStringValue();
    }

    @Override
    public String visitStringLiteral(StringLiteral expr, StringValueContext ctx) {
        if (ctx.isInComplexType()) {
            return wrapWithQuotes(expr.getStringValue(), ctx);
        }
        return expr.getStringValue();
    }

    @Override
    public String visitIPv4Literal(IPv4Literal expr, StringValueContext ctx) {
        if (ctx.isInComplexType()) {
            return wrapWithQuotes(expr.getStringValue(), ctx);
        }
        return expr.getStringValue();
    }

    @Override
    public String visitIPv6Literal(IPv6Literal expr, StringValueContext ctx) {
        if (ctx.isInComplexType()) {
            return wrapWithQuotes(expr.getStringValue(), ctx);
        }
        return expr.getStringValue();
    }

    @Override
    public String visitVarBinaryLiteral(VarBinaryLiteral expr, StringValueContext ctx) {
        if (ctx.isInComplexType()) {
            return wrapWithQuotes(expr.getStringValue(), ctx);
        }
        return expr.getStringValue();
    }

    @Override
    public String visitPlaceHolderExpr(PlaceHolderExpr expr, StringValueContext ctx) {
        if (ctx.isInComplexType()) {
            return wrapWithQuotes(expr.getStringValue(), ctx);
        }
        return expr.getStringValue();
    }

    @Override
    public String visitJsonLiteral(JsonLiteral expr, StringValueContext ctx) {
        if (ctx.isInComplexType()) {
            return null;
        }
        return expr.getStringValue();
    }

    @Override
    public String visitMaxLiteral(MaxLiteral expr, StringValueContext ctx) {
        if (ctx.isInComplexType()) {
            return null;
        }
        return expr.getStringValue();
    }

    private String wrapWithQuotes(String value, StringValueContext ctx) {
        String wrapper = ctx.getFormatOptions().getNestedStringWrapper();
        return wrapper + value + wrapper;
    }
}