StringArithmetic.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.nereids.trees.expressions.functions.executable;

import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.trees.expressions.ExecFunction;
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.DateTimeLiteral;
import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
import org.apache.doris.nereids.trees.expressions.literal.DecimalLiteral;
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.LargeIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.Literal;
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.StringLikeLiteral;
import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral;
import org.apache.doris.nereids.types.ArrayType;

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

import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

/**
 * executable functions:
 * concat
 */
public class StringArithmetic {
    private static Literal castStringLikeLiteral(StringLikeLiteral first, String value) {
        if (first instanceof StringLiteral) {
            return new StringLiteral(value);
        } else if (first instanceof VarcharLiteral) {
            return new VarcharLiteral(value);
        }
        throw new AnalysisException("Unsupported string literal type: " + first.getClass().getSimpleName());
    }

    /**
     * Executable arithmetic functions concat
     */
    @ExecFunction(name = "concat")
    public static Expression concatVarchar(StringLikeLiteral... values) {
        final StringBuilder sb = new StringBuilder();
        for (StringLikeLiteral value : values) {
            sb.append(value.getValue());
        }
        return castStringLikeLiteral(values[0], sb.toString());
    }

    private static String substringImpl(String first, int second, int third) {
        int stringLength = first.codePointCount(0, first.length());
        if (stringLength == 0) {
            return "";
        }
        long leftIndex = 0;
        if (second < (- stringLength)) {
            return "";
        } else if (second < 0) {
            leftIndex = stringLength + second;
        } else if (second <= stringLength) {
            leftIndex = second - 1;
        } else {
            return "";
        }
        long rightIndex = 0;
        if (third <= 0) {
            return "";
        } else if ((third + leftIndex) > stringLength) {
            rightIndex = stringLength;
        } else {
            rightIndex = third + leftIndex;
        }
        // at here leftIndex and rightIndex can not be exceeding boundary
        int finalLeftIndex = first.offsetByCodePoints(0, (int) leftIndex);
        int finalRightIndex = first.offsetByCodePoints(0, (int) rightIndex);
        // left index and right index are in integer range because of definition, so we can safely cast it to int
        return first.substring(finalLeftIndex, finalRightIndex);
    }

    /**
     * Executable arithmetic functions substring
     */
    @ExecFunction(name = "substring")
    public static Expression substringVarcharIntInt(StringLikeLiteral first,
                                                        IntegerLiteral second, IntegerLiteral third) {
        return castStringLikeLiteral(first, substringImpl(first.getValue(), second.getValue(), third.getValue()));
    }

    /**
     * Executable arithmetic functions length
     */
    @ExecFunction(name = "length")
    public static Expression lengthVarchar(StringLikeLiteral first) {
        return new IntegerLiteral(first.getValue().getBytes(StandardCharsets.UTF_8).length);
    }

    /**
     * Executable arithmetic functions Lower
     */
    @ExecFunction(name = "lower")
    public static Expression lowerVarchar(StringLikeLiteral first) {
        StringBuilder result = new StringBuilder(first.getValue().length());
        for (char c : first.getValue().toCharArray()) {
            result.append(Character.toLowerCase(c));
        }
        return castStringLikeLiteral(first, result.toString());
    }

    /**
     * Executable arithmetic functions Upper
     */
    @ExecFunction(name = "upper")
    public static Expression upperVarchar(StringLikeLiteral first) {
        StringBuilder result = new StringBuilder(first.getValue().length());
        for (char c : first.getValue().toCharArray()) {
            result.append(Character.toUpperCase(c));
        }
        return castStringLikeLiteral(first, result.toString());
    }

    private static String trimImpl(String first, String second, boolean left, boolean right) {
        String result = first;
        String afterReplace = first;
        if (left) {
            do {
                result = afterReplace;
                if (result.startsWith(second)) {
                    afterReplace = result.substring(second.length());
                }
            } while (!afterReplace.equals(result));
        }
        if (right) {
            do {
                result = afterReplace;
                if (result.endsWith(second)) {
                    afterReplace = result.substring(0, result.length() - second.length());
                }
            } while (!afterReplace.equals(result));
        }
        return result;
    }

    private static String trimInImpl(String first, String second, boolean left, boolean right) {
        StringBuilder result = new StringBuilder(first);

        if (left) {
            int start = 0;
            while (start < result.length() && second.indexOf(result.charAt(start)) != -1) {
                start++;
            }
            result.delete(0, start);
        }
        if (right) {
            int end = result.length();
            while (end > 0 && second.indexOf(result.charAt(end - 1)) != -1) {
                end--;
            }
            result.delete(end, result.length());
        }

        return result.toString();
    }

    /**
     * Executable arithmetic functions Trim
     */
    @ExecFunction(name = "trim")
    public static Expression trimVarchar(StringLikeLiteral first) {
        return castStringLikeLiteral(first, trimImpl(first.getValue(), " ", true, true));
    }

    /**
     * Executable arithmetic functions Trim
     */
    @ExecFunction(name = "trim")
    public static Expression trimVarcharVarchar(StringLikeLiteral first, StringLikeLiteral second) {
        return castStringLikeLiteral(first, trimImpl(first.getValue(), second.getValue(), true, true));
    }

    /**
     * Executable arithmetic functions ltrim
     */
    @ExecFunction(name = "ltrim")
    public static Expression ltrimVarchar(StringLikeLiteral first) {
        return castStringLikeLiteral(first, trimImpl(first.getValue(), " ", true, false));
    }

    /**
     * Executable arithmetic functions ltrim
     */
    @ExecFunction(name = "ltrim")
    public static Expression ltrimVarcharVarchar(StringLikeLiteral first, StringLikeLiteral second) {
        return castStringLikeLiteral(first, trimImpl(first.getValue(), second.getValue(), true, false));
    }

    /**
     * Executable arithmetic functions rtrim
     */
    @ExecFunction(name = "rtrim")
    public static Expression rtrimVarchar(StringLikeLiteral first) {
        return castStringLikeLiteral(first, trimImpl(first.getValue(), " ", false, true));
    }

    /**
     * Executable arithmetic functions rtrim
     */
    @ExecFunction(name = "rtrim")
    public static Expression rtrimVarcharVarchar(StringLikeLiteral first, StringLikeLiteral second) {
        return castStringLikeLiteral(first, trimImpl(first.getValue(), second.getValue(), false, true));
    }

    /**
     * Executable arithmetic functions Trim_In
     */
    @ExecFunction(name = "trim_in")
    public static Expression trimInVarchar(StringLikeLiteral first) {
        return castStringLikeLiteral(first, trimInImpl(first.getValue(), " ", true, true));
    }

    /**
     * Executable arithmetic functions Trim_In
     */
    @ExecFunction(name = "trim_in")
    public static Expression trimInVarcharVarchar(StringLikeLiteral first, StringLikeLiteral second) {
        return castStringLikeLiteral(first, trimInImpl(first.getValue(), second.getValue(), true, true));
    }

    /**
     * Executable arithmetic functions ltrim_in
     */
    @ExecFunction(name = "ltrim_in")
    public static Expression ltrimInVarchar(StringLikeLiteral first) {
        return castStringLikeLiteral(first, trimInImpl(first.getValue(), " ", true, false));
    }

    /**
     * Executable arithmetic functions ltrim_in
     */
    @ExecFunction(name = "ltrim_in")
    public static Expression ltrimInVarcharVarchar(StringLikeLiteral first, StringLikeLiteral second) {
        return castStringLikeLiteral(first, trimInImpl(first.getValue(), second.getValue(), true, false));
    }

    /**
     * Executable arithmetic functions rtrim_in
     */
    @ExecFunction(name = "rtrim_in")
    public static Expression rtrimInVarchar(StringLikeLiteral first) {
        return castStringLikeLiteral(first, trimInImpl(first.getValue(), " ", false, true));
    }

    /**
     * Executable arithmetic functions rtrim_in
     */
    @ExecFunction(name = "rtrim_in")
    public static Expression rtrimInVarcharVarchar(StringLikeLiteral first, StringLikeLiteral second) {
        return castStringLikeLiteral(first, trimInImpl(first.getValue(), second.getValue(), false, true));
    }

    /**
     * Executable arithmetic functions Replace
     */
    @ExecFunction(name = "replace")
    public static Expression replace(StringLikeLiteral first, StringLikeLiteral second, StringLikeLiteral third) {
        if (second.getValue().length() == 0) {
            return castStringLikeLiteral(first, first.getValue());
        }
        return castStringLikeLiteral(first, first.getValue().replace(second.getValue(), third.getValue()));
    }

    /**
     * Executable arithmetic functions Left
     */
    @ExecFunction(name = "left")
    public static Expression left(StringLikeLiteral first, IntegerLiteral second) {
        int inputLength = first.getValue().codePointCount(0, first.getValue().length());
        if (second.getValue() <= 0) {
            return castStringLikeLiteral(first, "");
        } else if (second.getValue() >= inputLength) {
            return first;
        } else {
            // at here leftIndex and rightIndex can not be exceeding boundary
            int index = first.getValue().offsetByCodePoints(0, second.getValue());
            return castStringLikeLiteral(first, first.getValue().substring(0, index));
        }
    }

    /**
     * Executable arithmetic functions Right
     */
    @ExecFunction(name = "right")
    public static Expression right(StringLikeLiteral first, IntegerLiteral second) {
        int inputLength = first.getValue().codePointCount(0, first.getValue().length());
        if (second.getValue() < (- inputLength) || Math.abs(second.getValue()) == 0) {
            return castStringLikeLiteral(first, "");
        } else if (second.getValue() >= inputLength) {
            return first;
        } else {
            // at here second can not be exceeding boundary
            if (second.getValue() >= 0) {
                int index = first.getValue().offsetByCodePoints(0, second.getValue());
                return castStringLikeLiteral(first, first.getValue().substring(
                    inputLength - index, inputLength));
            } else {
                int index = first.getValue().offsetByCodePoints(0, Math.abs(second.getValue()) - 1);
                return castStringLikeLiteral(first, first.getValue().substring(
                    index, inputLength));
            }
        }
    }

    /**
     * Executable arithmetic functions Locate
     */
    @ExecFunction(name = "locate")
    public static Expression locate(StringLikeLiteral first, StringLikeLiteral second) {
        return locate(first, second, new IntegerLiteral(1));
    }

    /**
     * Executable arithmetic functions Locate
     */
    @ExecFunction(name = "locate")
    public static Expression locate(StringLikeLiteral first, StringLikeLiteral second, IntegerLiteral third) {
        String searchStr = first.getValue();
        String targetStr = second.getValue();
        int startPos = third.getValue();
        if (searchStr.isEmpty()) {
            int byteLength = targetStr.getBytes(StandardCharsets.UTF_8).length;
            return (startPos >= 1 && startPos <= byteLength)
                    ? new IntegerLiteral(startPos)
                    : new IntegerLiteral(startPos == 1 ? 1 : 0);
        }

        int strLength = targetStr.codePointCount(0, targetStr.length());
        if (startPos < 1 || startPos > strLength) {
            return new IntegerLiteral(0);
        }
        int offset = targetStr.offsetByCodePoints(0, startPos - 1);
        int loc = targetStr.indexOf(searchStr, offset);
        return loc == -1 ? new IntegerLiteral(0) : new IntegerLiteral(targetStr.codePointCount(0, loc) + 1);
    }

    /**
     * Executable arithmetic functions Instr
     */
    @ExecFunction(name = "instr")
    public static Expression instr(StringLikeLiteral first, StringLikeLiteral second) {
        return new IntegerLiteral(first.getValue().indexOf(second.getValue()) + 1);
    }

    /**
     * Executable arithmetic functions Ascii
     */
    @ExecFunction(name = "ascii")
    public static Expression ascii(StringLikeLiteral first) throws UnsupportedEncodingException {
        if (first.getValue().length() == 0) {
            return new IntegerLiteral(0);
        }
        String character = first.getValue();
        byte[] utf8Bytes = character.getBytes("UTF-8");
        int firstByteAscii = utf8Bytes[0] & 0xFF;
        return new IntegerLiteral(firstByteAscii);
    }

    /**
     * Executable arithmetic functions Bin
     */
    @ExecFunction(name = "bin")
    public static Expression bin(BigIntLiteral first) {
        return new VarcharLiteral(Long.toBinaryString(first.getValue()));
    }

    /**
     * Executable arithmetic functions ConcatWs
     */
    @ExecFunction(name = "concat_ws")
    public static Expression concatWsVarcharArray(StringLikeLiteral first, ArrayLiteral second) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < second.getValue().size() - 1; i++) {
            if (!(second.getValue().get(i) instanceof NullLiteral)) {
                sb.append(second.getValue().get(i).getValue());
                sb.append(first.getValue());
            }
        }
        sb.append(second.getValue().get(second.getValue().size() - 1).getValue());
        return castStringLikeLiteral(first, sb.toString());
    }

    /**
     * Executable arithmetic functions ConcatWs
     */
    @ExecFunction(name = "concat_ws")
    public static Expression concatWsVarcharVarchar(StringLikeLiteral first, StringLikeLiteral... second) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < second.length - 1; i++) {
            sb.append(second[i].getValue());
            sb.append(first.getValue());
        }
        sb.append(second[second.length - 1].getValue());
        return castStringLikeLiteral(first, sb.toString());
    }

    /**
     * Executable arithmetic functions CharacterLength
     */
    @ExecFunction(name = "character_length")
    public static Expression characterLength(StringLikeLiteral first) {
        return new IntegerLiteral(first.getValue().codePointCount(0, first.getValue().length()));
    }

    /**
     * Executable arithmetic functions initCap
     */
    @ExecFunction(name = "initcap")
    public static Expression initCap(StringLikeLiteral first) {
        StringBuilder result = new StringBuilder(first.getValue().length());
        boolean capitalizeNext = true;

        for (char c : first.getValue().toCharArray()) {
            if (!Character.isLetterOrDigit(c)) {
                result.append(c);
                capitalizeNext = true;  // Next character should be capitalized
            } else if (capitalizeNext) {
                result.append(Character.toUpperCase(c));
                capitalizeNext = false;
            } else {
                result.append(Character.toLowerCase(c));
            }
        }
        return castStringLikeLiteral(first, result.toString());
    }

    /**
     * Executable arithmetic functions md5
     */
    @ExecFunction(name = "md5")
    public static Expression md5(StringLikeLiteral first) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            // Update the digest with the input bytes
            md.update(first.getValue().getBytes());
            return castStringLikeLiteral(first, bytesToHex(md.digest()));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Executable arithmetic functions md5
     */
    @ExecFunction(name = "md5sum")
    public static Expression md5Sum(StringLikeLiteral... first) {
        try {
            // Step 1: Create a MessageDigest instance for MD5
            MessageDigest md = MessageDigest.getInstance("MD5");

            // Step 2: Concatenate all strings in the list into one string
            StringBuilder combinedInput = new StringBuilder();
            for (StringLikeLiteral input : first) {
                combinedInput.append(input.getValue());
            }

            // Step 3: Convert the combined string to a byte array and pass it to the digest() method
            byte[] messageDigest = md.digest(combinedInput.toString().getBytes());

            // Step 4: Convert the byte array into a hexadecimal string
            StringBuilder hexString = new StringBuilder();
            for (byte b : messageDigest) {
                String hex = Integer.toHexString(0xff & b);
                if (hex.length() == 1) {
                    hexString.append('0'); // Add leading zero if needed
                }
                hexString.append(hex);
            }

            // Step 5: Return the hexadecimal string
            return castStringLikeLiteral(first[0], hexString.toString());

        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    // Helper method to convert a byte array to a hexadecimal string
    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    private static int compareLiteral(Literal first, Literal... second) {
        for (int i = 0; i < second.length; i++) {
            if (second[i].getValue().equals(first.getValue())) {
                return i + 1;
            }
        }
        return 0;
    }

    /**
     * Executable arithmetic functions field
     */
    @ExecFunction(name = "field")
    public static Expression fieldInt(IntegerLiteral first, IntegerLiteral... second) {
        return new IntegerLiteral(compareLiteral(first, second));
    }

    /**
     * Executable arithmetic functions field
     */
    @ExecFunction(name = "field")
    public static Expression fieldTinyInt(TinyIntLiteral first, TinyIntLiteral... second) {
        return new IntegerLiteral(compareLiteral(first, second));
    }

    /**
     * Executable arithmetic functions field
     */
    @ExecFunction(name = "field")
    public static Expression fieldSmallInt(SmallIntLiteral first, SmallIntLiteral... second) {
        return new IntegerLiteral(compareLiteral(first, second));
    }

    /**
     * Executable arithmetic functions field
     */
    @ExecFunction(name = "field")
    public static Expression fieldBigInt(BigIntLiteral first, BigIntLiteral... second) {
        return new IntegerLiteral(compareLiteral(first, second));
    }

    /**
     * Executable arithmetic functions field
     */
    @ExecFunction(name = "field")
    public static Expression fieldLargeInt(LargeIntLiteral first, LargeIntLiteral... second) {
        return new IntegerLiteral(compareLiteral(first, second));
    }

    /**
     * Executable arithmetic functions field
     */
    @ExecFunction(name = "field")
    public static Expression fieldFloat(FloatLiteral first, FloatLiteral... second) {
        return new IntegerLiteral(compareLiteral(first, second));
    }

    /**
     * Executable arithmetic functions field
     */
    @ExecFunction(name = "field")
    public static Expression fieldDouble(DoubleLiteral first, DoubleLiteral... second) {
        return new IntegerLiteral(compareLiteral(first, second));
    }

    /**
     * Executable arithmetic functions field
     */
    @ExecFunction(name = "field")
    public static Expression fieldDecimalV2(DecimalLiteral first, DecimalLiteral... second) {
        return new IntegerLiteral(compareLiteral(first, second));
    }

    /**
     * Executable arithmetic functions field
     */
    @ExecFunction(name = "field")
    public static Expression fieldDecimalV3(DecimalV3Literal first, DecimalV3Literal... second) {
        return new IntegerLiteral(compareLiteral(first, second));
    }

    /**
     * Executable arithmetic functions field
     */
    @ExecFunction(name = "field")
    public static Expression fieldDateTime(DateTimeLiteral first, DateTimeLiteral... second) {
        return new IntegerLiteral(compareLiteral(first, second));
    }

    /**
     * Executable arithmetic functions field
     */
    @ExecFunction(name = "field")
    public static Expression fieldDateTimeV2(DateTimeV2Literal first, DateTimeV2Literal... second) {
        return new IntegerLiteral(compareLiteral(first, second));
    }

    /**
     * Executable arithmetic functions field
     */
    @ExecFunction(name = "field")
    public static Expression fieldVarchar(StringLikeLiteral first, StringLikeLiteral... second) {
        return new IntegerLiteral(compareLiteral(first, second));
    }

    private static int findStringInSet(String target, String input) {
        String[] split = input.split(",", -1);
        for (int i = 0; i < split.length; i++) {
            if (split[i].equals(target)) {
                return i + 1;
            }
        }
        return 0;
    }

    /**
     * Executable arithmetic functions find_in_set
     */
    @ExecFunction(name = "find_in_set")
    public static Expression findInSetVarchar(StringLikeLiteral first, StringLikeLiteral second) {
        return new IntegerLiteral(findStringInSet(first.getValue(), second.getValue()));
    }

    /**
     * Executable arithmetic functions repeat
     */
    @ExecFunction(name = "repeat")
    public static Expression repeat(StringLikeLiteral first, IntegerLiteral second) {
        // when it is too large for fe to make result string, do not folding on fe, limit 1 MB
        if ((first.getValue().length() * second.getValue()) > 1000000) {
            throw new AnalysisException("repeat too large to fold const by fe");
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < second.getValue(); i++) {
            sb.append(first.getValue());
        }
        return castStringLikeLiteral(first, sb.toString());
    }

    /**
     * Executable arithmetic functions reverse
     */
    @ExecFunction(name = "reverse")
    public static Expression reverseVarchar(StringLikeLiteral first) {
        StringBuilder sb = new StringBuilder();
        sb.append(first.getValue());
        return castStringLikeLiteral(first, sb.reverse().toString());
    }

    /**
     * Executable arithmetic functions space
     */
    @ExecFunction(name = "space")
    public static Expression space(IntegerLiteral first) {
        // when it is too large for fe to make result string, do not folding on fe, limit 1 MB
        if (first.getValue() > 1000000) {
            throw new AnalysisException("space too large to fold const by fe");
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < first.getValue(); i++) {
            sb.append(' ');
        }
        return new VarcharLiteral(sb.toString());
    }

    /**
     * split by char by empty string considering emoji
     * @param str input string to be split
     * @return ArrayLiteral
     */
    public static List<String> splitByGrapheme(StringLikeLiteral str) {
        List<String> result = Lists.newArrayListWithExpectedSize(str.getValue().length());
        int length = str.getValue().length();
        for (int i = 0; i < length; ) {
            int codePoint = str.getValue().codePointAt(i);
            int charCount = Character.charCount(codePoint);
            result.add(new String(new int[]{codePoint}, 0, 1));
            i += charCount;
        }
        return result;
    }

    /**
     * Executable arithmetic functions split_by_string
     */
    @ExecFunction(name = "split_by_string")
    public static Expression splitByString(StringLikeLiteral first, StringLikeLiteral second) {
        if (first.getValue().isEmpty()) {
            return new ArrayLiteral(ImmutableList.of(), ArrayType.of(first.getDataType()));
        }
        if (second.getValue().isEmpty()) {
            List<Literal> result = Lists.newArrayListWithExpectedSize(first.getValue().length());
            for (String resultStr : splitByGrapheme(first)) {
                result.add(castStringLikeLiteral(first, resultStr));
            }
            return new ArrayLiteral(result);
        }
        String[] result = first.getValue().split(Pattern.quote(second.getValue()), -1);
        List<Literal> items = new ArrayList<>();
        for (String s : result) {
            items.add(castStringLikeLiteral(first, s));
        }
        return new ArrayLiteral(items);
    }

    /**
     * Executable arithmetic functions split_part
     */
    @ExecFunction(name = "split_part")
    public static Expression splitPart(StringLikeLiteral first, StringLikeLiteral chr, IntegerLiteral number) {
        if (number.getValue() == 0) {
            return new NullLiteral(first.getDataType());
        }
        if (chr.getValue().isEmpty()) {
            return castStringLikeLiteral(first, "");
        }
        if (first.getValue().isEmpty()) {
            return new NullLiteral(first.getDataType());
        }
        if (first.getValue().equals(chr.getValue())) {
            if (Math.abs(number.getValue()) == 1 || Math.abs(number.getValue()) == 2) {
                return castStringLikeLiteral(first, "");
            } else {
                return new NullLiteral(first.getDataType());
            }
        }
        if (!first.getValue().contains(chr.getValue())) {
            return new NullLiteral(first.getDataType());
        }
        String separator = chr.getValue();
        String[] parts;
        if (number.getValue() < 0) {
            StringBuilder sb = new StringBuilder(first.getValue());
            StringBuilder separatorBuilder = new StringBuilder(separator);
            separator = separatorBuilder.reverse().toString();
            parts = sb.reverse().toString().split(Pattern.quote(separator), -1);
        } else {
            parts = first.getValue().split(Pattern.quote(separator), -1);
        }

        if (parts.length < Math.abs(number.getValue())) {
            return new NullLiteral(first.getDataType());
        } else if (number.getValue() < 0) {
            StringBuilder result = new StringBuilder(parts[Math.abs(number.getValue()) - 1]);
            return castStringLikeLiteral(first, result.reverse().toString());
        } else {
            return castStringLikeLiteral(first, parts[number.getValue() - 1]);
        }
    }

    /**
     * Executable arithmetic functions substring_index
     */
    @ExecFunction(name = "substring_index")
    public static Expression substringIndex(StringLikeLiteral first, StringLikeLiteral chr, IntegerLiteral number) {
        if (chr.getValue().isEmpty()) {
            return chr;
        }
        String[] parts = first.getValue().split(Pattern.quote(chr.getValue()), -1);
        if (Math.abs(number.getValue()) >= parts.length) {
            return first;
        }
        int leftIndex;
        int rightIndex;
        if (parts.length < number.getValue() || number.getValue() < (- parts.length) || number.getValue() == 0) {
            return castStringLikeLiteral(first, "");
        } else if (number.getValue() < 0) {
            leftIndex = parts.length + number.getValue();
            rightIndex = parts.length;
        } else {
            leftIndex = 0;
            rightIndex = number.getValue();
        }
        StringBuilder sb = new StringBuilder();
        for (int i = leftIndex; i < rightIndex - 1; i++) {
            sb.append(parts[i]);
            sb.append(chr.getValue());
        }
        sb.append(parts[rightIndex - 1]);
        return castStringLikeLiteral(first, sb.toString());
    }

    /**
     * Executable arithmetic functions strcmp
     */
    @ExecFunction(name = "strcmp")
    public static Expression strcmp(StringLikeLiteral first, StringLikeLiteral second) {
        int result = first.getValue().compareTo(second.getValue());
        if (result == 0) {
            return new TinyIntLiteral((byte) 0);
        } else if (result < 0) {
            return new TinyIntLiteral((byte) -1);
        } else {
            return new TinyIntLiteral((byte) 1);
        }
    }

    /**
     * Executable arithmetic functions overlay
     */
    @ExecFunction(name = "overlay")
    public static Expression overlay(StringLikeLiteral originStr,
            IntegerLiteral pos, IntegerLiteral len, StringLikeLiteral insertStr) {
        StringBuilder sb = new StringBuilder();
        int totalLength = originStr.getValue().codePointCount(0, originStr.getValue().length());
        if (pos.getValue() <= 0 || pos.getValue() > totalLength) {
            return originStr;
        } else {
            if (len.getValue() < 0 || len.getValue() > (totalLength - pos.getValue())) {
                sb.append(substringImpl(originStr.getValue(), 1, pos.getValue() - 1));
                sb.append(insertStr.getValue());
                return castStringLikeLiteral(originStr, sb.toString());
            } else {
                sb.append(substringImpl(originStr.getValue(), 1, pos.getValue() - 1));
                sb.append(insertStr.getValue());
                sb.append(substringImpl(originStr.getValue(), pos.getValue() + len.getValue(), totalLength));
                return castStringLikeLiteral(originStr, sb.toString());
            }
        }
    }

    /**
     * Executable arithmetic functions parseurl
     */
    @ExecFunction(name = "parse_url")
    public static Expression parseurl(StringLikeLiteral first, StringLikeLiteral second) {
        URI uri = null;
        try {
            uri = new URI(first.getValue());
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
        StringBuilder sb = new StringBuilder();
        switch (second.getValue().toUpperCase()) {
            case "PROTOCOL":
                String scheme = uri.getScheme();
                if (scheme == null) {
                    return new NullLiteral(first.getDataType());
                }
                sb.append(scheme); // e.g., http, https
                break;
            case "HOST":
                String host = uri.getHost();
                if (host == null) {
                    return new NullLiteral(first.getDataType());
                }
                sb.append(host);  // e.g., www.example.com
                break;
            case "PATH":
                String path = uri.getPath();
                if (path == null) {
                    return new NullLiteral(first.getDataType());
                }
                sb.append(path);  // e.g., /page
                break;
            case "REF":
                try {
                    String ref = uri.toURL().getRef();
                    if (ref == null) {
                        return new NullLiteral(first.getDataType());
                    }
                    sb.append(ref);  // e.g., /page
                } catch (MalformedURLException e) {
                    throw new RuntimeException(e);
                }
                break;
            case "AUTHORITY":
                String authority = uri.getAuthority();
                if (authority == null) {
                    return new NullLiteral(first.getDataType());
                }
                sb.append(authority);  // e.g., param1=value1&param2=value2
                break;
            case "FILE":
                try {
                    String file = uri.toURL().getFile();
                    if (file == null) {
                        return new NullLiteral(first.getDataType());
                    }
                    sb.append(file);  // e.g., param1=value1&param2=value2
                } catch (MalformedURLException e) {
                    throw new RuntimeException(e);
                }
                break;
            case "QUERY":
                String query = uri.getQuery();
                if (query == null) {
                    return new NullLiteral(first.getDataType());
                }
                sb.append(query);  // e.g., param1=value1&param2=value2
                break;
            case "PORT":
                sb.append(uri.getPort());
                break;
            case "USERINFO":
                String userInfo = uri.getUserInfo();
                if (userInfo == null) {
                    return new NullLiteral(first.getDataType());
                }
                sb.append(userInfo);  // e.g., user:pass
                break;
            default:
                throw new RuntimeException("Valid URL parts are 'PROTOCOL', 'HOST', "
                        + "'PATH', 'REF', 'AUTHORITY', 'FILE', 'USERINFO', 'PORT' and 'QUERY'");
        }
        return castStringLikeLiteral(first, sb.toString());
    }

    /**
     * Executable arithmetic functions urldecode
     */
    @ExecFunction(name = "url_decode")
    public static Expression urlDecode(StringLikeLiteral first) {
        try {
            return castStringLikeLiteral(first, URLDecoder.decode(first.getValue(), StandardCharsets.UTF_8.name()));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Executable arithmetic functions urlencode
     */
    @ExecFunction(name = "url_encode")
    public static Expression urlEncode(StringLikeLiteral first) {
        try {
            return castStringLikeLiteral(first, URLEncoder.encode(first.getValue(), StandardCharsets.UTF_8.name()));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Executable arithmetic functions append_trailing_char_if_absent
     */
    @ExecFunction(name = "append_trailing_char_if_absent")
    public static Expression appendTrailingCharIfAbsent(StringLikeLiteral first, StringLikeLiteral second) {
        if (second.getValue().codePointCount(0, second.getValue().length()) != 1) {
            return new NullLiteral(first.getDataType());
        }
        if (first.getValue().endsWith(second.getValue())) {
            return first;
        } else {
            return castStringLikeLiteral(first, first.getValue() + second.getValue());
        }
    }

    /**
     * Executable arithmetic functions endsWith
     */
    @ExecFunction(name = "ends_with")
    public static Expression endsWith(StringLikeLiteral first, StringLikeLiteral second) {
        if (first.getValue().endsWith(second.getValue())) {
            return BooleanLiteral.TRUE;
        } else {
            return BooleanLiteral.FALSE;
        }
    }

    /**
     * Executable arithmetic functions extractUrlParameter
     */
    @ExecFunction(name = "extract_url_parameter")
    public static Expression extractUrlParameter(StringLikeLiteral first, StringLikeLiteral second) {
        if (first.getValue() == null || first.getValue().indexOf('?') == -1) {
            return castStringLikeLiteral(first, "");
        }
        URI uri;
        try {
            uri = new URI(first.getValue());
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        String query = uri.getQuery();
        if (query != null) {
            String[] pairs = query.split("&", -1);

            for (String pair : pairs) {
                String[] keyValue = pair.split("=", -1);
                if (second.getValue().equals(keyValue[0])) {
                    return castStringLikeLiteral(first, keyValue[1]);
                }
            }
        }
        return castStringLikeLiteral(first, "");
    }

    /**
     * Executable arithmetic functions quote
     */
    @ExecFunction(name = "quote")
    public static Expression quote(StringLikeLiteral first) {
        return castStringLikeLiteral(first, "\'" + first.getValue() + "\'");
    }

    /**
     * Executable arithmetic functions replaceEmpty
     */
    @ExecFunction(name = "replace_empty")
    public static Expression replaceEmpty(StringLikeLiteral first, StringLikeLiteral second, StringLikeLiteral third) {
        if (second.getValue().isEmpty()) {
            if (first.getValue().isEmpty()) {
                return castStringLikeLiteral(first, third.getValue());
            }
            List<String> inputs = splitByGrapheme(first);
            StringBuilder sb = new StringBuilder();
            sb.append(third.getValue());
            for (String input : inputs) {
                sb.append(input);
                sb.append(third.getValue());
            }
            return castStringLikeLiteral(first, sb.toString());
        }
        return castStringLikeLiteral(first, first.getValue().replace(second.getValue(), third.getValue()));
    }

}